[fcm] 01/01: Upstream 2014.09.0 release
Alastair McKinstry
mckinstry at moszumanska.debian.org
Mon Jun 15 11:24:36 UTC 2015
This is an automated email from the git hooks/post-receive script.
mckinstry pushed a commit to tag upstream_2014.09.0
in repository fcm.
commit c8da138072f45848becb5e0570706e9ad6d3a586
Author: Alastair McKinstry <mckinstry at debian.org>
Date: Sun Nov 9 17:49:20 2014 +0000
Upstream 2014.09.0 release
---
ACKNOWLEDGEMENT.md | 19 +
CHANGES.md | 356 ++
CONTRIBUTING.md | 77 +
COPYING | 674 +++
README.md | 35 +
bin/fcm | 134 +
bin/fcm_graphic_diff | 130 +
bin/fcm_graphic_merge | 130 +
bin/fcm_gui | 1346 +++++
bin/fcm_internal | 629 ++
bin/fcm_test_battery | 24 +
doc/collaboration/feeding-back-patch.png | Bin 0 -> 11655 bytes
doc/collaboration/index.html | 481 ++
doc/collaboration/managing-local-changes.png | Bin 0 -> 9112 bytes
doc/collaboration/merging-patch-multi.png | Bin 0 -> 4620 bytes
doc/collaboration/merging-patch-one.png | Bin 0 -> 4427 bytes
doc/collaboration/mirroring-trunk.png | Bin 0 -> 7604 bytes
doc/collaboration/updating-branch.png | Bin 0 -> 6324 bytes
doc/collaboration/updating-shared-branch.png | Bin 0 -> 9032 bytes
doc/collaboration/working-as-collaborator.png | Bin 0 -> 10409 bytes
doc/etc/bootstrap/css/bootstrap-responsive.css | 1109 ++++
doc/etc/bootstrap/css/bootstrap-responsive.min.css | 9 +
doc/etc/bootstrap/css/bootstrap.css | 6167 ++++++++++++++++++++
doc/etc/bootstrap/css/bootstrap.min.css | 9 +
.../bootstrap/img/glyphicons-halflings-white.png | Bin 0 -> 8777 bytes
doc/etc/bootstrap/img/glyphicons-halflings.png | Bin 0 -> 12799 bytes
doc/etc/bootstrap/js/bootstrap.js | 2280 ++++++++
doc/etc/bootstrap/js/bootstrap.min.js | 6 +
doc/etc/fcm-icon.png | Bin 0 -> 1712 bytes
doc/etc/fcm-terms-of-use.html | 95 +
doc/etc/fcm-version.js | 1 +
doc/etc/fcm.css | 28 +
doc/etc/fcm.js | 166 +
doc/etc/fcm.png | Bin 0 -> 6763 bytes
doc/etc/jquery.min.js | 4 +
doc/etc/moment.min.js | 6 +
doc/index.html | 93 +
doc/installation/index.html | 354 ++
doc/release_notes/1-1.html | 475 ++
doc/release_notes/1-2.html | 400 ++
doc/release_notes/1-3.html | 585 ++
doc/release_notes/1-4.html | 395 ++
doc/release_notes/1-5.html | 483 ++
doc/release_notes/2-0.html | 754 +++
doc/release_notes/2-1.html | 457 ++
doc/release_notes/2-2.html | 600 ++
doc/release_notes/2-3-1.html | 117 +
doc/release_notes/2-3.html | 136 +
doc/release_notes/index.html | 105 +
doc/user_guide/annex_bld_cfg.html | 931 +++
doc/user_guide/annex_cfg.html | 1433 +++++
doc/user_guide/annex_ext_cfg.html | 397 ++
doc/user_guide/annex_fcm_cfg.html | 198 +
doc/user_guide/annex_quick_ref.html | 256 +
doc/user_guide/annex_quick_ref_tree_conflicts.html | 283 +
doc/user_guide/api.html | 222 +
doc/user_guide/build.html | 1646 ++++++
doc/user_guide/changeset.png | Bin 0 -> 58553 bytes
doc/user_guide/code_management.html | 1243 ++++
doc/user_guide/command_ref.html | 2005 +++++++
doc/user_guide/create_branch.png | Bin 0 -> 71893 bytes
doc/user_guide/extract.html | 1171 ++++
doc/user_guide/fcm_overview.png | Bin 0 -> 13033 bytes
doc/user_guide/getting_started.html | 1490 +++++
doc/user_guide/gui1.png | Bin 0 -> 18770 bytes
doc/user_guide/gui2.png | Bin 0 -> 32347 bytes
doc/user_guide/index.html | 102 +
doc/user_guide/introduction.html | 115 +
doc/user_guide/make.html | 2302 ++++++++
doc/user_guide/overview.html | 155 +
doc/user_guide/system_admin.html | 612 ++
doc/user_guide/working_practices.html | 822 +++
doc/user_guide/xxdiff1.png | Bin 0 -> 27430 bytes
doc/user_guide/xxdiff2.png | Bin 0 -> 26199 bytes
doc/user_guide/xxdiff_tutorial.png | Bin 0 -> 22140 bytes
etc/fcm/admin.cfg.example | 86 +
etc/fcm/external.cfg.example | 9 +
etc/fcm/keyword.cfg.example | 9 +
etc/fcm/make.cfg.example | 9 +
etc/svn-hooks/post-commit | 23 +
etc/svn-hooks/post-revprop-change | 23 +
etc/svn-hooks/pre-commit | 22 +
etc/svn-hooks/pre-revprop-change | 22 +
index.html | 14 +
lib/FCM/Admin/Config.pm | 353 ++
lib/FCM/Admin/Project.pm | 260 +
lib/FCM/Admin/Runner.pm | 299 +
lib/FCM/Admin/System.pm | 1387 +++++
lib/FCM/Admin/User.pm | 90 +
lib/FCM/Admin/Users/LDAP.pm | 149 +
lib/FCM/Admin/Users/Passwd.pm | 141 +
lib/FCM/Admin/Util.pm | 386 ++
lib/FCM/CLI.pm | 291 +
lib/FCM/CLI/Exception.pm | 57 +
lib/FCM/CLI/Parser.pm | 459 ++
lib/FCM/CLI/fcm-add.pod | 22 +
lib/FCM/CLI/fcm-branch-create.pod | 86 +
lib/FCM/CLI/fcm-branch-delete.pod | 61 +
lib/FCM/CLI/fcm-branch-diff.pod | 52 +
lib/FCM/CLI/fcm-branch-info.pod | 43 +
lib/FCM/CLI/fcm-branch-list.pod | 52 +
lib/FCM/CLI/fcm-branch.pod | 37 +
lib/FCM/CLI/fcm-browse.pod | 30 +
lib/FCM/CLI/fcm-build.pod | 91 +
lib/FCM/CLI/fcm-cfg-print.pod | 23 +
lib/FCM/CLI/fcm-changelist.pod | 9 +
lib/FCM/CLI/fcm-cmp-ext-cfg.pod | 30 +
lib/FCM/CLI/fcm-commit.pod | 31 +
lib/FCM/CLI/fcm-conflicts.pod | 17 +
lib/FCM/CLI/fcm-delete.pod | 22 +
lib/FCM/CLI/fcm-diff.pod | 45 +
lib/FCM/CLI/fcm-export-items.pod | 57 +
lib/FCM/CLI/fcm-extract.pod | 46 +
lib/FCM/CLI/fcm-gui.pod | 12 +
lib/FCM/CLI/fcm-help.pod | 25 +
lib/FCM/CLI/fcm-keyword-print.pod | 27 +
lib/FCM/CLI/fcm-loc-layout.pod | 25 +
lib/FCM/CLI/fcm-make.pod | 60 +
lib/FCM/CLI/fcm-merge.pod | 76 +
lib/FCM/CLI/fcm-mkpatch.pod | 61 +
lib/FCM/CLI/fcm-project-create.pod | 34 +
lib/FCM/CLI/fcm-switch.pod | 16 +
lib/FCM/CLI/fcm-test-battery.pod | 25 +
lib/FCM/CLI/fcm-update.pod | 14 +
lib/FCM/CLI/fcm-version.pod | 11 +
lib/FCM/Class/CODE.pm | 307 +
lib/FCM/Class/Exception.pm | 134 +
lib/FCM/Class/HASH.pm | 340 ++
lib/FCM/Context/ConfigEntry.pm | 160 +
lib/FCM/Context/Event.pm | 383 ++
lib/FCM/Context/Keyword.pm | 221 +
lib/FCM/Context/Locator.pm | 144 +
lib/FCM/Context/Make.pm | 164 +
lib/FCM/Context/Make/Build.pm | 481 ++
lib/FCM/Context/Make/Extract.pm | 494 ++
lib/FCM/Context/Make/Mirror.pm | 119 +
lib/FCM/Context/Make/Share/Property.pm | 138 +
lib/FCM/Context/Task.pm | 105 +
lib/FCM/Exception.pm | 113 +
lib/FCM/System.pm | 287 +
lib/FCM/System/CM.pm | 709 +++
lib/FCM/System/CM/CommitMessage.pm | 312 +
lib/FCM/System/CM/Prompt.pm | 221 +
lib/FCM/System/CM/ResolveConflicts.pm | 669 +++
lib/FCM/System/CM/SVN.pm | 1003 ++++
lib/FCM/System/Exception.pm | 421 ++
lib/FCM/System/Make.pm | 451 ++
lib/FCM/System/Make/Build.pm | 1650 ++++++
lib/FCM/System/Make/Build/FileType.pm | 226 +
lib/FCM/System/Make/Build/FileType/C.pm | 156 +
lib/FCM/System/Make/Build/FileType/CPP.pm | 92 +
lib/FCM/System/Make/Build/FileType/CXX.pm | 72 +
lib/FCM/System/Make/Build/FileType/Data.pm | 82 +
lib/FCM/System/Make/Build/FileType/FPP.pm | 67 +
lib/FCM/System/Make/Build/FileType/Fortran.pm | 404 ++
lib/FCM/System/Make/Build/FileType/H.pm | 131 +
lib/FCM/System/Make/Build/FileType/NS.pm | 210 +
lib/FCM/System/Make/Build/FileType/Script.pm | 100 +
lib/FCM/System/Make/Build/Task/Archive.pm | 133 +
lib/FCM/System/Make/Build/Task/Compile.pm | 143 +
lib/FCM/System/Make/Build/Task/Compile/C.pm | 70 +
lib/FCM/System/Make/Build/Task/Compile/CXX.pm | 70 +
lib/FCM/System/Make/Build/Task/Compile/Fortran.pm | 114 +
lib/FCM/System/Make/Build/Task/ExtractInterface.pm | 536 ++
lib/FCM/System/Make/Build/Task/Install.pm | 90 +
lib/FCM/System/Make/Build/Task/Link.pm | 195 +
lib/FCM/System/Make/Build/Task/Link/C.pm | 72 +
lib/FCM/System/Make/Build/Task/Link/CXX.pm | 72 +
lib/FCM/System/Make/Build/Task/Link/Fortran.pm | 72 +
lib/FCM/System/Make/Build/Task/Preprocess.pm | 131 +
lib/FCM/System/Make/Build/Task/Preprocess/C.pm | 66 +
.../System/Make/Build/Task/Preprocess/Fortran.pm | 67 +
lib/FCM/System/Make/Build/Task/Share.pm | 94 +
lib/FCM/System/Make/Extract.pm | 1212 ++++
lib/FCM/System/Make/Mirror.pm | 415 ++
lib/FCM/System/Make/Preprocess.pm | 87 +
lib/FCM/System/Make/Share/Config.pm | 404 ++
lib/FCM/System/Make/Share/Dest.pm | 438 ++
lib/FCM/System/Make/Share/Subsystem.pm | 322 +
lib/FCM/System/Misc.pm | 354 ++
lib/FCM/System/Old.pm | 142 +
lib/FCM/Util.pm | 1023 ++++
lib/FCM/Util/ConfigReader.pm | 610 ++
lib/FCM/Util/ConfigUpgrade.pm | 136 +
lib/FCM/Util/Event.pm | 1244 ++++
lib/FCM/Util/Exception.pm | 219 +
lib/FCM/Util/Locator.pm | 763 +++
lib/FCM/Util/Locator/FS.pm | 202 +
lib/FCM/Util/Locator/SSH.pm | 251 +
lib/FCM/Util/Locator/SVN.pm | 458 ++
lib/FCM/Util/Reporter.pm | 382 ++
lib/FCM/Util/Shell.pm | 306 +
lib/FCM/Util/TaskRunner.pm | 380 ++
lib/FCM1/Base.pm | 125 +
lib/FCM1/Build.pm | 1630 ++++++
lib/FCM1/Build/Fortran.pm | 549 ++
lib/FCM1/BuildSrc.pm | 1508 +++++
lib/FCM1/BuildTask.pm | 353 ++
lib/FCM1/CfgFile.pm | 597 ++
lib/FCM1/CfgLine.pm | 346 ++
lib/FCM1/Cm.pm | 2264 +++++++
lib/FCM1/CmBranch.pm | 1009 ++++
lib/FCM1/CmUrl.pm | 639 ++
lib/FCM1/Config.pm | 898 +++
lib/FCM1/ConfigSystem.pm | 752 +++
lib/FCM1/Dest.pm | 899 +++
lib/FCM1/Exception.pm | 108 +
lib/FCM1/Extract.pm | 1132 ++++
lib/FCM1/ExtractConfigComparator.pm | 371 ++
lib/FCM1/ExtractFile.pm | 423 ++
lib/FCM1/ExtractSrc.pm | 100 +
lib/FCM1/Interactive.pm | 144 +
lib/FCM1/Interactive/InputGetter.pm | 135 +
lib/FCM1/Interactive/InputGetter/CLI.pm | 100 +
lib/FCM1/Interactive/InputGetter/GUI.pm | 261 +
lib/FCM1/Keyword.pm | 175 +
lib/FCM1/ReposBranch.pm | 528 ++
lib/FCM1/SrcDirLayer.pm | 277 +
lib/FCM1/Timer.pm | 85 +
lib/FCM1/Util.pm | 564 ++
lib/FCM1/Util/ClassLoader.pm | 93 +
licences/Apache2 | 202 +
licences/GPL3 | 1 +
man/man1/fcm.1 | 42 +
sbin/fcm-add-svn-repos | 105 +
sbin/fcm-add-svn-repos-and-trac-env | 119 +
sbin/fcm-add-trac-env | 117 +
sbin/fcm-backup-svn-repos | 137 +
sbin/fcm-backup-trac-env | 118 +
sbin/fcm-commit-update | 170 +
sbin/fcm-daily-update | 190 +
sbin/fcm-install-svn-hook | 115 +
sbin/fcm-manage-trac-env-session | 96 +
sbin/fcm-manage-users | 119 +
sbin/fcm-recover-svn-repos | 136 +
sbin/fcm-recover-trac-env | 112 +
sbin/fcm-rpmbuild | 112 +
sbin/fcm-user-to-email | 66 +
sbin/fcm-vacuum-trac-env-db | 101 +
sbin/my-regular-update.example | 43 +
sbin/post-commit-bg | 160 +
sbin/post-commit-bg-notify-who | 103 +
sbin/post-revprop-change-bg | 111 +
sbin/pre-commit | 110 +
sbin/pre-commit-verify-branch-owner | 100 +
sbin/pre-revprop-change | 84 +
sbin/svnperms.py | 368 ++
sbin/trac_hook | 65 +
t/etc/repo_files/lib/python/info/__init__.py | 0
t/etc/repo_files/lib/python/info/poems.py | 24 +
t/etc/repo_files/module/hello_constants.f90 | 5 +
t/etc/repo_files/module/hello_constants.inc | 1 +
t/etc/repo_files/module/hello_constants_dummy.inc | 1 +
t/etc/repo_files/pro/hello.pro | 2 +
t/etc/repo_files/pro/plot.pro | 3 +
t/etc/repo_files/program/hello.F90 | 20 +
t/etc/repo_files/subroutine/hello_c.c | 5 +
t/etc/repo_files/subroutine/hello_sub.F90 | 24 +
t/etc/repo_files/subroutine/hello_sub.h | 1 +
t/etc/repo_files/subroutine/hello_sub_dummy.h | 1 +
t/fcm-add-trac-env/00-basic.t | 83 +
t/fcm-add-trac-env/test_header | 1 +
t/fcm-add/00-simple.t | 139 +
t/fcm-add/test_header | 232 +
t/fcm-backup-svn-repos/00-basic.t | 103 +
t/fcm-backup-svn-repos/test_header | 1 +
t/fcm-branch-create/00-simple.t | 96 +
t/fcm-branch-create/test_header | 232 +
t/fcm-branch-delete/00-simple.t | 65 +
t/fcm-branch-delete/test_header | 232 +
t/fcm-branch-diff/00-simple.t | 664 +++
t/fcm-branch-diff/test_header | 232 +
t/fcm-branch-info/00-simple.t | 154 +
t/fcm-branch-info/test_header | 232 +
t/fcm-branch-list/00-simple.t | 133 +
t/fcm-branch-list/test_header | 232 +
t/fcm-commit/00-simple.t | 134 +
t/fcm-commit/01-subtree.t | 137 +
t/fcm-commit/02-bad.t | 48 +
t/fcm-commit/03-message-file.t | 58 +
t/fcm-commit/test_header | 232 +
t/fcm-conflicts/00-tree-add-add.t | 154 +
t/fcm-conflicts/01-tree-delete-delete.t | 97 +
t/fcm-conflicts/02-tree-delete-edit.t | 132 +
t/fcm-conflicts/03-tree-delete-rename.t | 111 +
t/fcm-conflicts/04-tree-edit-delete.t | 130 +
t/fcm-conflicts/05-tree-edit-rename.t | 185 +
t/fcm-conflicts/06-tree-rename-delete.t | 111 +
t/fcm-conflicts/07-tree-rename-edit.t | 142 +
t/fcm-conflicts/08-tree-rename-rename-diff.t | 144 +
t/fcm-conflicts/09-tree-rename-rename-same.t | 220 +
t/fcm-conflicts/10-text.t | 180 +
t/fcm-conflicts/test_header | 232 +
t/fcm-diff/00-simple.t | 239 +
t/fcm-diff/test_header | 232 +
t/fcm-install-svn-hook/00-basic.t | 144 +
t/fcm-install-svn-hook/00-basic/clean-2.out | 1 +
t/fcm-install-svn-hook/00-basic/clean.out | 17 +
t/fcm-install-svn-hook/00-basic/commit-conf-2.out | 6 +
t/fcm-install-svn-hook/00-basic/commit-conf.out | 10 +
t/fcm-install-svn-hook/00-basic/new-2.out | 4 +
t/fcm-install-svn-hook/00-basic/new.out | 8 +
.../00-basic/svnperms-conf-2.out | 5 +
t/fcm-install-svn-hook/00-basic/svnperms-conf.out | 9 +
t/fcm-install-svn-hook/01-housekeep-log.t | 192 +
t/fcm-install-svn-hook/01-housekeep-log/0-cmd0.out | 16 +
t/fcm-install-svn-hook/01-housekeep-log/0-cmd1.out | 8 +
t/fcm-install-svn-hook/01-housekeep-log/28-cmd.out | 36 +
t/fcm-install-svn-hook/01-housekeep-log/7-cmd.out | 32 +
t/fcm-install-svn-hook/02-env.t | 59 +
t/fcm-install-svn-hook/test_header | 1 +
t/fcm-install-svn-hook/test_header_more | 25 +
t/fcm-keyword-print/00-simple.t | 67 +
t/fcm-keyword-print/test_header | 1 +
t/fcm-loc-layout/00-simple.t | 163 +
t/fcm-loc-layout/test_header | 231 +
t/fcm-make/00-build-basic.t | 85 +
t/fcm-make/00-build-basic/bin/my-ld | 21 +
t/fcm-make/00-build-basic/fcm-make.cfg | 5 +
t/fcm-make/00-build-basic/src/hello.f90 | 4 +
t/fcm-make/00-build-basic/src/world.f90 | 8 +
t/fcm-make/01-build-link-opts | 1 +
t/fcm-make/01-build-link-opts.t | 80 +
t/fcm-make/02-build-ext-iface.t | 46 +
.../02-build-ext-iface/expected/t1.interface | 70 +
.../02-build-ext-iface/expected/t2.interface | 4 +
t/fcm-make/02-build-ext-iface/fcm-make.cfg | 4 +
t/fcm-make/02-build-ext-iface/src/m1.f90 | 26 +
t/fcm-make/02-build-ext-iface/src/t1.f90 | 163 +
t/fcm-make/02-build-ext-iface/src/t2.f90 | 3 +
t/fcm-make/03-build-include-paths.t | 69 +
t/fcm-make/03-build-include-paths/fcm-make.cfg | 6 +
.../include/world1/worldx.f90 | 1 +
.../include/world2/worldx.f90 | 1 +
t/fcm-make/03-build-include-paths/src/hello.f90 | 4 +
t/fcm-make/03-build-include-paths/src/world.f90 | 8 +
t/fcm-make/04-build-libs.t | 71 +
t/fcm-make/04-build-libs/fcm-make.cfg | 6 +
t/fcm-make/04-build-libs/src-lib/earth.f90 | 4 +
t/fcm-make/04-build-libs/src-lib/greet.f90 | 4 +
t/fcm-make/04-build-libs/src-lib/moon.f90 | 4 +
t/fcm-make/04-build-libs/src/hello.f90 | 13 +
t/fcm-make/05-build-c-cxx-basic.t | 84 +
t/fcm-make/05-build-c-cxx-basic/fcm-make.cfg | 8 +
t/fcm-make/05-build-c-cxx-basic/src/chello.c | 6 +
t/fcm-make/05-build-c-cxx-basic/src/cxxhello.cxx | 6 +
t/fcm-make/06-extract-ssh.t | 108 +
t/fcm-make/07-build-ns-dep.t | 68 +
t/fcm-make/07-build-ns-dep/fcm-make.cfg | 5 +
t/fcm-make/07-build-ns-dep/src/lib/earth.f90 | 4 +
t/fcm-make/07-build-ns-dep/src/lib/greet.f90 | 4 +
t/fcm-make/07-build-ns-dep/src/main/hello.f90 | 13 +
t/fcm-make/08-build-dup-dep.t | 60 +
t/fcm-make/08-build-dup-dep/fcm-make.cfg | 4 +
t/fcm-make/08-build-dup-dep/src/lib/earth.f90 | 4 +
t/fcm-make/08-build-dup-dep/src/lib/greet.f90 | 4 +
t/fcm-make/08-build-dup-dep/src/lib/moon.f90 | 4 +
t/fcm-make/08-build-dup-dep/src/main/hello.f90 | 13 +
t/fcm-make/09-build-dep-o.t | 64 +
t/fcm-make/09-build-dep-o/fcm-make.cfg | 4 +
t/fcm-make/09-build-dep-o/src/lib/earth.f90 | 4 +
t/fcm-make/09-build-dep-o/src/lib/greet.f90 | 5 +
.../09-build-dep-o/src/lib/greet_fmt_mod.f90 | 3 +
t/fcm-make/09-build-dep-o/src/main/hello.f90 | 13 +
t/fcm-make/09-build-dep-o/src/main/hi.f90 | 13 +
t/fcm-make/10-log | 1 +
t/fcm-make/10-log.t | 54 +
t/fcm-make/11-preprocess-include-path.t | 59 +
t/fcm-make/11-preprocess-include-path/fcm-make.cfg | 4 +
.../include/world1/worldx.h | 1 +
.../include/world2/worldx.h | 1 +
.../11-preprocess-include-path/src/world.F90 | 8 +
t/fcm-make/12-build-class-prop.t | 68 +
t/fcm-make/12-build-class-prop/bin/my-fc | 2 +
t/fcm-make/12-build-class-prop/fcm-make.cfg | 14 +
t/fcm-make/12-build-class-prop/src/hello.f90 | 3 +
t/fcm-make/12-build-class-prop/src/hello_house.f90 | 3 +
.../12-build-class-prop/src/hello_office.f90 | 3 +
t/fcm-make/12-build-class-prop/src/hello_road.f90 | 3 +
t/fcm-make/13-build-target-prop.t | 53 +
t/fcm-make/13-build-target-prop/bin/my-fc | 20 +
t/fcm-make/13-build-target-prop/fcm-make.cfg | 4 +
t/fcm-make/13-build-target-prop/src/hello.f90 | 4 +
t/fcm-make/13-build-target-prop/src/world.f90 | 8 +
t/fcm-make/14-build-etc.t | 60 +
t/fcm-make/14-build-etc/fcm-make.cfg | 4 +
t/fcm-make/14-build-etc/src/foo | 2 +
t/fcm-make/14-build-etc/src/hello.txt | 1 +
t/fcm-make/14-build-etc/src/hi/hi-earth.txt | 1 +
t/fcm-make/14-build-etc/src/hi/hi-mars.txt | 1 +
t/fcm-make/15-extract-loc-reset.t | 182 +
t/fcm-make/16-build-dep-o-2.t | 50 +
t/fcm-make/16-build-dep-o-2/fcm-make.cfg | 4 +
t/fcm-make/16-build-dep-o-2/src/hello.f90 | 3 +
t/fcm-make/16-build-dep-o-2/src/hello_mod.f90 | 3 +
t/fcm-make/16-build-dep-o-2/src/hello_sub.f90 | 4 +
t/fcm-make/17-build-cyclic.t | 42 +
t/fcm-make/17-build-cyclic/fcm-make.cfg | 3 +
t/fcm-make/17-build-cyclic/src/bar.f90 | 9 +
t/fcm-make/17-build-cyclic/src/baz.f90 | 10 +
t/fcm-make/17-build-cyclic/src/foo.f90 | 10 +
t/fcm-make/17-build-cyclic/src/hello.f90 | 7 +
t/fcm-make/17-build-cyclic/src/meow.f90 | 11 +
t/fcm-make/17-build-cyclic/src/quack.f90 | 9 +
t/fcm-make/18-build-use-intrinsic.t | 60 +
t/fcm-make/18-build-use-intrinsic/fcm-make.cfg | 3 +
t/fcm-make/18-build-use-intrinsic/src/greet.f90 | 14 +
t/fcm-make/18-build-use-intrinsic/src/hello.f90 | 4 +
t/fcm-make/18-build-use-intrinsic/src/hi.f90 | 4 +
t/fcm-make/19-build-inherit-prop.t | 50 +
t/fcm-make/19-build-inherit-prop/fcm-make.cfg | 4 +
t/fcm-make/19-build-inherit-prop/src/hello.F90 | 6 +
t/fcm-make/20-args.t | 50 +
t/fcm-make/20-args/fcm-make.cfg | 3 +
t/fcm-make/20-args/src/greet.f90 | 3 +
t/fcm-make/20-args/src/hello.f90 | 3 +
t/fcm-make/21-inherit-steps.t | 68 +
t/fcm-make/21-inherit-steps/fcm-make.cfg | 8 +
t/fcm-make/21-inherit-steps/src1/hello.f90 | 3 +
t/fcm-make/21-inherit-steps/src2/salute.f90 | 3 +
t/fcm-make/21-inherit-steps/src3/greet.f90 | 3 +
t/fcm-make/22-build-2-bad-mod-over-inherit.t | 59 +
.../22-build-2-bad-mod-over-inherit/fcm-make.cfg | 3 +
.../22-build-2-bad-mod-over-inherit/src-i/m1.f90 | 6 +
.../22-build-2-bad-mod-over-inherit/src-i/m2.f90 | 6 +
.../22-build-2-bad-mod-over-inherit/src/m1.f90 | 6 +
.../22-build-2-bad-mod-over-inherit/src/m2.f90 | 6 +
.../22-build-2-bad-mod-over-inherit/src/p1.f90 | 6 +
t/fcm-make/23-build-omp.t | 66 +
t/fcm-make/23-build-omp/fcm-make.cfg | 4 +
t/fcm-make/23-build-omp/src/i1.f90 | 1 +
t/fcm-make/23-build-omp/src/i2.f90 | 1 +
t/fcm-make/23-build-omp/src/m1.f90 | 14 +
t/fcm-make/23-build-omp/src/m2.f90 | 14 +
t/fcm-make/23-build-omp/src/p1.f90 | 21 +
t/fcm-make/23-build-omp/src/s3.f90 | 7 +
t/fcm-make/24-build-c-main-camel.t | 36 +
t/fcm-make/24-build-c-main-camel/fcm-make.cfg | 4 +
t/fcm-make/24-build-c-main-camel/src/Hello.c | 5 +
t/fcm-make/25-build-cyclic-2.t | 40 +
t/fcm-make/25-build-cyclic-2/fcm-make.cfg | 3 +
t/fcm-make/25-build-cyclic-2/src/foo.f90 | 4 +
t/fcm-make/25-build-cyclic-2/src/m1.f90 | 12 +
t/fcm-make/25-build-cyclic-2/src/m2.f90 | 8 +
t/fcm-make/26-no-config.t | 33 +
t/fcm-make/27-args-only.t | 41 +
t/fcm-make/28-bad-arg.t | 41 +
t/fcm-make/29-relative-cfg.t | 85 +
t/fcm-make/30-relative-cfg-in-svn.t | 52 +
t/fcm-make/31-relative-cfg-in-ssh.t | 69 +
t/fcm-make/32-include-relative-cfg.t | 54 +
t/fcm-make/33-include-relative-cfg-in-svn.t | 73 +
t/fcm-make/34-include-relative-cfg-in-ssh.t | 75 +
t/fcm-make/35-include-relative-cfg-in-2-dirs.t | 62 +
t/fcm-make/36-build-fail-cont-basic.t | 135 +
t/fcm-make/36-build-fail-cont-basic/fcm-make.cfg | 4 +
.../36-build-fail-cont-basic/src/greet_mod.f90 | 9 +
t/fcm-make/36-build-fail-cont-basic/src/hello.f90 | 6 +
t/fcm-make/36-build-fail-cont-basic/src/hello2.f90 | 6 +
t/fcm-make/36-build-fail-cont-basic/src/hello3.f90 | 6 +
t/fcm-make/36-build-fail-cont-basic/src/hello4.f90 | 5 +
.../36-build-fail-cont-basic/src/hello_sub.f90 | 5 +
.../36-build-fail-cont-basic/src/world_mod.f90 | 4 +
t/fcm-make/test_header | 1 +
t/fcm-merge/00-simple.t | 321 +
t/fcm-merge/01-complex.t | 1646 ++++++
t/fcm-merge/test_header | 232 +
t/fcm-recover-svn-repos/00-basic.t | 108 +
t/fcm-recover-svn-repos/test_header | 1 +
t/fcm-status/00-simple.t | 83 +
t/fcm-status/test_header | 232 +
t/fcm-switch/00-simple.t | 109 +
t/fcm-switch/01-subtree.t | 149 +
t/fcm-switch/test_header | 232 +
t/fcm-update/00-simple.t | 144 +
t/fcm-update/01-subtree.t | 146 +
t/fcm-update/test_header | 232 +
t/lib/bash/test_header | 216 +
t/svn-hooks/00-pre-revprop-change.t | 83 +
t/svn-hooks/01-post-revprop-change-bg.t | 119 +
t/svn-hooks/02-pre-commit.t | 287 +
t/svn-hooks/03-post-commit-bg.t | 301 +
t/svn-hooks/test_header | 1 +
t/svn-hooks/test_header_more | 114 +
t/svn-username/00-branch.t | 103 +
t/svn-username/test_header | 232 +
test/compare_results_fcm1 | 217 +
test/compare_results_fcm2 | 205 +
test/compare_times_fcm1-2 | 27 +
test/create_hpc_batch_script | 90 +
test/create_repos | 197 +
test/get_hpc_results | 34 +
test/perform_test_fcm1 | 165 +
test/perform_test_fcm2 | 204 +
test/report_hpc_results | 14 +
test/repos/add_subroutine/hello.F90 | 26 +
test/repos/add_subroutine/hello.F90.add_lines | 28 +
test/repos/add_subroutine/hello_sub2.f90 | 11 +
test/repos/cyclic_dependency/hello.F90 | 14 +
.../cyclic_dependency/hello_constants.f90.fail | 19 +
.../repos/cyclic_dependency/hello_constants.f90.ok | 19 +
test/repos/cyclic_dependency/hello_sub2.f90 | 11 +
test/repos/trunk/blockdata/hello_blockdata.F90 | 9 +
test/repos/trunk/cfg/fcm1_add_directory.cfg | 6 +
test/repos/trunk/cfg/fcm1_add_directory_expsrc.cfg | 7 +
test/repos/trunk/cfg/fcm1_add_file.cfg | 6 +
test/repos/trunk/cfg/fcm1_add_file_inherit.cfg | 8 +
test/repos/trunk/cfg/fcm1_base.cfg | 6 +
test/repos/trunk/cfg/fcm1_base_inc.cfg | 23 +
test/repos/trunk/cfg/fcm1_branches_clash.cfg | 7 +
test/repos/trunk/cfg/fcm1_branches_merge.cfg | 8 +
.../cfg/fcm1_branches_merge_conflict_fail.cfg | 10 +
.../cfg/fcm1_branches_merge_conflict_override.cfg | 10 +
.../trunk/cfg/fcm1_branches_merge_inherit.cfg | 10 +
.../fcm1_branches_merge_inherit_wrong_include.cfg | 12 +
.../trunk/cfg/fcm1_branches_merge_wcopies.cfg | 8 +
test/repos/trunk/cfg/fcm1_branches_merge_wcopy.cfg | 8 +
test/repos/trunk/cfg/fcm1_cflags.cfg | 6 +
test/repos/trunk/cfg/fcm1_change_src_type.cfg | 7 +
test/repos/trunk/cfg/fcm1_delete_directory.cfg | 7 +
.../trunk/cfg/fcm1_delete_directory_inherit.cfg | 9 +
test/repos/trunk/cfg/fcm1_delete_file.cfg | 8 +
test/repos/trunk/cfg/fcm1_delete_file_inherit.cfg | 10 +
test/repos/trunk/cfg/fcm1_delete_inc_file.cfg | 6 +
.../trunk/cfg/fcm1_delete_inc_file_inherit.cfg | 8 +
.../cfg/fcm1_delete_inc_file_inherit_force.cfg | 10 +
test/repos/trunk/cfg/fcm1_delete_pp_file.cfg | 8 +
.../trunk/cfg/fcm1_delete_pp_file_inherit.cfg | 10 +
test/repos/trunk/cfg/fcm1_delete_ppinc_file.cfg | 6 +
.../trunk/cfg/fcm1_delete_ppinc_file_inherit.cfg | 8 +
.../cfg/fcm1_delete_ppinc_file_inherit_force.cfg | 9 +
test/repos/trunk/cfg/fcm1_duplicate_target.cfg | 7 +
test/repos/trunk/cfg/fcm1_exclude_dependency.cfg | 6 +
test/repos/trunk/cfg/fcm1_exe_permissions.cfg | 7 +
test/repos/trunk/cfg/fcm1_exe_rename.cfg | 7 +
test/repos/trunk/cfg/fcm1_fc.cfg | 6 +
test/repos/trunk/cfg/fcm1_fflags1.cfg | 6 +
test/repos/trunk/cfg/fcm1_fflags2.cfg | 6 +
test/repos/trunk/cfg/fcm1_fflags_inherit.cfg | 8 +
test/repos/trunk/cfg/fcm1_inc_devnull.cfg | 9 +
test/repos/trunk/cfg/fcm1_inherit_invalid_path.cfg | 6 +
test/repos/trunk/cfg/fcm1_inherit_target.cfg | 8 +
test/repos/trunk/cfg/fcm1_invalid_base_url.cfg | 6 +
test/repos/trunk/cfg/fcm1_invalid_branch_url.cfg | 6 +
test/repos/trunk/cfg/fcm1_invalid_inc.cfg | 4 +
test/repos/trunk/cfg/fcm1_invalid_namespace.cfg | 6 +
test/repos/trunk/cfg/fcm1_invalid_variable.cfg | 6 +
test/repos/trunk/cfg/fcm1_ld.cfg | 6 +
test/repos/trunk/cfg/fcm1_library.cfg | 6 +
test/repos/trunk/cfg/fcm1_library_rename.cfg | 7 +
test/repos/trunk/cfg/fcm1_mirror.cfg | 8 +
test/repos/trunk/cfg/fcm1_mirror_inherit.cfg | 12 +
.../trunk/cfg/fcm1_modify_subroutine_inherit.cfg | 8 +
.../fcm1_modify_subroutine_interface_inherit.cfg | 8 +
test/repos/trunk/cfg/fcm1_multi_inherit.cfg | 9 +
test/repos/trunk/cfg/fcm1_no_dep.cfg | 6 +
test/repos/trunk/cfg/fcm1_ops.cfg | 211 +
test/repos/trunk/cfg/fcm1_postproc_hpc.cfg | 209 +
test/repos/trunk/cfg/fcm1_pp_change_blockdata.cfg | 6 +
test/repos/trunk/cfg/fcm1_pp_change_dependency.cfg | 6 +
test/repos/trunk/cfg/fcm1_pp_change_include.cfg | 6 +
.../trunk/cfg/fcm1_pp_change_include_inherit.cfg | 8 +
test/repos/trunk/cfg/fcm1_pp_empty_subroutine.cfg | 6 +
.../trunk/cfg/fcm1_pp_empty_subroutine_inherit.cfg | 8 +
.../cfg/fcm1_pp_empty_subroutine_inherit_force.cfg | 9 +
test/repos/trunk/cfg/fcm1_revmatch_false.cfg | 13 +
test/repos/trunk/cfg/fcm1_revmatch_true.cfg | 6 +
test/repos/trunk/cfg/fcm1_sps.cfg | 129 +
test/repos/trunk/cfg/fcm1_suite.cfg | 8 +
test/repos/trunk/cfg/fcm1_symbolic_link.cfg | 6 +
test/repos/trunk/cfg/fcm1_um.cfg | 54 +
test/repos/trunk/cfg/fcm1_um_hpc.cfg | 60 +
test/repos/trunk/cfg/fcm1_um_inherit.cfg | 49 +
test/repos/trunk/cfg/fcm1_um_inherit_hpc.cfg | 51 +
test/repos/trunk/cfg/fcm1_var.cfg | 232 +
test/repos/trunk/cfg/fcm1_var_hpc.cfg | 238 +
test/repos/trunk/cfg/fcm2_add_directory_expsrc.cfg | 3 +
test/repos/trunk/cfg/fcm2_add_file.cfg | 3 +
test/repos/trunk/cfg/fcm2_add_file_inherit.cfg | 3 +
test/repos/trunk/cfg/fcm2_base.cfg | 3 +
test/repos/trunk/cfg/fcm2_base_inc.cfg | 6 +
test/repos/trunk/cfg/fcm2_base_inc2.cfg | 15 +
test/repos/trunk/cfg/fcm2_branches_clash.cfg | 5 +
test/repos/trunk/cfg/fcm2_branches_merge.cfg | 6 +
.../trunk/cfg/fcm2_branches_merge_duplicate.cfg | 6 +
.../trunk/cfg/fcm2_branches_merge_inherit.cfg | 6 +
.../fcm2_branches_merge_inherit_wrong_include.cfg | 8 +
.../trunk/cfg/fcm2_branches_merge_wcopies.cfg | 6 +
test/repos/trunk/cfg/fcm2_branches_merge_wcopy.cfg | 6 +
test/repos/trunk/cfg/fcm2_cflags.cfg | 3 +
test/repos/trunk/cfg/fcm2_change_variable.cfg | 4 +
test/repos/trunk/cfg/fcm2_cyclic_dep_fail.cfg | 3 +
test/repos/trunk/cfg/fcm2_cyclic_dep_ok.cfg | 3 +
test/repos/trunk/cfg/fcm2_delete_directory.cfg | 21 +
.../trunk/cfg/fcm2_delete_directory_inherit.cfg | 7 +
test/repos/trunk/cfg/fcm2_delete_file.cfg | 6 +
test/repos/trunk/cfg/fcm2_delete_file_inherit.cfg | 6 +
test/repos/trunk/cfg/fcm2_delete_inc_file.cfg | 3 +
.../trunk/cfg/fcm2_delete_inc_file_inherit.cfg | 3 +
.../cfg/fcm2_delete_inc_file_inherit_force.cfg | 5 +
test/repos/trunk/cfg/fcm2_delete_pp_file.cfg | 24 +
.../trunk/cfg/fcm2_delete_pp_file_inherit.cfg | 6 +
test/repos/trunk/cfg/fcm2_delete_ppinc_file.cfg | 3 +
.../trunk/cfg/fcm2_delete_ppinc_file_inherit.cfg | 3 +
.../cfg/fcm2_delete_ppinc_file_inherit_force.cfg | 5 +
test/repos/trunk/cfg/fcm2_dep_o.cfg | 5 +
test/repos/trunk/cfg/fcm2_dep_o_all.cfg | 5 +
test/repos/trunk/cfg/fcm2_dep_o_invalid.cfg | 5 +
test/repos/trunk/cfg/fcm2_duplicate_target.cfg | 4 +
test/repos/trunk/cfg/fcm2_exclude_dependency.cfg | 3 +
test/repos/trunk/cfg/fcm2_exe_permissions.cfg | 6 +
test/repos/trunk/cfg/fcm2_exe_rename.cfg | 6 +
.../trunk/cfg/fcm2_extract_path_excl_no_ns.cfg | 9 +
test/repos/trunk/cfg/fcm2_fc.cfg | 3 +
test/repos/trunk/cfg/fcm2_fflags1.cfg | 3 +
test/repos/trunk/cfg/fcm2_fflags2.cfg | 3 +
test/repos/trunk/cfg/fcm2_fflags_inherit.cfg | 3 +
test/repos/trunk/cfg/fcm2_flag-output.cfg | 3 +
test/repos/trunk/cfg/fcm2_inc_devnull.cfg | 6 +
test/repos/trunk/cfg/fcm2_inherit_invalid_path.cfg | 1 +
.../repos/trunk/cfg/fcm2_inherit_redefine_fail.cfg | 5 +
test/repos/trunk/cfg/fcm2_inherit_redefine_ok.cfg | 3 +
test/repos/trunk/cfg/fcm2_invalid_base_url.cfg | 4 +
test/repos/trunk/cfg/fcm2_invalid_branch_url.cfg | 4 +
test/repos/trunk/cfg/fcm2_invalid_branch_url2.cfg | 4 +
test/repos/trunk/cfg/fcm2_invalid_inc.cfg | 1 +
test/repos/trunk/cfg/fcm2_invalid_label.cfg | 3 +
test/repos/trunk/cfg/fcm2_invalid_modifier.cfg | 3 +
test/repos/trunk/cfg/fcm2_invalid_modifiers.cfg | 3 +
test/repos/trunk/cfg/fcm2_invalid_namespace.cfg | 3 +
test/repos/trunk/cfg/fcm2_invalid_namespace2.cfg | 3 +
test/repos/trunk/cfg/fcm2_invalid_target.cfg | 3 +
test/repos/trunk/cfg/fcm2_invalid_variable.cfg | 5 +
test/repos/trunk/cfg/fcm2_library.cfg | 3 +
test/repos/trunk/cfg/fcm2_library_rename.cfg | 4 +
test/repos/trunk/cfg/fcm2_mirror.cfg | 6 +
test/repos/trunk/cfg/fcm2_mirror_after_pp.cfg | 6 +
test/repos/trunk/cfg/fcm2_mirror_inherit.cfg | 8 +
.../repos/trunk/cfg/fcm2_mirror_inherit_fflags.cfg | 6 +
.../trunk/cfg/fcm2_mirror_inherit_notarget.cfg | 6 +
.../trunk/cfg/fcm2_modify_subroutine_inherit.cfg | 3 +
.../fcm2_modify_subroutine_interface_inherit.cfg | 3 +
test/repos/trunk/cfg/fcm2_multi_inherit.cfg | 4 +
test/repos/trunk/cfg/fcm2_multiple_build.cfg | 16 +
.../trunk/cfg/fcm2_multiple_build_inherit.cfg | 6 +
test/repos/trunk/cfg/fcm2_multiple_pp-build.cfg | 25 +
.../trunk/cfg/fcm2_multiple_pp-build_inherit.cfg | 6 +
test/repos/trunk/cfg/fcm2_no_dep.cfg | 3 +
test/repos/trunk/cfg/fcm2_ns-dep_o.cfg | 5 +
test/repos/trunk/cfg/fcm2_ns-dep_o_all.cfg | 5 +
test/repos/trunk/cfg/fcm2_ns-dep_o_file.cfg | 5 +
test/repos/trunk/cfg/fcm2_ns-dep_o_invalid.cfg | 5 +
test/repos/trunk/cfg/fcm2_ops.cfg | 82 +
test/repos/trunk/cfg/fcm2_postproc_hpc.cfg | 171 +
test/repos/trunk/cfg/fcm2_pp_change_blockdata.cfg | 3 +
test/repos/trunk/cfg/fcm2_pp_change_dependency.cfg | 3 +
test/repos/trunk/cfg/fcm2_pp_change_include.cfg | 3 +
.../trunk/cfg/fcm2_pp_change_include_inherit.cfg | 3 +
test/repos/trunk/cfg/fcm2_pp_empty_subroutine.cfg | 3 +
.../trunk/cfg/fcm2_pp_empty_subroutine_inherit.cfg | 3 +
.../cfg/fcm2_pp_empty_subroutine_inherit_force.cfg | 5 +
test/repos/trunk/cfg/fcm2_revmatch_false.cfg | 7 +
test/repos/trunk/cfg/fcm2_single_file.cfg | 3 +
test/repos/trunk/cfg/fcm2_space_in_name.cfg | 5 +
test/repos/trunk/cfg/fcm2_sps.cfg | 46 +
test/repos/trunk/cfg/fcm2_symbolic_link.cfg | 3 +
test/repos/trunk/cfg/fcm2_um.cfg | 36 +
test/repos/trunk/cfg/fcm2_um77.cfg | 74 +
test/repos/trunk/cfg/fcm2_um77_hpc.cfg | 89 +
test/repos/trunk/cfg/fcm2_um77_inherit.cfg | 15 +
test/repos/trunk/cfg/fcm2_um77_inherit_hpc.cfg | 17 +
test/repos/trunk/cfg/fcm2_um_hpc.cfg | 42 +
test/repos/trunk/cfg/fcm2_um_inherit.cfg | 17 +
test/repos/trunk/cfg/fcm2_um_inherit_hpc.cfg | 19 +
test/repos/trunk/cfg/fcm2_var.cfg | 80 +
test/repos/trunk/cfg/fcm2_var_hpc.cfg | 87 +
test/repos/trunk/module/hello_constants.f90 | 5 +
test/repos/trunk/module/hello_constants.inc | 1 +
test/repos/trunk/module/hello_constants_dummy.inc | 1 +
test/repos/trunk/namelist/namelist.NL | 3 +
test/repos/trunk/pro/hello.pro | 2 +
test/repos/trunk/pro/plot.pro | 3 +
test/repos/trunk/program/hello.F90 | 26 +
test/repos/trunk/script/hello.sh | 5 +
test/repos/trunk/subroutine/hello_c.c | 5 +
test/repos/trunk/subroutine/hello_sub.F90 | 24 +
test/repos/trunk/subroutine/hello_sub.h | 1 +
test/repos/trunk/subroutine/hello_sub_dummy.h | 1 +
test/run_tests | 259 +
test/test_config/fcm1_add_directory | 3 +
test/test_config/fcm1_add_directory_expsrc | 2 +
test/test_config/fcm1_branches_clash | 1 +
test/test_config/fcm1_branches_merge_conflict_fail | 1 +
test/test_config/fcm1_branches_merge_incremental | 1 +
.../fcm1_branches_merge_inherit_wrong_include | 2 +
test/test_config/fcm1_branches_merge_wcopies | 4 +
test/test_config/fcm1_branches_merge_wcopy | 1 +
test/test_config/fcm1_cflags_incremental | 1 +
test/test_config/fcm1_change_src_type_incremental | 5 +
test/test_config/fcm1_delete_directory | 3 +
test/test_config/fcm1_delete_directory_inherit | 3 +
test/test_config/fcm1_delete_file | 1 +
test/test_config/fcm1_delete_file_inherit | 1 +
test/test_config/fcm1_delete_inc_file | 1 +
test/test_config/fcm1_delete_inc_file_inherit | 4 +
.../test_config/fcm1_delete_inc_file_inherit_force | 4 +
test/test_config/fcm1_delete_pp_file | 1 +
test/test_config/fcm1_delete_pp_file_inherit | 1 +
test/test_config/fcm1_delete_ppinc_file | 1 +
test/test_config/fcm1_delete_ppinc_file_inherit | 4 +
.../fcm1_delete_ppinc_file_inherit_force | 4 +
test/test_config/fcm1_duplicate_target | 4 +
test/test_config/fcm1_exclude_dependency | 1 +
test/test_config/fcm1_exe_permissions | 3 +
test/test_config/fcm1_exe_rename_incremental | 1 +
test/test_config/fcm1_fc_incremental | 1 +
test/test_config/fcm1_fflags_incremental | 1 +
test/test_config/fcm1_inc_devnull | 1 +
test/test_config/fcm1_inherit_invalid_path | 3 +
test/test_config/fcm1_invalid_base_url | 1 +
test/test_config/fcm1_invalid_branch_url | 1 +
test/test_config/fcm1_invalid_inc | 1 +
test/test_config/fcm1_invalid_namespace | 1 +
test/test_config/fcm1_invalid_variable | 1 +
test/test_config/fcm1_ld_incremental | 1 +
test/test_config/fcm1_library | 1 +
test/test_config/fcm1_library_rename | 1 +
test/test_config/fcm1_mirror | 1 +
test/test_config/fcm1_mirror_inherit | 1 +
test/test_config/fcm1_no_dep | 1 +
test/test_config/fcm1_ops_parallel | 2 +
test/test_config/fcm1_pp_change_include_inherit | 2 +
test/test_config/fcm1_pp_change_keys_incremental | 1 +
test/test_config/fcm1_pp_empty_subroutine | 1 +
test/test_config/fcm1_pp_empty_subroutine_inherit | 3 +
.../fcm1_pp_empty_subroutine_inherit_force | 5 +
test/test_config/fcm1_revmatch_true | 1 +
test/test_config/fcm1_sps_parallel | 2 +
test/test_config/fcm1_um | 2 +
test/test_config/fcm1_um_inherit | 2 +
test/test_config/fcm1_var_parallel | 2 +
test/test_config/fcm2_branches_clash | 1 +
test/test_config/fcm2_branches_merge_incremental | 1 +
.../fcm2_branches_merge_inherit_wrong_include | 2 +
test/test_config/fcm2_branches_merge_wcopies | 4 +
test/test_config/fcm2_branches_merge_wcopy | 1 +
test/test_config/fcm2_cflags_incremental | 1 +
test/test_config/fcm2_cyclic_dep_fail | 1 +
test/test_config/fcm2_delete_file | 2 +
test/test_config/fcm2_delete_file_inherit | 2 +
test/test_config/fcm2_delete_inc_file | 1 +
test/test_config/fcm2_delete_inc_file_inherit | 1 +
.../test_config/fcm2_delete_inc_file_inherit_force | 2 +
test/test_config/fcm2_delete_pp_file | 2 +
test/test_config/fcm2_delete_pp_file_inherit | 2 +
test/test_config/fcm2_delete_ppinc_file | 2 +
test/test_config/fcm2_delete_ppinc_file_inherit | 1 +
.../fcm2_delete_ppinc_file_inherit_force | 2 +
test/test_config/fcm2_dep_o_invalid | 1 +
test/test_config/fcm2_duplicate_target | 2 +
test/test_config/fcm2_exclude_dependency | 2 +
test/test_config/fcm2_exe_permissions | 3 +
test/test_config/fcm2_exe_rename_incremental | 1 +
test/test_config/fcm2_fc_incremental | 1 +
test/test_config/fcm2_fflags_incremental | 2 +
test/test_config/fcm2_inc_devnull | 2 +
test/test_config/fcm2_inherit_invalid_path | 1 +
test/test_config/fcm2_inherit_redefine_fail | 1 +
test/test_config/fcm2_invalid_base_url | 1 +
test/test_config/fcm2_invalid_branch_url | 1 +
test/test_config/fcm2_invalid_branch_url2 | 1 +
test/test_config/fcm2_invalid_inc | 1 +
test/test_config/fcm2_invalid_label | 1 +
test/test_config/fcm2_invalid_modifier | 1 +
test/test_config/fcm2_invalid_modifiers | 1 +
test/test_config/fcm2_invalid_namespace | 2 +
test/test_config/fcm2_invalid_namespace2 | 1 +
test/test_config/fcm2_invalid_target | 1 +
test/test_config/fcm2_invalid_variable | 2 +
test/test_config/fcm2_library | 2 +
test/test_config/fcm2_library_rename | 1 +
test/test_config/fcm2_mirror | 1 +
test/test_config/fcm2_mirror_after_pp | 2 +
test/test_config/fcm2_mirror_inherit | 1 +
test/test_config/fcm2_mirror_inherit_fflags | 1 +
test/test_config/fcm2_mirror_inherit_notarget | 1 +
test/test_config/fcm2_modify_subroutine_inherit | 1 +
test/test_config/fcm2_multi_inherit | 1 +
test/test_config/fcm2_no_dep | 1 +
test/test_config/fcm2_ns-dep_o_invalid | 1 +
test/test_config/fcm2_ops_parallel | 2 +
test/test_config/fcm2_override_variable | 2 +
test/test_config/fcm2_pp_change_include_inherit | 4 +
test/test_config/fcm2_pp_change_keys_incremental | 1 +
test/test_config/fcm2_pp_empty_subroutine | 2 +
test/test_config/fcm2_pp_empty_subroutine_inherit | 1 +
.../fcm2_pp_empty_subroutine_inherit_force | 2 +
test/test_config/fcm2_sps_parallel | 2 +
test/test_config/fcm2_um | 2 +
test/test_config/fcm2_um77 | 2 +
test/test_config/fcm2_um77_inherit | 2 +
test/test_config/fcm2_um_inherit | 2 +
test/test_config/fcm2_var_parallel | 2 +
test/test_include/inc/fortran.inc | 1 +
test/test_include/prog/fortran.inc | 1 +
test/test_include/prog/test_fortran_inc.f90 | 5 +
test/test_include/prog/test_prepro_inc.F90 | 5 +
test/test_include/test.sh | 38 +
test/tests_functional.list | 158 +
test/tests_perf_local.list | 23 +
test/tests_perf_remote.list | 19 +
test/wrapper_scripts/wrap_ar | 2 +
test/wrapper_scripts/wrap_cc | 2 +
test/wrapper_scripts/wrap_fc | 2 +
test/wrapper_scripts/wrap_fc2 | 2 +
test/wrapper_scripts/wrap_ld | 2 +
test/wrapper_scripts/wrap_ld2 | 2 +
test/wrapper_scripts/wrap_mpicc | 2 +
test/wrapper_scripts/wrap_mpif90 | 2 +
test/wrapper_scripts/wrap_pp | 2 +
tutorial/README | 14 +
tutorial/fcm-tutorial-repos-create | 80 +
tutorial/hooks/pre-commit | 7 +
tutorial/trunk-r1/doc/hello.html | 14 +
tutorial/trunk-r1/fcm-make.cfg | 5 +
tutorial/trunk-r1/src/module/hello_constants.f90 | 3 +
tutorial/trunk-r1/src/module/hello_num.f90 | 17 +
tutorial/trunk-r1/src/program/hello.f90 | 12 +
tutorial/trunk-r1/src/subroutine/hello_c.c | 5 +
tutorial/trunk-r1/src/subroutine/hello_sub.f90 | 15 +
usr/bin/fcm | 31 +
831 files changed, 101129 insertions(+)
diff --git a/ACKNOWLEDGEMENT.md b/ACKNOWLEDGEMENT.md
new file mode 100644
index 0000000..d531101
--- /dev/null
+++ b/ACKNOWLEDGEMENT.md
@@ -0,0 +1,19 @@
+# Acknowledgement for non-FCM Work
+
+Licences for non-FCM works included in this distribution can be
+found in the licences/ directory.
+
+Non-FCM works included in this distribution are listed below:
+
+doc/etc/bootstrap/:
+* Unmodified external software library copyright 2013 Twitter Inc
+ released under the Apache 2.0 license.
+ See [Bootstrap](http://getbootstrap.com/).
+
+svn-hooks/svnperms.py:
+* Subversion repository pre-commit path-based permission checking utility,
+ written by [Gustavo Niemeyer](mailto:gustavo at niemeyer.net) and released under
+ Apache 2.0 license.
+ Original source downloaded from r1295006 at:
+ https://svn.apache.org/viewvc/subversion/trunk/tools/hook-scripts/svnperms.py
+ This version is modified to allow custom permission message per repository.
diff --git a/CHANGES.md b/CHANGES.md
new file mode 100644
index 0000000..57df69a
--- /dev/null
+++ b/CHANGES.md
@@ -0,0 +1,356 @@
+# FCM Changes
+
+Go to https://github.com/metomi/fcm/milestones?state=closed
+for a full listing of issues for each release.
+
+--------------------------------------------------------------------------------
+
+## 2014.09.0 (2014-09-17)
+
+FCM release 20.
+
+### Highlighted Changes
+
+[#138](https://github.com/metomi/fcm/pull/138):
+fcm make: build: continue on failure.
+* The build system will continue as much as possible after a failure, and
+ only repeat failed tasks in incremental modes.
+* This change also fixes a problem where the system could lose information
+ after a failure. Tasks that would be run after the failed task would not get
+ their context recorded correctly. In a subsequent incremental build, the
+ system would end up doing more work than necessary.
+
+[#135](https://github.com/metomi/fcm/pull/135):
+fcm make: multiple config files and search paths.
+* You can now specify multiple `-F PATH` options to specify the search paths
+ for locating configuration files specified as relative paths.
+* You can now specify multiple `-f FILE` options.
+* New `include-path` configuration declaration for specifying the search path
+ for configuration files specified as relative paths.
+* Improve CLI argument diagnostics.
+ * The command dies if an argument is missing an equal sign.
+ * Suggest command line syntax if argument ends with `.cfg`.
+
+[#129](https://github.com/metomi/fcm/pull/129),
+[#136](https://github.com/metomi/fcm/pull/136),
+[#143](https://github.com/metomi/fcm/pull/143),
+[#144](https://github.com/metomi/fcm/pull/144):
+Major improvements to the admin sub-system:
+* Improve hook installation.
+ Write, store and housekeep hook logs at `$REPOS/log/`.
+ Clean options for hook installation.
+ Install `svnperms.conf` from repository root.
+ `TZ=UTC` for all hook scripts.
+* Improve diagnostics for hooks.
+ Custom configuations per repositories.
+ Configurable `pre-revprop-change` permissions.
+ Hooks to work best under Subversion 1.8+.
+ Add modified `svnperms.py` in distribution.
+ Trac 0.12+ changeset added and modified notification.
+* Trac URL template.
+* `fcm-add-trac-env`: add Trac comment edit permission.
+* Separate `InterTrac` configurations from `trac.ini` into `intertrac.ini`.
+* Fix usage of `FCM_CONF_PATH` for admin.
+* Improve documentation and logic for admin configuration.
+* Get user info via LDAP or traditional Unix password file.
+* New admin commands:
+ * `fcm-add-svn-repos-and-trac-env`
+ * `fcm-add-svn-repos`
+ * `fcm-manage-trac-env-session`
+* `pre-commit`: optionally block branch create with bad owner.
+* `post-commit-bg`: rename repository dump.
+* `post-commit-bg`: optionally notify branch owner if author is not owner.
+* `post-*` hooks: configurable notification `From:` field.
+* Test batteries for hooks, and selected admin utilities.
+
+### Noteworthy Changes
+
+[#140](https://github.com/metomi/fcm/pull/140):
+fcm mkpatch: Changes required for use with svn 1.8 + other minor bug fixes.
+
+[#139](https://github.com/metomi/fcm/pull/139):
+fcm commit: fail a commit if it includes the `#commit_message#` file.
+
+[#137](https://github.com/metomi/fcm/pull/137):
+fcm merge: basic support for `kdiff3`.
+
+[#129](https://github.com/metomi/fcm/pull/129):
+`fcm commit`/`fcm branch-rm`: fix branch owner test to use correct user ID.
+
+--------------------------------------------------------------------------------
+
+## 2014.06.0 (2014-06-10)
+
+### Highlighted Changes
+
+-none-
+
+### Noteworthy Changes
+
+[#125](https://github.com/metomi/fcm/pull/125):
+fcm make: build: handle adjacent cyclic dependency correctly.
+
+[#128](https://github.com/metomi/fcm/pull/128):
+Remove unnecessary `-r`, `-w` and `-x` tests to avoid ACL problems.
+Use Perl's `filetest` pragma where necessary to correctly handle ACL.
+
+--------------------------------------------------------------------------------
+
+## 2014-04 (2014-04-23)
+
+### Highlighted Changes
+
+[#114](https://github.com/metomi/fcm/pull/#114),
+[#117](https://github.com/metomi/fcm/pull/#117),
+[#118](https://github.com/metomi/fcm/pull/#118):
+fcm make: build: now recognises statements with Fortran
+OpenMP sentinels that affect build dependencies.
+These dependencies are normally ignored.
+However, if a relevant `build.prop{fc.flag-omp}` property is specified, the
+build system will treat these statements as normal dependency statements.
+
+### Noteworthy Changes
+
+[#121](https://github.com/metomi/fcm/pull/#121):
+fcm make: extract via SSH: improve performance by using `find -printf`
+instead of `find -exec stat`.
+
+[#120](https://github.com/metomi/fcm/pull/#120):
+fcm make: build will now correctly handle C source files that has camel
+case names and `main` functions.
+
+[#111](https://github.com/metomi/fcm/pull/#111):
+fcm make: build in inherit mode: fix incorrect success in repeated
+incremental mode.
+
+[#105](https://github.com/metomi/fcm/pull/#105):
+`FCM_CONF_PATH`: new environment variable that can be used to override
+site/user configuration paths.
+
+[#103](https://github.com/metomi/fcm/pull/#103):
+fcm make: extract: detect diff trees that are the same as the base tree.
+
+--------------------------------------------------------------------------------
+
+## 2014-03 (2014-03-03)
+
+### Highlighted Changes
+
+[#96](https://github.com/metomi/fcm/pull/#96):
+fcm make: arguments as extra configurations. This change allows the
+`fcm make` command to accept command line arguments. Each argument will be
+appended in order as a new line in the current `fcm-make.cfg`. This allows
+users to override the configuration on the command line.
+
+### Noteworthy Changes
+
+[#101](https://github.com/metomi/fcm/pull/#101):
+fcm make: do not inherit `steps` if it is already set in the current
+configuration. This allows `steps=` to be declared before `use=`.
+
+[#100](https://github.com/metomi/fcm/pull/#100):
+fcm make: reduce memory usage in incremental mode. Invoking `fcm make`
+with many steps was causing Perl to exit with SIGSEGV previously.
+
+[#98](https://github.com/metomi/fcm/pull/#98):
+fcm make: extract: fix ssh location efficiency.
+
+[#93](https://github.com/metomi/fcm/pull/#93):
+fcm make: fix `use=` properties override. This change allows `use=`
+declarations to be placed anywhere in an `fcm-make.cfg` without interfering
+other `*.prop` declarations.
+
+[#92](https://github.com/metomi/fcm/pull/#92):
+fcm branch-create/list: support alternate username using information in
+users' `~/.subversion/servers` file.
+
+[#91](https://github.com/metomi/fcm/pull/#91):
+fcm make: remove config-on-success on failure.
+
+--------------------------------------------------------------------------------
+
+## 2014-02 (2014-02-03)
+
+### Highlighted Changes
+
+[#83](https://github.com/metomi/fcm/pull/#83):
+fcm make: build: an initial attempt to support some Fortran 2K features.
+* Recognise `iso_fortran_env` as an intrinsic module.
+* Recognise `use, intrinsic ::` statements.
+* Recognise `class`, `double complex` and `procedure` as types.
+* Recognise new type declaration attributes.
+* Recognise `abstract interface` blocks.
+* Recognise `impure elemental` as a valid function or subroutine attribute.
+* Recognise `submodule` blocks.
+
+### Noteworthy Changes
+
+[#89](https://github.com/metomi/fcm/pull/#89):
+fcm merge, fcm switch, etc: Subversion 1.8 `svn upgrade` command may
+not write a `.svn/entries` file at the working copy root. Several FCM wrappers
+were failing because they were unable to determine the working copy root. This
+is fixed by using the new entry available in Subversion 1.8 `svn info` to
+determine the working copy root.
+
+[#87](https://github.com/metomi/fcm/pull/#87):
+fcm make: build: print sources to targets diagnostics on `-vv` mode and
+in the log.
+
+--------------------------------------------------------------------------------
+
+## 2014-01 (2014-01-20)
+
+### Highlighted Changes
+
+-none-
+
+### Noteworthy Changes
+
+[#81](https://github.com/metomi/fcm/pull/#81):
+fcm make: build: fix cyclic dependency logic.
+
+[#80](https://github.com/metomi/fcm/pull/#80):
+fcm make: extract: support `extract.location` declarations reset.
+
+[#79](https://github.com/metomi/fcm/pull/#79):
+fcm make: extract: SSH location: ignore dot files.
+
+--------------------------------------------------------------------------------
+
+## 2013-12 (2013-12-02)
+
+### Highlighted Changes
+
+-none-
+
+### Noteworthy Changes
+
+[#77](https://github.com/metomi/fcm/pull/#77):
+fcm make: mirror and build: fix etc files install. This was broken by
+[#65](https://github.com/metomi/fcm/pull/#65)
+which causes etc files to be installed to `bin/`.
+
+[#74](https://github.com/metomi/fcm/pull/#74):
+Handle date in `svn log --xml`, which may have trailing spaces and lines.
+
+--------------------------------------------------------------------------------
+
+## 2013-11 (2013-11-22)
+
+### Highlighted Changes
+
+[#65](https://github.com/metomi/fcm/pull/#65):
+fcm make: support declaration of class default properties using the
+syntax e.g. `build.prop{class,fc}=my-fc`.
+
+[#65](https://github.com/metomi/fcm/pull/#65):
+fcm make: build: support target name as name-space for target properties,
+e.g. `build.prop{fc}[myprog.exe]=my-fc`. N.B. Dependency properties are
+regarded as source properties, and so are not supported by this change.
+
+### Noteworthy Changes
+
+[#73](https://github.com/metomi/fcm/pull/#73):
+fcm mkpatch: use `/usr/bin/env bash` in generated scripts.
+
+[#72](https://github.com/metomi/fcm/pull/#72):
+fcm conflicts: fix incompatibility with SVN 1.8.
+
+[#70](https://github.com/metomi/fcm/pull/#70):
+fcm CLI: support new SVN 1.8 commands.
+
+[#68](https://github.com/metomi/fcm/pull/#68):
+sbin/fcm-backup-\*: hotcopy before verifying the hotcopy.
+
+[#63](https://github.com/metomi/fcm/pull/#63):
+fcm make: log file improvements. Print FCM version in beginning of log
+file.
+
+[#63](https://github.com/metomi/fcm/pull/#63):
+fcm --version: new command to print FCM version.
+
+[#63](https://github.com/metomi/fcm/pull/#63):
+FCM is no longer dependent on the `HTTP::Date` Perl module.
+
+--------------------------------------------------------------------------------
+
+## 2013-10 (2013-10-30)
+
+### Highlighted Changes
+
+Changes that have significant impact on user experience.
+
+[#52](https://github.com/metomi/fcm/pull/#52):
+fcm make: build: new properties for C++ source files, separated from
+C source files. File extension for C and C++ source files is rationalised to
+follow what is documented in the GCC manual.
+
+[#50](https://github.com/metomi/fcm/pull/#50),
+[#54](https://github.com/metomi/fcm/pull/#54):
+fcm make: build/preprocess.prop: include-paths/lib-paths/libs:
+New build properties to specify a list of include paths for compile
+tasks, and library paths and libraries for link tasks.
+
+### Noteworthy Changes
+
+Bug fixes and minor enhancements:
+
+[#59](https://github.com/metomi/fcm/pull/#59):
+fcm make: fix invalid cyclic dependency error when `build.prop{dep.o}` is
+declared on the root name-space.
+
+[#58](https://github.com/metomi/fcm/pull/#58):
+fcm make: build: improve diagnostics for duplicated targets and bad values
+in `build.prop{ns-dep.o}` declarations.
+
+[#55](https://github.com/metomi/fcm/pull/#55):
+fcm make: extract: can now extract from a location that is accessible via
+`ssh` and `rsync`.
+
+[#53](https://github.com/metomi/fcm/pull/#53):
+fcm make: `.fcm-make/log` can now be accessed as `fcm-make.log`.
+
+[#51](https://github.com/metomi/fcm/pull/#51):
+FCM documentation: style updated using Bootstrap.
+
+--------------------------------------------------------------------------------
+
+## 2013-09 (2013-09-26)
+
+### Highlighted Changes
+
+Changes that have significant impact on user experience.
+
+-None-
+
+### Noteworthy Changes
+
+Bug fixes and minor enhancements:
+
+[#45](https://github.com/metomi/fcm/pull/#45):
+An attempt to allow FCM to work under a case insensitive file system.
+
+[#39](https://github.com/metomi/fcm/pull/#39),
+[#40](https://github.com/metomi/fcm/pull/#40),
+[#41](https://github.com/metomi/fcm/pull/#41):
+CM commands are now tested under Subversion 1.8.
+
+[#37](https://github.com/metomi/fcm/pull/#37):
+fcm make: build: fixed hanging of `ext-iface` tasks when there is an
+unbalanced quote or bracket in a relevant Fortran source file.
+
+[#20](https://github.com/metomi/fcm/pull/#20):
+fcm make: build: allow separate linker command and add ability to keep
+the intermediate library archive while linking an executable.
+
+[#19](https://github.com/metomi/fcm/pull/#19):
+added test suite for code management commands to the distribution.
+
+r4955: fcm extract: fix failure caused by the checking of latest version of a
+deleted branch.
+
+--------------------------------------------------------------------------------
+
+## FCM-2-3-1 and Prior Releases
+
+See <http://metomi.github.io/fcm/doc/release_notes/>.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..abd2140
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,77 @@
+# FCM: How to Contribute
+
+## Report Bugs
+
+Report bugs and request enhancement by opening an issue on
+[FCM issues @ Github](https://github.com/metomi/fcm/issues). If reporting a
+bug, add a recipe for repeating it. If requesting an enhancement,
+describe the use case in detail.
+
+## Contribute Code
+
+All contributions to FCM are made via pull requests against the *master*
+branch of [metomi/fcm](https://github.com/metomi/fcm). New contributors
+should add their details to the [Code Contributors](#code-contributors)
+section of this file as part of their first request. The developer who
+reviews each pull request is responsible for checking that the
+contributor's name is listed in this file before merging the pull request
+into *master*.
+
+## Code Contributors
+
+The following people have contributed to this code under the terms of
+the Contributor Licence Agreement and Certificate of Origin detailed
+below:
+
+* Jim Bolton (Met Office, UK)
+* Ben Fitzpatrick (Met Office, UK)
+* Dave Matthews (Met Office, UK)
+* Stephen Oxley (Met Office, UK)
+* Matt Shin (Met Office, UK)
+* Matt Pryor (Met Office, UK)
+
+(All contributors are identifiable with email addresses in the version
+control logs or otherwise.)
+
+## Contributor Licence Agreement and Certificate of Origin
+
+By making a contribution to this project, I certify that:
+
+(a) The contribution was created in whole or in part by me and I have
+ the right to submit it, either on my behalf or on behalf of my
+ employer, under the terms and conditions as described by this file;
+ or
+
+(b) The contribution is based upon previous work that, to the best of
+ my knowledge, is covered under an appropriate licence and I have
+ the right or permission from the copyright owner under that licence
+ to submit that work with modifications, whether created in whole or
+ in part by me, under the terms and conditions as described by
+ this file; or
+
+(c) The contribution was provided directly to me by some other person
+ who certified (a) or (b) and I have not modified it.
+
+(d) I understand and agree that this project and the contribution
+ are public and that a record of the contribution (including my
+ name and email address) is maintained indefinitely and may be
+ redistributed consistent with this project or the licence(s)
+ involved.
+
+(e) I, or my employer, grant to the UK Met Office and all recipients of
+ this software a perpetual, worldwide, non-exclusive, no-charge,
+ royalty-free, irrevocable copyright licence to reproduce, modify,
+ prepare derivative works of, publicly display, publicly perform,
+ sub-licence, and distribute this contribution and such modifications
+ and derivative works consistent with this project or the licence(s)
+ involved or other appropriate open source licence(s) specified by
+ the project and approved by the
+ [Open Source Initiative (OSI)](http://www.opensource.org/).
+
+(f) If I become aware of anything that would make any of the above
+ inaccurate, in any way, I will let the UK Met Office know as soon as
+ I become aware.
+
+(The FCM Contributor Licence Agreement and Certificate of Origin is
+inspired by the Certificate of Origin used by Enyo and the Linux
+Kernel.)
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..94a9ed0
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ 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, either version 3 of the License, or
+ (at your option) any later version.
+
+ 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, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ <program> Copyright (C) <year> <name of author>
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..bed2f5e
--- /dev/null
+++ b/README.md
@@ -0,0 +1,35 @@
+# FCM
+
+FCM: a modern Fortran build system,
+and wrappers to Subversion for scientific software development
+
+[Installation](http://metomi.github.io/fcm/doc/installation/) |
+[User Guide](http://metomi.github.io/fcm/doc/user_guide/) |
+[How to Contribute](CONTRIBUTING.md)
+
+## Copyright and Terms of Use
+
+(C) British Crown Copyright 2006-14 Met Office.
+
+FCM 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, either version 3 of the License, or
+(at your option) any later version.
+
+FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+
+FCM documentation is licensed under the British Open Government
+Licence. See doc/etc/fcm-terms-of-use.html and
+<http://www.nationalarchives.gov.uk/doc/open-government-licence/>
+
+See <http://metomi.github.io/fcm/doc/etc/fcm-terms-of-use.html>.
+
+## Acknowledgement for Non-FCM Work
+
+See [Acknowledgement for Non-FCM Work](ACKNOWLEDGEMENT.md).
diff --git a/bin/fcm b/bin/fcm
new file mode 100755
index 0000000..58c2c74
--- /dev/null
+++ b/bin/fcm
@@ -0,0 +1,134 @@
+#!/usr/bin/env perl
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+
+use FindBin;
+use lib "$FindBin::Bin/../lib";
+use FCM::CLI;
+
+our $GUI;
+
+# ------------------------------------------------------------------------------
+if (!caller()) {
+ main(@ARGV);
+}
+
+# ------------------------------------------------------------------------------
+sub main {
+ my @args = @_;
+ local $ENV{'PATH'} = $ENV{'PATH'};
+ if (index($ENV{'PATH'}, $FindBin::Bin . ':') != 0) {
+ $ENV{'PATH'} = $FindBin::Bin . ':' . $ENV{'PATH'};
+ }
+ my $gui;
+ if (@args && $args[0] eq 'gui-internal') {
+ (undef, $gui, @args) = @args;
+ }
+ FCM::CLI->new({'gui' => $gui})->main(@args);
+}
+
+__END__
+
+=head1 NAME
+
+fcm
+
+=head1 SYNOPSIS
+
+ fcm [APPLICATION] [OPTIONS] [ARGUMENTS]
+
+=head1 OVERVIEW
+
+B<fcm> is the command line interface of the Flexible Configuration Management
+(FCM) system. For full detail of the system, please refer to the FCM user
+guide, which you should receive with this distribution in both HTML and PDF
+formats.
+
+Run "fcm help" to access the built-in tool documentation.
+
+=head1 ARGUMENTS
+
+B<fcm> provides the following applications:
+
+ branch-create, bcreate, bc
+ branch-delete, bdelete, bdel, brm
+ branch-diff, bdiff, bdi
+ branch-info, binfo
+ branch-list, bls
+ browse, trac, www
+ build
+ cfg-print, cfg
+ cmp-ext-cfg
+ conflicts, cf
+ export-items
+ extract
+ gui
+ keyword-print, kp
+ loc-layout
+ make
+ mkpatch
+ test-battery
+
+B<fcm> overrides the following B<svn> applications:
+
+ add
+ commit, ci
+ delete, del, remove, rm
+ diff, di
+ merge
+ switch, sw
+ update, up
+
+B<fcm> explicitly doesn't support the following B<svn> applications:
+
+ changelist
+
+Type "fcm help APPLICATION" for help on individual application.
+
+Type "svn help APPLICATION" for help on other B<svn> application.
+
+=head1 AUTHOR
+
+FCM Team L<fcm-team at metoffice.gov.uk>.
+Please feedback any bug reports or feature requests to us by e-mail.
+
+=head1 SEE ALSO
+
+L<svn (1)|svn>,
+L<perl (1)| perl>,
+L<FCM::CLI|FCM::CLI>
+
+=head1 COPYRIGHT
+
+FCM 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, either version 3 of the License, or
+(at your option) any later version.
+
+FCM 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 FCM. If not, see L<http://www.gnu.org/licenses/>.
+
+=cut
diff --git a/bin/fcm_graphic_diff b/bin/fcm_graphic_diff
new file mode 100755
index 0000000..a867d0d
--- /dev/null
+++ b/bin/fcm_graphic_diff
@@ -0,0 +1,130 @@
+#!/usr/bin/env perl
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+
+use strict;
+use warnings;
+
+use Getopt::Long qw{GetOptions};
+use Pod::Usage qw{pod2usage};
+
+my $RE_SVN_EMPTY_FILE = qr{\.svn/empty-file}msx;
+
+my %S = (
+ 'LABEL' => "--- %s\n+++ %s",
+ 'SKIP_ADD' => "Skipping since file has been added (or old file is empty)",
+ 'SKIP_DEL' => "Skipping since file has been deleted (or new file is empty)",
+ 'SKIP_BIN' => "Skipping binary file",
+);
+my %LABELS_HANDLER_FOR = (
+ 'tkdiff' => sub {map {('-L', $_)} @_},
+ 'xxdiff' => sub {('--title1', $_[0], '--title2', $_[1])},
+);
+
+if (!caller()) {
+ # svn diff expects:
+ # 0 - no diff
+ # 1 - diff
+ # other return code - fatal
+ exit main(@ARGV);
+}
+
+sub main {
+ local(@ARGV) = @_;
+ my %option;
+ my $rc = GetOptions(\%option, 'u', 'L=s@');
+ if (!$rc || @ARGV != 2 || grep {!-f $_} @ARGV) {
+ pod2usage(1);
+ }
+ my ($old, $new) = @ARGV;
+ ( $old =~ $RE_SVN_EMPTY_FILE || -z $old ? message('SKIP_ADD')
+ : $new =~ $RE_SVN_EMPTY_FILE || -z $new ? message('SKIP_DEL')
+ : -B $new ? message('SKIP_BIN')
+ : command(\%option, @ARGV)
+ );
+}
+
+sub command {
+ my ($option_hash_ref, $old, $new) = @_;
+ my @labels;
+ if ($option_hash_ref->{'L'} && @{$option_hash_ref->{'L'}} >= 2) {
+ @labels = @{$option_hash_ref->{'L'}};
+ message('LABEL', @labels);
+ }
+ my $diff_command
+ = exists($ENV{FCM_GRAPHIC_DIFF}) ? $ENV{FCM_GRAPHIC_DIFF} : 'xxdiff';
+ if (!$diff_command) {
+ return;
+ }
+ my @command = (
+ $diff_command,
+ ( @labels && exists($LABELS_HANDLER_FOR{$diff_command})
+ ? $LABELS_HANDLER_FOR{$diff_command}->(@labels) : ()
+ ),
+ $old, $new,
+ );
+ system(@command);
+}
+
+sub message {
+ my $format = shift();
+ printf($S{$format} . "\n\n", @_);
+ 1;
+}
+
+__END__
+
+=head1 NAME
+
+fcm_graphic_diff
+
+=head1 SYNOPSIS
+
+ fcm_graphic_diff [-u] [-L OLD_LABEL] [-L NEW_LABEL] OLD NEW
+
+=head1 DESCRIPTION
+
+Invokes L<xxdiff|xxdiff> (or the command specified in the FCM_GRAPHIC_DIFF
+environment variable) to compare the OLD and NEW files, where possible.
+
+If either file does not exist or is empty, or if the NEW file is a binary, the
+command will only print a diagnostic message.
+
+The -u option is not used, and is for compatibility with the L<svn diff|svn>
+command only.
+
+If OLD_LABEL and NEW_LABEL are set, they are printed in the format:
+
+ ---- OLD_LABEL
+ ++++ NEW_LABEL
+
+The command makes use of the labels when the diff command is either
+L<xxdiff|xxdiff> or L<tkdiff|tkdiff>:
+
+ xxdiff --title1 OLD_LABEL --title2 NEW_LABEL OLD NEW
+ tkdiff -L OLD_LABEL -L NEW_LABEL OLD NEW
+
+The command returns 0 if the files are the same or 1 if the files differ. All
+other return codes should be regarded as fatal errors.
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/bin/fcm_graphic_merge b/bin/fcm_graphic_merge
new file mode 100755
index 0000000..6e4043f
--- /dev/null
+++ b/bin/fcm_graphic_merge
@@ -0,0 +1,130 @@
+#!/usr/bin/env perl
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+
+use strict;
+use warnings;
+
+my %IMPL = ('fcm-dummy-diff' => \&_fcm_dummy_diff,
+ 'xxdiff' => \&_xxdiff,
+ 'kdiff3' => \&_kdiff3);
+
+my %UNRESOLVED = (
+ 'nodecision' => "You have made no decision.\n",
+ 'merged' => "You have not resolved all the conflicts.\n",
+);
+
+if (!caller()) {
+ # 0 - no diff
+ # 1 - diff
+ # other return code - fatal
+ exit main(@ARGV);
+}
+
+sub main {
+ my $command = 'xxdiff';
+ if (exists($ENV{FCM_GRAPHIC_MERGE}) && $ENV{FCM_GRAPHIC_MERGE}) {
+ $command = $ENV{FCM_GRAPHIC_MERGE};
+ }
+ if (!exists($IMPL{$command})) {
+ die("$command: merge tool not yet supported.\n");
+ }
+ $IMPL{$command}->(@_);
+}
+
+sub _fcm_dummy_diff {
+ my ($base, $mine, $older, $yours) = @_;
+ my @command = (qw{diff3}, $mine, $older, $yours);
+ print(join(" ", @command) . "\n");
+ my @out_lines = qx{@command};
+ for my $line (@out_lines) {
+ print($line);
+ }
+ return 0;
+}
+
+sub _xxdiff {
+ my ($base, $mine, $older, $yours) = @_;
+ my @command = (qw{xxdiff -m -M}, $base, qw{-O -X}, $mine, $older, $yours);
+ my @out_lines = qx{@command};
+ my $rc = $?;
+ if (!@out_lines) {
+ return 2;
+ }
+ my ($decision) = map {chomp($_); lc($_);} @out_lines;
+ if ($rc && exists($UNRESOLVED{$decision})) {
+ print($UNRESOLVED{$decision});
+ return 1;
+ }
+ printf("You %s all the changes.\n", $decision);
+ return 0;
+}
+
+sub _kdiff3 {
+ my ($base, $mine, $older, $yours) = @_;
+ my @command = (qw{kdiff3 -o}, $base, $mine, $older, $yours);
+ my @out_lines = qx{@command};
+ my $rc = $?;
+ # kdiff3 produces a file called $base.orig, so we delete that
+ unlink $base . ".orig";
+ # kdiff3 doesn't produce any output, so we just assume a non-zero
+ # exit code means unresolved merges
+ if ($rc) {
+ print($UNRESOLVED{'merged'});
+ return 1;
+ }
+ printf("You merged all the changes.\n");
+ return 0;
+}
+
+__END__
+
+=head1 NAME
+
+fcm_graphic_merge
+
+=head1 SYNOPSIS
+
+ fcm_graphic_merge BASE MINE OLDER YOURS
+
+=head1 DESCRIPTION
+
+Wrap L<xxdiff|xxdiff>. Invoke L<xxdiff|xxdiff> as:
+
+ xxdiff -m -M BASE -O -X MINE OLDER YOURS
+
+Print friendlier decision messages.
+
+Return 0 if no diff remains, 1 if any diff remains, and 2 for fatal errors.
+
+=head1 ARGUMENTS
+
+BASE is the file you want to save the merge result into.
+
+MINE is the original file.
+
+YOURS is the file you want MINE to merge with.
+
+OLDER is the common ancestor of MINE and YOURS.
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/bin/fcm_gui b/bin/fcm_gui
new file mode 100755
index 0000000..e1f2210
--- /dev/null
+++ b/bin/fcm_gui
@@ -0,0 +1,1346 @@
+#!/usr/bin/env perl
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+
+use strict;
+use warnings;
+
+use FindBin;
+use lib "$FindBin::Bin/../lib";
+use Cwd qw{cwd};
+use FCM::Context::Event;
+use FCM::Util;
+use FCM1::Config;
+use FCM1::Keyword;
+use FCM1::Timer qw{timestamp_command};
+use FCM1::Util qw{get_url_of_wc get_wct is_wc};
+use File::Basename qw{basename};
+use File::Spec::Functions qw{catfile rel2abs};
+use Tk;
+use Tk::ROText;
+
+# ------------------------------------------------------------------------------
+
+# Argument
+if (@ARGV) {
+ my $dir = shift @ARGV;
+ chdir $dir if -d $dir;
+}
+
+FCM1::Keyword::set_util(FCM::Util->new());
+
+# Get configuration settings
+my $config = FCM1::Config->new ();
+$config->get_config ();
+
+# ------------------------------------------------------------------------------
+
+# FCM subcommands
+my @subcmds = qw/CHECKOUT BRANCH STATUS DIFF ADD DELETE MERGE CONFLICTS COMMIT
+ UPDATE SWITCH/;
+
+# Subcommands allowed when CWD is not a WC
+my @nwc_subcmds = qw/CHECKOUT BRANCH/;
+
+# Subcommands allowed, when CWD is a WC
+my @wc_subcmds = qw/STATUS BRANCH DIFF ADD DELETE MERGE CONFLICTS COMMIT UPDATE
+ SWITCH/;
+
+# Subcommands that apply to WC only
+my @wco_subcmds = qw/BRANCH STATUS DIFF ADD DELETE MERGE CONFLICTS COMMIT UPDATE
+ SWITCH/;
+
+# Subcommands that apply to top level WC only
+my @wcto_subcmds = qw/BRANCH MERGE COMMIT UPDATE SWITCH/;
+
+# Selected subcommand
+my $selsubcmd = '';
+
+# Selected subcommand is running?
+my $cmdrunning = 0;
+
+# PID of running subcommand
+my $cmdpid = undef;
+
+# List of subcommand frames
+my %subcmd_f;
+
+# List of subcommand buttons
+my %subcmd_b;
+
+# List of subcommand button help strings
+my %subcmd_help = (
+ BRANCH => 'list information about, create or delete a branch.',
+ CHECKOUT => 'check out a working copy from a repository.',
+ STATUS => 'print the status of working copy files and directories.',
+ DIFF => 'display the differences in modified files.',
+ ADD => 'put files and directories under version control.',
+ DELETE => 'remove files and directories from version control.',
+ MERGE => 'merge changes into your working copy.',
+ CONFLICTS => 'use a graphical tool to resolve conflicts in your working copy.',
+ COMMIT => 'send changes from your working copy to the repository.',
+ UPDATE => 'bring changes from the repository into your working copy.',
+ SWITCH => 'update your working copy to a different URL.',
+);
+
+for (keys %subcmd_help) {
+ $subcmd_help{$_} = 'Select the "' . lc ($_) . '" sub-command - ' .
+ $subcmd_help{$_};
+}
+
+# List of subcommand button bindings (key name and underline position)
+my %subcmd_bind = (
+ BRANCH => {KEY => '<Alt-Key-b>', U => 0},
+ CHECKOUT => {KEY => '<Alt-Key-o>', U => 5},
+ STATUS => {KEY => '<Alt-Key-s>', U => 0},
+ DIFF => {KEY => '<Alt-Key-d>', U => 0},
+ ADD => {KEY => '<Alt-Key-a>', U => 0},
+ DELETE => {KEY => '<Alt-Key-t>', U => 4},
+ MERGE => {KEY => '<Alt-Key-m>', U => 0},
+ CONFLICTS => {KEY => '<Alt-Key-f>', U => 3},
+ COMMIT => {KEY => '<Alt-Key-c>', U => 0},
+ UPDATE => {KEY => '<Alt-Key-u>', U => 0},
+ SWITCH => {KEY => '<Alt-Key-w>', U => 1},
+);
+
+# List of subcommand variables
+my %subcmdvar = (
+ CWD => cwd (),
+ WCT => '',
+ CWD_URL => '',
+ WCT_URL => '',
+
+ BRANCH => {
+ OPT => 'info',
+ URL => '',
+ NAME => '',
+ TYPE => 'DEV',
+ REVFLAG => 'NORMAL',
+ TICKET => '',
+ SRCTYPE => 'trunk',
+ S_CHD => 0,
+ S_SIB => 0,
+ S_OTH => 0,
+ VERBOSE => 0,
+ OTHER => '',
+ },
+
+ CHECKOUT => {
+ URL => '',
+ REV => 'HEAD',
+ PATH => '',
+ OTHER => '',
+ },
+
+ STATUS => {
+ USEWCT => 0,
+ UPDATE => 0,
+ VERBOSE => 0,
+ OTHER => '',
+ },
+
+ DIFF => {
+ USEWCT => 0,
+ TOOL => 'graphical',
+ BRANCH => 0,
+ URL => '',
+ OTHER => '',
+ },
+
+ ADD => {
+ USEWCT => 0,
+ CHECK => 1,
+ OTHER => '',
+ },
+
+ DELETE => {
+ USEWCT => 0,
+ CHECK => 1,
+ OTHER => '',
+ },
+
+ MERGE => {
+ USEWCT => 1,
+ SRC => '',
+ MODE => 'automatic',
+ DRYRUN => 0,
+ VERBOSE => 0,
+ REV => '',
+ OTHER => '',
+ },
+
+ CONFLICTS => {
+ USEWCT => 0,
+ OTHER => '',
+ },
+
+ COMMIT => {
+ USEWCT => 1,
+ DRYRUN => 0,
+ OTHER => '',
+ },
+
+ UPDATE => {
+ USEWCT => 1,
+ OTHER => '',
+ },
+
+ SWITCH => {
+ USEWCT => 1,
+ URL => '',
+ OTHER => '',
+ },
+);
+
+# List of action buttons
+my %action_b;
+
+# List of action button help strings
+my %action_help = (
+ QUIT => 'Quit fcm gui',
+ HELP => 'Print help to the output text box for the selected sub-command',
+ CLEAR => 'Clear the output text box',
+ RUN => 'Run the selected sub-command',
+);
+
+# List of action button bindings
+my %action_bind = (
+ QUIT => {KEY => '<Control-Key-q>', U => undef},
+ HELP => {KEY => '<F1>' , U => undef},
+ CLEAR => {KEY => '<Alt-Key-l>' , U => 1},
+ RUN => {KEY => '<Alt-Key-r>' , U => 0},
+);
+
+# List of branch subcommand options
+my %branch_opt = (
+ INFO => undef,
+ CREATE => undef,
+ DELETE => undef,
+ LIST => undef,
+);
+
+# List of branch create types
+my %branch_type = (
+ 'DEV' => undef,
+ 'DEV::SHARE' => undef,
+ 'TEST' => undef,
+ 'TEST::SHARE' => undef,
+ 'PKG' => undef,
+ 'PKG::SHARE' => undef,
+ 'PKG::CONFIG' => undef,
+ 'PKG::REL' => undef,
+);
+
+# List of branch create source type
+my %branch_srctype = (
+ TRUNK => undef,
+ BRANCH => undef,
+);
+
+# List of branch create revision prefix option
+my %branch_revflag = (
+ NORMAL => undef,
+ NUMBER => undef,
+ NONE => undef,
+);
+
+# List of branch info/delete options
+my %branch_info_opt = (
+ S_CHD => 'Show children',
+ S_SIB => 'Show siblings',
+ S_OTH => 'Show other',
+ VERBOSE => 'Print extra information',
+);
+
+# List of diff display options
+my %diff_display_opt = (
+ default => 'Default mode',
+ graphical => 'Graphical tool',
+ trac => 'Trac (only for diff relative to the base of the branch)',
+);
+
+# Text in the status bar
+my $statustext = '';
+
+# ------------------------------------------------------------------------------
+
+my $mw = MainWindow->new ();
+
+my $mw_title = 'FCM GUI';
+$mw->title ($mw_title);
+
+# Frame containing subcommand selection buttons
+my $top_f = $mw->Frame ()->grid (
+ '-row' => 0,
+ '-column' => 0,
+ '-sticky' => 'w',
+);
+
+# Frame containing subcommand options
+my $mid_f = $mw->Frame ()->grid (
+ '-row' => 1,
+ '-column' => 0,
+ '-sticky' => 'ew',
+);
+
+# Frame containing action buttons
+my $bot_f = $mw->Frame ()->grid (
+ '-row' => 2,
+ '-column' => 0,
+ '-sticky' => 'ew',
+);
+
+# Text box to display output
+my $out_t = $mw->Scrolled ('ROText', '-scrollbars' => 'osow')->grid (
+ '-row' => 3,
+ '-column' => 0,
+ '-sticky' => 'news',
+);
+
+# Text box - allow scroll with mouse wheel
+$out_t->bind (
+ '<4>' => sub {
+ $_[0]->yview ('scroll', -1, 'units') unless $Tk::strictMotif;
+ },
+);
+
+$out_t->bind (
+ '<5>' => sub {
+ $_[0]->yview ('scroll', +1, 'units') unless $Tk::strictMotif;
+ },
+);
+
+# Status bar
+$mw->Label (
+ '-textvariable' => \$statustext,
+ '-relief' => 'groove',
+)->grid (
+ '-row' => 4,
+ '-column' => 0,
+ '-sticky' => 'ews',
+);
+
+# Main window grid configure
+{
+ my ($cols, $rows) = $mw->gridSize ();
+ $mw->gridColumnconfigure ($_, '-weight' => 1) for (0 .. $cols - 1);
+ $mw->gridRowconfigure ( 3, '-weight' => 1);
+}
+
+# Frame grid configure
+{
+ my ($cols, $rows) = $mid_f->gridSize ();
+ $bot_f->gridColumnconfigure (3, '-weight' => 1);
+}
+
+$mid_f->gridRowconfigure (0, '-weight' => 1);
+$mid_f->gridColumnconfigure (0, '-weight' => 1);
+
+# ------------------------------------------------------------------------------
+
+# Buttons to select subcommands
+{
+ my $col = 0;
+ for my $name (@subcmds) {
+ $subcmd_b{$name} = $top_f->Button (
+ '-text' => uc (substr ($name, 0, 1)) . lc (substr ($name, 1)),
+ '-command' => [\&button_clicked, $name],
+ '-width' => 8,
+ )->grid (
+ '-row' => 0,
+ '-column' => $col++,
+ '-sticky' => 'w',
+ );
+
+ $subcmd_b{$name}->bind ('<Enter>', sub {$statustext = $subcmd_help{$name}});
+ $subcmd_b{$name}->bind ('<Leave>', sub {$statustext = ''});
+
+ $subcmd_b{$name}->configure ('-underline' => $subcmd_bind{$name}{U})
+ if defined $subcmd_bind{$name}{U};
+
+ $mw->bind ($subcmd_bind{$name}{KEY}, sub {$subcmd_b{$name}->invoke});
+ }
+}
+
+# ------------------------------------------------------------------------------
+
+# Frames to contain subcommands options
+{
+ my %row = ();
+
+ for my $name (@subcmds) {
+ $subcmd_f{$name} = $mid_f->Frame ();
+ $subcmd_f{$name}->gridColumnconfigure (1, '-weight' => 1);
+
+ $row{$name} = 0;
+
+ # Widgets common to all sub-commands
+ $subcmd_f{$name}->Label ('-text' => 'Current working directory: ')->grid (
+ '-row' => $row{$name},
+ '-column' => 0,
+ '-sticky' => 'w',
+ );
+ $subcmd_f{$name}->Label ('-textvariable' => \($subcmdvar{CWD}))->grid (
+ '-row' => $row{$name}++,
+ '-column' => 1,
+ '-sticky' => 'w',
+ );
+ }
+
+ # Widgets common to all sub-commands that apply to working copies
+ for my $name (@wco_subcmds) {
+ my @labtxts = (
+ 'Corresponding URL: ',
+ 'Working copy top: ',
+ 'Corresponding URL: ',
+ );
+ my @varrefs = \(
+ $subcmdvar{URL_CWD},
+ $subcmdvar{WCT},
+ $subcmdvar{URL_WCT},
+ );
+
+ for my $i (0 .. $#varrefs) {
+ $subcmd_f{$name}->Label ('-text' => $labtxts[$i])->grid (
+ '-row' => $row{$name},
+ '-column' => 0,
+ '-sticky' => 'w',
+ );
+ $subcmd_f{$name}->Label ('-textvariable' => $varrefs[$i])->grid (
+ '-row' => $row{$name}++,
+ '-column' => 1,
+ '-sticky' => 'w',
+ );
+ }
+
+ $subcmd_f{$name}->Checkbutton (
+ '-text' => 'Apply sub-command to working copy top',
+ '-variable' => \($subcmdvar{$name}{USEWCT}),
+ '-state' => (grep ({$_ eq $name} @wcto_subcmds) ? 'disabled' : 'normal'),
+ )->grid (
+ '-row' => $row{$name}++,
+ '-column' => 0,
+ '-columnspan' => 2,
+ '-sticky' => 'w',
+ );
+ }
+
+ # Widget for the Branch sub-command
+ {
+ my $name = 'BRANCH';
+
+ # Radio buttons to select the sub-option of the branch sub-command
+ my $opt_f = $subcmd_f{$name}->Frame ()->grid (
+ '-row' => $row{$name}++,
+ '-column' => 0,
+ '-columnspan' => 2,
+ '-sticky' => 'w',
+ );
+
+ my $col = 0;
+ for my $key (sort keys %branch_opt) {
+ my $opt = lc $key;
+
+ $branch_opt{$key} = $opt_f->Radiobutton (
+ '-text' => $opt,
+ '-value' => $opt,
+ '-variable' => \($subcmdvar{$name}{OPT}),
+ '-state' => 'normal',
+ )->grid (
+ '-row' => 0,
+ '-column' => $col++,
+ '-sticky' => 'w',
+ );
+ }
+
+ # Label and entry box for specifying URL
+ $subcmd_f{$name}->Label ('-text' => 'URL: ')->grid (
+ '-row' => $row{$name},
+ '-column' => 0,
+ '-sticky' => 'w',
+ );
+ $subcmd_f{$name}->Entry (
+ '-textvariable' => \($subcmdvar{$name}{URL}),
+ )->grid (
+ '-row' => $row{$name}++,
+ '-column' => 1,
+ '-sticky' => 'ew',
+ );
+
+ # Label and entry box for specifying create branch name
+ $subcmd_f{$name}->Label (
+ '-text' => 'Branch name (create only): ',
+ )->grid (
+ '-row' => $row{$name},
+ '-column' => 0,
+ '-sticky' => 'w',
+ );
+ $subcmd_f{$name}->Entry (
+ '-textvariable' => \($subcmdvar{$name}{NAME}),
+ )->grid (
+ '-row' => $row{$name}++,
+ '-column' => 1,
+ '-sticky' => 'ew',
+ );
+
+ # Label and radio buttons box for specifying create branch type
+ $subcmd_f{$name}->Label (
+ '-text' => 'Branch type (create only): ',
+ )->grid (
+ '-row' => $row{$name},
+ '-column' => 0,
+ '-sticky' => 'w',
+ );
+
+ {
+ my $opt_f = $subcmd_f{$name}->Frame ()->grid (
+ '-row' => $row{$name}++,
+ '-column' => 1,
+ '-sticky' => 'w',
+ );
+
+ my $col = 0;
+ for my $key (sort keys %branch_type) {
+ my $txt = lc $key;
+ my $opt = $key;
+
+ $branch_opt{$key} = $opt_f->Radiobutton (
+ '-text' => $txt,
+ '-value' => $opt,
+ '-variable' => \($subcmdvar{$name}{TYPE}),
+ '-state' => 'normal',
+ )->grid (
+ '-row' => 0,
+ '-column' => $col++,
+ '-sticky' => 'w',
+ );
+ }
+ }
+
+ # Label and radio buttons box for specifying create source type
+ $subcmd_f{$name}->Label (
+ '-text' => 'Source type (create only): ',
+ )->grid (
+ '-row' => $row{$name},
+ '-column' => 0,
+ '-sticky' => 'w',
+ );
+
+ {
+ my $opt_f = $subcmd_f{$name}->Frame ()->grid (
+ '-row' => $row{$name}++,
+ '-column' => 1,
+ '-sticky' => 'w',
+ );
+
+ my $col = 0;
+ for my $key (sort keys %branch_srctype) {
+ my $txt = lc $key;
+ my $opt = lc $key;
+
+ $branch_opt{$key} = $opt_f->Radiobutton (
+ '-text' => $txt,
+ '-value' => $opt,
+ '-variable' => \($subcmdvar{$name}{SRCTYPE}),
+ '-state' => 'normal',
+ )->grid (
+ '-row' => 0,
+ '-column' => $col++,
+ '-sticky' => 'w',
+ );
+ }
+ }
+
+ # Label and radio buttons box for specifying create prefix option
+ $subcmd_f{$name}->Label (
+ '-text' => 'Prefix option (create only): ',
+ )->grid (
+ '-row' => $row{$name},
+ '-column' => 0,
+ '-sticky' => 'w',
+ );
+
+ {
+ my $opt_f = $subcmd_f{$name}->Frame ()->grid (
+ '-row' => $row{$name}++,
+ '-column' => 1,
+ '-sticky' => 'w',
+ );
+
+ my $col = 0;
+ for my $key (sort keys %branch_revflag) {
+ my $txt = lc $key;
+ my $opt = $key;
+
+ $branch_opt{$key} = $opt_f->Radiobutton (
+ '-text' => $txt,
+ '-value' => $opt,
+ '-variable' => \($subcmdvar{$name}{REVFLAG}),
+ '-state' => 'normal',
+ )->grid (
+ '-row' => 0,
+ '-column' => $col++,
+ '-sticky' => 'w',
+ );
+ }
+ }
+
+ # Label and entry box for specifying ticket number
+ $subcmd_f{$name}->Label (
+ '-text' => 'Related Trac ticket(s) (create only): ',
+ )->grid (
+ '-row' => $row{$name},
+ '-column' => 0,
+ '-sticky' => 'w',
+ );
+ $subcmd_f{$name}->Entry (
+ '-textvariable' => \($subcmdvar{$name}{TICKET}),
+ )->grid (
+ '-row' => $row{$name}++,
+ '-column' => 1,
+ '-sticky' => 'ew',
+ );
+
+ # Check button for info/delete
+ # --show-children, --show-siblings, --show-other, --verbose
+ $subcmd_f{$name}->Label (
+ '-text' => 'Options for info/delete only: ',
+ )->grid (
+ '-row' => $row{$name},
+ '-column' => 0,
+ '-sticky' => 'w',
+ );
+
+ {
+ my $opt_f = $subcmd_f{$name}->Frame ()->grid (
+ '-row' => $row{$name}++,
+ '-column' => 1,
+ '-sticky' => 'w',
+ );
+
+ my $col = 0;
+
+ for my $key (sort keys %branch_info_opt) {
+ $opt_f->Checkbutton (
+ '-text' => $branch_info_opt{$key},
+ '-variable' => \($subcmdvar{$name}{$key}),
+ )->grid (
+ '-row' => 0,
+ '-column' => $col++,
+ '-sticky' => 'w',
+ );
+ }
+ }
+ }
+
+ # Widget for the Checkout sub-command
+ {
+ my $name = 'CHECKOUT';
+
+ # Label and entry boxes for specifying URL and revision
+ my @labtxts = (
+ 'URL: ',
+ 'Revision: ',
+ 'Path: ',
+ );
+ my @varrefs = \(
+ $subcmdvar{$name}{URL},
+ $subcmdvar{$name}{REV},
+ $subcmdvar{$name}{PATH},
+ );
+
+ for my $i (0 .. $#varrefs) {
+ $subcmd_f{$name}->Label ('-text' => $labtxts[$i])->grid (
+ '-row' => $row{$name},
+ '-column' => 0,
+ '-sticky' => 'w',
+ );
+ $subcmd_f{$name}->Entry (
+ '-textvariable' => $varrefs[$i],
+ )->grid (
+ '-row' => $row{$name}++,
+ '-column' => 1,
+ '-sticky' => 'ew',
+ );
+ }
+ }
+
+ # Widget for the Status sub-command
+ {
+ my $name = 'STATUS';
+
+ # Checkbuttons for various options
+ my @labtxts = (
+ 'Display update information',
+ 'Print extra information',
+ );
+ my @varrefs = \(
+ $subcmdvar{$name}{UPDATE},
+ $subcmdvar{$name}{VERBOSE},
+ );
+
+ for my $i (0 .. $#varrefs) {
+ $subcmd_f{$name}->Checkbutton (
+ '-text' => $labtxts[$i],
+ '-variable' => $varrefs[$i],
+ )->grid (
+ '-row' => $row{$name}++,
+ '-column' => 0,
+ '-columnspan' => 2,
+ '-sticky' => 'w',
+ );
+ }
+ }
+
+ # Widget for the Diff sub-command
+ {
+ my $name = 'DIFF';
+
+ my $entry;
+ $subcmd_f{$name}->Checkbutton (
+ '-text' => 'Show differences relative to the base of the branch',
+ '-variable' => \($subcmdvar{$name}{BRANCH}),
+ '-command' => sub {
+ $entry->configure (
+ '-state' => ($subcmdvar{$name}{BRANCH} ? 'normal' : 'disabled'),
+ );
+ },
+ )->grid (
+ '-row' => $row{$name}++,
+ '-column' => 0,
+ '-columnspan' => 2,
+ '-sticky' => 'w',
+ );
+
+ # Label and radio buttons box for specifying tool
+ $subcmd_f{$name}->Label (
+ '-text' => 'Display diff in: ',
+ )->grid (
+ '-row' => $row{$name},
+ '-column' => 0,
+ '-sticky' => 'w',
+ );
+
+ {
+ my $opt_f = $subcmd_f{$name}->Frame ()->grid (
+ '-row' => $row{$name}++,
+ '-column' => 1,
+ '-sticky' => 'w',
+ );
+
+ my $col = 0;
+ for my $key (qw/default graphical trac/) {
+ my $txt = $diff_display_opt{$key};
+ my $opt = $key;
+
+ $branch_opt{$key} = $opt_f->Radiobutton (
+ '-text' => $txt,
+ '-value' => $opt,
+ '-variable' => \($subcmdvar{$name}{TOOL}),
+ '-state' => 'normal',
+ )->grid (
+ '-row' => 0,
+ '-column' => $col++,
+ '-sticky' => 'w',
+ );
+ }
+ }
+
+ $subcmd_f{$name}->Label ('-text' => 'Branch URL')->grid (
+ '-row' => $row{$name},
+ '-column' => 0,
+ '-sticky' => 'w',
+ );
+
+ $entry = $subcmd_f{$name}->Entry (
+ '-textvariable' => \($subcmdvar{$name}{URL}),
+ '-state' => ($subcmdvar{$name}{BRANCH} ? 'normal' : 'disabled'),
+ )->grid (
+ '-row' => $row{$name}++,
+ '-column' => 1,
+ '-sticky' => 'ew',
+ );
+ }
+
+ # Widget for the Add/Delete sub-command
+ for my $name (qw/ADD DELETE/) {
+
+ # Checkbuttons for various options
+ $subcmd_f{$name}->Checkbutton (
+ '-text' => 'Check for files or directories not under version control',
+ '-variable' => \($subcmdvar{$name}{CHECK}),
+ )->grid (
+ '-row' => $row{$name}++,
+ '-column' => 0,
+ '-columnspan' => 2,
+ '-sticky' => 'w',
+ );
+ }
+
+ # Widget for the Merge sub-command
+ {
+ my $name = 'MERGE';
+
+ # Label and radio buttons box for specifying merge mode
+ $subcmd_f{$name}->Label (
+ '-text' => 'Mode: ',
+ )->grid (
+ '-row' => $row{$name},
+ '-column' => 0,
+ '-sticky' => 'w',
+ );
+
+ {
+ my $opt_f = $subcmd_f{$name}->Frame ()->grid (
+ '-row' => $row{$name}++,
+ '-column' => 1,
+ '-sticky' => 'w',
+ );
+
+ my $col = 0;
+ for my $key (qw/automatic custom reverse/) {
+ my $txt = lc $key;
+ my $opt = $key;
+
+ $branch_opt{$key} = $opt_f->Radiobutton (
+ '-text' => $txt,
+ '-value' => $opt,
+ '-variable' => \($subcmdvar{$name}{MODE}),
+ '-state' => 'normal',
+ )->grid (
+ '-row' => 0,
+ '-column' => $col++,
+ '-sticky' => 'w',
+ );
+ }
+ }
+
+ # Check buttons for dry-run
+ $subcmd_f{$name}->Checkbutton (
+ '-text' => 'Dry run',
+ '-variable' => \($subcmdvar{$name}{DRYRUN}),
+ )->grid (
+ '-row' => $row{$name}++,
+ '-column' => 0,
+ '-columnspan' => 2,
+ '-sticky' => 'w',
+ );
+
+ # Check buttons for verbose mode
+ $subcmd_f{$name}->Checkbutton (
+ '-text' => 'Print extra information',
+ '-variable' => \($subcmdvar{$name}{VERBOSE}),
+ )->grid (
+ '-row' => $row{$name}++,
+ '-column' => 0,
+ '-columnspan' => 2,
+ '-sticky' => 'w',
+ );
+
+ # Label and entry boxes for specifying merge source
+ $subcmd_f{$name}->Label (
+ '-text' => 'Source (automatic/custom only): ',
+ )->grid (
+ '-row' => $row{$name},
+ '-column' => 0,
+ '-sticky' => 'w',
+ );
+ $subcmd_f{$name}->Entry (
+ '-textvariable' => \($subcmdvar{$name}{SRC}),
+ )->grid (
+ '-row' => $row{$name}++,
+ '-column' => 1,
+ '-sticky' => 'ew',
+ );
+
+ # Label and entry boxes for specifying merge revision (range)
+ $subcmd_f{$name}->Label (
+ '-text' => 'Revision (custom/reverse only): ',
+ )->grid (
+ '-row' => $row{$name},
+ '-column' => 0,
+ '-sticky' => 'w',
+ );
+ $subcmd_f{$name}->Entry (
+ '-textvariable' => \($subcmdvar{$name}{REV}),
+ )->grid (
+ '-row' => $row{$name}++,
+ '-column' => 1,
+ '-sticky' => 'ew',
+ );
+ }
+
+ # Widget for the Commit sub-command
+ {
+ my $name = 'COMMIT';
+
+ # Checkbuttons for various options
+ $subcmd_f{$name}->Checkbutton (
+ '-text' => 'Dry run',
+ '-variable' => \($subcmdvar{$name}{DRYRUN}),
+ )->grid (
+ '-row' => $row{$name}++,
+ '-column' => 0,
+ '-columnspan' => 2,
+ '-sticky' => 'w',
+ );
+ }
+
+ # Widget for the Switch sub-command
+ {
+ my $name = 'SWITCH';
+
+ # Label and entry boxes for specifying switch URL
+ $subcmd_f{$name}->Label ('-text' => 'URL: ')->grid (
+ '-row' => $row{$name},
+ '-column' => 0,
+ '-sticky' => 'w',
+ );
+ $subcmd_f{$name}->Entry (
+ '-textvariable' => \($subcmdvar{$name}{URL}),
+ )->grid (
+ '-row' => $row{$name}++,
+ '-column' => 1,
+ '-sticky' => 'ew',
+ );
+ }
+
+ # Widgets common to all sub-commands
+ for my $name (@subcmds) {
+ $subcmd_f{$name}->Label ('-text' => 'Other options: ')->grid (
+ '-row' => $row{$name},
+ '-column' => 0,
+ '-sticky' => 'w',
+ );
+ $subcmd_f{$name}->Entry (
+ '-textvariable' => \($subcmdvar{$name}{OTHER}),
+ )->grid (
+ '-row' => $row{$name}++,
+ '-column' => 1,
+ '-sticky' => 'ew',
+ );
+ }
+}
+
+# ------------------------------------------------------------------------------
+
+# Buttons to perform main actions
+{
+ my $col = 0;
+ for my $name (qw/QUIT HELP CLEAR RUN/) {
+ $action_b{$name} = $bot_f->Button (
+ '-text' => uc (substr ($name, 0, 1)) . lc (substr ($name, 1)),
+ '-command' => [\&button_clicked, $name],
+ '-width' => 8,
+ )->grid (
+ '-row' => 0,
+ '-column' => $col++,
+ '-sticky' => ($name eq 'RUN' ? 'ew' : 'w'),
+ );
+
+ $action_b{$name}->bind ('<Enter>', sub {$statustext = $action_help{$name}});
+ $action_b{$name}->bind ('<Leave>', sub {$statustext = ''});
+
+ $action_b{$name}->configure ('-underline' => $action_bind{$name}{U})
+ if defined $action_bind{$name}{U};
+
+ $mw->bind ($action_bind{$name}{KEY}, sub {$action_b{$name}->invoke});
+ }
+}
+
+&change_cwd ($subcmdvar{CWD});
+
+# ------------------------------------------------------------------------------
+
+# Handle the situation when the user attempts to quit the window while a
+# sub-command is running
+
+$mw->protocol ('WM_DELETE_WINDOW', sub {
+ if (defined $cmdpid) {
+ my $ans = $mw->messageBox (
+ '-title' => $mw_title,
+ '-message' => $selsubcmd . ' is still running. Really quit?',
+ '-type' => 'YesNo',
+ '-default' => 'No',
+ );
+
+ if ($ans eq 'Yes') {
+ kill 9, $cmdpid; # Need to kill the sub-process before quitting
+
+ } else {
+ return; # Do not quit
+ }
+ }
+
+ exit;
+});
+
+MainLoop;
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# &change_cwd ($dir);
+#
+# DESCRIPTION
+# Change current working directory to $dir
+# ------------------------------------------------------------------------------
+
+sub change_cwd {
+ my $dir = $_[0];
+ my @allowed_subcmds = (&is_wc ($dir) ? @wc_subcmds : @nwc_subcmds);
+
+ for my $subcmd (@subcmds) {
+ if (grep {$_ eq $subcmd} @allowed_subcmds) {
+ $subcmd_b{$subcmd}->configure ('-state' => 'normal');
+
+ } else {
+ $subcmd_b{$subcmd}->configure ('-state' => 'disabled');
+ }
+ }
+
+ &display_subcmd_frame ($allowed_subcmds[0])
+ if not grep {$_ eq $selsubcmd} @allowed_subcmds;
+
+ chdir $dir;
+ $subcmdvar{CWD} = $dir;
+
+ if (&is_wc ($dir)) {
+ $subcmdvar{WCT} = &get_wct ($dir);
+ $subcmdvar{URL_CWD} = &get_url_of_wc ($dir);
+ $subcmdvar{URL_WCT} = &get_url_of_wc ($subcmdvar{WCT});
+
+ $branch_opt{INFO} ->configure ('-state' => 'normal');
+ $branch_opt{DELETE}->configure ('-state' => 'normal');
+ $subcmdvar{BRANCH}{OPT} = 'info';
+
+ } else {
+ $branch_opt{INFO} ->configure ('-state' => 'disabled');
+ $branch_opt{DELETE}->configure ('-state' => 'disabled');
+ $subcmdvar{BRANCH}{OPT} = 'create';
+ }
+
+ return;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# &button_clicked ($name);
+#
+# DESCRIPTION
+# Call back function to handle a click on a command button named $name.
+# ------------------------------------------------------------------------------
+
+sub button_clicked {
+ my $name = $_[0];
+
+ if (grep {$_ eq $name} keys %subcmd_b) {
+ &display_subcmd_frame ($name);
+
+ } elsif ($name eq 'CLEAR') {
+ $out_t->delete ('1.0', 'end');
+
+ } elsif ($name eq 'QUIT') {
+ exit;
+
+ } elsif ($name eq 'HELP') {
+ &invoke_cmd ('help ' . lc ($selsubcmd));
+
+ } elsif ($name eq 'RUN') {
+ &invoke_cmd (&setup_cmd ($selsubcmd));
+
+ } else {
+ $out_t->insert ('end', $name . ': function to be implemented' . "\n");
+ $out_t->yviewMoveto (1);
+ }
+
+ return;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# &display_subcmd_frame ($name);
+#
+# DESCRIPTION
+# Change selected subcommand to $name, and display the frame containing the
+# widgets for configuring the options and arguments of that subcommand.
+# ------------------------------------------------------------------------------
+
+sub display_subcmd_frame {
+ my $name = $_[0];
+
+ if ($selsubcmd ne $name and not $cmdrunning) {
+ $subcmd_b{$name }->configure ('-relief' => 'sunken');
+ $subcmd_b{$selsubcmd}->configure ('-relief' => 'raised') if $selsubcmd;
+
+ $subcmd_f{$name }->grid ('-sticky' => 'new');
+ $subcmd_f{$selsubcmd}->gridForget if $selsubcmd;
+
+ $selsubcmd = $name;
+ }
+
+ return;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $pos = &get_wm_pos ();
+#
+# DESCRIPTION
+# Returns the position part of the geometry string of the main window.
+# ------------------------------------------------------------------------------
+
+sub get_wm_pos {
+ my $geometry = $mw->geometry ();
+ $geometry =~ /^=?(?:\d+x\d+)?([+-]\d+[+-]\d+)$/;
+ return $1;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $command = &setup_cmd ($name);
+#
+# DESCRIPTION
+# Setup the system command for the sub-command $name.
+# ------------------------------------------------------------------------------
+
+sub setup_cmd {
+ my $name = $_[0];
+ my $cmd = '';
+
+ if ($name eq 'BRANCH') {
+ if ($subcmdvar{$name}{OPT} eq 'create') {
+ $cmd .= 'branch-create';
+ $cmd .= ' --svn-non-interactive';
+ $cmd .= ' -t ' . $subcmdvar{$name}{TYPE};
+ $cmd .= ' --rev-flag ' . $subcmdvar{$name}{REVFLAG};
+ $cmd .= ' -k ' . $subcmdvar{$name}{TICKET} if $subcmdvar{$name}{TICKET};
+ $cmd .= ' --branch-of-branch ' if $subcmdvar{$name}{SRCTYPE} eq 'branch';
+ $cmd .= ' ' . $subcmdvar{$name}{NAME};
+
+ } elsif ($subcmdvar{$name}{OPT} eq 'delete') {
+ $cmd .= 'branch-delete';
+ $cmd .= ' -v' if $subcmdvar{$name}{VERBOSE};
+ $cmd .= ' --svn-non-interactive';
+
+ } elsif ($subcmdvar{$name}{OPT} eq 'list') {
+ $cmd .= 'branch-list';
+
+ } else {
+ $cmd .= 'branch-info';
+ $cmd .= ' --show-children' if $subcmdvar{$name}{S_CHD};
+ $cmd .= ' --show-siblings' if $subcmdvar{$name}{S_SIB};
+ $cmd .= ' --show-other' if $subcmdvar{$name}{S_OTH};
+ $cmd .= ' -v' if $subcmdvar{$name}{VERBOSE};
+ }
+ $cmd .= ' ' . $subcmdvar{$name}{URL} if $subcmdvar{$name}{URL};
+ $cmd .= ' ' . $subcmdvar{$name}{OTHER} if $subcmdvar{$name}{OTHER};
+
+ } elsif ($name eq 'CHECKOUT') {
+ $cmd .= lc ($name);
+ $cmd .= ' -r' . $subcmdvar{$name}{REV} if $subcmdvar{$name}{REV};
+ $cmd .= ' ' . $subcmdvar{$name}{OTHER} if $subcmdvar{$name}{OTHER};
+ $cmd .= ' ' . $subcmdvar{$name}{URL};
+ $cmd .= ' ' . $subcmdvar{$name}{PATH} if $subcmdvar{$name}{PATH};
+
+ } elsif ($name eq 'STATUS') {
+ $cmd .= lc ($name);
+ $cmd .= ' -u' if $subcmdvar{$name}{UPDATE};
+ $cmd .= ' -v' if $subcmdvar{$name}{VERBOSE};
+ $cmd .= ' ' . $subcmdvar{$name}{OTHER} if $subcmdvar{$name}{OTHER};
+
+ } elsif ($name eq 'DIFF') {
+ if ($subcmdvar{$name}{BRANCH}) {
+ $cmd .= 'branch-diff';
+ $cmd .= ' -t' if $subcmdvar{$name}{TOOL} eq 'trac';
+ $cmd .= ' ' . $subcmdvar{$name}{URL} if $subcmdvar{$name}{URL};
+ }
+ else {
+ $cmd .= 'diff';
+ }
+
+ $cmd .= ' -g' if $subcmdvar{$name}{TOOL} eq 'graphical';
+ $cmd .= ' ' . $subcmdvar{$name}{OTHER} if $subcmdvar{$name}{OTHER};
+
+ } elsif ($name eq 'ADD' or $name eq 'DELETE') {
+ $cmd .= lc ($name);
+ $cmd .= ' -c' if $subcmdvar{$name}{CHECK};
+ $cmd .= ' --non-interactive'
+ if $name eq 'DELETE' and not $subcmdvar{$name}{CHECK};
+ $cmd .= ' ' . $subcmdvar{$name}{OTHER} if $subcmdvar{$name}{OTHER};
+
+ } elsif ($name eq 'MERGE') {
+ $cmd .= lc ($name);
+
+ if ($subcmdvar{$name}{MODE} ne 'automatic') {
+ $cmd .= ' --' . $subcmdvar{$name}{MODE};
+ $cmd .= ' --revision ' . $subcmdvar{$name}{REV} if $subcmdvar{$name}{REV};
+ }
+
+ $cmd .= ' --dry-run' if $subcmdvar{$name}{DRYRUN};
+ $cmd .= ' -v' if $subcmdvar{$name}{VERBOSE};
+ $cmd .= ' ' . $subcmdvar{$name}{SRC} if $subcmdvar{$name}{SRC};
+ $cmd .= ' ' . $subcmdvar{$name}{OTHER} if $subcmdvar{$name}{OTHER};
+
+ } elsif ($name eq 'CONFLICTS') {
+ $cmd .= lc ($name);
+ $cmd .= ' ' . $subcmdvar{$name}{OTHER} if $subcmdvar{$name}{OTHER};
+
+ } elsif ($name eq 'COMMIT') {
+ $cmd .= lc ($name);
+ $cmd .= ' --dry-run' if $subcmdvar{$name}{DRYRUN};
+ $cmd .= ' --svn-non-interactive';
+ $cmd .= ' ' . $subcmdvar{$name}{OTHER} if $subcmdvar{$name}{OTHER};
+
+ } elsif ($name eq 'SWITCH') {
+ $cmd .= lc ($name);
+ $cmd .= ' ' . $subcmdvar{$name}{URL} if $subcmdvar{$name}{URL};
+ $cmd .= ' ' . $subcmdvar{$name}{OTHER} if $subcmdvar{$name}{OTHER};
+
+ } elsif ($name eq 'UPDATE') {
+ $cmd .= lc ($name);
+ $cmd .= ' ' . $subcmdvar{$name}{OTHER} if $subcmdvar{$name}{OTHER};
+
+ }
+
+ return $cmd;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# &invoke_cmd ($cmd);
+#
+# DESCRIPTION
+# Invoke the command $cmd.
+# ------------------------------------------------------------------------------
+
+sub invoke_cmd {
+ my $cmd = $_[0];
+ return unless $cmd;
+
+ my $disp_cmd = 'fcm ' . $cmd;
+ $cmd = (index ($cmd, 'help ') == 0)
+ ? $disp_cmd
+ : ('fcm gui-internal ' . &get_wm_pos () . ' ' . $cmd);
+
+ # Change directory to working copy top if necessary
+ if ($subcmdvar{$selsubcmd}{USEWCT} and $subcmdvar{WCT} ne $subcmdvar{CWD}) {
+ chdir $subcmdvar{WCT};
+ $out_t->insert ('end', 'cd ' . $subcmdvar{WCT} . "\n");
+ $out_t->yviewMoveto (1);
+ }
+
+ # Report start of command
+ $out_t->insert ('end', timestamp_command ($disp_cmd, 'Start'));
+ $out_t->yviewMoveto (1);
+
+ # Open the command as a pipe
+ if ($cmdpid = open CMD, '-|', $cmd . ' 2>&1') {
+ # Disable all action buttons
+ $action_b{$_}->configure ('-state' => 'disabled') for (keys %action_b);
+ $cmdrunning = 1;
+
+ # Set up a file event to read output from the command
+ $mw->fileevent (\*CMD, readable => sub {
+ if (sysread CMD, my ($buf), 1024) {
+ # Insert text into the output text box as it becomes available
+ $out_t->insert ('end', $buf);
+ $out_t->yviewMoveto (1);
+
+ } else {
+ # Delete the file event and close the file when the command finishes
+ $mw->fileevent(\*CMD, readable => '');
+ close CMD;
+ $cmdpid = undef;
+
+ # Check return status
+ if ($?) {
+ $out_t->insert (
+ 'end', '"' . $disp_cmd . '" failed (' . $? . ')' . "\n",
+ );
+ $out_t->yviewMoveto (1);
+ }
+
+ # Report end of command
+ $out_t->insert ('end', timestamp_command ($disp_cmd, 'End'));
+ $out_t->yviewMoveto (1);
+
+ # Change back to CWD if necessary
+ if ($subcmdvar{$selsubcmd}{USEWCT} and
+ $subcmdvar{WCT} ne $subcmdvar{CWD}) {
+ chdir $subcmdvar{CWD};
+ $out_t->insert ('end', 'cd ' . $subcmdvar{CWD} . "\n");
+ $out_t->yviewMoveto (1);
+ }
+
+ # Enable all action buttons again
+ $action_b{$_}->configure ('-state' => 'normal') for (keys %action_b);
+ $cmdrunning = 0;
+
+ # If the command is "checkout", change directory to working copy
+ if (lc ($selsubcmd) eq 'checkout' && $subcmdvar{CHECKOUT}{URL}) {
+ my $url = FCM1::Keyword::expand($subcmdvar{CHECKOUT}{URL});
+ my $dir = $subcmdvar{CHECKOUT}{PATH}
+ ? $subcmdvar{CHECKOUT}{PATH}
+ : basename($url);
+ $dir = rel2abs($dir);
+ &change_cwd ($dir);
+
+ # If the command is "switch", change URL
+ } elsif (lc ($selsubcmd) eq 'switch') {
+ $subcmdvar{URL_CWD} = &get_url_of_wc ($subcmdvar{CWD}, 1);
+ $subcmdvar{URL_WCT} = &get_url_of_wc ($subcmdvar{WCT}, 1);
+ }
+ }
+ 1;
+ });
+
+ } else {
+ $mw->messageBox (
+ '-title' => 'Error',
+ '-message' => 'Error running "' . $cmd . '"',
+ '-icon' => 'error',
+ );
+ }
+
+ return;
+}
+
+# ------------------------------------------------------------------------------
+
+__END__
+
+=head1 NAME
+
+fcm_gui
+
+=head1 SYNOPSIS
+
+fcm_gui [DIR]
+
+=head1 DESCRIPTION
+
+The fcm_gui command is a simple graphical user interface for some of the
+commands of the FCM system. The optional argument DIR modifies the initial
+working directory.
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/bin/fcm_internal b/bin/fcm_internal
new file mode 100755
index 0000000..809a19e
--- /dev/null
+++ b/bin/fcm_internal
@@ -0,0 +1,629 @@
+#!/usr/bin/env perl
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+
+use strict;
+use warnings;
+
+use FCM1::Timer qw{timestamp_command};
+
+# Function declarations
+sub catfile;
+sub basename;
+sub dirname;
+
+# ------------------------------------------------------------------------------
+
+# Module level variables
+my %unusual_tool_name = ();
+
+# ------------------------------------------------------------------------------
+
+MAIN: {
+ # Name of program
+ my $this = basename $0;
+
+ # Arguments
+ my $subcommand = shift @ARGV;
+ my ($function, $type) = split /:/, $subcommand;
+
+ my ($srcpackage, $src, $target, $requirepp, @objects, @blockdata);
+
+ if ($function eq 'archive') {
+ ($target, @objects) = @ARGV;
+
+ } elsif ($function eq 'load') {
+ ($srcpackage, $src, $target, @blockdata) = @ARGV;
+
+ } else {
+ ($srcpackage, $src, $target, $requirepp) = @ARGV;
+ }
+
+ # Set up hash reference for all the required information
+ my %info = (
+ SRCPACKAGE => $srcpackage,
+ SRC => $src,
+ TYPE => $type,
+ TARGET => $target,
+ REQUIREPP => $requirepp,
+ OBJECTS => \@objects,
+ BLOCKDATA => \@blockdata,
+ );
+
+ # Get list of unusual tools
+ my $i = 0;
+ while (my $label = &get_env ('FCM_UNUSUAL_TOOL_LABEL' . $i)) {
+ my $value = &get_env ('FCM_UNUSUAL_TOOL_VALUE' . $i);
+ $unusual_tool_name{$label} = $value;
+ $i++;
+ }
+
+ # Invoke the action
+ my $rc = 0;
+ if ($function eq 'compile') {
+ $rc = &compile (\%info);
+
+ } elsif ($function eq 'load') {
+ $rc = &load (\%info);
+
+ } elsif ($function eq 'archive') {
+ $rc = &archive (\%info);
+
+ } else {
+ print STDERR $this, ': incorrect usage, abort';
+ $rc = 1;
+ }
+
+ # Throw error if action failed
+ if ($rc) {
+ print STDERR $this, ' ', $function, ' failed (', $rc, ')', "\n";
+ exit 1;
+
+ } else {
+ exit;
+ }
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = &compile (\%info);
+#
+# DESCRIPTION
+# This method invokes the correct compiler with the correct options to
+# compile the source file into the required target. The argument $info is a
+# hash reference set up in MAIN. The following environment variables are
+# used, where * is the source file type (F for Fortran, and C for C/C++):
+#
+# *C - compiler command
+# *C_OUTPUT - *C option to specify the name of the output file
+# *C_DEFINE - *C option to declare a pre-processor def
+# *C_INCLUDE - *C option to declare an include directory
+# *C_MODSEARCH- *C option to declare a module search directory
+# *C_COMPILE - *C option to ask the compiler to perform compile only
+# *CFLAGS - *C user options
+# *PPKEYS - list of pre-processor defs (may have sub-package suffix)
+# FCM_VERBOSE - verbose level
+# FCM_OBJDIR - destination directory of object file
+# FCM_TMPDIR - temporary destination directory of object file
+# ------------------------------------------------------------------------------
+
+sub compile {
+ my $info = shift;
+
+ # Verbose mode
+ my $verbose = &get_env ('FCM_VERBOSE');
+ $verbose = 1 unless defined ($verbose);
+
+ my @command = ();
+
+ # Guess file type for backward compatibility
+ my $type = $info->{TYPE} ? $info->{TYPE} : &guess_file_type ($info->{SRC});
+
+ # Compiler
+ push @command, &get_env ($type . 'C', 1);
+
+ # Compile output target (typical -o option)
+ push @command, &get_env ($type . 'C_OUTPUT', 1), $info->{TARGET};
+
+ # Pre-processor definition macros
+ if ($info->{REQUIREPP}) {
+ my @ppkeys = split /\s+/, &select_flags ($info, $type . 'PPKEYS');
+ my $defopt = &get_env ($type . 'C_DEFINE', 1);
+
+ push @command, (map {$defopt . $_} @ppkeys);
+ }
+
+ # Include search path
+ my $incopt = &get_env ($type . 'C_INCLUDE', 1);
+ my @incpath = split /:/, &get_env ('FCM_INCPATH');
+ push @command, (map {$incopt . $_} @incpath);
+
+ # Compiled module search path
+ my $modopt = &get_env ($type . 'C_MODSEARCH');
+ if ($modopt) {
+ push @command, (map {$modopt . $_} @incpath);
+ }
+
+ # Other compiler flags
+ my $flags = &select_flags ($info, $type . 'FLAGS');
+ push @command, $flags if $flags;
+
+ my $compile_only = &get_env ($type . 'C_COMPILE');
+ if ($flags !~ /(?:^|\s)$compile_only\b/) {
+ push @command, &get_env ($type . 'C_COMPILE');
+ }
+
+ # Name of source file
+ push @command, $info->{SRC};
+
+ # Execute command
+ my $objdir = &get_env ('FCM_OBJDIR', 1);
+ my $tmpdir = &get_env ('FCM_TMPDIR', 1);
+ chdir $tmpdir;
+
+ my $command = join ' ', @command;
+ if ($verbose > 1) {
+ print 'cd ', $tmpdir, "\n";
+ print ×tamp_command ($command, 'Start');
+
+ } elsif ($verbose) {
+ print $command, "\n";
+ }
+
+ my $rc = system $command;
+
+ print ×tamp_command ($command, 'End ') if $verbose > 1;
+
+ # Move temporary output to correct location on success
+ # Otherwise, remove temporary output
+ if ($rc) { # error
+ unlink $info->{TARGET};
+
+ } else { # success
+ print 'mv ', $info->{TARGET}, ' ', $objdir, "\n" if $verbose > 1;
+ rename $info->{TARGET}, &catfile ($objdir, $info->{TARGET});
+ }
+
+ # Move any Fortran module definition files to the INC directory
+ my @modfiles = <*.mod *.MOD>;
+ for my $file (@modfiles) {
+ rename $file, &catfile ($incpath[0], $file);
+ }
+
+ return $rc;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = &load (\%info);
+#
+# DESCRIPTION
+# This method invokes the correct loader with the correct options to link
+# the main program object into an executable. The argument $info is a hash
+# reference set up in MAIN. The following environment variables are used:
+#
+# LD - * linker command
+# LD_OUTPUT - LD option to specify the name of the output file
+# LD_LIBSEARCH - LD option to declare a directory in the library search path
+# LD_LIBLINK - LD option to declare an object library
+# LDFLAGS - LD user options
+# FCM_VERBOSE - verbose level
+# FCM_LIBDIR - destination directory of object libraries
+# FCM_OBJDIR - destination directory of object files
+# FCM_BINDIR - destination directory of executable file
+# FCM_TMPDIR - temporary destination directory of executable file
+#
+# * If LD is not set, it will attempt to guess the file type and use the
+# compiler as the linker.
+# ------------------------------------------------------------------------------
+
+sub load {
+ my $info = shift;
+
+ my $rc = 0;
+
+ # Verbose mode
+ my $verbose = &get_env ('FCM_VERBOSE');
+ $verbose = 1 unless defined ($verbose);
+
+ # Create temporary object library
+ (my $name = $info->{TARGET}) =~ s/\.\S+$//;
+ my $libname = '__fcm__' . $name;
+ my $lib = 'lib' . $libname . '.a';
+ my $libfile = catfile (&get_env ('FCM_LIBDIR', 1), $lib);
+ $rc = &archive ({TARGET => $lib});
+
+ unless ($rc) {
+ my @command = ();
+
+ # Linker
+ my $ld = &select_flags ($info, 'LD');
+ if (not $ld) {
+ # Guess file type for backward compatibility
+ my $type = $info->{TYPE} ? $info->{TYPE} : &guess_file_type ($info->{SRC});
+ $ld = &get_env ($type . 'C', 1);
+ }
+ push @command, $ld;
+
+ # Linker output target (typical -o option)
+ push @command, &get_env ('LD_OUTPUT', 1), $info->{TARGET};
+
+ # Name of main object file
+ my $mainobj = (basename ($info->{SRC}) eq $info->{SRC})
+ ? catfile (&get_env ('FCM_OBJDIR'), $info->{SRC})
+ : $info->{SRC};
+ push @command, $mainobj;
+
+ # Link with Fortran BLOCKDATA objects if necessary
+ if (@{ $info->{BLOCKDATA} }) {
+ my @blockdata = @{ $info->{BLOCKDATA} };
+ my @objpath = split /:/, &get_env ('FCM_OBJPATH');
+
+ # Search each BLOCKDATA object file from the object search path
+ for my $file (@blockdata) {
+ for my $dir (@objpath) {
+ my $full = catfile ($dir, $file);
+
+ if (-f $full) {
+ $file = $full;
+ last;
+ }
+ }
+
+ push @command, $file;
+ }
+ }
+
+ # Library search path
+ my $libopt = &get_env ('LD_LIBSEARCH', 1);
+ my @libpath = split /:/, &get_env ('FCM_LIBPATH');
+ push @command, (map {$libopt . $_} @libpath);
+
+ # Link with temporary object library if it exists
+ push @command, &get_env ('LD_LIBLINK', 1) . $libname if -f $libfile;
+
+ # Other linker flags
+ my $flags = &select_flags ($info, 'LDFLAGS');
+ push @command, $flags;
+
+ # Execute command
+ my $tmpdir = &get_env ('FCM_TMPDIR', 1);
+ my $bindir = &get_env ('FCM_BINDIR', 1);
+ chdir $tmpdir;
+
+ my $command = join ' ', @command;
+ if ($verbose > 1) {
+ print 'cd ', $tmpdir, "\n";
+ print ×tamp_command ($command, 'Start');
+
+ } elsif ($verbose) {
+ print $command, "\n";
+ }
+
+ $rc = system $command;
+
+ print ×tamp_command ($command, 'End ') if $verbose > 1;
+
+ # Move temporary output to correct location on success
+ # Otherwise, remove temporary output
+ if ($rc) { # error
+ unlink $info->{TARGET};
+
+ } else { # success
+ print 'mv ', $info->{TARGET}, ' ', $bindir, "\n" if $verbose > 1;
+ rename $info->{TARGET}, &catfile ($bindir, $info->{TARGET});
+ }
+ }
+
+ # Remove the temporary object library
+ unlink $libfile if -f $libfile;
+
+ return $rc;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = &archive (\%info);
+#
+# DESCRIPTION
+# This method invokes the library archiver to create an object library. The
+# argument $info is a hash reference set up in MAIN. The following
+# environment variables are used:
+#
+# AR - archiver command
+# ARFLAGS - AR options to update/create an object library
+# FCM_VERBOSE - verbose level
+# FCM_LIBDIR - destination directory of object libraries
+# FCM_OBJPATH - search path of object files
+# FCM_OBJDIR - destination directory of object files
+# FCM_TMPDIR - temporary destination directory of executable file
+# ------------------------------------------------------------------------------
+
+sub archive {
+ my $info = shift;
+
+ my $rc = 0;
+
+ # Verbose mode
+ my $verbose = &get_env ('FCM_VERBOSE');
+ $verbose = 1 unless defined ($verbose);
+
+ # Set up the archive command
+ my $lib = &basename ($info->{TARGET});
+ my $tmplib = &catfile (&get_env ('FCM_TMPDIR', 1), $lib);
+ my @ar_cmd = ();
+ push @ar_cmd, (&get_env ('AR', 1), &get_env ('ARFLAGS', 1));
+ push @ar_cmd, $tmplib;
+
+ # Get object directories and their files
+ my %objdir;
+ if (exists $info->{OBJECTS}) {
+ # List of objects set in the argument, sort into directory/file list
+ for my $name (@{ $info->{OBJECTS} }) {
+ my $dir = (&dirname ($name) eq '.')
+ ? &get_env ('FCM_OBJDIR', 1) : &dirname ($name);
+ $objdir{$dir}{&basename ($name)} = 1;
+ }
+
+ } else {
+ # Objects not listed in argument, search object path for all files
+ my @objpath = split /:/, &get_env ('FCM_OBJPATH', 1);
+ my %objbase = ();
+
+ # Get registered objects into a hash (keys = objects, values = 1)
+
+ my %objects = map {($_, 1)} split (/\s+/, &get_env ('OBJECTS', 1));
+
+ # Seach object path for all files
+ for my $dir (@objpath) {
+ next unless -d $dir;
+
+ chdir $dir;
+
+ # Use all files from each directory in the object search path
+ for ((glob ('*'))) {
+ next unless exists $objects{$_}; # consider registered objects only
+ $objdir{$dir}{$_} = 1 unless exists $objbase{$_};
+ $objbase{$_} = 1;
+ }
+ }
+ }
+
+ for my $dir (sort keys %objdir) {
+ next unless -d $dir;
+
+ # Go to each object directory and executes the library archive command
+ chdir $dir;
+ my $command = join ' ', (@ar_cmd, sort keys %{ $objdir{$dir} });
+
+ if ($verbose > 1) {
+ print 'cd ', $dir, "\n";
+ print ×tamp_command ($command, 'Start');
+
+ } else {
+ print $command, "\n" if exists $info->{OBJECTS};
+ }
+
+ $rc = system $command;
+
+ print ×tamp_command ($command, 'End ')
+ if $verbose > 1;
+ last if $rc;
+ }
+
+ # Move temporary output to correct location on success
+ # Otherwise, remove temporary output
+ if ($rc) { # error
+ unlink $tmplib;
+
+ } else { # success
+ my $libdir = &get_env ('FCM_LIBDIR', 1);
+
+ print 'mv ', $tmplib, ' ', $libdir, "\n" if $verbose > 1;
+ rename $tmplib, &catfile ($libdir, $lib);
+ }
+
+ return $rc;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $type = &guess_file_type ($filename);
+#
+# DESCRIPTION
+# This function attempts to guess the file type by looking at the extension
+# of the $filename. Only C and Fortran at the moment.
+# ------------------------------------------------------------------------------
+
+sub guess_file_type {
+ return (($_[0] =~ /\.c(\w+)?$/i) ? 'C' : 'F');
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $flags = &select_flags (\%info, $set);
+#
+# DESCRIPTION
+# This function selects the correct compiler/linker flags for the current
+# sub-package from the environment variable prefix $set. The argument $info
+# is a hash reference set up in MAIN.
+# ------------------------------------------------------------------------------
+
+sub select_flags {
+ my ($info, $set) = @_;
+
+ my $srcbase = &basename ($info->{SRC});
+ my @names = ($set);
+ push @names, split (/__/, $info->{SRCPACKAGE} . '__' . $srcbase);
+
+ my $string = '';
+ for my $i (reverse (0 .. $#names)) {
+ my $var = &get_env (join ('__', (@names[0 .. $i])));
+
+ $var = &get_env (join ('__', (@names[0 .. $i])))
+ if (not defined ($var)) and $i and $names[-1] =~ s/\.[^\.]+$//;
+
+ next unless defined $var;
+ $string = $var;
+ last;
+ }
+
+ return $string;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $variable = &get_env ($name);
+# $variable = &get_env ($name, $compulsory);
+#
+# DESCRIPTION
+# This internal method gets a variable from $ENV{$name}. If $compulsory is
+# set to true, it throws an error if the variable is a not set or is an empty
+# string. Otherwise, it returns C<undef> if the variable is not set.
+# ------------------------------------------------------------------------------
+
+sub get_env {
+ (my $name, my $compulsory) = @_;
+ my $string;
+
+ if ($name =~ /^\w+$/) {
+ # $name contains only word characters, variable is exported normally
+ die 'The environment variable "', $name, '" must be set, abort'
+ if $compulsory and not exists $ENV{$name};
+
+ $string = exists $ENV{$name} ? $ENV{$name} : undef;
+
+ } else {
+ # $name contains unusual characters
+ die 'The environment variable "', $name, '" must be set, abort'
+ if $compulsory and not exists $unusual_tool_name{$name};
+
+ $string = exists $unusual_tool_name{$name}
+ ? $unusual_tool_name{$name} : undef;
+ }
+
+ return $string;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $path = &catfile (@paths);
+#
+# DESCRIPTION
+# This is a local implementation of what is in the File::Spec module.
+# ------------------------------------------------------------------------------
+
+sub catfile {
+ my @names = split (m!/!, join ('/', @_));
+ my $path = shift @names;
+
+ for my $name (@names) {
+ $path .= '/' . $name if $name;
+ }
+
+ return $path;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $basename = &basename ($path);
+#
+# DESCRIPTION
+# This is a local implementation of what is in the File::Basename module.
+# ------------------------------------------------------------------------------
+
+sub basename {
+ my $name = $_[0];
+
+ $name =~ s{/*$}{}; # remove trailing slashes
+
+ if ($name =~ m#.*/([^/]+)$#) {
+ return $1;
+
+ } else {
+ return $name;
+ }
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $dirname = &dirname ($path);
+#
+# DESCRIPTION
+# This is a local implementation of what is in the File::Basename module.
+# ------------------------------------------------------------------------------
+
+sub dirname {
+ my $name = $_[0];
+
+ if ($name =~ m#^/+$#) {
+ return '/'; # dirname of root is root
+
+ } else {
+ $name =~ s{/*$}{}; # remove trailing slashes
+
+ if ($name =~ m#^(.*)/[^/]+$#) {
+ my $dir = $1;
+ $dir =~ s{/*$}{}; # remove trailing slashes
+ return $dir;
+
+ } else {
+ return '.';
+ }
+ }
+}
+
+# ------------------------------------------------------------------------------
+
+__END__
+
+=head1 NAME
+
+fcm_internal
+
+=head1 SYNOPSIS
+
+ fcm_internal SUBCOMMAND ARGS
+
+=head1 DESCRIPTION
+
+The fcm_internal command is a frontend for some of the internal commands of
+the FCM build system. The subcommand can be "compile", "load" or "archive"
+for invoking the compiler, loader and library archiver respectively. If
+"compile" or "load" is specified, it can be suffixed with ":TYPE" to
+specify the nature of the source file. If TYPE is not specified, it is set
+to C if the file extension begins with ".c". For all other file types, it
+is set to F (for Fortran source). For compile and load, the other arguments
+are 1) the name of the container package of the source file, 2) the path to
+the source file and 3) the target name after compiling or loading the
+source file. For compile, the 4th argument is a flag to indicate whether
+pre-processing is required for compiling the source file. For load, the
+4th and the rest of the arguments is a list of object files that cannot be
+archived into the temporary load library and must be linked into the target
+through the linker command. (E.g. Fortran BLOCKDATA program units must be
+linked this way.) If archive is specified, the first argument should be the
+name of the library archive target and the rest should be the object files
+to be included in the archive. This command is invoked via the build system
+and should never be called directly by the user.
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/bin/fcm_test_battery b/bin/fcm_test_battery
new file mode 100755
index 0000000..2ac1a6a
--- /dev/null
+++ b/bin/fcm_test_battery
@@ -0,0 +1,24 @@
+#!/usr/bin/env bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+cd $(dirname $0)/..
+if [[ "$PWD" != "$OLDPWD" ]]; then
+ echo "[INFO] cd $PWD"
+fi
+exec prove -j 9 -s -r "${@:-t}"
diff --git a/doc/collaboration/feeding-back-patch.png b/doc/collaboration/feeding-back-patch.png
new file mode 100644
index 0000000..8b05611
Binary files /dev/null and b/doc/collaboration/feeding-back-patch.png differ
diff --git a/doc/collaboration/index.html b/doc/collaboration/index.html
new file mode 100644
index 0000000..cfabe81
--- /dev/null
+++ b/doc/collaboration/index.html
@@ -0,0 +1,481 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>FCM: External Distribution & Collaboration for FCM Projects</title>
+ <meta name="author" content="FCM team" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link rel="icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link rel="shortcut icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link href="../etc/bootstrap/css/bootstrap.min.css" rel="stylesheet" media="screen" />
+ <link href="../etc/fcm.css" rel="stylesheet" media="screen" />
+</head>
+<body>
+ <div class="navbar navbar-inverse">
+ <div class="navbar-inner">
+ <a class="brand" href=".."><span class="fcm-version">FCM</span></a>
+ <ul class="nav">
+ <li><a href="../installation/">Installation</a></li>
+
+ <li><a href="../user_guide/">User Guide</a></li>
+ </ul>
+ </div>
+ </div>
+
+ <div class="page-header">
+ <div class="fcm-page-content pull-right well well-small"></div>
+ <h1>FCM: Distribution & Collaboration for FCM Projects</h1>
+ </div>
+
+ <div class="container">
+ <div class="row">
+ <div class="span12">
+
+ <h2 id="introduction">Introduction</h2>
+
+ <p>This document describes how projects configured under FCM can be
+ distributed externally. Particular attention is given to collaborative
+ distributions - where the external user regularly returns code for
+ consolidation into the central repositories which hold the master copies of
+ the code.</p>
+
+ <p><dfn>Note:</dfn> This document assumes that the repositories are
+ inaccessible to the external user, due to issues of security and
+ practicality.</p>
+
+ <h2 id="distribution">Creating a Distribution</h2>
+
+ <p>A system configured under FCM can be distributed by packaging a known
+ revision (usually corresponding to a stable release) into an archive (e.g. a
+ tarball) of directories and files. Various issues need to be considered:</p>
+
+ <ul>
+ <li>A distribution may contain a variety of different files including
+ source code, scripts, benchmark and validation tests, documentation,
+ etc.</li>
+
+ <li>A system may consist of several different <em>projects</em> which
+ should be put into separate directories in the distribution. Please refer
+ to the section <a href=
+ "../user_guide/system_admin.html#svn_design">Repository design</a> in the
+ FCM user guide for an explanation of what is meant by a project in this
+ context.</li>
+
+ <li>Some files in a project may not be included in the distribution. This
+ may be because they are of no interest to external users or because of
+ license restrictions. Such files will need to be filtered out when creating
+ the distribution.</li>
+
+ <li>The distribution may also contain some files which are not maintained
+ under FCM version control (test results for instance).</li>
+
+ <li>Some systems share code with other systems.
+
+ <ul>
+ <li>If a distribution is intended to be used standalone then the
+ necessary files from these other systems will need to be included. e.g.
+ The VAR system requires code from the OPS and GEN systems.</li>
+
+ <li>If the distribution is part of a wider collaboration then it is
+ likely that the files from the other systems will be distributed
+ separately. It is best if stable releases of the various systems can be
+ synchronised so that, for example, a VAR stable release uses code from
+ an OPS stable release which both use code from the same GEN
+ release.</li>
+ </ul>
+ </li>
+
+ <li>Release notes should be prepared to accompany a distribution which
+ explain, among other things, how the distribution is structured.</li>
+
+ <li>The distribution should contain a file which identifies the repository
+ revision(s) contained in the distribution.</li>
+
+ <li>System managers will probably wish to maintain a script which automates
+ the generation of these distributions.</li>
+ </ul>
+
+ <h2 id="feedback">Feeding Back Changes</h2>
+
+ <p>Although we would encourage all collaborators to make use of the FCM
+ system for version control, we recognise that they may already have their own
+ preferred systems in place. There is no particular problem with this. The
+ main requirement is that any proposed changes are provided as a modification
+ relative to the provided distribution. The changeset could be provided in the
+ form of a modified project tree or as a patchfile (refer to the later section
+ <a href="#patchfiles">Exchanging Changesets using Patchfiles</a> for further
+ discussion). If the change involves any renaming or removal of files or
+ directories then special instructions should be provided plus a script to
+ perform the changes.</p>
+
+ <p>At the central repository, the changeset should be applied to a branch
+ created from the repository revision which formed the basis of the changeset
+ (possibly making use of the Subversion utility <a href=
+ "http://svnbook.red-bean.com/en/1.4/svn.advanced.vendorbr.html#svn.advanced.vendorbr.svn_load_dirs">
+ svn_load_dirs.pl</a>). Note that extra care is needed with changesets
+ provided as modified project trees if there are any files in the project
+ which are excluded from the distribution. Once imported, the changeset should
+ then undergo any necessary testing or review before being merged into the
+ trunk.</p>
+
+ <h2 id="usingfcm">Collaborating Using FCM for Version Control</h2>
+
+ <p>There are a number of advantages if the FCM system is used for version
+ control by the collaborator. In particular it means that:</p>
+
+ <ul>
+ <li>Collaborators will be able to see all of the individual changesets
+ which went in to a new release rather than only being able to view each new
+ release as one big change.</li>
+
+ <li>The process of sending a proposed change to the central repository can
+ be standardised through the use of an <em>FCM patch</em> (explained
+ later).</li>
+
+ <li>The FCM Extract system can be fully utilised.</li>
+
+ <li>Common tools will help to ease communication. We will all use technical
+ terms to mean the same thing.</li>
+ </ul>
+
+ <p>This section explains the recommended way of using FCM in a
+ collaboration.</p>
+
+ <h3 id="initsvn">Initialising the Subversion Repositories</h3>
+
+ <p>The collaborator needs to set up a repository and import each of the
+ projects. Please see the section <a href=
+ "../user_guide/system_admin.html#svn_create">Creating a repository</a> in the
+ FCM user guide for advice. Collaborators may wish to use separate
+ repositories and Trac systems for each project or they may prefer to use a
+ single repository for all projects and use a single Trac system. Either
+ option should be fine so long as the same set of projects is retained.</p>
+
+ <p>After completing the initial import, the collaborator should have the
+ required set of projects available in Subversion where the initial version of
+ the trunk of each project corresponds with the initial stable release
+ provided in the distribution.</p>
+
+ <h3 id="prepchanges">Preparing Changes at the Collaborator's Site</h3>
+
+ <p>The recommended way of preparing changes is illustrated in Figure 1a:</p>
+
+ <p><strong>Figure 1a: working at the collaborator's
+ site</strong></p>
+
+ <p><img src="working-as-collaborator.png" alt=
+ "Figure 1a: working at the collaborator's site" class="img-polaroid" /></p>
+
+ <p>The collaborator will create a shared package branch from the latest
+ stable release on the trunk. This branch will contain all the changes that
+ will eventually be fed back to the central repository. Developers will also
+ create their own development branches. These may be branched from the latest
+ stable release on the trunk. Alternatively, if the change needs to build on
+ other changes then a branch can be created from the shared package branch.
+ When the changes are ready (i.e. tested, documented, reviewed, etc) then they
+ are merged into the shared package branch. The trunk is not used for the
+ shared changes as it is reserved for changes received from the central
+ repository.</p>
+
+ <p>Should it be required, a second shared package branch can be created from
+ the same point to contain any local modifications that will not be fed back
+ to the central repository. A configuration branch can then be used to combine
+ the local changes with those destined to be fed back. This is illustrated in
+ Figure 1b:</p>
+
+ <p><strong>Figure 1b: managing local changes</strong></p>
+
+ <p ><img src="managing-local-changes.png" alt=
+ "Figure 1b: managing local changes" class="img-polaroid" /></p>
+
+ <h3 id="feedbackfcm">Feeding Back Changes Using FCM</h3>
+
+ <p>Eventually, a series of changesets will exist on the first package branch.
+ These changes will be fed back to the central repository via an <em>FCM
+ patch</em>. This contains a series of differences associated with changesets
+ in a given branch of development, created by the <code>fcm mkpatch</code>
+ command. For further information about the command, please refer to its
+ <a href="../user_guide/command_ref.html#fcm-mkpatch">command
+ reference</a> in the FCM user guide.</p>
+
+ <p>At the central repository, the changeset will be applied to a branch
+ created from the repository revision which formed the basis of the changeset.
+ This is illustrated in Figure 2:</p>
+
+ <p><strong>Figure 2: feeding back changes</strong></p>
+ <p><img src="feeding-back-patch.png" alt=
+ "Figure 2: feeding back changes" class="img-polaroid" /></p>
+
+ <p>Patches will usually be exchanged in the form of a tarball. To apply the
+ patch it must first be extracted to a directory. In this directory there
+ should be a shell script called <code>fcm-import-patch</code>. A TARGET needs
+ to be specified when invoking the script. The TARGET must either be a URL or
+ a working copy of a valid project tree that can accept the import of the
+ patches. It is essential that this target matches the version of the project
+ from which the patch was created (usually this means a particular stable
+ release). The script contains a series of <code>cp</code> and
+ <code>svn</code> commands to import the changesets one by one. Note that the
+ changesets are committed automatically with no user interaction. It is worth
+ ensuring that an up to date backup of the repository is available in case of
+ problems.</p>
+
+ <h3 id="changescentral">Incorporating Changes into the Trunk of the Central
+ Repository</h3>
+
+ <p>Once the changes have undergone any necessary testing or review they can
+ be merged into the trunk. There are three ways of approaching this:</p>
+
+ <ol>
+ <li>As one changeset: all changes in the branch will be merged into the
+ trunk as a single changeset. This approach is the easiest and has the
+ advantage that any conflicts only need to be resolved once. However, the
+ drawback of this approach is that the logical changesets as fed back by the
+ collaborator will be combined into a large single changeset on the trunk,
+ which may not be the most desirable (although the logical changesets will
+ still be available to examine on the import branch). This is illustrated in
+ Figure 3a:
+ </li>
+ </ol>
+
+ <p><strong>Figure 3a: merging a patch in a single changeset</strong></p>
+
+ <p><img src="merging-patch-one.png" alt=
+ "Figure 3a: merging a patch in a single changeset"
+ class="img-polaroid" /></p>
+
+ <ol start="2">
+ <li>As multiple changesets: each changeset in the branch will be merged
+ into the trunk in order. This can be quite complicated and time consuming,
+ especially if you have a large number of changesets and there are a lot of
+ clashes. The advantage is that each logical changeset will retain its
+ logical identity, which may be more desirable in the long run, when you
+ come to inspect the history. This is illustrated in Figure 3b:
+ </li>
+ </ol>
+
+ <p><strong>Figure 3b: merging a patch in multiple changesets</strong></p>
+
+ <p><img src="merging-patch-multi.png" alt=
+ "Figure 3b: merging a patch in multiple changesets"
+ class="img-polaroid" /></p>
+
+ <ol start="3">
+ <li>As a mixture of the above: you may want to combine the above two
+ approaches when it makes sense to do so. For example, there may be a series
+ of small changesets that can be combined logically, or there may be a
+ changeset that fixes a bug introduced in the previous one. The bottom line
+ is that the project/system manager should use his/her own judgement in the
+ matter for what is best for the future of the project.</li>
+ </ol>
+
+ <h3 id="changescollab">Incorporating Updates at the Collaborator's Site</h3>
+
+ <p>Once a new stable release is available it will be supplied in the form of
+ a distribution tarball as described earlier. However, collaborators will also
+ be supplied with an <em>FCM patch</em> (as described earlier) for each
+ project containing all the changes made since the previous stable release.
+ Note that this assumes that stable releases are prepared on the trunk and not
+ in branches.</p>
+
+ <p>Each patch should be applied to the trunk of the collaborator's
+ repository. This means that the collaborator's trunk will always be mirroring
+ that of the central repository. This is illustrated in Figure 4:</p>
+
+ <p><strong>Figure 4: mirroring the trunk at the
+ collaborator's site</strong></p>
+
+ <p><img src="mirroring-trunk.png" alt=
+ "Figure 4: mirroring the trunk at the collaborator's site"
+ class="img-polaroid"/></p>
+
+ <p>In order to be certain that the patch has worked correctly, we recommend
+ that a check is performed to ensure that the new stable release on the trunk
+ matches the files provided in the distribution (preferably using a copy of
+ the repository for testing purposes before applying the patch to the live
+ repository).</p>
+
+ <h3 id="updatebranches">Updating Existing Branches</h3>
+
+ <p>Old branches that are still active at the collaborators site should be
+ updated to the latest stable release when it becomes available. Developers
+ should create a new branch from the latest stable release and then merge the
+ changes from the old branch to the new branch. The old branch should be
+ deleted once it is no longer required. This is illustrated in Figure 5a:</p>
+
+ <p><strong>Figure 5a: updating a branch to the latest
+ stable release</strong></p>
+
+ <p><img src="updating-branch.png" alt=
+ "Figure 5a: updating a branch to the latest stable release"
+ class="img-polaroid"/></p>
+
+ <p>Note that the merge will be easiest if the old branch was created from the
+ previous stable release. If it was created from the shared package branch
+ then a custom merge will be required to achieve the desired result (a normal
+ FCM merge command would choose the wrong base for comparison). This is
+ illustrated in Figure 5b:</p>
+
+ <p><strong>Figure 5b: updating a branch of the shared package
+ branch</strong></p>
+
+ <p><img src="updating-shared-branch.png" alt=
+ "Figure 5b: updating a branch of the shared package branch"
+ class="img-polaroid"/></p>
+
+ <h3 id="other">Other Scenarios</h3>
+
+ <p>The previous sections have only considered how developments on the trunk
+ of a central repository can be shared with a single collaborator. However,
+ the same techniques can be applied to more complex situations.</p>
+
+ <ul>
+ <li>If there are multiple external collaborators each working with their
+ own repository then hopefully it is clear that this does not alter things
+ in any way. Inevitably there will be an increased workload on the
+ maintainers of the central repository. There will also be an increased need
+ for coordination of planned code changes. However, the method of code
+ exchange is unaltered.</li>
+
+ <li>Sometimes there may be the need to collaborate on development of a
+ branch (i.e. to exchange code which is not yet ready to be incorporated
+ onto the trunk). The collaborator would maintain the trunk of their
+ repository as before, importing patches to keep their trunk alligned with
+ the stable releases from the central repository. In addition, they would
+ receive an <em>FCM patch</em> from the central repository representing the
+ changes on the shared branch relative to the stable release. The
+ collaborator should create a branch from the stable release and the patch
+ should then be imported onto this branch. They should then create a branch
+ from this branch on which to prepare their changes. When ready the changes
+ would be returned in the form of an <em>FCM patch</em>, and so on.
+ Hopefully it can be seen that the same process can be applied to this
+ shared branch as we have previously described for trunk developments.</li>
+ </ul>
+
+ <h3 id="alternative">An Alternative Branching Strategy</h3>
+
+ <p>We have described the branching strategy we believe will work best for
+ collaborators. However, this is by no means the only branching strategy that
+ can be used. In particular, some collaborators may prefer to keep the latest
+ copies of the code they are using on the trunk. This effectively means
+ getting rid of the shared package branches for shared and local changes and
+ merging all changes on to the trunk. A separate branch would be used for
+ keeping a pristine copy of the main site and merging changes from new stable
+ builds on to the trunk.</p>
+
+ <p>This approach is certainly possible and has the advantage that developers
+ at the collaborator's site may find it easier to work with. However there are
+ two disadvantages that need to be considered:</p>
+
+ <ol>
+ <li>Merging in changes from a new stable release may be more difficult. If
+ the new stable release includes changes which were fed back by the
+ collaborator then these will already be present on the collaborators trunk.
+ If these changes were modified in any way or if they overlap with other
+ changes then this will result in a conflict which could be tricky to
+ resolve.</li>
+
+ <li>Any changes which need to be fed back by the collaborator need to be
+ made relative to a stable release. However, changes will have been prepared
+ relative to some version of the trunk. This means that a separate branch
+ will need to be taken (from the branch containing the pristine copy of the
+ main site) and a custom merge will be required in order to achieve the
+ desired result.</li>
+ </ol>
+
+ <h3 id="patchfiles">Exchanging Changesets using Patchfiles</h3>
+
+ <p>In some cases, an <em>FCM patch</em> may not be the best way of exchanging
+ changesets. For instance, when distributing code changes which have not yet
+ been finalised, you probably wouldn't want to send a patch containing all the
+ individual commits to the branch on which the change is being developed. What
+ you want is a summary of the changes in a single changeset. In this case you
+ will often be better to use a patchfile (which can be applied using the Unix
+ command <code>patch</code>). A patchfile is simply the output from an
+ <code>fcm diff</code> command. For example:</p>
+ <pre>
+fcm diff --branch fcm:myproj-br/dev/frdm/r2134_my_branch > my_patchfile
+</pre>
+
+ <p>The patchfile must be applied to a working copy of the project which
+ corresponds to the same revision from which the patchfile was generated. The
+ option <code>-p0</code> must be used with the <code>patch</code> command. For
+ example:</p>
+ <pre>
+patch -p0 < my_patchfile
+</pre>
+
+ <p>Patchfiles have the advantage that they are simple to generate and
+ exchange and that they can combine the changes from a number of changsets
+ into one. However, they have a number of limitations such as:</p>
+
+ <ul>
+ <li>Binary files are ignored.</li>
+
+ <li>Deleted directories are ignored.</li>
+
+ <li>Deleted files are left as empty files.</li>
+
+ <li>Copied files appear as new files.</li>
+
+ <li>A moved file is treated as a deleted file and a new file.</li>
+ </ul>
+
+ <p>Fortunately these limitations will not be an issue for the majority of
+ changes and, where they are a problem, there are various options such as
+ providing additional instructions with the patchfile, using an <em>FCM
+ patch</em>, or exchanging a modified project tree.</p>
+
+ <h2 id="further">Further Considerations</h2>
+
+ <p>The previous sections have only considered the version control aspects of
+ a collaboration. This section lists some other aspects of the collaboration
+ which will need to be considered.</p>
+
+ <ul>
+ <li>The FCM build system can be used regardless of what version control
+ system is used. This avoids effort being wasted trying to maintain
+ compatibility with an alternate build system. It also ensures that any code
+ changes prepared by the collaborator are compatible with the coding
+ standards which the FCM build system requires. Even if there are good
+ reasons for the collaborator not to use FCM for version control, it is
+ highly recommended that the FCM build system is used (assuming that is what
+ is used at the central repository).</li>
+
+ <li>Coding standards should be agreed by all collaborators.</li>
+
+ <li>Working practices should be agreed which should define, amongst other
+ things, what level of testing, review and documentation is expected to
+ accompany any proposed change.</li>
+
+ <li>All parties in the collaboration should note the advice given in the
+ <a href="../user_guide/code_management.html#svn_problems">FCM user
+ guide</a> to avoid renaming files or directories unless you can ensure that
+ no-one is working in parallel on the affected areas of the project.</li>
+
+ <li><acronym title="intellectual property rights">IPR</acronym>, copyright
+ and license issues should be agreed by all collaborators.</li>
+ </ul>
+
+ </div>
+ </div>
+ </div>
+
+ <hr/>
+ <div class="container-fluid text-center">
+ <div class="row-fluid"><div class="span12">
+ <address><small>
+ © British Crown Copyright 2006-14
+ <a href="http://www.metoffice.gov.uk">Met Office</a>.
+ See <a href="../etc/fcm-terms-of-use.html">Terms of Use</a>.<br />
+ This document is released under the British <a href=
+ "http://www.nationalarchives.gov.uk/doc/open-government-licence/" rel=
+ "license">Open Government Licence</a>.<br />
+ </small></address>
+ </div></div>
+ </div>
+
+ <script type="text/javascript" src="../etc/jquery.min.js"></script>
+ <script type="text/javascript" src="../etc/bootstrap/js/bootstrap.min.js"></script>
+ <script type="text/javascript" src="../etc/fcm.js"></script>
+ <script type="text/javascript" src="../etc/fcm-version.js"></script>
+</body>
+</html>
diff --git a/doc/collaboration/managing-local-changes.png b/doc/collaboration/managing-local-changes.png
new file mode 100644
index 0000000..66b237a
Binary files /dev/null and b/doc/collaboration/managing-local-changes.png differ
diff --git a/doc/collaboration/merging-patch-multi.png b/doc/collaboration/merging-patch-multi.png
new file mode 100644
index 0000000..5239205
Binary files /dev/null and b/doc/collaboration/merging-patch-multi.png differ
diff --git a/doc/collaboration/merging-patch-one.png b/doc/collaboration/merging-patch-one.png
new file mode 100644
index 0000000..762eec3
Binary files /dev/null and b/doc/collaboration/merging-patch-one.png differ
diff --git a/doc/collaboration/mirroring-trunk.png b/doc/collaboration/mirroring-trunk.png
new file mode 100644
index 0000000..2624021
Binary files /dev/null and b/doc/collaboration/mirroring-trunk.png differ
diff --git a/doc/collaboration/updating-branch.png b/doc/collaboration/updating-branch.png
new file mode 100644
index 0000000..9cd4967
Binary files /dev/null and b/doc/collaboration/updating-branch.png differ
diff --git a/doc/collaboration/updating-shared-branch.png b/doc/collaboration/updating-shared-branch.png
new file mode 100644
index 0000000..88d68aa
Binary files /dev/null and b/doc/collaboration/updating-shared-branch.png differ
diff --git a/doc/collaboration/working-as-collaborator.png b/doc/collaboration/working-as-collaborator.png
new file mode 100644
index 0000000..5e0bd3f
Binary files /dev/null and b/doc/collaboration/working-as-collaborator.png differ
diff --git a/doc/etc/bootstrap/css/bootstrap-responsive.css b/doc/etc/bootstrap/css/bootstrap-responsive.css
new file mode 100644
index 0000000..c0bba15
--- /dev/null
+++ b/doc/etc/bootstrap/css/bootstrap-responsive.css
@@ -0,0 +1,1109 @@
+/*!
+ * Bootstrap Responsive v2.3.2
+ *
+ * Copyright 2013 Twitter, Inc
+ * Licensed under the Apache License v2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Designed and built with all the love in the world by @mdo and @fat.
+ */
+
+.clearfix {
+ *zoom: 1;
+}
+
+.clearfix:before,
+.clearfix:after {
+ display: table;
+ line-height: 0;
+ content: "";
+}
+
+.clearfix:after {
+ clear: both;
+}
+
+.hide-text {
+ font: 0/0 a;
+ color: transparent;
+ text-shadow: none;
+ background-color: transparent;
+ border: 0;
+}
+
+.input-block-level {
+ display: block;
+ width: 100%;
+ min-height: 30px;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+
+ at -ms-viewport {
+ width: device-width;
+}
+
+.hidden {
+ display: none;
+ visibility: hidden;
+}
+
+.visible-phone {
+ display: none !important;
+}
+
+.visible-tablet {
+ display: none !important;
+}
+
+.hidden-desktop {
+ display: none !important;
+}
+
+.visible-desktop {
+ display: inherit !important;
+}
+
+ at media (min-width: 768px) and (max-width: 979px) {
+ .hidden-desktop {
+ display: inherit !important;
+ }
+ .visible-desktop {
+ display: none !important ;
+ }
+ .visible-tablet {
+ display: inherit !important;
+ }
+ .hidden-tablet {
+ display: none !important;
+ }
+}
+
+ at media (max-width: 767px) {
+ .hidden-desktop {
+ display: inherit !important;
+ }
+ .visible-desktop {
+ display: none !important;
+ }
+ .visible-phone {
+ display: inherit !important;
+ }
+ .hidden-phone {
+ display: none !important;
+ }
+}
+
+.visible-print {
+ display: none !important;
+}
+
+ at media print {
+ .visible-print {
+ display: inherit !important;
+ }
+ .hidden-print {
+ display: none !important;
+ }
+}
+
+ at media (min-width: 1200px) {
+ .row {
+ margin-left: -30px;
+ *zoom: 1;
+ }
+ .row:before,
+ .row:after {
+ display: table;
+ line-height: 0;
+ content: "";
+ }
+ .row:after {
+ clear: both;
+ }
+ [class*="span"] {
+ float: left;
+ min-height: 1px;
+ margin-left: 30px;
+ }
+ .container,
+ .navbar-static-top .container,
+ .navbar-fixed-top .container,
+ .navbar-fixed-bottom .container {
+ width: 1170px;
+ }
+ .span12 {
+ width: 1170px;
+ }
+ .span11 {
+ width: 1070px;
+ }
+ .span10 {
+ width: 970px;
+ }
+ .span9 {
+ width: 870px;
+ }
+ .span8 {
+ width: 770px;
+ }
+ .span7 {
+ width: 670px;
+ }
+ .span6 {
+ width: 570px;
+ }
+ .span5 {
+ width: 470px;
+ }
+ .span4 {
+ width: 370px;
+ }
+ .span3 {
+ width: 270px;
+ }
+ .span2 {
+ width: 170px;
+ }
+ .span1 {
+ width: 70px;
+ }
+ .offset12 {
+ margin-left: 1230px;
+ }
+ .offset11 {
+ margin-left: 1130px;
+ }
+ .offset10 {
+ margin-left: 1030px;
+ }
+ .offset9 {
+ margin-left: 930px;
+ }
+ .offset8 {
+ margin-left: 830px;
+ }
+ .offset7 {
+ margin-left: 730px;
+ }
+ .offset6 {
+ margin-left: 630px;
+ }
+ .offset5 {
+ margin-left: 530px;
+ }
+ .offset4 {
+ margin-left: 430px;
+ }
+ .offset3 {
+ margin-left: 330px;
+ }
+ .offset2 {
+ margin-left: 230px;
+ }
+ .offset1 {
+ margin-left: 130px;
+ }
+ .row-fluid {
+ width: 100%;
+ *zoom: 1;
+ }
+ .row-fluid:before,
+ .row-fluid:after {
+ display: table;
+ line-height: 0;
+ content: "";
+ }
+ .row-fluid:after {
+ clear: both;
+ }
+ .row-fluid [class*="span"] {
+ display: block;
+ float: left;
+ width: 100%;
+ min-height: 30px;
+ margin-left: 2.564102564102564%;
+ *margin-left: 2.5109110747408616%;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ }
+ .row-fluid [class*="span"]:first-child {
+ margin-left: 0;
+ }
+ .row-fluid .controls-row [class*="span"] + [class*="span"] {
+ margin-left: 2.564102564102564%;
+ }
+ .row-fluid .span12 {
+ width: 100%;
+ *width: 99.94680851063829%;
+ }
+ .row-fluid .span11 {
+ width: 91.45299145299145%;
+ *width: 91.39979996362975%;
+ }
+ .row-fluid .span10 {
+ width: 82.90598290598291%;
+ *width: 82.8527914166212%;
+ }
+ .row-fluid .span9 {
+ width: 74.35897435897436%;
+ *width: 74.30578286961266%;
+ }
+ .row-fluid .span8 {
+ width: 65.81196581196582%;
+ *width: 65.75877432260411%;
+ }
+ .row-fluid .span7 {
+ width: 57.26495726495726%;
+ *width: 57.21176577559556%;
+ }
+ .row-fluid .span6 {
+ width: 48.717948717948715%;
+ *width: 48.664757228587014%;
+ }
+ .row-fluid .span5 {
+ width: 40.17094017094017%;
+ *width: 40.11774868157847%;
+ }
+ .row-fluid .span4 {
+ width: 31.623931623931625%;
+ *width: 31.570740134569924%;
+ }
+ .row-fluid .span3 {
+ width: 23.076923076923077%;
+ *width: 23.023731587561375%;
+ }
+ .row-fluid .span2 {
+ width: 14.52991452991453%;
+ *width: 14.476723040552828%;
+ }
+ .row-fluid .span1 {
+ width: 5.982905982905983%;
+ *width: 5.929714493544281%;
+ }
+ .row-fluid .offset12 {
+ margin-left: 105.12820512820512%;
+ *margin-left: 105.02182214948171%;
+ }
+ .row-fluid .offset12:first-child {
+ margin-left: 102.56410256410257%;
+ *margin-left: 102.45771958537915%;
+ }
+ .row-fluid .offset11 {
+ margin-left: 96.58119658119658%;
+ *margin-left: 96.47481360247316%;
+ }
+ .row-fluid .offset11:first-child {
+ margin-left: 94.01709401709402%;
+ *margin-left: 93.91071103837061%;
+ }
+ .row-fluid .offset10 {
+ margin-left: 88.03418803418803%;
+ *margin-left: 87.92780505546462%;
+ }
+ .row-fluid .offset10:first-child {
+ margin-left: 85.47008547008548%;
+ *margin-left: 85.36370249136206%;
+ }
+ .row-fluid .offset9 {
+ margin-left: 79.48717948717949%;
+ *margin-left: 79.38079650845607%;
+ }
+ .row-fluid .offset9:first-child {
+ margin-left: 76.92307692307693%;
+ *margin-left: 76.81669394435352%;
+ }
+ .row-fluid .offset8 {
+ margin-left: 70.94017094017094%;
+ *margin-left: 70.83378796144753%;
+ }
+ .row-fluid .offset8:first-child {
+ margin-left: 68.37606837606839%;
+ *margin-left: 68.26968539734497%;
+ }
+ .row-fluid .offset7 {
+ margin-left: 62.393162393162385%;
+ *margin-left: 62.28677941443899%;
+ }
+ .row-fluid .offset7:first-child {
+ margin-left: 59.82905982905982%;
+ *margin-left: 59.72267685033642%;
+ }
+ .row-fluid .offset6 {
+ margin-left: 53.84615384615384%;
+ *margin-left: 53.739770867430444%;
+ }
+ .row-fluid .offset6:first-child {
+ margin-left: 51.28205128205128%;
+ *margin-left: 51.175668303327875%;
+ }
+ .row-fluid .offset5 {
+ margin-left: 45.299145299145295%;
+ *margin-left: 45.1927623204219%;
+ }
+ .row-fluid .offset5:first-child {
+ margin-left: 42.73504273504273%;
+ *margin-left: 42.62865975631933%;
+ }
+ .row-fluid .offset4 {
+ margin-left: 36.75213675213675%;
+ *margin-left: 36.645753773413354%;
+ }
+ .row-fluid .offset4:first-child {
+ margin-left: 34.18803418803419%;
+ *margin-left: 34.081651209310785%;
+ }
+ .row-fluid .offset3 {
+ margin-left: 28.205128205128204%;
+ *margin-left: 28.0987452264048%;
+ }
+ .row-fluid .offset3:first-child {
+ margin-left: 25.641025641025642%;
+ *margin-left: 25.53464266230224%;
+ }
+ .row-fluid .offset2 {
+ margin-left: 19.65811965811966%;
+ *margin-left: 19.551736679396257%;
+ }
+ .row-fluid .offset2:first-child {
+ margin-left: 17.094017094017094%;
+ *margin-left: 16.98763411529369%;
+ }
+ .row-fluid .offset1 {
+ margin-left: 11.11111111111111%;
+ *margin-left: 11.004728132387708%;
+ }
+ .row-fluid .offset1:first-child {
+ margin-left: 8.547008547008547%;
+ *margin-left: 8.440625568285142%;
+ }
+ input,
+ textarea,
+ .uneditable-input {
+ margin-left: 0;
+ }
+ .controls-row [class*="span"] + [class*="span"] {
+ margin-left: 30px;
+ }
+ input.span12,
+ textarea.span12,
+ .uneditable-input.span12 {
+ width: 1156px;
+ }
+ input.span11,
+ textarea.span11,
+ .uneditable-input.span11 {
+ width: 1056px;
+ }
+ input.span10,
+ textarea.span10,
+ .uneditable-input.span10 {
+ width: 956px;
+ }
+ input.span9,
+ textarea.span9,
+ .uneditable-input.span9 {
+ width: 856px;
+ }
+ input.span8,
+ textarea.span8,
+ .uneditable-input.span8 {
+ width: 756px;
+ }
+ input.span7,
+ textarea.span7,
+ .uneditable-input.span7 {
+ width: 656px;
+ }
+ input.span6,
+ textarea.span6,
+ .uneditable-input.span6 {
+ width: 556px;
+ }
+ input.span5,
+ textarea.span5,
+ .uneditable-input.span5 {
+ width: 456px;
+ }
+ input.span4,
+ textarea.span4,
+ .uneditable-input.span4 {
+ width: 356px;
+ }
+ input.span3,
+ textarea.span3,
+ .uneditable-input.span3 {
+ width: 256px;
+ }
+ input.span2,
+ textarea.span2,
+ .uneditable-input.span2 {
+ width: 156px;
+ }
+ input.span1,
+ textarea.span1,
+ .uneditable-input.span1 {
+ width: 56px;
+ }
+ .thumbnails {
+ margin-left: -30px;
+ }
+ .thumbnails > li {
+ margin-left: 30px;
+ }
+ .row-fluid .thumbnails {
+ margin-left: 0;
+ }
+}
+
+ at media (min-width: 768px) and (max-width: 979px) {
+ .row {
+ margin-left: -20px;
+ *zoom: 1;
+ }
+ .row:before,
+ .row:after {
+ display: table;
+ line-height: 0;
+ content: "";
+ }
+ .row:after {
+ clear: both;
+ }
+ [class*="span"] {
+ float: left;
+ min-height: 1px;
+ margin-left: 20px;
+ }
+ .container,
+ .navbar-static-top .container,
+ .navbar-fixed-top .container,
+ .navbar-fixed-bottom .container {
+ width: 724px;
+ }
+ .span12 {
+ width: 724px;
+ }
+ .span11 {
+ width: 662px;
+ }
+ .span10 {
+ width: 600px;
+ }
+ .span9 {
+ width: 538px;
+ }
+ .span8 {
+ width: 476px;
+ }
+ .span7 {
+ width: 414px;
+ }
+ .span6 {
+ width: 352px;
+ }
+ .span5 {
+ width: 290px;
+ }
+ .span4 {
+ width: 228px;
+ }
+ .span3 {
+ width: 166px;
+ }
+ .span2 {
+ width: 104px;
+ }
+ .span1 {
+ width: 42px;
+ }
+ .offset12 {
+ margin-left: 764px;
+ }
+ .offset11 {
+ margin-left: 702px;
+ }
+ .offset10 {
+ margin-left: 640px;
+ }
+ .offset9 {
+ margin-left: 578px;
+ }
+ .offset8 {
+ margin-left: 516px;
+ }
+ .offset7 {
+ margin-left: 454px;
+ }
+ .offset6 {
+ margin-left: 392px;
+ }
+ .offset5 {
+ margin-left: 330px;
+ }
+ .offset4 {
+ margin-left: 268px;
+ }
+ .offset3 {
+ margin-left: 206px;
+ }
+ .offset2 {
+ margin-left: 144px;
+ }
+ .offset1 {
+ margin-left: 82px;
+ }
+ .row-fluid {
+ width: 100%;
+ *zoom: 1;
+ }
+ .row-fluid:before,
+ .row-fluid:after {
+ display: table;
+ line-height: 0;
+ content: "";
+ }
+ .row-fluid:after {
+ clear: both;
+ }
+ .row-fluid [class*="span"] {
+ display: block;
+ float: left;
+ width: 100%;
+ min-height: 30px;
+ margin-left: 2.7624309392265194%;
+ *margin-left: 2.709239449864817%;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ }
+ .row-fluid [class*="span"]:first-child {
+ margin-left: 0;
+ }
+ .row-fluid .controls-row [class*="span"] + [class*="span"] {
+ margin-left: 2.7624309392265194%;
+ }
+ .row-fluid .span12 {
+ width: 100%;
+ *width: 99.94680851063829%;
+ }
+ .row-fluid .span11 {
+ width: 91.43646408839778%;
+ *width: 91.38327259903608%;
+ }
+ .row-fluid .span10 {
+ width: 82.87292817679558%;
+ *width: 82.81973668743387%;
+ }
+ .row-fluid .span9 {
+ width: 74.30939226519337%;
+ *width: 74.25620077583166%;
+ }
+ .row-fluid .span8 {
+ width: 65.74585635359117%;
+ *width: 65.69266486422946%;
+ }
+ .row-fluid .span7 {
+ width: 57.18232044198895%;
+ *width: 57.12912895262725%;
+ }
+ .row-fluid .span6 {
+ width: 48.61878453038674%;
+ *width: 48.56559304102504%;
+ }
+ .row-fluid .span5 {
+ width: 40.05524861878453%;
+ *width: 40.00205712942283%;
+ }
+ .row-fluid .span4 {
+ width: 31.491712707182323%;
+ *width: 31.43852121782062%;
+ }
+ .row-fluid .span3 {
+ width: 22.92817679558011%;
+ *width: 22.87498530621841%;
+ }
+ .row-fluid .span2 {
+ width: 14.3646408839779%;
+ *width: 14.311449394616199%;
+ }
+ .row-fluid .span1 {
+ width: 5.801104972375691%;
+ *width: 5.747913483013988%;
+ }
+ .row-fluid .offset12 {
+ margin-left: 105.52486187845304%;
+ *margin-left: 105.41847889972962%;
+ }
+ .row-fluid .offset12:first-child {
+ margin-left: 102.76243093922652%;
+ *margin-left: 102.6560479605031%;
+ }
+ .row-fluid .offset11 {
+ margin-left: 96.96132596685082%;
+ *margin-left: 96.8549429881274%;
+ }
+ .row-fluid .offset11:first-child {
+ margin-left: 94.1988950276243%;
+ *margin-left: 94.09251204890089%;
+ }
+ .row-fluid .offset10 {
+ margin-left: 88.39779005524862%;
+ *margin-left: 88.2914070765252%;
+ }
+ .row-fluid .offset10:first-child {
+ margin-left: 85.6353591160221%;
+ *margin-left: 85.52897613729868%;
+ }
+ .row-fluid .offset9 {
+ margin-left: 79.8342541436464%;
+ *margin-left: 79.72787116492299%;
+ }
+ .row-fluid .offset9:first-child {
+ margin-left: 77.07182320441989%;
+ *margin-left: 76.96544022569647%;
+ }
+ .row-fluid .offset8 {
+ margin-left: 71.2707182320442%;
+ *margin-left: 71.16433525332079%;
+ }
+ .row-fluid .offset8:first-child {
+ margin-left: 68.50828729281768%;
+ *margin-left: 68.40190431409427%;
+ }
+ .row-fluid .offset7 {
+ margin-left: 62.70718232044199%;
+ *margin-left: 62.600799341718584%;
+ }
+ .row-fluid .offset7:first-child {
+ margin-left: 59.94475138121547%;
+ *margin-left: 59.838368402492065%;
+ }
+ .row-fluid .offset6 {
+ margin-left: 54.14364640883978%;
+ *margin-left: 54.037263430116376%;
+ }
+ .row-fluid .offset6:first-child {
+ margin-left: 51.38121546961326%;
+ *margin-left: 51.27483249088986%;
+ }
+ .row-fluid .offset5 {
+ margin-left: 45.58011049723757%;
+ *margin-left: 45.47372751851417%;
+ }
+ .row-fluid .offset5:first-child {
+ margin-left: 42.81767955801105%;
+ *margin-left: 42.71129657928765%;
+ }
+ .row-fluid .offset4 {
+ margin-left: 37.01657458563536%;
+ *margin-left: 36.91019160691196%;
+ }
+ .row-fluid .offset4:first-child {
+ margin-left: 34.25414364640884%;
+ *margin-left: 34.14776066768544%;
+ }
+ .row-fluid .offset3 {
+ margin-left: 28.45303867403315%;
+ *margin-left: 28.346655695309746%;
+ }
+ .row-fluid .offset3:first-child {
+ margin-left: 25.69060773480663%;
+ *margin-left: 25.584224756083227%;
+ }
+ .row-fluid .offset2 {
+ margin-left: 19.88950276243094%;
+ *margin-left: 19.783119783707537%;
+ }
+ .row-fluid .offset2:first-child {
+ margin-left: 17.12707182320442%;
+ *margin-left: 17.02068884448102%;
+ }
+ .row-fluid .offset1 {
+ margin-left: 11.32596685082873%;
+ *margin-left: 11.219583872105325%;
+ }
+ .row-fluid .offset1:first-child {
+ margin-left: 8.56353591160221%;
+ *margin-left: 8.457152932878806%;
+ }
+ input,
+ textarea,
+ .uneditable-input {
+ margin-left: 0;
+ }
+ .controls-row [class*="span"] + [class*="span"] {
+ margin-left: 20px;
+ }
+ input.span12,
+ textarea.span12,
+ .uneditable-input.span12 {
+ width: 710px;
+ }
+ input.span11,
+ textarea.span11,
+ .uneditable-input.span11 {
+ width: 648px;
+ }
+ input.span10,
+ textarea.span10,
+ .uneditable-input.span10 {
+ width: 586px;
+ }
+ input.span9,
+ textarea.span9,
+ .uneditable-input.span9 {
+ width: 524px;
+ }
+ input.span8,
+ textarea.span8,
+ .uneditable-input.span8 {
+ width: 462px;
+ }
+ input.span7,
+ textarea.span7,
+ .uneditable-input.span7 {
+ width: 400px;
+ }
+ input.span6,
+ textarea.span6,
+ .uneditable-input.span6 {
+ width: 338px;
+ }
+ input.span5,
+ textarea.span5,
+ .uneditable-input.span5 {
+ width: 276px;
+ }
+ input.span4,
+ textarea.span4,
+ .uneditable-input.span4 {
+ width: 214px;
+ }
+ input.span3,
+ textarea.span3,
+ .uneditable-input.span3 {
+ width: 152px;
+ }
+ input.span2,
+ textarea.span2,
+ .uneditable-input.span2 {
+ width: 90px;
+ }
+ input.span1,
+ textarea.span1,
+ .uneditable-input.span1 {
+ width: 28px;
+ }
+}
+
+ at media (max-width: 767px) {
+ body {
+ padding-right: 20px;
+ padding-left: 20px;
+ }
+ .navbar-fixed-top,
+ .navbar-fixed-bottom,
+ .navbar-static-top {
+ margin-right: -20px;
+ margin-left: -20px;
+ }
+ .container-fluid {
+ padding: 0;
+ }
+ .dl-horizontal dt {
+ float: none;
+ width: auto;
+ clear: none;
+ text-align: left;
+ }
+ .dl-horizontal dd {
+ margin-left: 0;
+ }
+ .container {
+ width: auto;
+ }
+ .row-fluid {
+ width: 100%;
+ }
+ .row,
+ .thumbnails {
+ margin-left: 0;
+ }
+ .thumbnails > li {
+ float: none;
+ margin-left: 0;
+ }
+ [class*="span"],
+ .uneditable-input[class*="span"],
+ .row-fluid [class*="span"] {
+ display: block;
+ float: none;
+ width: 100%;
+ margin-left: 0;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ }
+ .span12,
+ .row-fluid .span12 {
+ width: 100%;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ }
+ .row-fluid [class*="offset"]:first-child {
+ margin-left: 0;
+ }
+ .input-large,
+ .input-xlarge,
+ .input-xxlarge,
+ input[class*="span"],
+ select[class*="span"],
+ textarea[class*="span"],
+ .uneditable-input {
+ display: block;
+ width: 100%;
+ min-height: 30px;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ }
+ .input-prepend input,
+ .input-append input,
+ .input-prepend input[class*="span"],
+ .input-append input[class*="span"] {
+ display: inline-block;
+ width: auto;
+ }
+ .controls-row [class*="span"] + [class*="span"] {
+ margin-left: 0;
+ }
+ .modal {
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ left: 20px;
+ width: auto;
+ margin: 0;
+ }
+ .modal.fade {
+ top: -100px;
+ }
+ .modal.fade.in {
+ top: 20px;
+ }
+}
+
+ at media (max-width: 480px) {
+ .nav-collapse {
+ -webkit-transform: translate3d(0, 0, 0);
+ }
+ .page-header h1 small {
+ display: block;
+ line-height: 20px;
+ }
+ input[type="checkbox"],
+ input[type="radio"] {
+ border: 1px solid #ccc;
+ }
+ .form-horizontal .control-label {
+ float: none;
+ width: auto;
+ padding-top: 0;
+ text-align: left;
+ }
+ .form-horizontal .controls {
+ margin-left: 0;
+ }
+ .form-horizontal .control-list {
+ padding-top: 0;
+ }
+ .form-horizontal .form-actions {
+ padding-right: 10px;
+ padding-left: 10px;
+ }
+ .media .pull-left,
+ .media .pull-right {
+ display: block;
+ float: none;
+ margin-bottom: 10px;
+ }
+ .media-object {
+ margin-right: 0;
+ margin-left: 0;
+ }
+ .modal {
+ top: 10px;
+ right: 10px;
+ left: 10px;
+ }
+ .modal-header .close {
+ padding: 10px;
+ margin: -10px;
+ }
+ .carousel-caption {
+ position: static;
+ }
+}
+
+ at media (max-width: 979px) {
+ body {
+ padding-top: 0;
+ }
+ .navbar-fixed-top,
+ .navbar-fixed-bottom {
+ position: static;
+ }
+ .navbar-fixed-top {
+ margin-bottom: 20px;
+ }
+ .navbar-fixed-bottom {
+ margin-top: 20px;
+ }
+ .navbar-fixed-top .navbar-inner,
+ .navbar-fixed-bottom .navbar-inner {
+ padding: 5px;
+ }
+ .navbar .container {
+ width: auto;
+ padding: 0;
+ }
+ .navbar .brand {
+ padding-right: 10px;
+ padding-left: 10px;
+ margin: 0 0 0 -5px;
+ }
+ .nav-collapse {
+ clear: both;
+ }
+ .nav-collapse .nav {
+ float: none;
+ margin: 0 0 10px;
+ }
+ .nav-collapse .nav > li {
+ float: none;
+ }
+ .nav-collapse .nav > li > a {
+ margin-bottom: 2px;
+ }
+ .nav-collapse .nav > .divider-vertical {
+ display: none;
+ }
+ .nav-collapse .nav .nav-header {
+ color: #777777;
+ text-shadow: none;
+ }
+ .nav-collapse .nav > li > a,
+ .nav-collapse .dropdown-menu a {
+ padding: 9px 15px;
+ font-weight: bold;
+ color: #777777;
+ -webkit-border-radius: 3px;
+ -moz-border-radius: 3px;
+ border-radius: 3px;
+ }
+ .nav-collapse .btn {
+ padding: 4px 10px 4px;
+ font-weight: normal;
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+ }
+ .nav-collapse .dropdown-menu li + li a {
+ margin-bottom: 2px;
+ }
+ .nav-collapse .nav > li > a:hover,
+ .nav-collapse .nav > li > a:focus,
+ .nav-collapse .dropdown-menu a:hover,
+ .nav-collapse .dropdown-menu a:focus {
+ background-color: #f2f2f2;
+ }
+ .navbar-inverse .nav-collapse .nav > li > a,
+ .navbar-inverse .nav-collapse .dropdown-menu a {
+ color: #999999;
+ }
+ .navbar-inverse .nav-collapse .nav > li > a:hover,
+ .navbar-inverse .nav-collapse .nav > li > a:focus,
+ .navbar-inverse .nav-collapse .dropdown-menu a:hover,
+ .navbar-inverse .nav-collapse .dropdown-menu a:focus {
+ background-color: #111111;
+ }
+ .nav-collapse.in .btn-group {
+ padding: 0;
+ margin-top: 5px;
+ }
+ .nav-collapse .dropdown-menu {
+ position: static;
+ top: auto;
+ left: auto;
+ display: none;
+ float: none;
+ max-width: none;
+ padding: 0;
+ margin: 0 15px;
+ background-color: transparent;
+ border: none;
+ -webkit-border-radius: 0;
+ -moz-border-radius: 0;
+ border-radius: 0;
+ -webkit-box-shadow: none;
+ -moz-box-shadow: none;
+ box-shadow: none;
+ }
+ .nav-collapse .open > .dropdown-menu {
+ display: block;
+ }
+ .nav-collapse .dropdown-menu:before,
+ .nav-collapse .dropdown-menu:after {
+ display: none;
+ }
+ .nav-collapse .dropdown-menu .divider {
+ display: none;
+ }
+ .nav-collapse .nav > li > .dropdown-menu:before,
+ .nav-collapse .nav > li > .dropdown-menu:after {
+ display: none;
+ }
+ .nav-collapse .navbar-form,
+ .nav-collapse .navbar-search {
+ float: none;
+ padding: 10px 15px;
+ margin: 10px 0;
+ border-top: 1px solid #f2f2f2;
+ border-bottom: 1px solid #f2f2f2;
+ -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);
+ -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);
+ }
+ .navbar-inverse .nav-collapse .navbar-form,
+ .navbar-inverse .nav-collapse .navbar-search {
+ border-top-color: #111111;
+ border-bottom-color: #111111;
+ }
+ .navbar .nav-collapse .nav.pull-right {
+ float: none;
+ margin-left: 0;
+ }
+ .nav-collapse,
+ .nav-collapse.collapse {
+ height: 0;
+ overflow: hidden;
+ }
+ .navbar .btn-navbar {
+ display: block;
+ }
+ .navbar-static .navbar-inner {
+ padding-right: 10px;
+ padding-left: 10px;
+ }
+}
+
+ at media (min-width: 980px) {
+ .nav-collapse.collapse {
+ height: auto !important;
+ overflow: visible !important;
+ }
+}
diff --git a/doc/etc/bootstrap/css/bootstrap-responsive.min.css b/doc/etc/bootstrap/css/bootstrap-responsive.min.css
new file mode 100644
index 0000000..96a435b
--- /dev/null
+++ b/doc/etc/bootstrap/css/bootstrap-responsive.min.css
@@ -0,0 +1,9 @@
+/*!
+ * Bootstrap Responsive v2.3.2
+ *
+ * Copyright 2013 Twitter, Inc
+ * Licensed under the Apache License v2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Designed and built with all the love in the world by @mdo and @fat.
+ */.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;line-height:0;content:""}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}@-ms-viewport{width:device-width}.hidden{display:none;visibility:hidden}.visible-phone{display:none!important}.visible-tablet{display:none [...]
diff --git a/doc/etc/bootstrap/css/bootstrap.css b/doc/etc/bootstrap/css/bootstrap.css
new file mode 100644
index 0000000..5b7fe7e
--- /dev/null
+++ b/doc/etc/bootstrap/css/bootstrap.css
@@ -0,0 +1,6167 @@
+/*!
+ * Bootstrap v2.3.2
+ *
+ * Copyright 2013 Twitter, Inc
+ * Licensed under the Apache License v2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Designed and built with all the love in the world by @mdo and @fat.
+ */
+
+.clearfix {
+ *zoom: 1;
+}
+
+.clearfix:before,
+.clearfix:after {
+ display: table;
+ line-height: 0;
+ content: "";
+}
+
+.clearfix:after {
+ clear: both;
+}
+
+.hide-text {
+ font: 0/0 a;
+ color: transparent;
+ text-shadow: none;
+ background-color: transparent;
+ border: 0;
+}
+
+.input-block-level {
+ display: block;
+ width: 100%;
+ min-height: 30px;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+
+article,
+aside,
+details,
+figcaption,
+figure,
+footer,
+header,
+hgroup,
+nav,
+section {
+ display: block;
+}
+
+audio,
+canvas,
+video {
+ display: inline-block;
+ *display: inline;
+ *zoom: 1;
+}
+
+audio:not([controls]) {
+ display: none;
+}
+
+html {
+ font-size: 100%;
+ -webkit-text-size-adjust: 100%;
+ -ms-text-size-adjust: 100%;
+}
+
+a:focus {
+ outline: thin dotted #333;
+ outline: 5px auto -webkit-focus-ring-color;
+ outline-offset: -2px;
+}
+
+a:hover,
+a:active {
+ outline: 0;
+}
+
+sub,
+sup {
+ position: relative;
+ font-size: 75%;
+ line-height: 0;
+ vertical-align: baseline;
+}
+
+sup {
+ top: -0.5em;
+}
+
+sub {
+ bottom: -0.25em;
+}
+
+img {
+ width: auto\9;
+ height: auto;
+ max-width: 100%;
+ vertical-align: middle;
+ border: 0;
+ -ms-interpolation-mode: bicubic;
+}
+
+#map_canvas img,
+.google-maps img {
+ max-width: none;
+}
+
+button,
+input,
+select,
+textarea {
+ margin: 0;
+ font-size: 100%;
+ vertical-align: middle;
+}
+
+button,
+input {
+ *overflow: visible;
+ line-height: normal;
+}
+
+button::-moz-focus-inner,
+input::-moz-focus-inner {
+ padding: 0;
+ border: 0;
+}
+
+button,
+html input[type="button"],
+input[type="reset"],
+input[type="submit"] {
+ cursor: pointer;
+ -webkit-appearance: button;
+}
+
+label,
+select,
+button,
+input[type="button"],
+input[type="reset"],
+input[type="submit"],
+input[type="radio"],
+input[type="checkbox"] {
+ cursor: pointer;
+}
+
+input[type="search"] {
+ -webkit-box-sizing: content-box;
+ -moz-box-sizing: content-box;
+ box-sizing: content-box;
+ -webkit-appearance: textfield;
+}
+
+input[type="search"]::-webkit-search-decoration,
+input[type="search"]::-webkit-search-cancel-button {
+ -webkit-appearance: none;
+}
+
+textarea {
+ overflow: auto;
+ vertical-align: top;
+}
+
+ at media print {
+ * {
+ color: #000 !important;
+ text-shadow: none !important;
+ background: transparent !important;
+ box-shadow: none !important;
+ }
+ a,
+ a:visited {
+ text-decoration: underline;
+ }
+ a[href]:after {
+ content: " (" attr(href) ")";
+ }
+ abbr[title]:after {
+ content: " (" attr(title) ")";
+ }
+ .ir a:after,
+ a[href^="javascript:"]:after,
+ a[href^="#"]:after {
+ content: "";
+ }
+ pre,
+ blockquote {
+ border: 1px solid #999;
+ page-break-inside: avoid;
+ }
+ thead {
+ display: table-header-group;
+ }
+ tr,
+ img {
+ page-break-inside: avoid;
+ }
+ img {
+ max-width: 100% !important;
+ }
+ @page {
+ margin: 0.5cm;
+ }
+ p,
+ h2,
+ h3 {
+ orphans: 3;
+ widows: 3;
+ }
+ h2,
+ h3 {
+ page-break-after: avoid;
+ }
+}
+
+body {
+ margin: 0;
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-size: 14px;
+ line-height: 20px;
+ color: #333333;
+ background-color: #ffffff;
+}
+
+a {
+ color: #0088cc;
+ text-decoration: none;
+}
+
+a:hover,
+a:focus {
+ color: #005580;
+ text-decoration: underline;
+}
+
+.img-rounded {
+ -webkit-border-radius: 6px;
+ -moz-border-radius: 6px;
+ border-radius: 6px;
+}
+
+.img-polaroid {
+ padding: 4px;
+ background-color: #fff;
+ border: 1px solid #ccc;
+ border: 1px solid rgba(0, 0, 0, 0.2);
+ -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+ -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+}
+
+.img-circle {
+ -webkit-border-radius: 500px;
+ -moz-border-radius: 500px;
+ border-radius: 500px;
+}
+
+.row {
+ margin-left: -20px;
+ *zoom: 1;
+}
+
+.row:before,
+.row:after {
+ display: table;
+ line-height: 0;
+ content: "";
+}
+
+.row:after {
+ clear: both;
+}
+
+[class*="span"] {
+ float: left;
+ min-height: 1px;
+ margin-left: 20px;
+}
+
+.container,
+.navbar-static-top .container,
+.navbar-fixed-top .container,
+.navbar-fixed-bottom .container {
+ width: 940px;
+}
+
+.span12 {
+ width: 940px;
+}
+
+.span11 {
+ width: 860px;
+}
+
+.span10 {
+ width: 780px;
+}
+
+.span9 {
+ width: 700px;
+}
+
+.span8 {
+ width: 620px;
+}
+
+.span7 {
+ width: 540px;
+}
+
+.span6 {
+ width: 460px;
+}
+
+.span5 {
+ width: 380px;
+}
+
+.span4 {
+ width: 300px;
+}
+
+.span3 {
+ width: 220px;
+}
+
+.span2 {
+ width: 140px;
+}
+
+.span1 {
+ width: 60px;
+}
+
+.offset12 {
+ margin-left: 980px;
+}
+
+.offset11 {
+ margin-left: 900px;
+}
+
+.offset10 {
+ margin-left: 820px;
+}
+
+.offset9 {
+ margin-left: 740px;
+}
+
+.offset8 {
+ margin-left: 660px;
+}
+
+.offset7 {
+ margin-left: 580px;
+}
+
+.offset6 {
+ margin-left: 500px;
+}
+
+.offset5 {
+ margin-left: 420px;
+}
+
+.offset4 {
+ margin-left: 340px;
+}
+
+.offset3 {
+ margin-left: 260px;
+}
+
+.offset2 {
+ margin-left: 180px;
+}
+
+.offset1 {
+ margin-left: 100px;
+}
+
+.row-fluid {
+ width: 100%;
+ *zoom: 1;
+}
+
+.row-fluid:before,
+.row-fluid:after {
+ display: table;
+ line-height: 0;
+ content: "";
+}
+
+.row-fluid:after {
+ clear: both;
+}
+
+.row-fluid [class*="span"] {
+ display: block;
+ float: left;
+ width: 100%;
+ min-height: 30px;
+ margin-left: 2.127659574468085%;
+ *margin-left: 2.074468085106383%;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+
+.row-fluid [class*="span"]:first-child {
+ margin-left: 0;
+}
+
+.row-fluid .controls-row [class*="span"] + [class*="span"] {
+ margin-left: 2.127659574468085%;
+}
+
+.row-fluid .span12 {
+ width: 100%;
+ *width: 99.94680851063829%;
+}
+
+.row-fluid .span11 {
+ width: 91.48936170212765%;
+ *width: 91.43617021276594%;
+}
+
+.row-fluid .span10 {
+ width: 82.97872340425532%;
+ *width: 82.92553191489361%;
+}
+
+.row-fluid .span9 {
+ width: 74.46808510638297%;
+ *width: 74.41489361702126%;
+}
+
+.row-fluid .span8 {
+ width: 65.95744680851064%;
+ *width: 65.90425531914893%;
+}
+
+.row-fluid .span7 {
+ width: 57.44680851063829%;
+ *width: 57.39361702127659%;
+}
+
+.row-fluid .span6 {
+ width: 48.93617021276595%;
+ *width: 48.88297872340425%;
+}
+
+.row-fluid .span5 {
+ width: 40.42553191489362%;
+ *width: 40.37234042553192%;
+}
+
+.row-fluid .span4 {
+ width: 31.914893617021278%;
+ *width: 31.861702127659576%;
+}
+
+.row-fluid .span3 {
+ width: 23.404255319148934%;
+ *width: 23.351063829787233%;
+}
+
+.row-fluid .span2 {
+ width: 14.893617021276595%;
+ *width: 14.840425531914894%;
+}
+
+.row-fluid .span1 {
+ width: 6.382978723404255%;
+ *width: 6.329787234042553%;
+}
+
+.row-fluid .offset12 {
+ margin-left: 104.25531914893617%;
+ *margin-left: 104.14893617021275%;
+}
+
+.row-fluid .offset12:first-child {
+ margin-left: 102.12765957446808%;
+ *margin-left: 102.02127659574467%;
+}
+
+.row-fluid .offset11 {
+ margin-left: 95.74468085106382%;
+ *margin-left: 95.6382978723404%;
+}
+
+.row-fluid .offset11:first-child {
+ margin-left: 93.61702127659574%;
+ *margin-left: 93.51063829787232%;
+}
+
+.row-fluid .offset10 {
+ margin-left: 87.23404255319149%;
+ *margin-left: 87.12765957446807%;
+}
+
+.row-fluid .offset10:first-child {
+ margin-left: 85.1063829787234%;
+ *margin-left: 84.99999999999999%;
+}
+
+.row-fluid .offset9 {
+ margin-left: 78.72340425531914%;
+ *margin-left: 78.61702127659572%;
+}
+
+.row-fluid .offset9:first-child {
+ margin-left: 76.59574468085106%;
+ *margin-left: 76.48936170212764%;
+}
+
+.row-fluid .offset8 {
+ margin-left: 70.2127659574468%;
+ *margin-left: 70.10638297872339%;
+}
+
+.row-fluid .offset8:first-child {
+ margin-left: 68.08510638297872%;
+ *margin-left: 67.9787234042553%;
+}
+
+.row-fluid .offset7 {
+ margin-left: 61.70212765957446%;
+ *margin-left: 61.59574468085106%;
+}
+
+.row-fluid .offset7:first-child {
+ margin-left: 59.574468085106375%;
+ *margin-left: 59.46808510638297%;
+}
+
+.row-fluid .offset6 {
+ margin-left: 53.191489361702125%;
+ *margin-left: 53.085106382978715%;
+}
+
+.row-fluid .offset6:first-child {
+ margin-left: 51.063829787234035%;
+ *margin-left: 50.95744680851063%;
+}
+
+.row-fluid .offset5 {
+ margin-left: 44.68085106382979%;
+ *margin-left: 44.57446808510638%;
+}
+
+.row-fluid .offset5:first-child {
+ margin-left: 42.5531914893617%;
+ *margin-left: 42.4468085106383%;
+}
+
+.row-fluid .offset4 {
+ margin-left: 36.170212765957444%;
+ *margin-left: 36.06382978723405%;
+}
+
+.row-fluid .offset4:first-child {
+ margin-left: 34.04255319148936%;
+ *margin-left: 33.93617021276596%;
+}
+
+.row-fluid .offset3 {
+ margin-left: 27.659574468085104%;
+ *margin-left: 27.5531914893617%;
+}
+
+.row-fluid .offset3:first-child {
+ margin-left: 25.53191489361702%;
+ *margin-left: 25.425531914893618%;
+}
+
+.row-fluid .offset2 {
+ margin-left: 19.148936170212764%;
+ *margin-left: 19.04255319148936%;
+}
+
+.row-fluid .offset2:first-child {
+ margin-left: 17.02127659574468%;
+ *margin-left: 16.914893617021278%;
+}
+
+.row-fluid .offset1 {
+ margin-left: 10.638297872340425%;
+ *margin-left: 10.53191489361702%;
+}
+
+.row-fluid .offset1:first-child {
+ margin-left: 8.51063829787234%;
+ *margin-left: 8.404255319148938%;
+}
+
+[class*="span"].hide,
+.row-fluid [class*="span"].hide {
+ display: none;
+}
+
+[class*="span"].pull-right,
+.row-fluid [class*="span"].pull-right {
+ float: right;
+}
+
+.container {
+ margin-right: auto;
+ margin-left: auto;
+ *zoom: 1;
+}
+
+.container:before,
+.container:after {
+ display: table;
+ line-height: 0;
+ content: "";
+}
+
+.container:after {
+ clear: both;
+}
+
+.container-fluid {
+ padding-right: 20px;
+ padding-left: 20px;
+ *zoom: 1;
+}
+
+.container-fluid:before,
+.container-fluid:after {
+ display: table;
+ line-height: 0;
+ content: "";
+}
+
+.container-fluid:after {
+ clear: both;
+}
+
+p {
+ margin: 0 0 10px;
+}
+
+.lead {
+ margin-bottom: 20px;
+ font-size: 21px;
+ font-weight: 200;
+ line-height: 30px;
+}
+
+small {
+ font-size: 85%;
+}
+
+strong {
+ font-weight: bold;
+}
+
+em {
+ font-style: italic;
+}
+
+cite {
+ font-style: normal;
+}
+
+.muted {
+ color: #999999;
+}
+
+a.muted:hover,
+a.muted:focus {
+ color: #808080;
+}
+
+.text-warning {
+ color: #c09853;
+}
+
+a.text-warning:hover,
+a.text-warning:focus {
+ color: #a47e3c;
+}
+
+.text-error {
+ color: #b94a48;
+}
+
+a.text-error:hover,
+a.text-error:focus {
+ color: #953b39;
+}
+
+.text-info {
+ color: #3a87ad;
+}
+
+a.text-info:hover,
+a.text-info:focus {
+ color: #2d6987;
+}
+
+.text-success {
+ color: #468847;
+}
+
+a.text-success:hover,
+a.text-success:focus {
+ color: #356635;
+}
+
+.text-left {
+ text-align: left;
+}
+
+.text-right {
+ text-align: right;
+}
+
+.text-center {
+ text-align: center;
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ margin: 10px 0;
+ font-family: inherit;
+ font-weight: bold;
+ line-height: 20px;
+ color: inherit;
+ text-rendering: optimizelegibility;
+}
+
+h1 small,
+h2 small,
+h3 small,
+h4 small,
+h5 small,
+h6 small {
+ font-weight: normal;
+ line-height: 1;
+ color: #999999;
+}
+
+h1,
+h2,
+h3 {
+ line-height: 40px;
+}
+
+h1 {
+ font-size: 38.5px;
+}
+
+h2 {
+ font-size: 31.5px;
+}
+
+h3 {
+ font-size: 24.5px;
+}
+
+h4 {
+ font-size: 17.5px;
+}
+
+h5 {
+ font-size: 14px;
+}
+
+h6 {
+ font-size: 11.9px;
+}
+
+h1 small {
+ font-size: 24.5px;
+}
+
+h2 small {
+ font-size: 17.5px;
+}
+
+h3 small {
+ font-size: 14px;
+}
+
+h4 small {
+ font-size: 14px;
+}
+
+.page-header {
+ padding-bottom: 9px;
+ margin: 20px 0 30px;
+ border-bottom: 1px solid #eeeeee;
+}
+
+ul,
+ol {
+ padding: 0;
+ margin: 0 0 10px 25px;
+}
+
+ul ul,
+ul ol,
+ol ol,
+ol ul {
+ margin-bottom: 0;
+}
+
+li {
+ line-height: 20px;
+}
+
+ul.unstyled,
+ol.unstyled {
+ margin-left: 0;
+ list-style: none;
+}
+
+ul.inline,
+ol.inline {
+ margin-left: 0;
+ list-style: none;
+}
+
+ul.inline > li,
+ol.inline > li {
+ display: inline-block;
+ *display: inline;
+ padding-right: 5px;
+ padding-left: 5px;
+ *zoom: 1;
+}
+
+dl {
+ margin-bottom: 20px;
+}
+
+dt,
+dd {
+ line-height: 20px;
+}
+
+dt {
+ font-weight: bold;
+}
+
+dd {
+ margin-left: 10px;
+}
+
+.dl-horizontal {
+ *zoom: 1;
+}
+
+.dl-horizontal:before,
+.dl-horizontal:after {
+ display: table;
+ line-height: 0;
+ content: "";
+}
+
+.dl-horizontal:after {
+ clear: both;
+}
+
+.dl-horizontal dt {
+ float: left;
+ width: 160px;
+ overflow: hidden;
+ clear: left;
+ text-align: right;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.dl-horizontal dd {
+ margin-left: 180px;
+}
+
+hr {
+ margin: 20px 0;
+ border: 0;
+ border-top: 1px solid #eeeeee;
+ border-bottom: 1px solid #ffffff;
+}
+
+abbr[title],
+abbr[data-original-title] {
+ cursor: help;
+ border-bottom: 1px dotted #999999;
+}
+
+abbr.initialism {
+ font-size: 90%;
+ text-transform: uppercase;
+}
+
+blockquote {
+ padding: 0 0 0 15px;
+ margin: 0 0 20px;
+ border-left: 5px solid #eeeeee;
+}
+
+blockquote p {
+ margin-bottom: 0;
+ font-size: 17.5px;
+ font-weight: 300;
+ line-height: 1.25;
+}
+
+blockquote small {
+ display: block;
+ line-height: 20px;
+ color: #999999;
+}
+
+blockquote small:before {
+ content: '\2014 \00A0';
+}
+
+blockquote.pull-right {
+ float: right;
+ padding-right: 15px;
+ padding-left: 0;
+ border-right: 5px solid #eeeeee;
+ border-left: 0;
+}
+
+blockquote.pull-right p,
+blockquote.pull-right small {
+ text-align: right;
+}
+
+blockquote.pull-right small:before {
+ content: '';
+}
+
+blockquote.pull-right small:after {
+ content: '\00A0 \2014';
+}
+
+q:before,
+q:after,
+blockquote:before,
+blockquote:after {
+ content: "";
+}
+
+address {
+ display: block;
+ margin-bottom: 20px;
+ font-style: normal;
+ line-height: 20px;
+}
+
+code,
+pre {
+ padding: 0 3px 2px;
+ font-family: Monaco, Menlo, Consolas, "Courier New", monospace;
+ font-size: 12px;
+ color: #333333;
+ -webkit-border-radius: 3px;
+ -moz-border-radius: 3px;
+ border-radius: 3px;
+}
+
+code {
+ padding: 2px 4px;
+ color: #d14;
+ white-space: nowrap;
+ background-color: #f7f7f9;
+ border: 1px solid #e1e1e8;
+}
+
+pre {
+ display: block;
+ padding: 9.5px;
+ margin: 0 0 10px;
+ font-size: 13px;
+ line-height: 20px;
+ word-break: break-all;
+ word-wrap: break-word;
+ white-space: pre;
+ white-space: pre-wrap;
+ background-color: #f5f5f5;
+ border: 1px solid #ccc;
+ border: 1px solid rgba(0, 0, 0, 0.15);
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+}
+
+pre.prettyprint {
+ margin-bottom: 20px;
+}
+
+pre code {
+ padding: 0;
+ color: inherit;
+ white-space: pre;
+ white-space: pre-wrap;
+ background-color: transparent;
+ border: 0;
+}
+
+.pre-scrollable {
+ max-height: 340px;
+ overflow-y: scroll;
+}
+
+form {
+ margin: 0 0 20px;
+}
+
+fieldset {
+ padding: 0;
+ margin: 0;
+ border: 0;
+}
+
+legend {
+ display: block;
+ width: 100%;
+ padding: 0;
+ margin-bottom: 20px;
+ font-size: 21px;
+ line-height: 40px;
+ color: #333333;
+ border: 0;
+ border-bottom: 1px solid #e5e5e5;
+}
+
+legend small {
+ font-size: 15px;
+ color: #999999;
+}
+
+label,
+input,
+button,
+select,
+textarea {
+ font-size: 14px;
+ font-weight: normal;
+ line-height: 20px;
+}
+
+input,
+button,
+select,
+textarea {
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+}
+
+label {
+ display: block;
+ margin-bottom: 5px;
+}
+
+select,
+textarea,
+input[type="text"],
+input[type="password"],
+input[type="datetime"],
+input[type="datetime-local"],
+input[type="date"],
+input[type="month"],
+input[type="time"],
+input[type="week"],
+input[type="number"],
+input[type="email"],
+input[type="url"],
+input[type="search"],
+input[type="tel"],
+input[type="color"],
+.uneditable-input {
+ display: inline-block;
+ height: 20px;
+ padding: 4px 6px;
+ margin-bottom: 10px;
+ font-size: 14px;
+ line-height: 20px;
+ color: #555555;
+ vertical-align: middle;
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+}
+
+input,
+textarea,
+.uneditable-input {
+ width: 206px;
+}
+
+textarea {
+ height: auto;
+}
+
+textarea,
+input[type="text"],
+input[type="password"],
+input[type="datetime"],
+input[type="datetime-local"],
+input[type="date"],
+input[type="month"],
+input[type="time"],
+input[type="week"],
+input[type="number"],
+input[type="email"],
+input[type="url"],
+input[type="search"],
+input[type="tel"],
+input[type="color"],
+.uneditable-input {
+ background-color: #ffffff;
+ border: 1px solid #cccccc;
+ -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+ -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+ box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+ -webkit-transition: border linear 0.2s, box-shadow linear 0.2s;
+ -moz-transition: border linear 0.2s, box-shadow linear 0.2s;
+ -o-transition: border linear 0.2s, box-shadow linear 0.2s;
+ transition: border linear 0.2s, box-shadow linear 0.2s;
+}
+
+textarea:focus,
+input[type="text"]:focus,
+input[type="password"]:focus,
+input[type="datetime"]:focus,
+input[type="datetime-local"]:focus,
+input[type="date"]:focus,
+input[type="month"]:focus,
+input[type="time"]:focus,
+input[type="week"]:focus,
+input[type="number"]:focus,
+input[type="email"]:focus,
+input[type="url"]:focus,
+input[type="search"]:focus,
+input[type="tel"]:focus,
+input[type="color"]:focus,
+.uneditable-input:focus {
+ border-color: rgba(82, 168, 236, 0.8);
+ outline: 0;
+ outline: thin dotted \9;
+ /* IE6-9 */
+
+ -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
+ -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
+ box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
+}
+
+input[type="radio"],
+input[type="checkbox"] {
+ margin: 4px 0 0;
+ margin-top: 1px \9;
+ *margin-top: 0;
+ line-height: normal;
+}
+
+input[type="file"],
+input[type="image"],
+input[type="submit"],
+input[type="reset"],
+input[type="button"],
+input[type="radio"],
+input[type="checkbox"] {
+ width: auto;
+}
+
+select,
+input[type="file"] {
+ height: 30px;
+ /* In IE7, the height of the select element cannot be changed by height, only font-size */
+
+ *margin-top: 4px;
+ /* For IE7, add top margin to align select with labels */
+
+ line-height: 30px;
+}
+
+select {
+ width: 220px;
+ background-color: #ffffff;
+ border: 1px solid #cccccc;
+}
+
+select[multiple],
+select[size] {
+ height: auto;
+}
+
+select:focus,
+input[type="file"]:focus,
+input[type="radio"]:focus,
+input[type="checkbox"]:focus {
+ outline: thin dotted #333;
+ outline: 5px auto -webkit-focus-ring-color;
+ outline-offset: -2px;
+}
+
+.uneditable-input,
+.uneditable-textarea {
+ color: #999999;
+ cursor: not-allowed;
+ background-color: #fcfcfc;
+ border-color: #cccccc;
+ -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025);
+ -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025);
+ box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025);
+}
+
+.uneditable-input {
+ overflow: hidden;
+ white-space: nowrap;
+}
+
+.uneditable-textarea {
+ width: auto;
+ height: auto;
+}
+
+input:-moz-placeholder,
+textarea:-moz-placeholder {
+ color: #999999;
+}
+
+input:-ms-input-placeholder,
+textarea:-ms-input-placeholder {
+ color: #999999;
+}
+
+input::-webkit-input-placeholder,
+textarea::-webkit-input-placeholder {
+ color: #999999;
+}
+
+.radio,
+.checkbox {
+ min-height: 20px;
+ padding-left: 20px;
+}
+
+.radio input[type="radio"],
+.checkbox input[type="checkbox"] {
+ float: left;
+ margin-left: -20px;
+}
+
+.controls > .radio:first-child,
+.controls > .checkbox:first-child {
+ padding-top: 5px;
+}
+
+.radio.inline,
+.checkbox.inline {
+ display: inline-block;
+ padding-top: 5px;
+ margin-bottom: 0;
+ vertical-align: middle;
+}
+
+.radio.inline + .radio.inline,
+.checkbox.inline + .checkbox.inline {
+ margin-left: 10px;
+}
+
+.input-mini {
+ width: 60px;
+}
+
+.input-small {
+ width: 90px;
+}
+
+.input-medium {
+ width: 150px;
+}
+
+.input-large {
+ width: 210px;
+}
+
+.input-xlarge {
+ width: 270px;
+}
+
+.input-xxlarge {
+ width: 530px;
+}
+
+input[class*="span"],
+select[class*="span"],
+textarea[class*="span"],
+.uneditable-input[class*="span"],
+.row-fluid input[class*="span"],
+.row-fluid select[class*="span"],
+.row-fluid textarea[class*="span"],
+.row-fluid .uneditable-input[class*="span"] {
+ float: none;
+ margin-left: 0;
+}
+
+.input-append input[class*="span"],
+.input-append .uneditable-input[class*="span"],
+.input-prepend input[class*="span"],
+.input-prepend .uneditable-input[class*="span"],
+.row-fluid input[class*="span"],
+.row-fluid select[class*="span"],
+.row-fluid textarea[class*="span"],
+.row-fluid .uneditable-input[class*="span"],
+.row-fluid .input-prepend [class*="span"],
+.row-fluid .input-append [class*="span"] {
+ display: inline-block;
+}
+
+input,
+textarea,
+.uneditable-input {
+ margin-left: 0;
+}
+
+.controls-row [class*="span"] + [class*="span"] {
+ margin-left: 20px;
+}
+
+input.span12,
+textarea.span12,
+.uneditable-input.span12 {
+ width: 926px;
+}
+
+input.span11,
+textarea.span11,
+.uneditable-input.span11 {
+ width: 846px;
+}
+
+input.span10,
+textarea.span10,
+.uneditable-input.span10 {
+ width: 766px;
+}
+
+input.span9,
+textarea.span9,
+.uneditable-input.span9 {
+ width: 686px;
+}
+
+input.span8,
+textarea.span8,
+.uneditable-input.span8 {
+ width: 606px;
+}
+
+input.span7,
+textarea.span7,
+.uneditable-input.span7 {
+ width: 526px;
+}
+
+input.span6,
+textarea.span6,
+.uneditable-input.span6 {
+ width: 446px;
+}
+
+input.span5,
+textarea.span5,
+.uneditable-input.span5 {
+ width: 366px;
+}
+
+input.span4,
+textarea.span4,
+.uneditable-input.span4 {
+ width: 286px;
+}
+
+input.span3,
+textarea.span3,
+.uneditable-input.span3 {
+ width: 206px;
+}
+
+input.span2,
+textarea.span2,
+.uneditable-input.span2 {
+ width: 126px;
+}
+
+input.span1,
+textarea.span1,
+.uneditable-input.span1 {
+ width: 46px;
+}
+
+.controls-row {
+ *zoom: 1;
+}
+
+.controls-row:before,
+.controls-row:after {
+ display: table;
+ line-height: 0;
+ content: "";
+}
+
+.controls-row:after {
+ clear: both;
+}
+
+.controls-row [class*="span"],
+.row-fluid .controls-row [class*="span"] {
+ float: left;
+}
+
+.controls-row .checkbox[class*="span"],
+.controls-row .radio[class*="span"] {
+ padding-top: 5px;
+}
+
+input[disabled],
+select[disabled],
+textarea[disabled],
+input[readonly],
+select[readonly],
+textarea[readonly] {
+ cursor: not-allowed;
+ background-color: #eeeeee;
+}
+
+input[type="radio"][disabled],
+input[type="checkbox"][disabled],
+input[type="radio"][readonly],
+input[type="checkbox"][readonly] {
+ background-color: transparent;
+}
+
+.control-group.warning .control-label,
+.control-group.warning .help-block,
+.control-group.warning .help-inline {
+ color: #c09853;
+}
+
+.control-group.warning .checkbox,
+.control-group.warning .radio,
+.control-group.warning input,
+.control-group.warning select,
+.control-group.warning textarea {
+ color: #c09853;
+}
+
+.control-group.warning input,
+.control-group.warning select,
+.control-group.warning textarea {
+ border-color: #c09853;
+ -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+ -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+ box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+}
+
+.control-group.warning input:focus,
+.control-group.warning select:focus,
+.control-group.warning textarea:focus {
+ border-color: #a47e3c;
+ -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e;
+ -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e;
+ box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e;
+}
+
+.control-group.warning .input-prepend .add-on,
+.control-group.warning .input-append .add-on {
+ color: #c09853;
+ background-color: #fcf8e3;
+ border-color: #c09853;
+}
+
+.control-group.error .control-label,
+.control-group.error .help-block,
+.control-group.error .help-inline {
+ color: #b94a48;
+}
+
+.control-group.error .checkbox,
+.control-group.error .radio,
+.control-group.error input,
+.control-group.error select,
+.control-group.error textarea {
+ color: #b94a48;
+}
+
+.control-group.error input,
+.control-group.error select,
+.control-group.error textarea {
+ border-color: #b94a48;
+ -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+ -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+ box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+}
+
+.control-group.error input:focus,
+.control-group.error select:focus,
+.control-group.error textarea:focus {
+ border-color: #953b39;
+ -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392;
+ -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392;
+ box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392;
+}
+
+.control-group.error .input-prepend .add-on,
+.control-group.error .input-append .add-on {
+ color: #b94a48;
+ background-color: #f2dede;
+ border-color: #b94a48;
+}
+
+.control-group.success .control-label,
+.control-group.success .help-block,
+.control-group.success .help-inline {
+ color: #468847;
+}
+
+.control-group.success .checkbox,
+.control-group.success .radio,
+.control-group.success input,
+.control-group.success select,
+.control-group.success textarea {
+ color: #468847;
+}
+
+.control-group.success input,
+.control-group.success select,
+.control-group.success textarea {
+ border-color: #468847;
+ -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+ -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+ box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+}
+
+.control-group.success input:focus,
+.control-group.success select:focus,
+.control-group.success textarea:focus {
+ border-color: #356635;
+ -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b;
+ -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b;
+ box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b;
+}
+
+.control-group.success .input-prepend .add-on,
+.control-group.success .input-append .add-on {
+ color: #468847;
+ background-color: #dff0d8;
+ border-color: #468847;
+}
+
+.control-group.info .control-label,
+.control-group.info .help-block,
+.control-group.info .help-inline {
+ color: #3a87ad;
+}
+
+.control-group.info .checkbox,
+.control-group.info .radio,
+.control-group.info input,
+.control-group.info select,
+.control-group.info textarea {
+ color: #3a87ad;
+}
+
+.control-group.info input,
+.control-group.info select,
+.control-group.info textarea {
+ border-color: #3a87ad;
+ -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+ -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+ box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+}
+
+.control-group.info input:focus,
+.control-group.info select:focus,
+.control-group.info textarea:focus {
+ border-color: #2d6987;
+ -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7ab5d3;
+ -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7ab5d3;
+ box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7ab5d3;
+}
+
+.control-group.info .input-prepend .add-on,
+.control-group.info .input-append .add-on {
+ color: #3a87ad;
+ background-color: #d9edf7;
+ border-color: #3a87ad;
+}
+
+input:focus:invalid,
+textarea:focus:invalid,
+select:focus:invalid {
+ color: #b94a48;
+ border-color: #ee5f5b;
+}
+
+input:focus:invalid:focus,
+textarea:focus:invalid:focus,
+select:focus:invalid:focus {
+ border-color: #e9322d;
+ -webkit-box-shadow: 0 0 6px #f8b9b7;
+ -moz-box-shadow: 0 0 6px #f8b9b7;
+ box-shadow: 0 0 6px #f8b9b7;
+}
+
+.form-actions {
+ padding: 19px 20px 20px;
+ margin-top: 20px;
+ margin-bottom: 20px;
+ background-color: #f5f5f5;
+ border-top: 1px solid #e5e5e5;
+ *zoom: 1;
+}
+
+.form-actions:before,
+.form-actions:after {
+ display: table;
+ line-height: 0;
+ content: "";
+}
+
+.form-actions:after {
+ clear: both;
+}
+
+.help-block,
+.help-inline {
+ color: #595959;
+}
+
+.help-block {
+ display: block;
+ margin-bottom: 10px;
+}
+
+.help-inline {
+ display: inline-block;
+ *display: inline;
+ padding-left: 5px;
+ vertical-align: middle;
+ *zoom: 1;
+}
+
+.input-append,
+.input-prepend {
+ display: inline-block;
+ margin-bottom: 10px;
+ font-size: 0;
+ white-space: nowrap;
+ vertical-align: middle;
+}
+
+.input-append input,
+.input-prepend input,
+.input-append select,
+.input-prepend select,
+.input-append .uneditable-input,
+.input-prepend .uneditable-input,
+.input-append .dropdown-menu,
+.input-prepend .dropdown-menu,
+.input-append .popover,
+.input-prepend .popover {
+ font-size: 14px;
+}
+
+.input-append input,
+.input-prepend input,
+.input-append select,
+.input-prepend select,
+.input-append .uneditable-input,
+.input-prepend .uneditable-input {
+ position: relative;
+ margin-bottom: 0;
+ *margin-left: 0;
+ vertical-align: top;
+ -webkit-border-radius: 0 4px 4px 0;
+ -moz-border-radius: 0 4px 4px 0;
+ border-radius: 0 4px 4px 0;
+}
+
+.input-append input:focus,
+.input-prepend input:focus,
+.input-append select:focus,
+.input-prepend select:focus,
+.input-append .uneditable-input:focus,
+.input-prepend .uneditable-input:focus {
+ z-index: 2;
+}
+
+.input-append .add-on,
+.input-prepend .add-on {
+ display: inline-block;
+ width: auto;
+ height: 20px;
+ min-width: 16px;
+ padding: 4px 5px;
+ font-size: 14px;
+ font-weight: normal;
+ line-height: 20px;
+ text-align: center;
+ text-shadow: 0 1px 0 #ffffff;
+ background-color: #eeeeee;
+ border: 1px solid #ccc;
+}
+
+.input-append .add-on,
+.input-prepend .add-on,
+.input-append .btn,
+.input-prepend .btn,
+.input-append .btn-group > .dropdown-toggle,
+.input-prepend .btn-group > .dropdown-toggle {
+ vertical-align: top;
+ -webkit-border-radius: 0;
+ -moz-border-radius: 0;
+ border-radius: 0;
+}
+
+.input-append .active,
+.input-prepend .active {
+ background-color: #a9dba9;
+ border-color: #46a546;
+}
+
+.input-prepend .add-on,
+.input-prepend .btn {
+ margin-right: -1px;
+}
+
+.input-prepend .add-on:first-child,
+.input-prepend .btn:first-child {
+ -webkit-border-radius: 4px 0 0 4px;
+ -moz-border-radius: 4px 0 0 4px;
+ border-radius: 4px 0 0 4px;
+}
+
+.input-append input,
+.input-append select,
+.input-append .uneditable-input {
+ -webkit-border-radius: 4px 0 0 4px;
+ -moz-border-radius: 4px 0 0 4px;
+ border-radius: 4px 0 0 4px;
+}
+
+.input-append input + .btn-group .btn:last-child,
+.input-append select + .btn-group .btn:last-child,
+.input-append .uneditable-input + .btn-group .btn:last-child {
+ -webkit-border-radius: 0 4px 4px 0;
+ -moz-border-radius: 0 4px 4px 0;
+ border-radius: 0 4px 4px 0;
+}
+
+.input-append .add-on,
+.input-append .btn,
+.input-append .btn-group {
+ margin-left: -1px;
+}
+
+.input-append .add-on:last-child,
+.input-append .btn:last-child,
+.input-append .btn-group:last-child > .dropdown-toggle {
+ -webkit-border-radius: 0 4px 4px 0;
+ -moz-border-radius: 0 4px 4px 0;
+ border-radius: 0 4px 4px 0;
+}
+
+.input-prepend.input-append input,
+.input-prepend.input-append select,
+.input-prepend.input-append .uneditable-input {
+ -webkit-border-radius: 0;
+ -moz-border-radius: 0;
+ border-radius: 0;
+}
+
+.input-prepend.input-append input + .btn-group .btn,
+.input-prepend.input-append select + .btn-group .btn,
+.input-prepend.input-append .uneditable-input + .btn-group .btn {
+ -webkit-border-radius: 0 4px 4px 0;
+ -moz-border-radius: 0 4px 4px 0;
+ border-radius: 0 4px 4px 0;
+}
+
+.input-prepend.input-append .add-on:first-child,
+.input-prepend.input-append .btn:first-child {
+ margin-right: -1px;
+ -webkit-border-radius: 4px 0 0 4px;
+ -moz-border-radius: 4px 0 0 4px;
+ border-radius: 4px 0 0 4px;
+}
+
+.input-prepend.input-append .add-on:last-child,
+.input-prepend.input-append .btn:last-child {
+ margin-left: -1px;
+ -webkit-border-radius: 0 4px 4px 0;
+ -moz-border-radius: 0 4px 4px 0;
+ border-radius: 0 4px 4px 0;
+}
+
+.input-prepend.input-append .btn-group:first-child {
+ margin-left: 0;
+}
+
+input.search-query {
+ padding-right: 14px;
+ padding-right: 4px \9;
+ padding-left: 14px;
+ padding-left: 4px \9;
+ /* IE7-8 doesn't have border-radius, so don't indent the padding */
+
+ margin-bottom: 0;
+ -webkit-border-radius: 15px;
+ -moz-border-radius: 15px;
+ border-radius: 15px;
+}
+
+/* Allow for input prepend/append in search forms */
+
+.form-search .input-append .search-query,
+.form-search .input-prepend .search-query {
+ -webkit-border-radius: 0;
+ -moz-border-radius: 0;
+ border-radius: 0;
+}
+
+.form-search .input-append .search-query {
+ -webkit-border-radius: 14px 0 0 14px;
+ -moz-border-radius: 14px 0 0 14px;
+ border-radius: 14px 0 0 14px;
+}
+
+.form-search .input-append .btn {
+ -webkit-border-radius: 0 14px 14px 0;
+ -moz-border-radius: 0 14px 14px 0;
+ border-radius: 0 14px 14px 0;
+}
+
+.form-search .input-prepend .search-query {
+ -webkit-border-radius: 0 14px 14px 0;
+ -moz-border-radius: 0 14px 14px 0;
+ border-radius: 0 14px 14px 0;
+}
+
+.form-search .input-prepend .btn {
+ -webkit-border-radius: 14px 0 0 14px;
+ -moz-border-radius: 14px 0 0 14px;
+ border-radius: 14px 0 0 14px;
+}
+
+.form-search input,
+.form-inline input,
+.form-horizontal input,
+.form-search textarea,
+.form-inline textarea,
+.form-horizontal textarea,
+.form-search select,
+.form-inline select,
+.form-horizontal select,
+.form-search .help-inline,
+.form-inline .help-inline,
+.form-horizontal .help-inline,
+.form-search .uneditable-input,
+.form-inline .uneditable-input,
+.form-horizontal .uneditable-input,
+.form-search .input-prepend,
+.form-inline .input-prepend,
+.form-horizontal .input-prepend,
+.form-search .input-append,
+.form-inline .input-append,
+.form-horizontal .input-append {
+ display: inline-block;
+ *display: inline;
+ margin-bottom: 0;
+ vertical-align: middle;
+ *zoom: 1;
+}
+
+.form-search .hide,
+.form-inline .hide,
+.form-horizontal .hide {
+ display: none;
+}
+
+.form-search label,
+.form-inline label,
+.form-search .btn-group,
+.form-inline .btn-group {
+ display: inline-block;
+}
+
+.form-search .input-append,
+.form-inline .input-append,
+.form-search .input-prepend,
+.form-inline .input-prepend {
+ margin-bottom: 0;
+}
+
+.form-search .radio,
+.form-search .checkbox,
+.form-inline .radio,
+.form-inline .checkbox {
+ padding-left: 0;
+ margin-bottom: 0;
+ vertical-align: middle;
+}
+
+.form-search .radio input[type="radio"],
+.form-search .checkbox input[type="checkbox"],
+.form-inline .radio input[type="radio"],
+.form-inline .checkbox input[type="checkbox"] {
+ float: left;
+ margin-right: 3px;
+ margin-left: 0;
+}
+
+.control-group {
+ margin-bottom: 10px;
+}
+
+legend + .control-group {
+ margin-top: 20px;
+ -webkit-margin-top-collapse: separate;
+}
+
+.form-horizontal .control-group {
+ margin-bottom: 20px;
+ *zoom: 1;
+}
+
+.form-horizontal .control-group:before,
+.form-horizontal .control-group:after {
+ display: table;
+ line-height: 0;
+ content: "";
+}
+
+.form-horizontal .control-group:after {
+ clear: both;
+}
+
+.form-horizontal .control-label {
+ float: left;
+ width: 160px;
+ padding-top: 5px;
+ text-align: right;
+}
+
+.form-horizontal .controls {
+ *display: inline-block;
+ *padding-left: 20px;
+ margin-left: 180px;
+ *margin-left: 0;
+}
+
+.form-horizontal .controls:first-child {
+ *padding-left: 180px;
+}
+
+.form-horizontal .help-block {
+ margin-bottom: 0;
+}
+
+.form-horizontal input + .help-block,
+.form-horizontal select + .help-block,
+.form-horizontal textarea + .help-block,
+.form-horizontal .uneditable-input + .help-block,
+.form-horizontal .input-prepend + .help-block,
+.form-horizontal .input-append + .help-block {
+ margin-top: 10px;
+}
+
+.form-horizontal .form-actions {
+ padding-left: 180px;
+}
+
+table {
+ max-width: 100%;
+ background-color: transparent;
+ border-collapse: collapse;
+ border-spacing: 0;
+}
+
+.table {
+ width: 100%;
+ margin-bottom: 20px;
+}
+
+.table th,
+.table td {
+ padding: 8px;
+ line-height: 20px;
+ text-align: left;
+ vertical-align: top;
+ border-top: 1px solid #dddddd;
+}
+
+.table th {
+ font-weight: bold;
+}
+
+.table thead th {
+ vertical-align: bottom;
+}
+
+.table caption + thead tr:first-child th,
+.table caption + thead tr:first-child td,
+.table colgroup + thead tr:first-child th,
+.table colgroup + thead tr:first-child td,
+.table thead:first-child tr:first-child th,
+.table thead:first-child tr:first-child td {
+ border-top: 0;
+}
+
+.table tbody + tbody {
+ border-top: 2px solid #dddddd;
+}
+
+.table .table {
+ background-color: #ffffff;
+}
+
+.table-condensed th,
+.table-condensed td {
+ padding: 4px 5px;
+}
+
+.table-bordered {
+ border: 1px solid #dddddd;
+ border-collapse: separate;
+ *border-collapse: collapse;
+ border-left: 0;
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+}
+
+.table-bordered th,
+.table-bordered td {
+ border-left: 1px solid #dddddd;
+}
+
+.table-bordered caption + thead tr:first-child th,
+.table-bordered caption + tbody tr:first-child th,
+.table-bordered caption + tbody tr:first-child td,
+.table-bordered colgroup + thead tr:first-child th,
+.table-bordered colgroup + tbody tr:first-child th,
+.table-bordered colgroup + tbody tr:first-child td,
+.table-bordered thead:first-child tr:first-child th,
+.table-bordered tbody:first-child tr:first-child th,
+.table-bordered tbody:first-child tr:first-child td {
+ border-top: 0;
+}
+
+.table-bordered thead:first-child tr:first-child > th:first-child,
+.table-bordered tbody:first-child tr:first-child > td:first-child,
+.table-bordered tbody:first-child tr:first-child > th:first-child {
+ -webkit-border-top-left-radius: 4px;
+ border-top-left-radius: 4px;
+ -moz-border-radius-topleft: 4px;
+}
+
+.table-bordered thead:first-child tr:first-child > th:last-child,
+.table-bordered tbody:first-child tr:first-child > td:last-child,
+.table-bordered tbody:first-child tr:first-child > th:last-child {
+ -webkit-border-top-right-radius: 4px;
+ border-top-right-radius: 4px;
+ -moz-border-radius-topright: 4px;
+}
+
+.table-bordered thead:last-child tr:last-child > th:first-child,
+.table-bordered tbody:last-child tr:last-child > td:first-child,
+.table-bordered tbody:last-child tr:last-child > th:first-child,
+.table-bordered tfoot:last-child tr:last-child > td:first-child,
+.table-bordered tfoot:last-child tr:last-child > th:first-child {
+ -webkit-border-bottom-left-radius: 4px;
+ border-bottom-left-radius: 4px;
+ -moz-border-radius-bottomleft: 4px;
+}
+
+.table-bordered thead:last-child tr:last-child > th:last-child,
+.table-bordered tbody:last-child tr:last-child > td:last-child,
+.table-bordered tbody:last-child tr:last-child > th:last-child,
+.table-bordered tfoot:last-child tr:last-child > td:last-child,
+.table-bordered tfoot:last-child tr:last-child > th:last-child {
+ -webkit-border-bottom-right-radius: 4px;
+ border-bottom-right-radius: 4px;
+ -moz-border-radius-bottomright: 4px;
+}
+
+.table-bordered tfoot + tbody:last-child tr:last-child td:first-child {
+ -webkit-border-bottom-left-radius: 0;
+ border-bottom-left-radius: 0;
+ -moz-border-radius-bottomleft: 0;
+}
+
+.table-bordered tfoot + tbody:last-child tr:last-child td:last-child {
+ -webkit-border-bottom-right-radius: 0;
+ border-bottom-right-radius: 0;
+ -moz-border-radius-bottomright: 0;
+}
+
+.table-bordered caption + thead tr:first-child th:first-child,
+.table-bordered caption + tbody tr:first-child td:first-child,
+.table-bordered colgroup + thead tr:first-child th:first-child,
+.table-bordered colgroup + tbody tr:first-child td:first-child {
+ -webkit-border-top-left-radius: 4px;
+ border-top-left-radius: 4px;
+ -moz-border-radius-topleft: 4px;
+}
+
+.table-bordered caption + thead tr:first-child th:last-child,
+.table-bordered caption + tbody tr:first-child td:last-child,
+.table-bordered colgroup + thead tr:first-child th:last-child,
+.table-bordered colgroup + tbody tr:first-child td:last-child {
+ -webkit-border-top-right-radius: 4px;
+ border-top-right-radius: 4px;
+ -moz-border-radius-topright: 4px;
+}
+
+.table-striped tbody > tr:nth-child(odd) > td,
+.table-striped tbody > tr:nth-child(odd) > th {
+ background-color: #f9f9f9;
+}
+
+.table-hover tbody tr:hover > td,
+.table-hover tbody tr:hover > th {
+ background-color: #f5f5f5;
+}
+
+table td[class*="span"],
+table th[class*="span"],
+.row-fluid table td[class*="span"],
+.row-fluid table th[class*="span"] {
+ display: table-cell;
+ float: none;
+ margin-left: 0;
+}
+
+.table td.span1,
+.table th.span1 {
+ float: none;
+ width: 44px;
+ margin-left: 0;
+}
+
+.table td.span2,
+.table th.span2 {
+ float: none;
+ width: 124px;
+ margin-left: 0;
+}
+
+.table td.span3,
+.table th.span3 {
+ float: none;
+ width: 204px;
+ margin-left: 0;
+}
+
+.table td.span4,
+.table th.span4 {
+ float: none;
+ width: 284px;
+ margin-left: 0;
+}
+
+.table td.span5,
+.table th.span5 {
+ float: none;
+ width: 364px;
+ margin-left: 0;
+}
+
+.table td.span6,
+.table th.span6 {
+ float: none;
+ width: 444px;
+ margin-left: 0;
+}
+
+.table td.span7,
+.table th.span7 {
+ float: none;
+ width: 524px;
+ margin-left: 0;
+}
+
+.table td.span8,
+.table th.span8 {
+ float: none;
+ width: 604px;
+ margin-left: 0;
+}
+
+.table td.span9,
+.table th.span9 {
+ float: none;
+ width: 684px;
+ margin-left: 0;
+}
+
+.table td.span10,
+.table th.span10 {
+ float: none;
+ width: 764px;
+ margin-left: 0;
+}
+
+.table td.span11,
+.table th.span11 {
+ float: none;
+ width: 844px;
+ margin-left: 0;
+}
+
+.table td.span12,
+.table th.span12 {
+ float: none;
+ width: 924px;
+ margin-left: 0;
+}
+
+.table tbody tr.success > td {
+ background-color: #dff0d8;
+}
+
+.table tbody tr.error > td {
+ background-color: #f2dede;
+}
+
+.table tbody tr.warning > td {
+ background-color: #fcf8e3;
+}
+
+.table tbody tr.info > td {
+ background-color: #d9edf7;
+}
+
+.table-hover tbody tr.success:hover > td {
+ background-color: #d0e9c6;
+}
+
+.table-hover tbody tr.error:hover > td {
+ background-color: #ebcccc;
+}
+
+.table-hover tbody tr.warning:hover > td {
+ background-color: #faf2cc;
+}
+
+.table-hover tbody tr.info:hover > td {
+ background-color: #c4e3f3;
+}
+
+[class^="icon-"],
+[class*=" icon-"] {
+ display: inline-block;
+ width: 14px;
+ height: 14px;
+ margin-top: 1px;
+ *margin-right: .3em;
+ line-height: 14px;
+ vertical-align: text-top;
+ background-image: url("../img/glyphicons-halflings.png");
+ background-position: 14px 14px;
+ background-repeat: no-repeat;
+}
+
+/* White icons with optional class, or on hover/focus/active states of certain elements */
+
+.icon-white,
+.nav-pills > .active > a > [class^="icon-"],
+.nav-pills > .active > a > [class*=" icon-"],
+.nav-list > .active > a > [class^="icon-"],
+.nav-list > .active > a > [class*=" icon-"],
+.navbar-inverse .nav > .active > a > [class^="icon-"],
+.navbar-inverse .nav > .active > a > [class*=" icon-"],
+.dropdown-menu > li > a:hover > [class^="icon-"],
+.dropdown-menu > li > a:focus > [class^="icon-"],
+.dropdown-menu > li > a:hover > [class*=" icon-"],
+.dropdown-menu > li > a:focus > [class*=" icon-"],
+.dropdown-menu > .active > a > [class^="icon-"],
+.dropdown-menu > .active > a > [class*=" icon-"],
+.dropdown-submenu:hover > a > [class^="icon-"],
+.dropdown-submenu:focus > a > [class^="icon-"],
+.dropdown-submenu:hover > a > [class*=" icon-"],
+.dropdown-submenu:focus > a > [class*=" icon-"] {
+ background-image: url("../img/glyphicons-halflings-white.png");
+}
+
+.icon-glass {
+ background-position: 0 0;
+}
+
+.icon-music {
+ background-position: -24px 0;
+}
+
+.icon-search {
+ background-position: -48px 0;
+}
+
+.icon-envelope {
+ background-position: -72px 0;
+}
+
+.icon-heart {
+ background-position: -96px 0;
+}
+
+.icon-star {
+ background-position: -120px 0;
+}
+
+.icon-star-empty {
+ background-position: -144px 0;
+}
+
+.icon-user {
+ background-position: -168px 0;
+}
+
+.icon-film {
+ background-position: -192px 0;
+}
+
+.icon-th-large {
+ background-position: -216px 0;
+}
+
+.icon-th {
+ background-position: -240px 0;
+}
+
+.icon-th-list {
+ background-position: -264px 0;
+}
+
+.icon-ok {
+ background-position: -288px 0;
+}
+
+.icon-remove {
+ background-position: -312px 0;
+}
+
+.icon-zoom-in {
+ background-position: -336px 0;
+}
+
+.icon-zoom-out {
+ background-position: -360px 0;
+}
+
+.icon-off {
+ background-position: -384px 0;
+}
+
+.icon-signal {
+ background-position: -408px 0;
+}
+
+.icon-cog {
+ background-position: -432px 0;
+}
+
+.icon-trash {
+ background-position: -456px 0;
+}
+
+.icon-home {
+ background-position: 0 -24px;
+}
+
+.icon-file {
+ background-position: -24px -24px;
+}
+
+.icon-time {
+ background-position: -48px -24px;
+}
+
+.icon-road {
+ background-position: -72px -24px;
+}
+
+.icon-download-alt {
+ background-position: -96px -24px;
+}
+
+.icon-download {
+ background-position: -120px -24px;
+}
+
+.icon-upload {
+ background-position: -144px -24px;
+}
+
+.icon-inbox {
+ background-position: -168px -24px;
+}
+
+.icon-play-circle {
+ background-position: -192px -24px;
+}
+
+.icon-repeat {
+ background-position: -216px -24px;
+}
+
+.icon-refresh {
+ background-position: -240px -24px;
+}
+
+.icon-list-alt {
+ background-position: -264px -24px;
+}
+
+.icon-lock {
+ background-position: -287px -24px;
+}
+
+.icon-flag {
+ background-position: -312px -24px;
+}
+
+.icon-headphones {
+ background-position: -336px -24px;
+}
+
+.icon-volume-off {
+ background-position: -360px -24px;
+}
+
+.icon-volume-down {
+ background-position: -384px -24px;
+}
+
+.icon-volume-up {
+ background-position: -408px -24px;
+}
+
+.icon-qrcode {
+ background-position: -432px -24px;
+}
+
+.icon-barcode {
+ background-position: -456px -24px;
+}
+
+.icon-tag {
+ background-position: 0 -48px;
+}
+
+.icon-tags {
+ background-position: -25px -48px;
+}
+
+.icon-book {
+ background-position: -48px -48px;
+}
+
+.icon-bookmark {
+ background-position: -72px -48px;
+}
+
+.icon-print {
+ background-position: -96px -48px;
+}
+
+.icon-camera {
+ background-position: -120px -48px;
+}
+
+.icon-font {
+ background-position: -144px -48px;
+}
+
+.icon-bold {
+ background-position: -167px -48px;
+}
+
+.icon-italic {
+ background-position: -192px -48px;
+}
+
+.icon-text-height {
+ background-position: -216px -48px;
+}
+
+.icon-text-width {
+ background-position: -240px -48px;
+}
+
+.icon-align-left {
+ background-position: -264px -48px;
+}
+
+.icon-align-center {
+ background-position: -288px -48px;
+}
+
+.icon-align-right {
+ background-position: -312px -48px;
+}
+
+.icon-align-justify {
+ background-position: -336px -48px;
+}
+
+.icon-list {
+ background-position: -360px -48px;
+}
+
+.icon-indent-left {
+ background-position: -384px -48px;
+}
+
+.icon-indent-right {
+ background-position: -408px -48px;
+}
+
+.icon-facetime-video {
+ background-position: -432px -48px;
+}
+
+.icon-picture {
+ background-position: -456px -48px;
+}
+
+.icon-pencil {
+ background-position: 0 -72px;
+}
+
+.icon-map-marker {
+ background-position: -24px -72px;
+}
+
+.icon-adjust {
+ background-position: -48px -72px;
+}
+
+.icon-tint {
+ background-position: -72px -72px;
+}
+
+.icon-edit {
+ background-position: -96px -72px;
+}
+
+.icon-share {
+ background-position: -120px -72px;
+}
+
+.icon-check {
+ background-position: -144px -72px;
+}
+
+.icon-move {
+ background-position: -168px -72px;
+}
+
+.icon-step-backward {
+ background-position: -192px -72px;
+}
+
+.icon-fast-backward {
+ background-position: -216px -72px;
+}
+
+.icon-backward {
+ background-position: -240px -72px;
+}
+
+.icon-play {
+ background-position: -264px -72px;
+}
+
+.icon-pause {
+ background-position: -288px -72px;
+}
+
+.icon-stop {
+ background-position: -312px -72px;
+}
+
+.icon-forward {
+ background-position: -336px -72px;
+}
+
+.icon-fast-forward {
+ background-position: -360px -72px;
+}
+
+.icon-step-forward {
+ background-position: -384px -72px;
+}
+
+.icon-eject {
+ background-position: -408px -72px;
+}
+
+.icon-chevron-left {
+ background-position: -432px -72px;
+}
+
+.icon-chevron-right {
+ background-position: -456px -72px;
+}
+
+.icon-plus-sign {
+ background-position: 0 -96px;
+}
+
+.icon-minus-sign {
+ background-position: -24px -96px;
+}
+
+.icon-remove-sign {
+ background-position: -48px -96px;
+}
+
+.icon-ok-sign {
+ background-position: -72px -96px;
+}
+
+.icon-question-sign {
+ background-position: -96px -96px;
+}
+
+.icon-info-sign {
+ background-position: -120px -96px;
+}
+
+.icon-screenshot {
+ background-position: -144px -96px;
+}
+
+.icon-remove-circle {
+ background-position: -168px -96px;
+}
+
+.icon-ok-circle {
+ background-position: -192px -96px;
+}
+
+.icon-ban-circle {
+ background-position: -216px -96px;
+}
+
+.icon-arrow-left {
+ background-position: -240px -96px;
+}
+
+.icon-arrow-right {
+ background-position: -264px -96px;
+}
+
+.icon-arrow-up {
+ background-position: -289px -96px;
+}
+
+.icon-arrow-down {
+ background-position: -312px -96px;
+}
+
+.icon-share-alt {
+ background-position: -336px -96px;
+}
+
+.icon-resize-full {
+ background-position: -360px -96px;
+}
+
+.icon-resize-small {
+ background-position: -384px -96px;
+}
+
+.icon-plus {
+ background-position: -408px -96px;
+}
+
+.icon-minus {
+ background-position: -433px -96px;
+}
+
+.icon-asterisk {
+ background-position: -456px -96px;
+}
+
+.icon-exclamation-sign {
+ background-position: 0 -120px;
+}
+
+.icon-gift {
+ background-position: -24px -120px;
+}
+
+.icon-leaf {
+ background-position: -48px -120px;
+}
+
+.icon-fire {
+ background-position: -72px -120px;
+}
+
+.icon-eye-open {
+ background-position: -96px -120px;
+}
+
+.icon-eye-close {
+ background-position: -120px -120px;
+}
+
+.icon-warning-sign {
+ background-position: -144px -120px;
+}
+
+.icon-plane {
+ background-position: -168px -120px;
+}
+
+.icon-calendar {
+ background-position: -192px -120px;
+}
+
+.icon-random {
+ width: 16px;
+ background-position: -216px -120px;
+}
+
+.icon-comment {
+ background-position: -240px -120px;
+}
+
+.icon-magnet {
+ background-position: -264px -120px;
+}
+
+.icon-chevron-up {
+ background-position: -288px -120px;
+}
+
+.icon-chevron-down {
+ background-position: -313px -119px;
+}
+
+.icon-retweet {
+ background-position: -336px -120px;
+}
+
+.icon-shopping-cart {
+ background-position: -360px -120px;
+}
+
+.icon-folder-close {
+ width: 16px;
+ background-position: -384px -120px;
+}
+
+.icon-folder-open {
+ width: 16px;
+ background-position: -408px -120px;
+}
+
+.icon-resize-vertical {
+ background-position: -432px -119px;
+}
+
+.icon-resize-horizontal {
+ background-position: -456px -118px;
+}
+
+.icon-hdd {
+ background-position: 0 -144px;
+}
+
+.icon-bullhorn {
+ background-position: -24px -144px;
+}
+
+.icon-bell {
+ background-position: -48px -144px;
+}
+
+.icon-certificate {
+ background-position: -72px -144px;
+}
+
+.icon-thumbs-up {
+ background-position: -96px -144px;
+}
+
+.icon-thumbs-down {
+ background-position: -120px -144px;
+}
+
+.icon-hand-right {
+ background-position: -144px -144px;
+}
+
+.icon-hand-left {
+ background-position: -168px -144px;
+}
+
+.icon-hand-up {
+ background-position: -192px -144px;
+}
+
+.icon-hand-down {
+ background-position: -216px -144px;
+}
+
+.icon-circle-arrow-right {
+ background-position: -240px -144px;
+}
+
+.icon-circle-arrow-left {
+ background-position: -264px -144px;
+}
+
+.icon-circle-arrow-up {
+ background-position: -288px -144px;
+}
+
+.icon-circle-arrow-down {
+ background-position: -312px -144px;
+}
+
+.icon-globe {
+ background-position: -336px -144px;
+}
+
+.icon-wrench {
+ background-position: -360px -144px;
+}
+
+.icon-tasks {
+ background-position: -384px -144px;
+}
+
+.icon-filter {
+ background-position: -408px -144px;
+}
+
+.icon-briefcase {
+ background-position: -432px -144px;
+}
+
+.icon-fullscreen {
+ background-position: -456px -144px;
+}
+
+.dropup,
+.dropdown {
+ position: relative;
+}
+
+.dropdown-toggle {
+ *margin-bottom: -3px;
+}
+
+.dropdown-toggle:active,
+.open .dropdown-toggle {
+ outline: 0;
+}
+
+.caret {
+ display: inline-block;
+ width: 0;
+ height: 0;
+ vertical-align: top;
+ border-top: 4px solid #000000;
+ border-right: 4px solid transparent;
+ border-left: 4px solid transparent;
+ content: "";
+}
+
+.dropdown .caret {
+ margin-top: 8px;
+ margin-left: 2px;
+}
+
+.dropdown-menu {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ z-index: 1000;
+ display: none;
+ float: left;
+ min-width: 160px;
+ padding: 5px 0;
+ margin: 2px 0 0;
+ list-style: none;
+ background-color: #ffffff;
+ border: 1px solid #ccc;
+ border: 1px solid rgba(0, 0, 0, 0.2);
+ *border-right-width: 2px;
+ *border-bottom-width: 2px;
+ -webkit-border-radius: 6px;
+ -moz-border-radius: 6px;
+ border-radius: 6px;
+ -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
+ -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
+ box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
+ -webkit-background-clip: padding-box;
+ -moz-background-clip: padding;
+ background-clip: padding-box;
+}
+
+.dropdown-menu.pull-right {
+ right: 0;
+ left: auto;
+}
+
+.dropdown-menu .divider {
+ *width: 100%;
+ height: 1px;
+ margin: 9px 1px;
+ *margin: -5px 0 5px;
+ overflow: hidden;
+ background-color: #e5e5e5;
+ border-bottom: 1px solid #ffffff;
+}
+
+.dropdown-menu > li > a {
+ display: block;
+ padding: 3px 20px;
+ clear: both;
+ font-weight: normal;
+ line-height: 20px;
+ color: #333333;
+ white-space: nowrap;
+}
+
+.dropdown-menu > li > a:hover,
+.dropdown-menu > li > a:focus,
+.dropdown-submenu:hover > a,
+.dropdown-submenu:focus > a {
+ color: #ffffff;
+ text-decoration: none;
+ background-color: #0081c2;
+ background-image: -moz-linear-gradient(top, #0088cc, #0077b3);
+ background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0077b3));
+ background-image: -webkit-linear-gradient(top, #0088cc, #0077b3);
+ background-image: -o-linear-gradient(top, #0088cc, #0077b3);
+ background-image: linear-gradient(to bottom, #0088cc, #0077b3);
+ background-repeat: repeat-x;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0077b3', GradientType=0);
+}
+
+.dropdown-menu > .active > a,
+.dropdown-menu > .active > a:hover,
+.dropdown-menu > .active > a:focus {
+ color: #ffffff;
+ text-decoration: none;
+ background-color: #0081c2;
+ background-image: -moz-linear-gradient(top, #0088cc, #0077b3);
+ background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0077b3));
+ background-image: -webkit-linear-gradient(top, #0088cc, #0077b3);
+ background-image: -o-linear-gradient(top, #0088cc, #0077b3);
+ background-image: linear-gradient(to bottom, #0088cc, #0077b3);
+ background-repeat: repeat-x;
+ outline: 0;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0077b3', GradientType=0);
+}
+
+.dropdown-menu > .disabled > a,
+.dropdown-menu > .disabled > a:hover,
+.dropdown-menu > .disabled > a:focus {
+ color: #999999;
+}
+
+.dropdown-menu > .disabled > a:hover,
+.dropdown-menu > .disabled > a:focus {
+ text-decoration: none;
+ cursor: default;
+ background-color: transparent;
+ background-image: none;
+ filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
+}
+
+.open {
+ *z-index: 1000;
+}
+
+.open > .dropdown-menu {
+ display: block;
+}
+
+.dropdown-backdrop {
+ position: fixed;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 990;
+}
+
+.pull-right > .dropdown-menu {
+ right: 0;
+ left: auto;
+}
+
+.dropup .caret,
+.navbar-fixed-bottom .dropdown .caret {
+ border-top: 0;
+ border-bottom: 4px solid #000000;
+ content: "";
+}
+
+.dropup .dropdown-menu,
+.navbar-fixed-bottom .dropdown .dropdown-menu {
+ top: auto;
+ bottom: 100%;
+ margin-bottom: 1px;
+}
+
+.dropdown-submenu {
+ position: relative;
+}
+
+.dropdown-submenu > .dropdown-menu {
+ top: 0;
+ left: 100%;
+ margin-top: -6px;
+ margin-left: -1px;
+ -webkit-border-radius: 0 6px 6px 6px;
+ -moz-border-radius: 0 6px 6px 6px;
+ border-radius: 0 6px 6px 6px;
+}
+
+.dropdown-submenu:hover > .dropdown-menu {
+ display: block;
+}
+
+.dropup .dropdown-submenu > .dropdown-menu {
+ top: auto;
+ bottom: 0;
+ margin-top: 0;
+ margin-bottom: -2px;
+ -webkit-border-radius: 5px 5px 5px 0;
+ -moz-border-radius: 5px 5px 5px 0;
+ border-radius: 5px 5px 5px 0;
+}
+
+.dropdown-submenu > a:after {
+ display: block;
+ float: right;
+ width: 0;
+ height: 0;
+ margin-top: 5px;
+ margin-right: -10px;
+ border-color: transparent;
+ border-left-color: #cccccc;
+ border-style: solid;
+ border-width: 5px 0 5px 5px;
+ content: " ";
+}
+
+.dropdown-submenu:hover > a:after {
+ border-left-color: #ffffff;
+}
+
+.dropdown-submenu.pull-left {
+ float: none;
+}
+
+.dropdown-submenu.pull-left > .dropdown-menu {
+ left: -100%;
+ margin-left: 10px;
+ -webkit-border-radius: 6px 0 6px 6px;
+ -moz-border-radius: 6px 0 6px 6px;
+ border-radius: 6px 0 6px 6px;
+}
+
+.dropdown .dropdown-menu .nav-header {
+ padding-right: 20px;
+ padding-left: 20px;
+}
+
+.typeahead {
+ z-index: 1051;
+ margin-top: 2px;
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+}
+
+.well {
+ min-height: 20px;
+ padding: 19px;
+ margin-bottom: 20px;
+ background-color: #f5f5f5;
+ border: 1px solid #e3e3e3;
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+ -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);
+ -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);
+ box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);
+}
+
+.well blockquote {
+ border-color: #ddd;
+ border-color: rgba(0, 0, 0, 0.15);
+}
+
+.well-large {
+ padding: 24px;
+ -webkit-border-radius: 6px;
+ -moz-border-radius: 6px;
+ border-radius: 6px;
+}
+
+.well-small {
+ padding: 9px;
+ -webkit-border-radius: 3px;
+ -moz-border-radius: 3px;
+ border-radius: 3px;
+}
+
+.fade {
+ opacity: 0;
+ -webkit-transition: opacity 0.15s linear;
+ -moz-transition: opacity 0.15s linear;
+ -o-transition: opacity 0.15s linear;
+ transition: opacity 0.15s linear;
+}
+
+.fade.in {
+ opacity: 1;
+}
+
+.collapse {
+ position: relative;
+ height: 0;
+ overflow: hidden;
+ -webkit-transition: height 0.35s ease;
+ -moz-transition: height 0.35s ease;
+ -o-transition: height 0.35s ease;
+ transition: height 0.35s ease;
+}
+
+.collapse.in {
+ height: auto;
+}
+
+.close {
+ float: right;
+ font-size: 20px;
+ font-weight: bold;
+ line-height: 20px;
+ color: #000000;
+ text-shadow: 0 1px 0 #ffffff;
+ opacity: 0.2;
+ filter: alpha(opacity=20);
+}
+
+.close:hover,
+.close:focus {
+ color: #000000;
+ text-decoration: none;
+ cursor: pointer;
+ opacity: 0.4;
+ filter: alpha(opacity=40);
+}
+
+button.close {
+ padding: 0;
+ cursor: pointer;
+ background: transparent;
+ border: 0;
+ -webkit-appearance: none;
+}
+
+.btn {
+ display: inline-block;
+ *display: inline;
+ padding: 4px 12px;
+ margin-bottom: 0;
+ *margin-left: .3em;
+ font-size: 14px;
+ line-height: 20px;
+ color: #333333;
+ text-align: center;
+ text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75);
+ vertical-align: middle;
+ cursor: pointer;
+ background-color: #f5f5f5;
+ *background-color: #e6e6e6;
+ background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6);
+ background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6));
+ background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6);
+ background-image: -o-linear-gradient(top, #ffffff, #e6e6e6);
+ background-image: linear-gradient(to bottom, #ffffff, #e6e6e6);
+ background-repeat: repeat-x;
+ border: 1px solid #cccccc;
+ *border: 0;
+ border-color: #e6e6e6 #e6e6e6 #bfbfbf;
+ border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
+ border-bottom-color: #b3b3b3;
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe6e6e6', GradientType=0);
+ filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
+ *zoom: 1;
+ -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
+ -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
+}
+
+.btn:hover,
+.btn:focus,
+.btn:active,
+.btn.active,
+.btn.disabled,
+.btn[disabled] {
+ color: #333333;
+ background-color: #e6e6e6;
+ *background-color: #d9d9d9;
+}
+
+.btn:active,
+.btn.active {
+ background-color: #cccccc \9;
+}
+
+.btn:first-child {
+ *margin-left: 0;
+}
+
+.btn:hover,
+.btn:focus {
+ color: #333333;
+ text-decoration: none;
+ background-position: 0 -15px;
+ -webkit-transition: background-position 0.1s linear;
+ -moz-transition: background-position 0.1s linear;
+ -o-transition: background-position 0.1s linear;
+ transition: background-position 0.1s linear;
+}
+
+.btn:focus {
+ outline: thin dotted #333;
+ outline: 5px auto -webkit-focus-ring-color;
+ outline-offset: -2px;
+}
+
+.btn.active,
+.btn:active {
+ background-image: none;
+ outline: 0;
+ -webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);
+ -moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);
+ box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);
+}
+
+.btn.disabled,
+.btn[disabled] {
+ cursor: default;
+ background-image: none;
+ opacity: 0.65;
+ filter: alpha(opacity=65);
+ -webkit-box-shadow: none;
+ -moz-box-shadow: none;
+ box-shadow: none;
+}
+
+.btn-large {
+ padding: 11px 19px;
+ font-size: 17.5px;
+ -webkit-border-radius: 6px;
+ -moz-border-radius: 6px;
+ border-radius: 6px;
+}
+
+.btn-large [class^="icon-"],
+.btn-large [class*=" icon-"] {
+ margin-top: 4px;
+}
+
+.btn-small {
+ padding: 2px 10px;
+ font-size: 11.9px;
+ -webkit-border-radius: 3px;
+ -moz-border-radius: 3px;
+ border-radius: 3px;
+}
+
+.btn-small [class^="icon-"],
+.btn-small [class*=" icon-"] {
+ margin-top: 0;
+}
+
+.btn-mini [class^="icon-"],
+.btn-mini [class*=" icon-"] {
+ margin-top: -1px;
+}
+
+.btn-mini {
+ padding: 0 6px;
+ font-size: 10.5px;
+ -webkit-border-radius: 3px;
+ -moz-border-radius: 3px;
+ border-radius: 3px;
+}
+
+.btn-block {
+ display: block;
+ width: 100%;
+ padding-right: 0;
+ padding-left: 0;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+
+.btn-block + .btn-block {
+ margin-top: 5px;
+}
+
+input[type="submit"].btn-block,
+input[type="reset"].btn-block,
+input[type="button"].btn-block {
+ width: 100%;
+}
+
+.btn-primary.active,
+.btn-warning.active,
+.btn-danger.active,
+.btn-success.active,
+.btn-info.active,
+.btn-inverse.active {
+ color: rgba(255, 255, 255, 0.75);
+}
+
+.btn-primary {
+ color: #ffffff;
+ text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
+ background-color: #006dcc;
+ *background-color: #0044cc;
+ background-image: -moz-linear-gradient(top, #0088cc, #0044cc);
+ background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc));
+ background-image: -webkit-linear-gradient(top, #0088cc, #0044cc);
+ background-image: -o-linear-gradient(top, #0088cc, #0044cc);
+ background-image: linear-gradient(to bottom, #0088cc, #0044cc);
+ background-repeat: repeat-x;
+ border-color: #0044cc #0044cc #002a80;
+ border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0044cc', GradientType=0);
+ filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
+}
+
+.btn-primary:hover,
+.btn-primary:focus,
+.btn-primary:active,
+.btn-primary.active,
+.btn-primary.disabled,
+.btn-primary[disabled] {
+ color: #ffffff;
+ background-color: #0044cc;
+ *background-color: #003bb3;
+}
+
+.btn-primary:active,
+.btn-primary.active {
+ background-color: #003399 \9;
+}
+
+.btn-warning {
+ color: #ffffff;
+ text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
+ background-color: #faa732;
+ *background-color: #f89406;
+ background-image: -moz-linear-gradient(top, #fbb450, #f89406);
+ background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406));
+ background-image: -webkit-linear-gradient(top, #fbb450, #f89406);
+ background-image: -o-linear-gradient(top, #fbb450, #f89406);
+ background-image: linear-gradient(to bottom, #fbb450, #f89406);
+ background-repeat: repeat-x;
+ border-color: #f89406 #f89406 #ad6704;
+ border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450', endColorstr='#fff89406', GradientType=0);
+ filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
+}
+
+.btn-warning:hover,
+.btn-warning:focus,
+.btn-warning:active,
+.btn-warning.active,
+.btn-warning.disabled,
+.btn-warning[disabled] {
+ color: #ffffff;
+ background-color: #f89406;
+ *background-color: #df8505;
+}
+
+.btn-warning:active,
+.btn-warning.active {
+ background-color: #c67605 \9;
+}
+
+.btn-danger {
+ color: #ffffff;
+ text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
+ background-color: #da4f49;
+ *background-color: #bd362f;
+ background-image: -moz-linear-gradient(top, #ee5f5b, #bd362f);
+ background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#bd362f));
+ background-image: -webkit-linear-gradient(top, #ee5f5b, #bd362f);
+ background-image: -o-linear-gradient(top, #ee5f5b, #bd362f);
+ background-image: linear-gradient(to bottom, #ee5f5b, #bd362f);
+ background-repeat: repeat-x;
+ border-color: #bd362f #bd362f #802420;
+ border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b', endColorstr='#ffbd362f', GradientType=0);
+ filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
+}
+
+.btn-danger:hover,
+.btn-danger:focus,
+.btn-danger:active,
+.btn-danger.active,
+.btn-danger.disabled,
+.btn-danger[disabled] {
+ color: #ffffff;
+ background-color: #bd362f;
+ *background-color: #a9302a;
+}
+
+.btn-danger:active,
+.btn-danger.active {
+ background-color: #942a25 \9;
+}
+
+.btn-success {
+ color: #ffffff;
+ text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
+ background-color: #5bb75b;
+ *background-color: #51a351;
+ background-image: -moz-linear-gradient(top, #62c462, #51a351);
+ background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#51a351));
+ background-image: -webkit-linear-gradient(top, #62c462, #51a351);
+ background-image: -o-linear-gradient(top, #62c462, #51a351);
+ background-image: linear-gradient(to bottom, #62c462, #51a351);
+ background-repeat: repeat-x;
+ border-color: #51a351 #51a351 #387038;
+ border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462', endColorstr='#ff51a351', GradientType=0);
+ filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
+}
+
+.btn-success:hover,
+.btn-success:focus,
+.btn-success:active,
+.btn-success.active,
+.btn-success.disabled,
+.btn-success[disabled] {
+ color: #ffffff;
+ background-color: #51a351;
+ *background-color: #499249;
+}
+
+.btn-success:active,
+.btn-success.active {
+ background-color: #408140 \9;
+}
+
+.btn-info {
+ color: #ffffff;
+ text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
+ background-color: #49afcd;
+ *background-color: #2f96b4;
+ background-image: -moz-linear-gradient(top, #5bc0de, #2f96b4);
+ background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#2f96b4));
+ background-image: -webkit-linear-gradient(top, #5bc0de, #2f96b4);
+ background-image: -o-linear-gradient(top, #5bc0de, #2f96b4);
+ background-image: linear-gradient(to bottom, #5bc0de, #2f96b4);
+ background-repeat: repeat-x;
+ border-color: #2f96b4 #2f96b4 #1f6377;
+ border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2f96b4', GradientType=0);
+ filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
+}
+
+.btn-info:hover,
+.btn-info:focus,
+.btn-info:active,
+.btn-info.active,
+.btn-info.disabled,
+.btn-info[disabled] {
+ color: #ffffff;
+ background-color: #2f96b4;
+ *background-color: #2a85a0;
+}
+
+.btn-info:active,
+.btn-info.active {
+ background-color: #24748c \9;
+}
+
+.btn-inverse {
+ color: #ffffff;
+ text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
+ background-color: #363636;
+ *background-color: #222222;
+ background-image: -moz-linear-gradient(top, #444444, #222222);
+ background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#444444), to(#222222));
+ background-image: -webkit-linear-gradient(top, #444444, #222222);
+ background-image: -o-linear-gradient(top, #444444, #222222);
+ background-image: linear-gradient(to bottom, #444444, #222222);
+ background-repeat: repeat-x;
+ border-color: #222222 #222222 #000000;
+ border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff444444', endColorstr='#ff222222', GradientType=0);
+ filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
+}
+
+.btn-inverse:hover,
+.btn-inverse:focus,
+.btn-inverse:active,
+.btn-inverse.active,
+.btn-inverse.disabled,
+.btn-inverse[disabled] {
+ color: #ffffff;
+ background-color: #222222;
+ *background-color: #151515;
+}
+
+.btn-inverse:active,
+.btn-inverse.active {
+ background-color: #080808 \9;
+}
+
+button.btn,
+input[type="submit"].btn {
+ *padding-top: 3px;
+ *padding-bottom: 3px;
+}
+
+button.btn::-moz-focus-inner,
+input[type="submit"].btn::-moz-focus-inner {
+ padding: 0;
+ border: 0;
+}
+
+button.btn.btn-large,
+input[type="submit"].btn.btn-large {
+ *padding-top: 7px;
+ *padding-bottom: 7px;
+}
+
+button.btn.btn-small,
+input[type="submit"].btn.btn-small {
+ *padding-top: 3px;
+ *padding-bottom: 3px;
+}
+
+button.btn.btn-mini,
+input[type="submit"].btn.btn-mini {
+ *padding-top: 1px;
+ *padding-bottom: 1px;
+}
+
+.btn-link,
+.btn-link:active,
+.btn-link[disabled] {
+ background-color: transparent;
+ background-image: none;
+ -webkit-box-shadow: none;
+ -moz-box-shadow: none;
+ box-shadow: none;
+}
+
+.btn-link {
+ color: #0088cc;
+ cursor: pointer;
+ border-color: transparent;
+ -webkit-border-radius: 0;
+ -moz-border-radius: 0;
+ border-radius: 0;
+}
+
+.btn-link:hover,
+.btn-link:focus {
+ color: #005580;
+ text-decoration: underline;
+ background-color: transparent;
+}
+
+.btn-link[disabled]:hover,
+.btn-link[disabled]:focus {
+ color: #333333;
+ text-decoration: none;
+}
+
+.btn-group {
+ position: relative;
+ display: inline-block;
+ *display: inline;
+ *margin-left: .3em;
+ font-size: 0;
+ white-space: nowrap;
+ vertical-align: middle;
+ *zoom: 1;
+}
+
+.btn-group:first-child {
+ *margin-left: 0;
+}
+
+.btn-group + .btn-group {
+ margin-left: 5px;
+}
+
+.btn-toolbar {
+ margin-top: 10px;
+ margin-bottom: 10px;
+ font-size: 0;
+}
+
+.btn-toolbar > .btn + .btn,
+.btn-toolbar > .btn-group + .btn,
+.btn-toolbar > .btn + .btn-group {
+ margin-left: 5px;
+}
+
+.btn-group > .btn {
+ position: relative;
+ -webkit-border-radius: 0;
+ -moz-border-radius: 0;
+ border-radius: 0;
+}
+
+.btn-group > .btn + .btn {
+ margin-left: -1px;
+}
+
+.btn-group > .btn,
+.btn-group > .dropdown-menu,
+.btn-group > .popover {
+ font-size: 14px;
+}
+
+.btn-group > .btn-mini {
+ font-size: 10.5px;
+}
+
+.btn-group > .btn-small {
+ font-size: 11.9px;
+}
+
+.btn-group > .btn-large {
+ font-size: 17.5px;
+}
+
+.btn-group > .btn:first-child {
+ margin-left: 0;
+ -webkit-border-bottom-left-radius: 4px;
+ border-bottom-left-radius: 4px;
+ -webkit-border-top-left-radius: 4px;
+ border-top-left-radius: 4px;
+ -moz-border-radius-bottomleft: 4px;
+ -moz-border-radius-topleft: 4px;
+}
+
+.btn-group > .btn:last-child,
+.btn-group > .dropdown-toggle {
+ -webkit-border-top-right-radius: 4px;
+ border-top-right-radius: 4px;
+ -webkit-border-bottom-right-radius: 4px;
+ border-bottom-right-radius: 4px;
+ -moz-border-radius-topright: 4px;
+ -moz-border-radius-bottomright: 4px;
+}
+
+.btn-group > .btn.large:first-child {
+ margin-left: 0;
+ -webkit-border-bottom-left-radius: 6px;
+ border-bottom-left-radius: 6px;
+ -webkit-border-top-left-radius: 6px;
+ border-top-left-radius: 6px;
+ -moz-border-radius-bottomleft: 6px;
+ -moz-border-radius-topleft: 6px;
+}
+
+.btn-group > .btn.large:last-child,
+.btn-group > .large.dropdown-toggle {
+ -webkit-border-top-right-radius: 6px;
+ border-top-right-radius: 6px;
+ -webkit-border-bottom-right-radius: 6px;
+ border-bottom-right-radius: 6px;
+ -moz-border-radius-topright: 6px;
+ -moz-border-radius-bottomright: 6px;
+}
+
+.btn-group > .btn:hover,
+.btn-group > .btn:focus,
+.btn-group > .btn:active,
+.btn-group > .btn.active {
+ z-index: 2;
+}
+
+.btn-group .dropdown-toggle:active,
+.btn-group.open .dropdown-toggle {
+ outline: 0;
+}
+
+.btn-group > .btn + .dropdown-toggle {
+ *padding-top: 5px;
+ padding-right: 8px;
+ *padding-bottom: 5px;
+ padding-left: 8px;
+ -webkit-box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
+ -moz-box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
+ box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
+}
+
+.btn-group > .btn-mini + .dropdown-toggle {
+ *padding-top: 2px;
+ padding-right: 5px;
+ *padding-bottom: 2px;
+ padding-left: 5px;
+}
+
+.btn-group > .btn-small + .dropdown-toggle {
+ *padding-top: 5px;
+ *padding-bottom: 4px;
+}
+
+.btn-group > .btn-large + .dropdown-toggle {
+ *padding-top: 7px;
+ padding-right: 12px;
+ *padding-bottom: 7px;
+ padding-left: 12px;
+}
+
+.btn-group.open .dropdown-toggle {
+ background-image: none;
+ -webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);
+ -moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);
+ box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);
+}
+
+.btn-group.open .btn.dropdown-toggle {
+ background-color: #e6e6e6;
+}
+
+.btn-group.open .btn-primary.dropdown-toggle {
+ background-color: #0044cc;
+}
+
+.btn-group.open .btn-warning.dropdown-toggle {
+ background-color: #f89406;
+}
+
+.btn-group.open .btn-danger.dropdown-toggle {
+ background-color: #bd362f;
+}
+
+.btn-group.open .btn-success.dropdown-toggle {
+ background-color: #51a351;
+}
+
+.btn-group.open .btn-info.dropdown-toggle {
+ background-color: #2f96b4;
+}
+
+.btn-group.open .btn-inverse.dropdown-toggle {
+ background-color: #222222;
+}
+
+.btn .caret {
+ margin-top: 8px;
+ margin-left: 0;
+}
+
+.btn-large .caret {
+ margin-top: 6px;
+}
+
+.btn-large .caret {
+ border-top-width: 5px;
+ border-right-width: 5px;
+ border-left-width: 5px;
+}
+
+.btn-mini .caret,
+.btn-small .caret {
+ margin-top: 8px;
+}
+
+.dropup .btn-large .caret {
+ border-bottom-width: 5px;
+}
+
+.btn-primary .caret,
+.btn-warning .caret,
+.btn-danger .caret,
+.btn-info .caret,
+.btn-success .caret,
+.btn-inverse .caret {
+ border-top-color: #ffffff;
+ border-bottom-color: #ffffff;
+}
+
+.btn-group-vertical {
+ display: inline-block;
+ *display: inline;
+ /* IE7 inline-block hack */
+
+ *zoom: 1;
+}
+
+.btn-group-vertical > .btn {
+ display: block;
+ float: none;
+ max-width: 100%;
+ -webkit-border-radius: 0;
+ -moz-border-radius: 0;
+ border-radius: 0;
+}
+
+.btn-group-vertical > .btn + .btn {
+ margin-top: -1px;
+ margin-left: 0;
+}
+
+.btn-group-vertical > .btn:first-child {
+ -webkit-border-radius: 4px 4px 0 0;
+ -moz-border-radius: 4px 4px 0 0;
+ border-radius: 4px 4px 0 0;
+}
+
+.btn-group-vertical > .btn:last-child {
+ -webkit-border-radius: 0 0 4px 4px;
+ -moz-border-radius: 0 0 4px 4px;
+ border-radius: 0 0 4px 4px;
+}
+
+.btn-group-vertical > .btn-large:first-child {
+ -webkit-border-radius: 6px 6px 0 0;
+ -moz-border-radius: 6px 6px 0 0;
+ border-radius: 6px 6px 0 0;
+}
+
+.btn-group-vertical > .btn-large:last-child {
+ -webkit-border-radius: 0 0 6px 6px;
+ -moz-border-radius: 0 0 6px 6px;
+ border-radius: 0 0 6px 6px;
+}
+
+.alert {
+ padding: 8px 35px 8px 14px;
+ margin-bottom: 20px;
+ text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
+ background-color: #fcf8e3;
+ border: 1px solid #fbeed5;
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+}
+
+.alert,
+.alert h4 {
+ color: #c09853;
+}
+
+.alert h4 {
+ margin: 0;
+}
+
+.alert .close {
+ position: relative;
+ top: -2px;
+ right: -21px;
+ line-height: 20px;
+}
+
+.alert-success {
+ color: #468847;
+ background-color: #dff0d8;
+ border-color: #d6e9c6;
+}
+
+.alert-success h4 {
+ color: #468847;
+}
+
+.alert-danger,
+.alert-error {
+ color: #b94a48;
+ background-color: #f2dede;
+ border-color: #eed3d7;
+}
+
+.alert-danger h4,
+.alert-error h4 {
+ color: #b94a48;
+}
+
+.alert-info {
+ color: #3a87ad;
+ background-color: #d9edf7;
+ border-color: #bce8f1;
+}
+
+.alert-info h4 {
+ color: #3a87ad;
+}
+
+.alert-block {
+ padding-top: 14px;
+ padding-bottom: 14px;
+}
+
+.alert-block > p,
+.alert-block > ul {
+ margin-bottom: 0;
+}
+
+.alert-block p + p {
+ margin-top: 5px;
+}
+
+.nav {
+ margin-bottom: 20px;
+ margin-left: 0;
+ list-style: none;
+}
+
+.nav > li > a {
+ display: block;
+}
+
+.nav > li > a:hover,
+.nav > li > a:focus {
+ text-decoration: none;
+ background-color: #eeeeee;
+}
+
+.nav > li > a > img {
+ max-width: none;
+}
+
+.nav > .pull-right {
+ float: right;
+}
+
+.nav-header {
+ display: block;
+ padding: 3px 15px;
+ font-size: 11px;
+ font-weight: bold;
+ line-height: 20px;
+ color: #999999;
+ text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
+ text-transform: uppercase;
+}
+
+.nav li + .nav-header {
+ margin-top: 9px;
+}
+
+.nav-list {
+ padding-right: 15px;
+ padding-left: 15px;
+ margin-bottom: 0;
+}
+
+.nav-list > li > a,
+.nav-list .nav-header {
+ margin-right: -15px;
+ margin-left: -15px;
+ text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
+}
+
+.nav-list > li > a {
+ padding: 3px 15px;
+}
+
+.nav-list > .active > a,
+.nav-list > .active > a:hover,
+.nav-list > .active > a:focus {
+ color: #ffffff;
+ text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2);
+ background-color: #0088cc;
+}
+
+.nav-list [class^="icon-"],
+.nav-list [class*=" icon-"] {
+ margin-right: 2px;
+}
+
+.nav-list .divider {
+ *width: 100%;
+ height: 1px;
+ margin: 9px 1px;
+ *margin: -5px 0 5px;
+ overflow: hidden;
+ background-color: #e5e5e5;
+ border-bottom: 1px solid #ffffff;
+}
+
+.nav-tabs,
+.nav-pills {
+ *zoom: 1;
+}
+
+.nav-tabs:before,
+.nav-pills:before,
+.nav-tabs:after,
+.nav-pills:after {
+ display: table;
+ line-height: 0;
+ content: "";
+}
+
+.nav-tabs:after,
+.nav-pills:after {
+ clear: both;
+}
+
+.nav-tabs > li,
+.nav-pills > li {
+ float: left;
+}
+
+.nav-tabs > li > a,
+.nav-pills > li > a {
+ padding-right: 12px;
+ padding-left: 12px;
+ margin-right: 2px;
+ line-height: 14px;
+}
+
+.nav-tabs {
+ border-bottom: 1px solid #ddd;
+}
+
+.nav-tabs > li {
+ margin-bottom: -1px;
+}
+
+.nav-tabs > li > a {
+ padding-top: 8px;
+ padding-bottom: 8px;
+ line-height: 20px;
+ border: 1px solid transparent;
+ -webkit-border-radius: 4px 4px 0 0;
+ -moz-border-radius: 4px 4px 0 0;
+ border-radius: 4px 4px 0 0;
+}
+
+.nav-tabs > li > a:hover,
+.nav-tabs > li > a:focus {
+ border-color: #eeeeee #eeeeee #dddddd;
+}
+
+.nav-tabs > .active > a,
+.nav-tabs > .active > a:hover,
+.nav-tabs > .active > a:focus {
+ color: #555555;
+ cursor: default;
+ background-color: #ffffff;
+ border: 1px solid #ddd;
+ border-bottom-color: transparent;
+}
+
+.nav-pills > li > a {
+ padding-top: 8px;
+ padding-bottom: 8px;
+ margin-top: 2px;
+ margin-bottom: 2px;
+ -webkit-border-radius: 5px;
+ -moz-border-radius: 5px;
+ border-radius: 5px;
+}
+
+.nav-pills > .active > a,
+.nav-pills > .active > a:hover,
+.nav-pills > .active > a:focus {
+ color: #ffffff;
+ background-color: #0088cc;
+}
+
+.nav-stacked > li {
+ float: none;
+}
+
+.nav-stacked > li > a {
+ margin-right: 0;
+}
+
+.nav-tabs.nav-stacked {
+ border-bottom: 0;
+}
+
+.nav-tabs.nav-stacked > li > a {
+ border: 1px solid #ddd;
+ -webkit-border-radius: 0;
+ -moz-border-radius: 0;
+ border-radius: 0;
+}
+
+.nav-tabs.nav-stacked > li:first-child > a {
+ -webkit-border-top-right-radius: 4px;
+ border-top-right-radius: 4px;
+ -webkit-border-top-left-radius: 4px;
+ border-top-left-radius: 4px;
+ -moz-border-radius-topright: 4px;
+ -moz-border-radius-topleft: 4px;
+}
+
+.nav-tabs.nav-stacked > li:last-child > a {
+ -webkit-border-bottom-right-radius: 4px;
+ border-bottom-right-radius: 4px;
+ -webkit-border-bottom-left-radius: 4px;
+ border-bottom-left-radius: 4px;
+ -moz-border-radius-bottomright: 4px;
+ -moz-border-radius-bottomleft: 4px;
+}
+
+.nav-tabs.nav-stacked > li > a:hover,
+.nav-tabs.nav-stacked > li > a:focus {
+ z-index: 2;
+ border-color: #ddd;
+}
+
+.nav-pills.nav-stacked > li > a {
+ margin-bottom: 3px;
+}
+
+.nav-pills.nav-stacked > li:last-child > a {
+ margin-bottom: 1px;
+}
+
+.nav-tabs .dropdown-menu {
+ -webkit-border-radius: 0 0 6px 6px;
+ -moz-border-radius: 0 0 6px 6px;
+ border-radius: 0 0 6px 6px;
+}
+
+.nav-pills .dropdown-menu {
+ -webkit-border-radius: 6px;
+ -moz-border-radius: 6px;
+ border-radius: 6px;
+}
+
+.nav .dropdown-toggle .caret {
+ margin-top: 6px;
+ border-top-color: #0088cc;
+ border-bottom-color: #0088cc;
+}
+
+.nav .dropdown-toggle:hover .caret,
+.nav .dropdown-toggle:focus .caret {
+ border-top-color: #005580;
+ border-bottom-color: #005580;
+}
+
+/* move down carets for tabs */
+
+.nav-tabs .dropdown-toggle .caret {
+ margin-top: 8px;
+}
+
+.nav .active .dropdown-toggle .caret {
+ border-top-color: #fff;
+ border-bottom-color: #fff;
+}
+
+.nav-tabs .active .dropdown-toggle .caret {
+ border-top-color: #555555;
+ border-bottom-color: #555555;
+}
+
+.nav > .dropdown.active > a:hover,
+.nav > .dropdown.active > a:focus {
+ cursor: pointer;
+}
+
+.nav-tabs .open .dropdown-toggle,
+.nav-pills .open .dropdown-toggle,
+.nav > li.dropdown.open.active > a:hover,
+.nav > li.dropdown.open.active > a:focus {
+ color: #ffffff;
+ background-color: #999999;
+ border-color: #999999;
+}
+
+.nav li.dropdown.open .caret,
+.nav li.dropdown.open.active .caret,
+.nav li.dropdown.open a:hover .caret,
+.nav li.dropdown.open a:focus .caret {
+ border-top-color: #ffffff;
+ border-bottom-color: #ffffff;
+ opacity: 1;
+ filter: alpha(opacity=100);
+}
+
+.tabs-stacked .open > a:hover,
+.tabs-stacked .open > a:focus {
+ border-color: #999999;
+}
+
+.tabbable {
+ *zoom: 1;
+}
+
+.tabbable:before,
+.tabbable:after {
+ display: table;
+ line-height: 0;
+ content: "";
+}
+
+.tabbable:after {
+ clear: both;
+}
+
+.tab-content {
+ overflow: auto;
+}
+
+.tabs-below > .nav-tabs,
+.tabs-right > .nav-tabs,
+.tabs-left > .nav-tabs {
+ border-bottom: 0;
+}
+
+.tab-content > .tab-pane,
+.pill-content > .pill-pane {
+ display: none;
+}
+
+.tab-content > .active,
+.pill-content > .active {
+ display: block;
+}
+
+.tabs-below > .nav-tabs {
+ border-top: 1px solid #ddd;
+}
+
+.tabs-below > .nav-tabs > li {
+ margin-top: -1px;
+ margin-bottom: 0;
+}
+
+.tabs-below > .nav-tabs > li > a {
+ -webkit-border-radius: 0 0 4px 4px;
+ -moz-border-radius: 0 0 4px 4px;
+ border-radius: 0 0 4px 4px;
+}
+
+.tabs-below > .nav-tabs > li > a:hover,
+.tabs-below > .nav-tabs > li > a:focus {
+ border-top-color: #ddd;
+ border-bottom-color: transparent;
+}
+
+.tabs-below > .nav-tabs > .active > a,
+.tabs-below > .nav-tabs > .active > a:hover,
+.tabs-below > .nav-tabs > .active > a:focus {
+ border-color: transparent #ddd #ddd #ddd;
+}
+
+.tabs-left > .nav-tabs > li,
+.tabs-right > .nav-tabs > li {
+ float: none;
+}
+
+.tabs-left > .nav-tabs > li > a,
+.tabs-right > .nav-tabs > li > a {
+ min-width: 74px;
+ margin-right: 0;
+ margin-bottom: 3px;
+}
+
+.tabs-left > .nav-tabs {
+ float: left;
+ margin-right: 19px;
+ border-right: 1px solid #ddd;
+}
+
+.tabs-left > .nav-tabs > li > a {
+ margin-right: -1px;
+ -webkit-border-radius: 4px 0 0 4px;
+ -moz-border-radius: 4px 0 0 4px;
+ border-radius: 4px 0 0 4px;
+}
+
+.tabs-left > .nav-tabs > li > a:hover,
+.tabs-left > .nav-tabs > li > a:focus {
+ border-color: #eeeeee #dddddd #eeeeee #eeeeee;
+}
+
+.tabs-left > .nav-tabs .active > a,
+.tabs-left > .nav-tabs .active > a:hover,
+.tabs-left > .nav-tabs .active > a:focus {
+ border-color: #ddd transparent #ddd #ddd;
+ *border-right-color: #ffffff;
+}
+
+.tabs-right > .nav-tabs {
+ float: right;
+ margin-left: 19px;
+ border-left: 1px solid #ddd;
+}
+
+.tabs-right > .nav-tabs > li > a {
+ margin-left: -1px;
+ -webkit-border-radius: 0 4px 4px 0;
+ -moz-border-radius: 0 4px 4px 0;
+ border-radius: 0 4px 4px 0;
+}
+
+.tabs-right > .nav-tabs > li > a:hover,
+.tabs-right > .nav-tabs > li > a:focus {
+ border-color: #eeeeee #eeeeee #eeeeee #dddddd;
+}
+
+.tabs-right > .nav-tabs .active > a,
+.tabs-right > .nav-tabs .active > a:hover,
+.tabs-right > .nav-tabs .active > a:focus {
+ border-color: #ddd #ddd #ddd transparent;
+ *border-left-color: #ffffff;
+}
+
+.nav > .disabled > a {
+ color: #999999;
+}
+
+.nav > .disabled > a:hover,
+.nav > .disabled > a:focus {
+ text-decoration: none;
+ cursor: default;
+ background-color: transparent;
+}
+
+.navbar {
+ *position: relative;
+ *z-index: 2;
+ margin-bottom: 20px;
+ overflow: visible;
+}
+
+.navbar-inner {
+ min-height: 40px;
+ padding-right: 20px;
+ padding-left: 20px;
+ background-color: #fafafa;
+ background-image: -moz-linear-gradient(top, #ffffff, #f2f2f2);
+ background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#f2f2f2));
+ background-image: -webkit-linear-gradient(top, #ffffff, #f2f2f2);
+ background-image: -o-linear-gradient(top, #ffffff, #f2f2f2);
+ background-image: linear-gradient(to bottom, #ffffff, #f2f2f2);
+ background-repeat: repeat-x;
+ border: 1px solid #d4d4d4;
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff2f2f2', GradientType=0);
+ *zoom: 1;
+ -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.065);
+ -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.065);
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.065);
+}
+
+.navbar-inner:before,
+.navbar-inner:after {
+ display: table;
+ line-height: 0;
+ content: "";
+}
+
+.navbar-inner:after {
+ clear: both;
+}
+
+.navbar .container {
+ width: auto;
+}
+
+.nav-collapse.collapse {
+ height: auto;
+ overflow: visible;
+}
+
+.navbar .brand {
+ display: block;
+ float: left;
+ padding: 10px 20px 10px;
+ margin-left: -20px;
+ font-size: 20px;
+ font-weight: 200;
+ color: #777777;
+ text-shadow: 0 1px 0 #ffffff;
+}
+
+.navbar .brand:hover,
+.navbar .brand:focus {
+ text-decoration: none;
+}
+
+.navbar-text {
+ margin-bottom: 0;
+ line-height: 40px;
+ color: #777777;
+}
+
+.navbar-link {
+ color: #777777;
+}
+
+.navbar-link:hover,
+.navbar-link:focus {
+ color: #333333;
+}
+
+.navbar .divider-vertical {
+ height: 40px;
+ margin: 0 9px;
+ border-right: 1px solid #ffffff;
+ border-left: 1px solid #f2f2f2;
+}
+
+.navbar .btn,
+.navbar .btn-group {
+ margin-top: 5px;
+}
+
+.navbar .btn-group .btn,
+.navbar .input-prepend .btn,
+.navbar .input-append .btn,
+.navbar .input-prepend .btn-group,
+.navbar .input-append .btn-group {
+ margin-top: 0;
+}
+
+.navbar-form {
+ margin-bottom: 0;
+ *zoom: 1;
+}
+
+.navbar-form:before,
+.navbar-form:after {
+ display: table;
+ line-height: 0;
+ content: "";
+}
+
+.navbar-form:after {
+ clear: both;
+}
+
+.navbar-form input,
+.navbar-form select,
+.navbar-form .radio,
+.navbar-form .checkbox {
+ margin-top: 5px;
+}
+
+.navbar-form input,
+.navbar-form select,
+.navbar-form .btn {
+ display: inline-block;
+ margin-bottom: 0;
+}
+
+.navbar-form input[type="image"],
+.navbar-form input[type="checkbox"],
+.navbar-form input[type="radio"] {
+ margin-top: 3px;
+}
+
+.navbar-form .input-append,
+.navbar-form .input-prepend {
+ margin-top: 5px;
+ white-space: nowrap;
+}
+
+.navbar-form .input-append input,
+.navbar-form .input-prepend input {
+ margin-top: 0;
+}
+
+.navbar-search {
+ position: relative;
+ float: left;
+ margin-top: 5px;
+ margin-bottom: 0;
+}
+
+.navbar-search .search-query {
+ padding: 4px 14px;
+ margin-bottom: 0;
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-size: 13px;
+ font-weight: normal;
+ line-height: 1;
+ -webkit-border-radius: 15px;
+ -moz-border-radius: 15px;
+ border-radius: 15px;
+}
+
+.navbar-static-top {
+ position: static;
+ margin-bottom: 0;
+}
+
+.navbar-static-top .navbar-inner {
+ -webkit-border-radius: 0;
+ -moz-border-radius: 0;
+ border-radius: 0;
+}
+
+.navbar-fixed-top,
+.navbar-fixed-bottom {
+ position: fixed;
+ right: 0;
+ left: 0;
+ z-index: 1030;
+ margin-bottom: 0;
+}
+
+.navbar-fixed-top .navbar-inner,
+.navbar-static-top .navbar-inner {
+ border-width: 0 0 1px;
+}
+
+.navbar-fixed-bottom .navbar-inner {
+ border-width: 1px 0 0;
+}
+
+.navbar-fixed-top .navbar-inner,
+.navbar-fixed-bottom .navbar-inner {
+ padding-right: 0;
+ padding-left: 0;
+ -webkit-border-radius: 0;
+ -moz-border-radius: 0;
+ border-radius: 0;
+}
+
+.navbar-static-top .container,
+.navbar-fixed-top .container,
+.navbar-fixed-bottom .container {
+ width: 940px;
+}
+
+.navbar-fixed-top {
+ top: 0;
+}
+
+.navbar-fixed-top .navbar-inner,
+.navbar-static-top .navbar-inner {
+ -webkit-box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1);
+ -moz-box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1);
+ box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1);
+}
+
+.navbar-fixed-bottom {
+ bottom: 0;
+}
+
+.navbar-fixed-bottom .navbar-inner {
+ -webkit-box-shadow: 0 -1px 10px rgba(0, 0, 0, 0.1);
+ -moz-box-shadow: 0 -1px 10px rgba(0, 0, 0, 0.1);
+ box-shadow: 0 -1px 10px rgba(0, 0, 0, 0.1);
+}
+
+.navbar .nav {
+ position: relative;
+ left: 0;
+ display: block;
+ float: left;
+ margin: 0 10px 0 0;
+}
+
+.navbar .nav.pull-right {
+ float: right;
+ margin-right: 0;
+}
+
+.navbar .nav > li {
+ float: left;
+}
+
+.navbar .nav > li > a {
+ float: none;
+ padding: 10px 15px 10px;
+ color: #777777;
+ text-decoration: none;
+ text-shadow: 0 1px 0 #ffffff;
+}
+
+.navbar .nav .dropdown-toggle .caret {
+ margin-top: 8px;
+}
+
+.navbar .nav > li > a:focus,
+.navbar .nav > li > a:hover {
+ color: #333333;
+ text-decoration: none;
+ background-color: transparent;
+}
+
+.navbar .nav > .active > a,
+.navbar .nav > .active > a:hover,
+.navbar .nav > .active > a:focus {
+ color: #555555;
+ text-decoration: none;
+ background-color: #e5e5e5;
+ -webkit-box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.125);
+ -moz-box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.125);
+ box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.125);
+}
+
+.navbar .btn-navbar {
+ display: none;
+ float: right;
+ padding: 7px 10px;
+ margin-right: 5px;
+ margin-left: 5px;
+ color: #ffffff;
+ text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
+ background-color: #ededed;
+ *background-color: #e5e5e5;
+ background-image: -moz-linear-gradient(top, #f2f2f2, #e5e5e5);
+ background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f2f2f2), to(#e5e5e5));
+ background-image: -webkit-linear-gradient(top, #f2f2f2, #e5e5e5);
+ background-image: -o-linear-gradient(top, #f2f2f2, #e5e5e5);
+ background-image: linear-gradient(to bottom, #f2f2f2, #e5e5e5);
+ background-repeat: repeat-x;
+ border-color: #e5e5e5 #e5e5e5 #bfbfbf;
+ border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2f2f2', endColorstr='#ffe5e5e5', GradientType=0);
+ filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
+ -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075);
+ -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075);
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075);
+}
+
+.navbar .btn-navbar:hover,
+.navbar .btn-navbar:focus,
+.navbar .btn-navbar:active,
+.navbar .btn-navbar.active,
+.navbar .btn-navbar.disabled,
+.navbar .btn-navbar[disabled] {
+ color: #ffffff;
+ background-color: #e5e5e5;
+ *background-color: #d9d9d9;
+}
+
+.navbar .btn-navbar:active,
+.navbar .btn-navbar.active {
+ background-color: #cccccc \9;
+}
+
+.navbar .btn-navbar .icon-bar {
+ display: block;
+ width: 18px;
+ height: 2px;
+ background-color: #f5f5f5;
+ -webkit-border-radius: 1px;
+ -moz-border-radius: 1px;
+ border-radius: 1px;
+ -webkit-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25);
+ -moz-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25);
+ box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25);
+}
+
+.btn-navbar .icon-bar + .icon-bar {
+ margin-top: 3px;
+}
+
+.navbar .nav > li > .dropdown-menu:before {
+ position: absolute;
+ top: -7px;
+ left: 9px;
+ display: inline-block;
+ border-right: 7px solid transparent;
+ border-bottom: 7px solid #ccc;
+ border-left: 7px solid transparent;
+ border-bottom-color: rgba(0, 0, 0, 0.2);
+ content: '';
+}
+
+.navbar .nav > li > .dropdown-menu:after {
+ position: absolute;
+ top: -6px;
+ left: 10px;
+ display: inline-block;
+ border-right: 6px solid transparent;
+ border-bottom: 6px solid #ffffff;
+ border-left: 6px solid transparent;
+ content: '';
+}
+
+.navbar-fixed-bottom .nav > li > .dropdown-menu:before {
+ top: auto;
+ bottom: -7px;
+ border-top: 7px solid #ccc;
+ border-bottom: 0;
+ border-top-color: rgba(0, 0, 0, 0.2);
+}
+
+.navbar-fixed-bottom .nav > li > .dropdown-menu:after {
+ top: auto;
+ bottom: -6px;
+ border-top: 6px solid #ffffff;
+ border-bottom: 0;
+}
+
+.navbar .nav li.dropdown > a:hover .caret,
+.navbar .nav li.dropdown > a:focus .caret {
+ border-top-color: #333333;
+ border-bottom-color: #333333;
+}
+
+.navbar .nav li.dropdown.open > .dropdown-toggle,
+.navbar .nav li.dropdown.active > .dropdown-toggle,
+.navbar .nav li.dropdown.open.active > .dropdown-toggle {
+ color: #555555;
+ background-color: #e5e5e5;
+}
+
+.navbar .nav li.dropdown > .dropdown-toggle .caret {
+ border-top-color: #777777;
+ border-bottom-color: #777777;
+}
+
+.navbar .nav li.dropdown.open > .dropdown-toggle .caret,
+.navbar .nav li.dropdown.active > .dropdown-toggle .caret,
+.navbar .nav li.dropdown.open.active > .dropdown-toggle .caret {
+ border-top-color: #555555;
+ border-bottom-color: #555555;
+}
+
+.navbar .pull-right > li > .dropdown-menu,
+.navbar .nav > li > .dropdown-menu.pull-right {
+ right: 0;
+ left: auto;
+}
+
+.navbar .pull-right > li > .dropdown-menu:before,
+.navbar .nav > li > .dropdown-menu.pull-right:before {
+ right: 12px;
+ left: auto;
+}
+
+.navbar .pull-right > li > .dropdown-menu:after,
+.navbar .nav > li > .dropdown-menu.pull-right:after {
+ right: 13px;
+ left: auto;
+}
+
+.navbar .pull-right > li > .dropdown-menu .dropdown-menu,
+.navbar .nav > li > .dropdown-menu.pull-right .dropdown-menu {
+ right: 100%;
+ left: auto;
+ margin-right: -1px;
+ margin-left: 0;
+ -webkit-border-radius: 6px 0 6px 6px;
+ -moz-border-radius: 6px 0 6px 6px;
+ border-radius: 6px 0 6px 6px;
+}
+
+.navbar-inverse .navbar-inner {
+ background-color: #1b1b1b;
+ background-image: -moz-linear-gradient(top, #222222, #111111);
+ background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#222222), to(#111111));
+ background-image: -webkit-linear-gradient(top, #222222, #111111);
+ background-image: -o-linear-gradient(top, #222222, #111111);
+ background-image: linear-gradient(to bottom, #222222, #111111);
+ background-repeat: repeat-x;
+ border-color: #252525;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff111111', GradientType=0);
+}
+
+.navbar-inverse .brand,
+.navbar-inverse .nav > li > a {
+ color: #999999;
+ text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
+}
+
+.navbar-inverse .brand:hover,
+.navbar-inverse .nav > li > a:hover,
+.navbar-inverse .brand:focus,
+.navbar-inverse .nav > li > a:focus {
+ color: #ffffff;
+}
+
+.navbar-inverse .brand {
+ color: #999999;
+}
+
+.navbar-inverse .navbar-text {
+ color: #999999;
+}
+
+.navbar-inverse .nav > li > a:focus,
+.navbar-inverse .nav > li > a:hover {
+ color: #ffffff;
+ background-color: transparent;
+}
+
+.navbar-inverse .nav .active > a,
+.navbar-inverse .nav .active > a:hover,
+.navbar-inverse .nav .active > a:focus {
+ color: #ffffff;
+ background-color: #111111;
+}
+
+.navbar-inverse .navbar-link {
+ color: #999999;
+}
+
+.navbar-inverse .navbar-link:hover,
+.navbar-inverse .navbar-link:focus {
+ color: #ffffff;
+}
+
+.navbar-inverse .divider-vertical {
+ border-right-color: #222222;
+ border-left-color: #111111;
+}
+
+.navbar-inverse .nav li.dropdown.open > .dropdown-toggle,
+.navbar-inverse .nav li.dropdown.active > .dropdown-toggle,
+.navbar-inverse .nav li.dropdown.open.active > .dropdown-toggle {
+ color: #ffffff;
+ background-color: #111111;
+}
+
+.navbar-inverse .nav li.dropdown > a:hover .caret,
+.navbar-inverse .nav li.dropdown > a:focus .caret {
+ border-top-color: #ffffff;
+ border-bottom-color: #ffffff;
+}
+
+.navbar-inverse .nav li.dropdown > .dropdown-toggle .caret {
+ border-top-color: #999999;
+ border-bottom-color: #999999;
+}
+
+.navbar-inverse .nav li.dropdown.open > .dropdown-toggle .caret,
+.navbar-inverse .nav li.dropdown.active > .dropdown-toggle .caret,
+.navbar-inverse .nav li.dropdown.open.active > .dropdown-toggle .caret {
+ border-top-color: #ffffff;
+ border-bottom-color: #ffffff;
+}
+
+.navbar-inverse .navbar-search .search-query {
+ color: #ffffff;
+ background-color: #515151;
+ border-color: #111111;
+ -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15);
+ -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15);
+ box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15);
+ -webkit-transition: none;
+ -moz-transition: none;
+ -o-transition: none;
+ transition: none;
+}
+
+.navbar-inverse .navbar-search .search-query:-moz-placeholder {
+ color: #cccccc;
+}
+
+.navbar-inverse .navbar-search .search-query:-ms-input-placeholder {
+ color: #cccccc;
+}
+
+.navbar-inverse .navbar-search .search-query::-webkit-input-placeholder {
+ color: #cccccc;
+}
+
+.navbar-inverse .navbar-search .search-query:focus,
+.navbar-inverse .navbar-search .search-query.focused {
+ padding: 5px 15px;
+ color: #333333;
+ text-shadow: 0 1px 0 #ffffff;
+ background-color: #ffffff;
+ border: 0;
+ outline: 0;
+ -webkit-box-shadow: 0 0 3px rgba(0, 0, 0, 0.15);
+ -moz-box-shadow: 0 0 3px rgba(0, 0, 0, 0.15);
+ box-shadow: 0 0 3px rgba(0, 0, 0, 0.15);
+}
+
+.navbar-inverse .btn-navbar {
+ color: #ffffff;
+ text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
+ background-color: #0e0e0e;
+ *background-color: #040404;
+ background-image: -moz-linear-gradient(top, #151515, #040404);
+ background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#151515), to(#040404));
+ background-image: -webkit-linear-gradient(top, #151515, #040404);
+ background-image: -o-linear-gradient(top, #151515, #040404);
+ background-image: linear-gradient(to bottom, #151515, #040404);
+ background-repeat: repeat-x;
+ border-color: #040404 #040404 #000000;
+ border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff151515', endColorstr='#ff040404', GradientType=0);
+ filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
+}
+
+.navbar-inverse .btn-navbar:hover,
+.navbar-inverse .btn-navbar:focus,
+.navbar-inverse .btn-navbar:active,
+.navbar-inverse .btn-navbar.active,
+.navbar-inverse .btn-navbar.disabled,
+.navbar-inverse .btn-navbar[disabled] {
+ color: #ffffff;
+ background-color: #040404;
+ *background-color: #000000;
+}
+
+.navbar-inverse .btn-navbar:active,
+.navbar-inverse .btn-navbar.active {
+ background-color: #000000 \9;
+}
+
+.breadcrumb {
+ padding: 8px 15px;
+ margin: 0 0 20px;
+ list-style: none;
+ background-color: #f5f5f5;
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+}
+
+.breadcrumb > li {
+ display: inline-block;
+ *display: inline;
+ text-shadow: 0 1px 0 #ffffff;
+ *zoom: 1;
+}
+
+.breadcrumb > li > .divider {
+ padding: 0 5px;
+ color: #ccc;
+}
+
+.breadcrumb > .active {
+ color: #999999;
+}
+
+.pagination {
+ margin: 20px 0;
+}
+
+.pagination ul {
+ display: inline-block;
+ *display: inline;
+ margin-bottom: 0;
+ margin-left: 0;
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+ *zoom: 1;
+ -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
+ -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
+}
+
+.pagination ul > li {
+ display: inline;
+}
+
+.pagination ul > li > a,
+.pagination ul > li > span {
+ float: left;
+ padding: 4px 12px;
+ line-height: 20px;
+ text-decoration: none;
+ background-color: #ffffff;
+ border: 1px solid #dddddd;
+ border-left-width: 0;
+}
+
+.pagination ul > li > a:hover,
+.pagination ul > li > a:focus,
+.pagination ul > .active > a,
+.pagination ul > .active > span {
+ background-color: #f5f5f5;
+}
+
+.pagination ul > .active > a,
+.pagination ul > .active > span {
+ color: #999999;
+ cursor: default;
+}
+
+.pagination ul > .disabled > span,
+.pagination ul > .disabled > a,
+.pagination ul > .disabled > a:hover,
+.pagination ul > .disabled > a:focus {
+ color: #999999;
+ cursor: default;
+ background-color: transparent;
+}
+
+.pagination ul > li:first-child > a,
+.pagination ul > li:first-child > span {
+ border-left-width: 1px;
+ -webkit-border-bottom-left-radius: 4px;
+ border-bottom-left-radius: 4px;
+ -webkit-border-top-left-radius: 4px;
+ border-top-left-radius: 4px;
+ -moz-border-radius-bottomleft: 4px;
+ -moz-border-radius-topleft: 4px;
+}
+
+.pagination ul > li:last-child > a,
+.pagination ul > li:last-child > span {
+ -webkit-border-top-right-radius: 4px;
+ border-top-right-radius: 4px;
+ -webkit-border-bottom-right-radius: 4px;
+ border-bottom-right-radius: 4px;
+ -moz-border-radius-topright: 4px;
+ -moz-border-radius-bottomright: 4px;
+}
+
+.pagination-centered {
+ text-align: center;
+}
+
+.pagination-right {
+ text-align: right;
+}
+
+.pagination-large ul > li > a,
+.pagination-large ul > li > span {
+ padding: 11px 19px;
+ font-size: 17.5px;
+}
+
+.pagination-large ul > li:first-child > a,
+.pagination-large ul > li:first-child > span {
+ -webkit-border-bottom-left-radius: 6px;
+ border-bottom-left-radius: 6px;
+ -webkit-border-top-left-radius: 6px;
+ border-top-left-radius: 6px;
+ -moz-border-radius-bottomleft: 6px;
+ -moz-border-radius-topleft: 6px;
+}
+
+.pagination-large ul > li:last-child > a,
+.pagination-large ul > li:last-child > span {
+ -webkit-border-top-right-radius: 6px;
+ border-top-right-radius: 6px;
+ -webkit-border-bottom-right-radius: 6px;
+ border-bottom-right-radius: 6px;
+ -moz-border-radius-topright: 6px;
+ -moz-border-radius-bottomright: 6px;
+}
+
+.pagination-mini ul > li:first-child > a,
+.pagination-small ul > li:first-child > a,
+.pagination-mini ul > li:first-child > span,
+.pagination-small ul > li:first-child > span {
+ -webkit-border-bottom-left-radius: 3px;
+ border-bottom-left-radius: 3px;
+ -webkit-border-top-left-radius: 3px;
+ border-top-left-radius: 3px;
+ -moz-border-radius-bottomleft: 3px;
+ -moz-border-radius-topleft: 3px;
+}
+
+.pagination-mini ul > li:last-child > a,
+.pagination-small ul > li:last-child > a,
+.pagination-mini ul > li:last-child > span,
+.pagination-small ul > li:last-child > span {
+ -webkit-border-top-right-radius: 3px;
+ border-top-right-radius: 3px;
+ -webkit-border-bottom-right-radius: 3px;
+ border-bottom-right-radius: 3px;
+ -moz-border-radius-topright: 3px;
+ -moz-border-radius-bottomright: 3px;
+}
+
+.pagination-small ul > li > a,
+.pagination-small ul > li > span {
+ padding: 2px 10px;
+ font-size: 11.9px;
+}
+
+.pagination-mini ul > li > a,
+.pagination-mini ul > li > span {
+ padding: 0 6px;
+ font-size: 10.5px;
+}
+
+.pager {
+ margin: 20px 0;
+ text-align: center;
+ list-style: none;
+ *zoom: 1;
+}
+
+.pager:before,
+.pager:after {
+ display: table;
+ line-height: 0;
+ content: "";
+}
+
+.pager:after {
+ clear: both;
+}
+
+.pager li {
+ display: inline;
+}
+
+.pager li > a,
+.pager li > span {
+ display: inline-block;
+ padding: 5px 14px;
+ background-color: #fff;
+ border: 1px solid #ddd;
+ -webkit-border-radius: 15px;
+ -moz-border-radius: 15px;
+ border-radius: 15px;
+}
+
+.pager li > a:hover,
+.pager li > a:focus {
+ text-decoration: none;
+ background-color: #f5f5f5;
+}
+
+.pager .next > a,
+.pager .next > span {
+ float: right;
+}
+
+.pager .previous > a,
+.pager .previous > span {
+ float: left;
+}
+
+.pager .disabled > a,
+.pager .disabled > a:hover,
+.pager .disabled > a:focus,
+.pager .disabled > span {
+ color: #999999;
+ cursor: default;
+ background-color: #fff;
+}
+
+.modal-backdrop {
+ position: fixed;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 1040;
+ background-color: #000000;
+}
+
+.modal-backdrop.fade {
+ opacity: 0;
+}
+
+.modal-backdrop,
+.modal-backdrop.fade.in {
+ opacity: 0.8;
+ filter: alpha(opacity=80);
+}
+
+.modal {
+ position: fixed;
+ top: 10%;
+ left: 50%;
+ z-index: 1050;
+ width: 560px;
+ margin-left: -280px;
+ background-color: #ffffff;
+ border: 1px solid #999;
+ border: 1px solid rgba(0, 0, 0, 0.3);
+ *border: 1px solid #999;
+ -webkit-border-radius: 6px;
+ -moz-border-radius: 6px;
+ border-radius: 6px;
+ outline: none;
+ -webkit-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3);
+ -moz-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3);
+ box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3);
+ -webkit-background-clip: padding-box;
+ -moz-background-clip: padding-box;
+ background-clip: padding-box;
+}
+
+.modal.fade {
+ top: -25%;
+ -webkit-transition: opacity 0.3s linear, top 0.3s ease-out;
+ -moz-transition: opacity 0.3s linear, top 0.3s ease-out;
+ -o-transition: opacity 0.3s linear, top 0.3s ease-out;
+ transition: opacity 0.3s linear, top 0.3s ease-out;
+}
+
+.modal.fade.in {
+ top: 10%;
+}
+
+.modal-header {
+ padding: 9px 15px;
+ border-bottom: 1px solid #eee;
+}
+
+.modal-header .close {
+ margin-top: 2px;
+}
+
+.modal-header h3 {
+ margin: 0;
+ line-height: 30px;
+}
+
+.modal-body {
+ position: relative;
+ max-height: 400px;
+ padding: 15px;
+ overflow-y: auto;
+}
+
+.modal-form {
+ margin-bottom: 0;
+}
+
+.modal-footer {
+ padding: 14px 15px 15px;
+ margin-bottom: 0;
+ text-align: right;
+ background-color: #f5f5f5;
+ border-top: 1px solid #ddd;
+ -webkit-border-radius: 0 0 6px 6px;
+ -moz-border-radius: 0 0 6px 6px;
+ border-radius: 0 0 6px 6px;
+ *zoom: 1;
+ -webkit-box-shadow: inset 0 1px 0 #ffffff;
+ -moz-box-shadow: inset 0 1px 0 #ffffff;
+ box-shadow: inset 0 1px 0 #ffffff;
+}
+
+.modal-footer:before,
+.modal-footer:after {
+ display: table;
+ line-height: 0;
+ content: "";
+}
+
+.modal-footer:after {
+ clear: both;
+}
+
+.modal-footer .btn + .btn {
+ margin-bottom: 0;
+ margin-left: 5px;
+}
+
+.modal-footer .btn-group .btn + .btn {
+ margin-left: -1px;
+}
+
+.modal-footer .btn-block + .btn-block {
+ margin-left: 0;
+}
+
+.tooltip {
+ position: absolute;
+ z-index: 1030;
+ display: block;
+ font-size: 11px;
+ line-height: 1.4;
+ opacity: 0;
+ filter: alpha(opacity=0);
+ visibility: visible;
+}
+
+.tooltip.in {
+ opacity: 0.8;
+ filter: alpha(opacity=80);
+}
+
+.tooltip.top {
+ padding: 5px 0;
+ margin-top: -3px;
+}
+
+.tooltip.right {
+ padding: 0 5px;
+ margin-left: 3px;
+}
+
+.tooltip.bottom {
+ padding: 5px 0;
+ margin-top: 3px;
+}
+
+.tooltip.left {
+ padding: 0 5px;
+ margin-left: -3px;
+}
+
+.tooltip-inner {
+ max-width: 200px;
+ padding: 8px;
+ color: #ffffff;
+ text-align: center;
+ text-decoration: none;
+ background-color: #000000;
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+}
+
+.tooltip-arrow {
+ position: absolute;
+ width: 0;
+ height: 0;
+ border-color: transparent;
+ border-style: solid;
+}
+
+.tooltip.top .tooltip-arrow {
+ bottom: 0;
+ left: 50%;
+ margin-left: -5px;
+ border-top-color: #000000;
+ border-width: 5px 5px 0;
+}
+
+.tooltip.right .tooltip-arrow {
+ top: 50%;
+ left: 0;
+ margin-top: -5px;
+ border-right-color: #000000;
+ border-width: 5px 5px 5px 0;
+}
+
+.tooltip.left .tooltip-arrow {
+ top: 50%;
+ right: 0;
+ margin-top: -5px;
+ border-left-color: #000000;
+ border-width: 5px 0 5px 5px;
+}
+
+.tooltip.bottom .tooltip-arrow {
+ top: 0;
+ left: 50%;
+ margin-left: -5px;
+ border-bottom-color: #000000;
+ border-width: 0 5px 5px;
+}
+
+.popover {
+ position: absolute;
+ top: 0;
+ left: 0;
+ z-index: 1010;
+ display: none;
+ max-width: 276px;
+ padding: 1px;
+ text-align: left;
+ white-space: normal;
+ background-color: #ffffff;
+ border: 1px solid #ccc;
+ border: 1px solid rgba(0, 0, 0, 0.2);
+ -webkit-border-radius: 6px;
+ -moz-border-radius: 6px;
+ border-radius: 6px;
+ -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
+ -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
+ box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
+ -webkit-background-clip: padding-box;
+ -moz-background-clip: padding;
+ background-clip: padding-box;
+}
+
+.popover.top {
+ margin-top: -10px;
+}
+
+.popover.right {
+ margin-left: 10px;
+}
+
+.popover.bottom {
+ margin-top: 10px;
+}
+
+.popover.left {
+ margin-left: -10px;
+}
+
+.popover-title {
+ padding: 8px 14px;
+ margin: 0;
+ font-size: 14px;
+ font-weight: normal;
+ line-height: 18px;
+ background-color: #f7f7f7;
+ border-bottom: 1px solid #ebebeb;
+ -webkit-border-radius: 5px 5px 0 0;
+ -moz-border-radius: 5px 5px 0 0;
+ border-radius: 5px 5px 0 0;
+}
+
+.popover-title:empty {
+ display: none;
+}
+
+.popover-content {
+ padding: 9px 14px;
+}
+
+.popover .arrow,
+.popover .arrow:after {
+ position: absolute;
+ display: block;
+ width: 0;
+ height: 0;
+ border-color: transparent;
+ border-style: solid;
+}
+
+.popover .arrow {
+ border-width: 11px;
+}
+
+.popover .arrow:after {
+ border-width: 10px;
+ content: "";
+}
+
+.popover.top .arrow {
+ bottom: -11px;
+ left: 50%;
+ margin-left: -11px;
+ border-top-color: #999;
+ border-top-color: rgba(0, 0, 0, 0.25);
+ border-bottom-width: 0;
+}
+
+.popover.top .arrow:after {
+ bottom: 1px;
+ margin-left: -10px;
+ border-top-color: #ffffff;
+ border-bottom-width: 0;
+}
+
+.popover.right .arrow {
+ top: 50%;
+ left: -11px;
+ margin-top: -11px;
+ border-right-color: #999;
+ border-right-color: rgba(0, 0, 0, 0.25);
+ border-left-width: 0;
+}
+
+.popover.right .arrow:after {
+ bottom: -10px;
+ left: 1px;
+ border-right-color: #ffffff;
+ border-left-width: 0;
+}
+
+.popover.bottom .arrow {
+ top: -11px;
+ left: 50%;
+ margin-left: -11px;
+ border-bottom-color: #999;
+ border-bottom-color: rgba(0, 0, 0, 0.25);
+ border-top-width: 0;
+}
+
+.popover.bottom .arrow:after {
+ top: 1px;
+ margin-left: -10px;
+ border-bottom-color: #ffffff;
+ border-top-width: 0;
+}
+
+.popover.left .arrow {
+ top: 50%;
+ right: -11px;
+ margin-top: -11px;
+ border-left-color: #999;
+ border-left-color: rgba(0, 0, 0, 0.25);
+ border-right-width: 0;
+}
+
+.popover.left .arrow:after {
+ right: 1px;
+ bottom: -10px;
+ border-left-color: #ffffff;
+ border-right-width: 0;
+}
+
+.thumbnails {
+ margin-left: -20px;
+ list-style: none;
+ *zoom: 1;
+}
+
+.thumbnails:before,
+.thumbnails:after {
+ display: table;
+ line-height: 0;
+ content: "";
+}
+
+.thumbnails:after {
+ clear: both;
+}
+
+.row-fluid .thumbnails {
+ margin-left: 0;
+}
+
+.thumbnails > li {
+ float: left;
+ margin-bottom: 20px;
+ margin-left: 20px;
+}
+
+.thumbnail {
+ display: block;
+ padding: 4px;
+ line-height: 20px;
+ border: 1px solid #ddd;
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+ -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.055);
+ -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.055);
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.055);
+ -webkit-transition: all 0.2s ease-in-out;
+ -moz-transition: all 0.2s ease-in-out;
+ -o-transition: all 0.2s ease-in-out;
+ transition: all 0.2s ease-in-out;
+}
+
+a.thumbnail:hover,
+a.thumbnail:focus {
+ border-color: #0088cc;
+ -webkit-box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25);
+ -moz-box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25);
+ box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25);
+}
+
+.thumbnail > img {
+ display: block;
+ max-width: 100%;
+ margin-right: auto;
+ margin-left: auto;
+}
+
+.thumbnail .caption {
+ padding: 9px;
+ color: #555555;
+}
+
+.media,
+.media-body {
+ overflow: hidden;
+ *overflow: visible;
+ zoom: 1;
+}
+
+.media,
+.media .media {
+ margin-top: 15px;
+}
+
+.media:first-child {
+ margin-top: 0;
+}
+
+.media-object {
+ display: block;
+}
+
+.media-heading {
+ margin: 0 0 5px;
+}
+
+.media > .pull-left {
+ margin-right: 10px;
+}
+
+.media > .pull-right {
+ margin-left: 10px;
+}
+
+.media-list {
+ margin-left: 0;
+ list-style: none;
+}
+
+.label,
+.badge {
+ display: inline-block;
+ padding: 2px 4px;
+ font-size: 11.844px;
+ font-weight: bold;
+ line-height: 14px;
+ color: #ffffff;
+ text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
+ white-space: nowrap;
+ vertical-align: baseline;
+ background-color: #999999;
+}
+
+.label {
+ -webkit-border-radius: 3px;
+ -moz-border-radius: 3px;
+ border-radius: 3px;
+}
+
+.badge {
+ padding-right: 9px;
+ padding-left: 9px;
+ -webkit-border-radius: 9px;
+ -moz-border-radius: 9px;
+ border-radius: 9px;
+}
+
+.label:empty,
+.badge:empty {
+ display: none;
+}
+
+a.label:hover,
+a.label:focus,
+a.badge:hover,
+a.badge:focus {
+ color: #ffffff;
+ text-decoration: none;
+ cursor: pointer;
+}
+
+.label-important,
+.badge-important {
+ background-color: #b94a48;
+}
+
+.label-important[href],
+.badge-important[href] {
+ background-color: #953b39;
+}
+
+.label-warning,
+.badge-warning {
+ background-color: #f89406;
+}
+
+.label-warning[href],
+.badge-warning[href] {
+ background-color: #c67605;
+}
+
+.label-success,
+.badge-success {
+ background-color: #468847;
+}
+
+.label-success[href],
+.badge-success[href] {
+ background-color: #356635;
+}
+
+.label-info,
+.badge-info {
+ background-color: #3a87ad;
+}
+
+.label-info[href],
+.badge-info[href] {
+ background-color: #2d6987;
+}
+
+.label-inverse,
+.badge-inverse {
+ background-color: #333333;
+}
+
+.label-inverse[href],
+.badge-inverse[href] {
+ background-color: #1a1a1a;
+}
+
+.btn .label,
+.btn .badge {
+ position: relative;
+ top: -1px;
+}
+
+.btn-mini .label,
+.btn-mini .badge {
+ top: 0;
+}
+
+ at -webkit-keyframes progress-bar-stripes {
+ from {
+ background-position: 40px 0;
+ }
+ to {
+ background-position: 0 0;
+ }
+}
+
+ at -moz-keyframes progress-bar-stripes {
+ from {
+ background-position: 40px 0;
+ }
+ to {
+ background-position: 0 0;
+ }
+}
+
+ at -ms-keyframes progress-bar-stripes {
+ from {
+ background-position: 40px 0;
+ }
+ to {
+ background-position: 0 0;
+ }
+}
+
+ at -o-keyframes progress-bar-stripes {
+ from {
+ background-position: 0 0;
+ }
+ to {
+ background-position: 40px 0;
+ }
+}
+
+ at keyframes progress-bar-stripes {
+ from {
+ background-position: 40px 0;
+ }
+ to {
+ background-position: 0 0;
+ }
+}
+
+.progress {
+ height: 20px;
+ margin-bottom: 20px;
+ overflow: hidden;
+ background-color: #f7f7f7;
+ background-image: -moz-linear-gradient(top, #f5f5f5, #f9f9f9);
+ background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f5f5f5), to(#f9f9f9));
+ background-image: -webkit-linear-gradient(top, #f5f5f5, #f9f9f9);
+ background-image: -o-linear-gradient(top, #f5f5f5, #f9f9f9);
+ background-image: linear-gradient(to bottom, #f5f5f5, #f9f9f9);
+ background-repeat: repeat-x;
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#fff9f9f9', GradientType=0);
+ -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
+ -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
+ box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
+}
+
+.progress .bar {
+ float: left;
+ width: 0;
+ height: 100%;
+ font-size: 12px;
+ color: #ffffff;
+ text-align: center;
+ text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
+ background-color: #0e90d2;
+ background-image: -moz-linear-gradient(top, #149bdf, #0480be);
+ background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#149bdf), to(#0480be));
+ background-image: -webkit-linear-gradient(top, #149bdf, #0480be);
+ background-image: -o-linear-gradient(top, #149bdf, #0480be);
+ background-image: linear-gradient(to bottom, #149bdf, #0480be);
+ background-repeat: repeat-x;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff149bdf', endColorstr='#ff0480be', GradientType=0);
+ -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
+ -moz-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
+ box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ -webkit-transition: width 0.6s ease;
+ -moz-transition: width 0.6s ease;
+ -o-transition: width 0.6s ease;
+ transition: width 0.6s ease;
+}
+
+.progress .bar + .bar {
+ -webkit-box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.15), inset 0 -1px 0 rgba(0, 0, 0, 0.15);
+ -moz-box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.15), inset 0 -1px 0 rgba(0, 0, 0, 0.15);
+ box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.15), inset 0 -1px 0 rgba(0, 0, 0, 0.15);
+}
+
+.progress-striped .bar {
+ background-color: #149bdf;
+ background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));
+ background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+ background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+ background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+ background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+ -webkit-background-size: 40px 40px;
+ -moz-background-size: 40px 40px;
+ -o-background-size: 40px 40px;
+ background-size: 40px 40px;
+}
+
+.progress.active .bar {
+ -webkit-animation: progress-bar-stripes 2s linear infinite;
+ -moz-animation: progress-bar-stripes 2s linear infinite;
+ -ms-animation: progress-bar-stripes 2s linear infinite;
+ -o-animation: progress-bar-stripes 2s linear infinite;
+ animation: progress-bar-stripes 2s linear infinite;
+}
+
+.progress-danger .bar,
+.progress .bar-danger {
+ background-color: #dd514c;
+ background-image: -moz-linear-gradient(top, #ee5f5b, #c43c35);
+ background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#c43c35));
+ background-image: -webkit-linear-gradient(top, #ee5f5b, #c43c35);
+ background-image: -o-linear-gradient(top, #ee5f5b, #c43c35);
+ background-image: linear-gradient(to bottom, #ee5f5b, #c43c35);
+ background-repeat: repeat-x;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b', endColorstr='#ffc43c35', GradientType=0);
+}
+
+.progress-danger.progress-striped .bar,
+.progress-striped .bar-danger {
+ background-color: #ee5f5b;
+ background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));
+ background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+ background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+ background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+ background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+}
+
+.progress-success .bar,
+.progress .bar-success {
+ background-color: #5eb95e;
+ background-image: -moz-linear-gradient(top, #62c462, #57a957);
+ background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#57a957));
+ background-image: -webkit-linear-gradient(top, #62c462, #57a957);
+ background-image: -o-linear-gradient(top, #62c462, #57a957);
+ background-image: linear-gradient(to bottom, #62c462, #57a957);
+ background-repeat: repeat-x;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462', endColorstr='#ff57a957', GradientType=0);
+}
+
+.progress-success.progress-striped .bar,
+.progress-striped .bar-success {
+ background-color: #62c462;
+ background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));
+ background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+ background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+ background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+ background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+}
+
+.progress-info .bar,
+.progress .bar-info {
+ background-color: #4bb1cf;
+ background-image: -moz-linear-gradient(top, #5bc0de, #339bb9);
+ background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#339bb9));
+ background-image: -webkit-linear-gradient(top, #5bc0de, #339bb9);
+ background-image: -o-linear-gradient(top, #5bc0de, #339bb9);
+ background-image: linear-gradient(to bottom, #5bc0de, #339bb9);
+ background-repeat: repeat-x;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff339bb9', GradientType=0);
+}
+
+.progress-info.progress-striped .bar,
+.progress-striped .bar-info {
+ background-color: #5bc0de;
+ background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));
+ background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+ background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+ background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+ background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+}
+
+.progress-warning .bar,
+.progress .bar-warning {
+ background-color: #faa732;
+ background-image: -moz-linear-gradient(top, #fbb450, #f89406);
+ background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406));
+ background-image: -webkit-linear-gradient(top, #fbb450, #f89406);
+ background-image: -o-linear-gradient(top, #fbb450, #f89406);
+ background-image: linear-gradient(to bottom, #fbb450, #f89406);
+ background-repeat: repeat-x;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450', endColorstr='#fff89406', GradientType=0);
+}
+
+.progress-warning.progress-striped .bar,
+.progress-striped .bar-warning {
+ background-color: #fbb450;
+ background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));
+ background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+ background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+ background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+ background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+}
+
+.accordion {
+ margin-bottom: 20px;
+}
+
+.accordion-group {
+ margin-bottom: 2px;
+ border: 1px solid #e5e5e5;
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+}
+
+.accordion-heading {
+ border-bottom: 0;
+}
+
+.accordion-heading .accordion-toggle {
+ display: block;
+ padding: 8px 15px;
+}
+
+.accordion-toggle {
+ cursor: pointer;
+}
+
+.accordion-inner {
+ padding: 9px 15px;
+ border-top: 1px solid #e5e5e5;
+}
+
+.carousel {
+ position: relative;
+ margin-bottom: 20px;
+ line-height: 1;
+}
+
+.carousel-inner {
+ position: relative;
+ width: 100%;
+ overflow: hidden;
+}
+
+.carousel-inner > .item {
+ position: relative;
+ display: none;
+ -webkit-transition: 0.6s ease-in-out left;
+ -moz-transition: 0.6s ease-in-out left;
+ -o-transition: 0.6s ease-in-out left;
+ transition: 0.6s ease-in-out left;
+}
+
+.carousel-inner > .item > img,
+.carousel-inner > .item > a > img {
+ display: block;
+ line-height: 1;
+}
+
+.carousel-inner > .active,
+.carousel-inner > .next,
+.carousel-inner > .prev {
+ display: block;
+}
+
+.carousel-inner > .active {
+ left: 0;
+}
+
+.carousel-inner > .next,
+.carousel-inner > .prev {
+ position: absolute;
+ top: 0;
+ width: 100%;
+}
+
+.carousel-inner > .next {
+ left: 100%;
+}
+
+.carousel-inner > .prev {
+ left: -100%;
+}
+
+.carousel-inner > .next.left,
+.carousel-inner > .prev.right {
+ left: 0;
+}
+
+.carousel-inner > .active.left {
+ left: -100%;
+}
+
+.carousel-inner > .active.right {
+ left: 100%;
+}
+
+.carousel-control {
+ position: absolute;
+ top: 40%;
+ left: 15px;
+ width: 40px;
+ height: 40px;
+ margin-top: -20px;
+ font-size: 60px;
+ font-weight: 100;
+ line-height: 30px;
+ color: #ffffff;
+ text-align: center;
+ background: #222222;
+ border: 3px solid #ffffff;
+ -webkit-border-radius: 23px;
+ -moz-border-radius: 23px;
+ border-radius: 23px;
+ opacity: 0.5;
+ filter: alpha(opacity=50);
+}
+
+.carousel-control.right {
+ right: 15px;
+ left: auto;
+}
+
+.carousel-control:hover,
+.carousel-control:focus {
+ color: #ffffff;
+ text-decoration: none;
+ opacity: 0.9;
+ filter: alpha(opacity=90);
+}
+
+.carousel-indicators {
+ position: absolute;
+ top: 15px;
+ right: 15px;
+ z-index: 5;
+ margin: 0;
+ list-style: none;
+}
+
+.carousel-indicators li {
+ display: block;
+ float: left;
+ width: 10px;
+ height: 10px;
+ margin-left: 5px;
+ text-indent: -999px;
+ background-color: #ccc;
+ background-color: rgba(255, 255, 255, 0.25);
+ border-radius: 5px;
+}
+
+.carousel-indicators .active {
+ background-color: #fff;
+}
+
+.carousel-caption {
+ position: absolute;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ padding: 15px;
+ background: #333333;
+ background: rgba(0, 0, 0, 0.75);
+}
+
+.carousel-caption h4,
+.carousel-caption p {
+ line-height: 20px;
+ color: #ffffff;
+}
+
+.carousel-caption h4 {
+ margin: 0 0 5px;
+}
+
+.carousel-caption p {
+ margin-bottom: 0;
+}
+
+.hero-unit {
+ padding: 60px;
+ margin-bottom: 30px;
+ font-size: 18px;
+ font-weight: 200;
+ line-height: 30px;
+ color: inherit;
+ background-color: #eeeeee;
+ -webkit-border-radius: 6px;
+ -moz-border-radius: 6px;
+ border-radius: 6px;
+}
+
+.hero-unit h1 {
+ margin-bottom: 0;
+ font-size: 60px;
+ line-height: 1;
+ letter-spacing: -1px;
+ color: inherit;
+}
+
+.hero-unit li {
+ line-height: 30px;
+}
+
+.pull-right {
+ float: right;
+}
+
+.pull-left {
+ float: left;
+}
+
+.hide {
+ display: none;
+}
+
+.show {
+ display: block;
+}
+
+.invisible {
+ visibility: hidden;
+}
+
+.affix {
+ position: fixed;
+}
diff --git a/doc/etc/bootstrap/css/bootstrap.min.css b/doc/etc/bootstrap/css/bootstrap.min.css
new file mode 100644
index 0000000..df96c86
--- /dev/null
+++ b/doc/etc/bootstrap/css/bootstrap.min.css
@@ -0,0 +1,9 @@
+/*!
+ * Bootstrap v2.3.2
+ *
+ * Copyright 2013 Twitter, Inc
+ * Licensed under the Apache License v2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Designed and built with all the love in the world by @mdo and @fat.
+ */.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;line-height:0;content:""}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:in [...]
diff --git a/doc/etc/bootstrap/img/glyphicons-halflings-white.png b/doc/etc/bootstrap/img/glyphicons-halflings-white.png
new file mode 100644
index 0000000..3bf6484
Binary files /dev/null and b/doc/etc/bootstrap/img/glyphicons-halflings-white.png differ
diff --git a/doc/etc/bootstrap/img/glyphicons-halflings.png b/doc/etc/bootstrap/img/glyphicons-halflings.png
new file mode 100644
index 0000000..a996999
Binary files /dev/null and b/doc/etc/bootstrap/img/glyphicons-halflings.png differ
diff --git a/doc/etc/bootstrap/js/bootstrap.js b/doc/etc/bootstrap/js/bootstrap.js
new file mode 100644
index 0000000..44109f6
--- /dev/null
+++ b/doc/etc/bootstrap/js/bootstrap.js
@@ -0,0 +1,2280 @@
+/* ===================================================
+ * bootstrap-transition.js v2.3.2
+ * http://getbootstrap.com/2.3.2/javascript.html#transitions
+ * ===================================================
+ * Copyright 2013 Twitter, Inc.
+ *
+ * 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.
+ * ========================================================== */
+
+
+!function ($) {
+
+ "use strict"; // jshint ;_;
+
+
+ /* CSS TRANSITION SUPPORT (http://www.modernizr.com/)
+ * ======================================================= */
+
+ $(function () {
+
+ $.support.transition = (function () {
+
+ var transitionEnd = (function () {
+
+ var el = document.createElement('bootstrap')
+ , transEndEventNames = {
+ 'WebkitTransition' : 'webkitTransitionEnd'
+ , 'MozTransition' : 'transitionend'
+ , 'OTransition' : 'oTransitionEnd otransitionend'
+ , 'transition' : 'transitionend'
+ }
+ , name
+
+ for (name in transEndEventNames){
+ if (el.style[name] !== undefined) {
+ return transEndEventNames[name]
+ }
+ }
+
+ }())
+
+ return transitionEnd && {
+ end: transitionEnd
+ }
+
+ })()
+
+ })
+
+}(window.jQuery);/* ==========================================================
+ * bootstrap-alert.js v2.3.2
+ * http://getbootstrap.com/2.3.2/javascript.html#alerts
+ * ==========================================================
+ * Copyright 2013 Twitter, Inc.
+ *
+ * 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.
+ * ========================================================== */
+
+
+!function ($) {
+
+ "use strict"; // jshint ;_;
+
+
+ /* ALERT CLASS DEFINITION
+ * ====================== */
+
+ var dismiss = '[data-dismiss="alert"]'
+ , Alert = function (el) {
+ $(el).on('click', dismiss, this.close)
+ }
+
+ Alert.prototype.close = function (e) {
+ var $this = $(this)
+ , selector = $this.attr('data-target')
+ , $parent
+
+ if (!selector) {
+ selector = $this.attr('href')
+ selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7
+ }
+
+ $parent = $(selector)
+
+ e && e.preventDefault()
+
+ $parent.length || ($parent = $this.hasClass('alert') ? $this : $this.parent())
+
+ $parent.trigger(e = $.Event('close'))
+
+ if (e.isDefaultPrevented()) return
+
+ $parent.removeClass('in')
+
+ function removeElement() {
+ $parent
+ .trigger('closed')
+ .remove()
+ }
+
+ $.support.transition && $parent.hasClass('fade') ?
+ $parent.on($.support.transition.end, removeElement) :
+ removeElement()
+ }
+
+
+ /* ALERT PLUGIN DEFINITION
+ * ======================= */
+
+ var old = $.fn.alert
+
+ $.fn.alert = function (option) {
+ return this.each(function () {
+ var $this = $(this)
+ , data = $this.data('alert')
+ if (!data) $this.data('alert', (data = new Alert(this)))
+ if (typeof option == 'string') data[option].call($this)
+ })
+ }
+
+ $.fn.alert.Constructor = Alert
+
+
+ /* ALERT NO CONFLICT
+ * ================= */
+
+ $.fn.alert.noConflict = function () {
+ $.fn.alert = old
+ return this
+ }
+
+
+ /* ALERT DATA-API
+ * ============== */
+
+ $(document).on('click.alert.data-api', dismiss, Alert.prototype.close)
+
+}(window.jQuery);/* ============================================================
+ * bootstrap-button.js v2.3.2
+ * http://getbootstrap.com/2.3.2/javascript.html#buttons
+ * ============================================================
+ * Copyright 2013 Twitter, Inc.
+ *
+ * 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.
+ * ============================================================ */
+
+
+!function ($) {
+
+ "use strict"; // jshint ;_;
+
+
+ /* BUTTON PUBLIC CLASS DEFINITION
+ * ============================== */
+
+ var Button = function (element, options) {
+ this.$element = $(element)
+ this.options = $.extend({}, $.fn.button.defaults, options)
+ }
+
+ Button.prototype.setState = function (state) {
+ var d = 'disabled'
+ , $el = this.$element
+ , data = $el.data()
+ , val = $el.is('input') ? 'val' : 'html'
+
+ state = state + 'Text'
+ data.resetText || $el.data('resetText', $el[val]())
+
+ $el[val](data[state] || this.options[state])
+
+ // push to event loop to allow forms to submit
+ setTimeout(function () {
+ state == 'loadingText' ?
+ $el.addClass(d).attr(d, d) :
+ $el.removeClass(d).removeAttr(d)
+ }, 0)
+ }
+
+ Button.prototype.toggle = function () {
+ var $parent = this.$element.closest('[data-toggle="buttons-radio"]')
+
+ $parent && $parent
+ .find('.active')
+ .removeClass('active')
+
+ this.$element.toggleClass('active')
+ }
+
+
+ /* BUTTON PLUGIN DEFINITION
+ * ======================== */
+
+ var old = $.fn.button
+
+ $.fn.button = function (option) {
+ return this.each(function () {
+ var $this = $(this)
+ , data = $this.data('button')
+ , options = typeof option == 'object' && option
+ if (!data) $this.data('button', (data = new Button(this, options)))
+ if (option == 'toggle') data.toggle()
+ else if (option) data.setState(option)
+ })
+ }
+
+ $.fn.button.defaults = {
+ loadingText: 'loading...'
+ }
+
+ $.fn.button.Constructor = Button
+
+
+ /* BUTTON NO CONFLICT
+ * ================== */
+
+ $.fn.button.noConflict = function () {
+ $.fn.button = old
+ return this
+ }
+
+
+ /* BUTTON DATA-API
+ * =============== */
+
+ $(document).on('click.button.data-api', '[data-toggle^=button]', function (e) {
+ var $btn = $(e.target)
+ if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn')
+ $btn.button('toggle')
+ })
+
+}(window.jQuery);/* ==========================================================
+ * bootstrap-carousel.js v2.3.2
+ * http://getbootstrap.com/2.3.2/javascript.html#carousel
+ * ==========================================================
+ * Copyright 2013 Twitter, Inc.
+ *
+ * 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.
+ * ========================================================== */
+
+
+!function ($) {
+
+ "use strict"; // jshint ;_;
+
+
+ /* CAROUSEL CLASS DEFINITION
+ * ========================= */
+
+ var Carousel = function (element, options) {
+ this.$element = $(element)
+ this.$indicators = this.$element.find('.carousel-indicators')
+ this.options = options
+ this.options.pause == 'hover' && this.$element
+ .on('mouseenter', $.proxy(this.pause, this))
+ .on('mouseleave', $.proxy(this.cycle, this))
+ }
+
+ Carousel.prototype = {
+
+ cycle: function (e) {
+ if (!e) this.paused = false
+ if (this.interval) clearInterval(this.interval);
+ this.options.interval
+ && !this.paused
+ && (this.interval = setInterval($.proxy(this.next, this), this.options.interval))
+ return this
+ }
+
+ , getActiveIndex: function () {
+ this.$active = this.$element.find('.item.active')
+ this.$items = this.$active.parent().children()
+ return this.$items.index(this.$active)
+ }
+
+ , to: function (pos) {
+ var activeIndex = this.getActiveIndex()
+ , that = this
+
+ if (pos > (this.$items.length - 1) || pos < 0) return
+
+ if (this.sliding) {
+ return this.$element.one('slid', function () {
+ that.to(pos)
+ })
+ }
+
+ if (activeIndex == pos) {
+ return this.pause().cycle()
+ }
+
+ return this.slide(pos > activeIndex ? 'next' : 'prev', $(this.$items[pos]))
+ }
+
+ , pause: function (e) {
+ if (!e) this.paused = true
+ if (this.$element.find('.next, .prev').length && $.support.transition.end) {
+ this.$element.trigger($.support.transition.end)
+ this.cycle(true)
+ }
+ clearInterval(this.interval)
+ this.interval = null
+ return this
+ }
+
+ , next: function () {
+ if (this.sliding) return
+ return this.slide('next')
+ }
+
+ , prev: function () {
+ if (this.sliding) return
+ return this.slide('prev')
+ }
+
+ , slide: function (type, next) {
+ var $active = this.$element.find('.item.active')
+ , $next = next || $active[type]()
+ , isCycling = this.interval
+ , direction = type == 'next' ? 'left' : 'right'
+ , fallback = type == 'next' ? 'first' : 'last'
+ , that = this
+ , e
+
+ this.sliding = true
+
+ isCycling && this.pause()
+
+ $next = $next.length ? $next : this.$element.find('.item')[fallback]()
+
+ e = $.Event('slide', {
+ relatedTarget: $next[0]
+ , direction: direction
+ })
+
+ if ($next.hasClass('active')) return
+
+ if (this.$indicators.length) {
+ this.$indicators.find('.active').removeClass('active')
+ this.$element.one('slid', function () {
+ var $nextIndicator = $(that.$indicators.children()[that.getActiveIndex()])
+ $nextIndicator && $nextIndicator.addClass('active')
+ })
+ }
+
+ if ($.support.transition && this.$element.hasClass('slide')) {
+ this.$element.trigger(e)
+ if (e.isDefaultPrevented()) return
+ $next.addClass(type)
+ $next[0].offsetWidth // force reflow
+ $active.addClass(direction)
+ $next.addClass(direction)
+ this.$element.one($.support.transition.end, function () {
+ $next.removeClass([type, direction].join(' ')).addClass('active')
+ $active.removeClass(['active', direction].join(' '))
+ that.sliding = false
+ setTimeout(function () { that.$element.trigger('slid') }, 0)
+ })
+ } else {
+ this.$element.trigger(e)
+ if (e.isDefaultPrevented()) return
+ $active.removeClass('active')
+ $next.addClass('active')
+ this.sliding = false
+ this.$element.trigger('slid')
+ }
+
+ isCycling && this.cycle()
+
+ return this
+ }
+
+ }
+
+
+ /* CAROUSEL PLUGIN DEFINITION
+ * ========================== */
+
+ var old = $.fn.carousel
+
+ $.fn.carousel = function (option) {
+ return this.each(function () {
+ var $this = $(this)
+ , data = $this.data('carousel')
+ , options = $.extend({}, $.fn.carousel.defaults, typeof option == 'object' && option)
+ , action = typeof option == 'string' ? option : options.slide
+ if (!data) $this.data('carousel', (data = new Carousel(this, options)))
+ if (typeof option == 'number') data.to(option)
+ else if (action) data[action]()
+ else if (options.interval) data.pause().cycle()
+ })
+ }
+
+ $.fn.carousel.defaults = {
+ interval: 5000
+ , pause: 'hover'
+ }
+
+ $.fn.carousel.Constructor = Carousel
+
+
+ /* CAROUSEL NO CONFLICT
+ * ==================== */
+
+ $.fn.carousel.noConflict = function () {
+ $.fn.carousel = old
+ return this
+ }
+
+ /* CAROUSEL DATA-API
+ * ================= */
+
+ $(document).on('click.carousel.data-api', '[data-slide], [data-slide-to]', function (e) {
+ var $this = $(this), href
+ , $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7
+ , options = $.extend({}, $target.data(), $this.data())
+ , slideIndex
+
+ $target.carousel(options)
+
+ if (slideIndex = $this.attr('data-slide-to')) {
+ $target.data('carousel').pause().to(slideIndex).cycle()
+ }
+
+ e.preventDefault()
+ })
+
+}(window.jQuery);/* =============================================================
+ * bootstrap-collapse.js v2.3.2
+ * http://getbootstrap.com/2.3.2/javascript.html#collapse
+ * =============================================================
+ * Copyright 2013 Twitter, Inc.
+ *
+ * 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.
+ * ============================================================ */
+
+
+!function ($) {
+
+ "use strict"; // jshint ;_;
+
+
+ /* COLLAPSE PUBLIC CLASS DEFINITION
+ * ================================ */
+
+ var Collapse = function (element, options) {
+ this.$element = $(element)
+ this.options = $.extend({}, $.fn.collapse.defaults, options)
+
+ if (this.options.parent) {
+ this.$parent = $(this.options.parent)
+ }
+
+ this.options.toggle && this.toggle()
+ }
+
+ Collapse.prototype = {
+
+ constructor: Collapse
+
+ , dimension: function () {
+ var hasWidth = this.$element.hasClass('width')
+ return hasWidth ? 'width' : 'height'
+ }
+
+ , show: function () {
+ var dimension
+ , scroll
+ , actives
+ , hasData
+
+ if (this.transitioning || this.$element.hasClass('in')) return
+
+ dimension = this.dimension()
+ scroll = $.camelCase(['scroll', dimension].join('-'))
+ actives = this.$parent && this.$parent.find('> .accordion-group > .in')
+
+ if (actives && actives.length) {
+ hasData = actives.data('collapse')
+ if (hasData && hasData.transitioning) return
+ actives.collapse('hide')
+ hasData || actives.data('collapse', null)
+ }
+
+ this.$element[dimension](0)
+ this.transition('addClass', $.Event('show'), 'shown')
+ $.support.transition && this.$element[dimension](this.$element[0][scroll])
+ }
+
+ , hide: function () {
+ var dimension
+ if (this.transitioning || !this.$element.hasClass('in')) return
+ dimension = this.dimension()
+ this.reset(this.$element[dimension]())
+ this.transition('removeClass', $.Event('hide'), 'hidden')
+ this.$element[dimension](0)
+ }
+
+ , reset: function (size) {
+ var dimension = this.dimension()
+
+ this.$element
+ .removeClass('collapse')
+ [dimension](size || 'auto')
+ [0].offsetWidth
+
+ this.$element[size !== null ? 'addClass' : 'removeClass']('collapse')
+
+ return this
+ }
+
+ , transition: function (method, startEvent, completeEvent) {
+ var that = this
+ , complete = function () {
+ if (startEvent.type == 'show') that.reset()
+ that.transitioning = 0
+ that.$element.trigger(completeEvent)
+ }
+
+ this.$element.trigger(startEvent)
+
+ if (startEvent.isDefaultPrevented()) return
+
+ this.transitioning = 1
+
+ this.$element[method]('in')
+
+ $.support.transition && this.$element.hasClass('collapse') ?
+ this.$element.one($.support.transition.end, complete) :
+ complete()
+ }
+
+ , toggle: function () {
+ this[this.$element.hasClass('in') ? 'hide' : 'show']()
+ }
+
+ }
+
+
+ /* COLLAPSE PLUGIN DEFINITION
+ * ========================== */
+
+ var old = $.fn.collapse
+
+ $.fn.collapse = function (option) {
+ return this.each(function () {
+ var $this = $(this)
+ , data = $this.data('collapse')
+ , options = $.extend({}, $.fn.collapse.defaults, $this.data(), typeof option == 'object' && option)
+ if (!data) $this.data('collapse', (data = new Collapse(this, options)))
+ if (typeof option == 'string') data[option]()
+ })
+ }
+
+ $.fn.collapse.defaults = {
+ toggle: true
+ }
+
+ $.fn.collapse.Constructor = Collapse
+
+
+ /* COLLAPSE NO CONFLICT
+ * ==================== */
+
+ $.fn.collapse.noConflict = function () {
+ $.fn.collapse = old
+ return this
+ }
+
+
+ /* COLLAPSE DATA-API
+ * ================= */
+
+ $(document).on('click.collapse.data-api', '[data-toggle=collapse]', function (e) {
+ var $this = $(this), href
+ , target = $this.attr('data-target')
+ || e.preventDefault()
+ || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') //strip for ie7
+ , option = $(target).data('collapse') ? 'toggle' : $this.data()
+ $this[$(target).hasClass('in') ? 'addClass' : 'removeClass']('collapsed')
+ $(target).collapse(option)
+ })
+
+}(window.jQuery);/* ============================================================
+ * bootstrap-dropdown.js v2.3.2
+ * http://getbootstrap.com/2.3.2/javascript.html#dropdowns
+ * ============================================================
+ * Copyright 2013 Twitter, Inc.
+ *
+ * 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.
+ * ============================================================ */
+
+
+!function ($) {
+
+ "use strict"; // jshint ;_;
+
+
+ /* DROPDOWN CLASS DEFINITION
+ * ========================= */
+
+ var toggle = '[data-toggle=dropdown]'
+ , Dropdown = function (element) {
+ var $el = $(element).on('click.dropdown.data-api', this.toggle)
+ $('html').on('click.dropdown.data-api', function () {
+ $el.parent().removeClass('open')
+ })
+ }
+
+ Dropdown.prototype = {
+
+ constructor: Dropdown
+
+ , toggle: function (e) {
+ var $this = $(this)
+ , $parent
+ , isActive
+
+ if ($this.is('.disabled, :disabled')) return
+
+ $parent = getParent($this)
+
+ isActive = $parent.hasClass('open')
+
+ clearMenus()
+
+ if (!isActive) {
+ if ('ontouchstart' in document.documentElement) {
+ // if mobile we we use a backdrop because click events don't delegate
+ $('<div class="dropdown-backdrop"/>').insertBefore($(this)).on('click', clearMenus)
+ }
+ $parent.toggleClass('open')
+ }
+
+ $this.focus()
+
+ return false
+ }
+
+ , keydown: function (e) {
+ var $this
+ , $items
+ , $active
+ , $parent
+ , isActive
+ , index
+
+ if (!/(38|40|27)/.test(e.keyCode)) return
+
+ $this = $(this)
+
+ e.preventDefault()
+ e.stopPropagation()
+
+ if ($this.is('.disabled, :disabled')) return
+
+ $parent = getParent($this)
+
+ isActive = $parent.hasClass('open')
+
+ if (!isActive || (isActive && e.keyCode == 27)) {
+ if (e.which == 27) $parent.find(toggle).focus()
+ return $this.click()
+ }
+
+ $items = $('[role=menu] li:not(.divider):visible a', $parent)
+
+ if (!$items.length) return
+
+ index = $items.index($items.filter(':focus'))
+
+ if (e.keyCode == 38 && index > 0) index-- // up
+ if (e.keyCode == 40 && index < $items.length - 1) index++ // down
+ if (!~index) index = 0
+
+ $items
+ .eq(index)
+ .focus()
+ }
+
+ }
+
+ function clearMenus() {
+ $('.dropdown-backdrop').remove()
+ $(toggle).each(function () {
+ getParent($(this)).removeClass('open')
+ })
+ }
+
+ function getParent($this) {
+ var selector = $this.attr('data-target')
+ , $parent
+
+ if (!selector) {
+ selector = $this.attr('href')
+ selector = selector && /#/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7
+ }
+
+ $parent = selector && $(selector)
+
+ if (!$parent || !$parent.length) $parent = $this.parent()
+
+ return $parent
+ }
+
+
+ /* DROPDOWN PLUGIN DEFINITION
+ * ========================== */
+
+ var old = $.fn.dropdown
+
+ $.fn.dropdown = function (option) {
+ return this.each(function () {
+ var $this = $(this)
+ , data = $this.data('dropdown')
+ if (!data) $this.data('dropdown', (data = new Dropdown(this)))
+ if (typeof option == 'string') data[option].call($this)
+ })
+ }
+
+ $.fn.dropdown.Constructor = Dropdown
+
+
+ /* DROPDOWN NO CONFLICT
+ * ==================== */
+
+ $.fn.dropdown.noConflict = function () {
+ $.fn.dropdown = old
+ return this
+ }
+
+
+ /* APPLY TO STANDARD DROPDOWN ELEMENTS
+ * =================================== */
+
+ $(document)
+ .on('click.dropdown.data-api', clearMenus)
+ .on('click.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() })
+ .on('click.dropdown.data-api' , toggle, Dropdown.prototype.toggle)
+ .on('keydown.dropdown.data-api', toggle + ', [role=menu]' , Dropdown.prototype.keydown)
+
+}(window.jQuery);
+/* =========================================================
+ * bootstrap-modal.js v2.3.2
+ * http://getbootstrap.com/2.3.2/javascript.html#modals
+ * =========================================================
+ * Copyright 2013 Twitter, Inc.
+ *
+ * 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.
+ * ========================================================= */
+
+
+!function ($) {
+
+ "use strict"; // jshint ;_;
+
+
+ /* MODAL CLASS DEFINITION
+ * ====================== */
+
+ var Modal = function (element, options) {
+ this.options = options
+ this.$element = $(element)
+ .delegate('[data-dismiss="modal"]', 'click.dismiss.modal', $.proxy(this.hide, this))
+ this.options.remote && this.$element.find('.modal-body').load(this.options.remote)
+ }
+
+ Modal.prototype = {
+
+ constructor: Modal
+
+ , toggle: function () {
+ return this[!this.isShown ? 'show' : 'hide']()
+ }
+
+ , show: function () {
+ var that = this
+ , e = $.Event('show')
+
+ this.$element.trigger(e)
+
+ if (this.isShown || e.isDefaultPrevented()) return
+
+ this.isShown = true
+
+ this.escape()
+
+ this.backdrop(function () {
+ var transition = $.support.transition && that.$element.hasClass('fade')
+
+ if (!that.$element.parent().length) {
+ that.$element.appendTo(document.body) //don't move modals dom position
+ }
+
+ that.$element.show()
+
+ if (transition) {
+ that.$element[0].offsetWidth // force reflow
+ }
+
+ that.$element
+ .addClass('in')
+ .attr('aria-hidden', false)
+
+ that.enforceFocus()
+
+ transition ?
+ that.$element.one($.support.transition.end, function () { that.$element.focus().trigger('shown') }) :
+ that.$element.focus().trigger('shown')
+
+ })
+ }
+
+ , hide: function (e) {
+ e && e.preventDefault()
+
+ var that = this
+
+ e = $.Event('hide')
+
+ this.$element.trigger(e)
+
+ if (!this.isShown || e.isDefaultPrevented()) return
+
+ this.isShown = false
+
+ this.escape()
+
+ $(document).off('focusin.modal')
+
+ this.$element
+ .removeClass('in')
+ .attr('aria-hidden', true)
+
+ $.support.transition && this.$element.hasClass('fade') ?
+ this.hideWithTransition() :
+ this.hideModal()
+ }
+
+ , enforceFocus: function () {
+ var that = this
+ $(document).on('focusin.modal', function (e) {
+ if (that.$element[0] !== e.target && !that.$element.has(e.target).length) {
+ that.$element.focus()
+ }
+ })
+ }
+
+ , escape: function () {
+ var that = this
+ if (this.isShown && this.options.keyboard) {
+ this.$element.on('keyup.dismiss.modal', function ( e ) {
+ e.which == 27 && that.hide()
+ })
+ } else if (!this.isShown) {
+ this.$element.off('keyup.dismiss.modal')
+ }
+ }
+
+ , hideWithTransition: function () {
+ var that = this
+ , timeout = setTimeout(function () {
+ that.$element.off($.support.transition.end)
+ that.hideModal()
+ }, 500)
+
+ this.$element.one($.support.transition.end, function () {
+ clearTimeout(timeout)
+ that.hideModal()
+ })
+ }
+
+ , hideModal: function () {
+ var that = this
+ this.$element.hide()
+ this.backdrop(function () {
+ that.removeBackdrop()
+ that.$element.trigger('hidden')
+ })
+ }
+
+ , removeBackdrop: function () {
+ this.$backdrop && this.$backdrop.remove()
+ this.$backdrop = null
+ }
+
+ , backdrop: function (callback) {
+ var that = this
+ , animate = this.$element.hasClass('fade') ? 'fade' : ''
+
+ if (this.isShown && this.options.backdrop) {
+ var doAnimate = $.support.transition && animate
+
+ this.$backdrop = $('<div class="modal-backdrop ' + animate + '" />')
+ .appendTo(document.body)
+
+ this.$backdrop.click(
+ this.options.backdrop == 'static' ?
+ $.proxy(this.$element[0].focus, this.$element[0])
+ : $.proxy(this.hide, this)
+ )
+
+ if (doAnimate) this.$backdrop[0].offsetWidth // force reflow
+
+ this.$backdrop.addClass('in')
+
+ if (!callback) return
+
+ doAnimate ?
+ this.$backdrop.one($.support.transition.end, callback) :
+ callback()
+
+ } else if (!this.isShown && this.$backdrop) {
+ this.$backdrop.removeClass('in')
+
+ $.support.transition && this.$element.hasClass('fade')?
+ this.$backdrop.one($.support.transition.end, callback) :
+ callback()
+
+ } else if (callback) {
+ callback()
+ }
+ }
+ }
+
+
+ /* MODAL PLUGIN DEFINITION
+ * ======================= */
+
+ var old = $.fn.modal
+
+ $.fn.modal = function (option) {
+ return this.each(function () {
+ var $this = $(this)
+ , data = $this.data('modal')
+ , options = $.extend({}, $.fn.modal.defaults, $this.data(), typeof option == 'object' && option)
+ if (!data) $this.data('modal', (data = new Modal(this, options)))
+ if (typeof option == 'string') data[option]()
+ else if (options.show) data.show()
+ })
+ }
+
+ $.fn.modal.defaults = {
+ backdrop: true
+ , keyboard: true
+ , show: true
+ }
+
+ $.fn.modal.Constructor = Modal
+
+
+ /* MODAL NO CONFLICT
+ * ================= */
+
+ $.fn.modal.noConflict = function () {
+ $.fn.modal = old
+ return this
+ }
+
+
+ /* MODAL DATA-API
+ * ============== */
+
+ $(document).on('click.modal.data-api', '[data-toggle="modal"]', function (e) {
+ var $this = $(this)
+ , href = $this.attr('href')
+ , $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) //strip for ie7
+ , option = $target.data('modal') ? 'toggle' : $.extend({ remote:!/#/.test(href) && href }, $target.data(), $this.data())
+
+ e.preventDefault()
+
+ $target
+ .modal(option)
+ .one('hide', function () {
+ $this.focus()
+ })
+ })
+
+}(window.jQuery);
+/* ===========================================================
+ * bootstrap-tooltip.js v2.3.2
+ * http://getbootstrap.com/2.3.2/javascript.html#tooltips
+ * Inspired by the original jQuery.tipsy by Jason Frame
+ * ===========================================================
+ * Copyright 2013 Twitter, Inc.
+ *
+ * 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.
+ * ========================================================== */
+
+
+!function ($) {
+
+ "use strict"; // jshint ;_;
+
+
+ /* TOOLTIP PUBLIC CLASS DEFINITION
+ * =============================== */
+
+ var Tooltip = function (element, options) {
+ this.init('tooltip', element, options)
+ }
+
+ Tooltip.prototype = {
+
+ constructor: Tooltip
+
+ , init: function (type, element, options) {
+ var eventIn
+ , eventOut
+ , triggers
+ , trigger
+ , i
+
+ this.type = type
+ this.$element = $(element)
+ this.options = this.getOptions(options)
+ this.enabled = true
+
+ triggers = this.options.trigger.split(' ')
+
+ for (i = triggers.length; i--;) {
+ trigger = triggers[i]
+ if (trigger == 'click') {
+ this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this))
+ } else if (trigger != 'manual') {
+ eventIn = trigger == 'hover' ? 'mouseenter' : 'focus'
+ eventOut = trigger == 'hover' ? 'mouseleave' : 'blur'
+ this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this))
+ this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this))
+ }
+ }
+
+ this.options.selector ?
+ (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) :
+ this.fixTitle()
+ }
+
+ , getOptions: function (options) {
+ options = $.extend({}, $.fn[this.type].defaults, this.$element.data(), options)
+
+ if (options.delay && typeof options.delay == 'number') {
+ options.delay = {
+ show: options.delay
+ , hide: options.delay
+ }
+ }
+
+ return options
+ }
+
+ , enter: function (e) {
+ var defaults = $.fn[this.type].defaults
+ , options = {}
+ , self
+
+ this._options && $.each(this._options, function (key, value) {
+ if (defaults[key] != value) options[key] = value
+ }, this)
+
+ self = $(e.currentTarget)[this.type](options).data(this.type)
+
+ if (!self.options.delay || !self.options.delay.show) return self.show()
+
+ clearTimeout(this.timeout)
+ self.hoverState = 'in'
+ this.timeout = setTimeout(function() {
+ if (self.hoverState == 'in') self.show()
+ }, self.options.delay.show)
+ }
+
+ , leave: function (e) {
+ var self = $(e.currentTarget)[this.type](this._options).data(this.type)
+
+ if (this.timeout) clearTimeout(this.timeout)
+ if (!self.options.delay || !self.options.delay.hide) return self.hide()
+
+ self.hoverState = 'out'
+ this.timeout = setTimeout(function() {
+ if (self.hoverState == 'out') self.hide()
+ }, self.options.delay.hide)
+ }
+
+ , show: function () {
+ var $tip
+ , pos
+ , actualWidth
+ , actualHeight
+ , placement
+ , tp
+ , e = $.Event('show')
+
+ if (this.hasContent() && this.enabled) {
+ this.$element.trigger(e)
+ if (e.isDefaultPrevented()) return
+ $tip = this.tip()
+ this.setContent()
+
+ if (this.options.animation) {
+ $tip.addClass('fade')
+ }
+
+ placement = typeof this.options.placement == 'function' ?
+ this.options.placement.call(this, $tip[0], this.$element[0]) :
+ this.options.placement
+
+ $tip
+ .detach()
+ .css({ top: 0, left: 0, display: 'block' })
+
+ this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element)
+
+ pos = this.getPosition()
+
+ actualWidth = $tip[0].offsetWidth
+ actualHeight = $tip[0].offsetHeight
+
+ switch (placement) {
+ case 'bottom':
+ tp = {top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2}
+ break
+ case 'top':
+ tp = {top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2}
+ break
+ case 'left':
+ tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth}
+ break
+ case 'right':
+ tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width}
+ break
+ }
+
+ this.applyPlacement(tp, placement)
+ this.$element.trigger('shown')
+ }
+ }
+
+ , applyPlacement: function(offset, placement){
+ var $tip = this.tip()
+ , width = $tip[0].offsetWidth
+ , height = $tip[0].offsetHeight
+ , actualWidth
+ , actualHeight
+ , delta
+ , replace
+
+ $tip
+ .offset(offset)
+ .addClass(placement)
+ .addClass('in')
+
+ actualWidth = $tip[0].offsetWidth
+ actualHeight = $tip[0].offsetHeight
+
+ if (placement == 'top' && actualHeight != height) {
+ offset.top = offset.top + height - actualHeight
+ replace = true
+ }
+
+ if (placement == 'bottom' || placement == 'top') {
+ delta = 0
+
+ if (offset.left < 0){
+ delta = offset.left * -2
+ offset.left = 0
+ $tip.offset(offset)
+ actualWidth = $tip[0].offsetWidth
+ actualHeight = $tip[0].offsetHeight
+ }
+
+ this.replaceArrow(delta - width + actualWidth, actualWidth, 'left')
+ } else {
+ this.replaceArrow(actualHeight - height, actualHeight, 'top')
+ }
+
+ if (replace) $tip.offset(offset)
+ }
+
+ , replaceArrow: function(delta, dimension, position){
+ this
+ .arrow()
+ .css(position, delta ? (50 * (1 - delta / dimension) + "%") : '')
+ }
+
+ , setContent: function () {
+ var $tip = this.tip()
+ , title = this.getTitle()
+
+ $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title)
+ $tip.removeClass('fade in top bottom left right')
+ }
+
+ , hide: function () {
+ var that = this
+ , $tip = this.tip()
+ , e = $.Event('hide')
+
+ this.$element.trigger(e)
+ if (e.isDefaultPrevented()) return
+
+ $tip.removeClass('in')
+
+ function removeWithAnimation() {
+ var timeout = setTimeout(function () {
+ $tip.off($.support.transition.end).detach()
+ }, 500)
+
+ $tip.one($.support.transition.end, function () {
+ clearTimeout(timeout)
+ $tip.detach()
+ })
+ }
+
+ $.support.transition && this.$tip.hasClass('fade') ?
+ removeWithAnimation() :
+ $tip.detach()
+
+ this.$element.trigger('hidden')
+
+ return this
+ }
+
+ , fixTitle: function () {
+ var $e = this.$element
+ if ($e.attr('title') || typeof($e.attr('data-original-title')) != 'string') {
+ $e.attr('data-original-title', $e.attr('title') || '').attr('title', '')
+ }
+ }
+
+ , hasContent: function () {
+ return this.getTitle()
+ }
+
+ , getPosition: function () {
+ var el = this.$element[0]
+ return $.extend({}, (typeof el.getBoundingClientRect == 'function') ? el.getBoundingClientRect() : {
+ width: el.offsetWidth
+ , height: el.offsetHeight
+ }, this.$element.offset())
+ }
+
+ , getTitle: function () {
+ var title
+ , $e = this.$element
+ , o = this.options
+
+ title = $e.attr('data-original-title')
+ || (typeof o.title == 'function' ? o.title.call($e[0]) : o.title)
+
+ return title
+ }
+
+ , tip: function () {
+ return this.$tip = this.$tip || $(this.options.template)
+ }
+
+ , arrow: function(){
+ return this.$arrow = this.$arrow || this.tip().find(".tooltip-arrow")
+ }
+
+ , validate: function () {
+ if (!this.$element[0].parentNode) {
+ this.hide()
+ this.$element = null
+ this.options = null
+ }
+ }
+
+ , enable: function () {
+ this.enabled = true
+ }
+
+ , disable: function () {
+ this.enabled = false
+ }
+
+ , toggleEnabled: function () {
+ this.enabled = !this.enabled
+ }
+
+ , toggle: function (e) {
+ var self = e ? $(e.currentTarget)[this.type](this._options).data(this.type) : this
+ self.tip().hasClass('in') ? self.hide() : self.show()
+ }
+
+ , destroy: function () {
+ this.hide().$element.off('.' + this.type).removeData(this.type)
+ }
+
+ }
+
+
+ /* TOOLTIP PLUGIN DEFINITION
+ * ========================= */
+
+ var old = $.fn.tooltip
+
+ $.fn.tooltip = function ( option ) {
+ return this.each(function () {
+ var $this = $(this)
+ , data = $this.data('tooltip')
+ , options = typeof option == 'object' && option
+ if (!data) $this.data('tooltip', (data = new Tooltip(this, options)))
+ if (typeof option == 'string') data[option]()
+ })
+ }
+
+ $.fn.tooltip.Constructor = Tooltip
+
+ $.fn.tooltip.defaults = {
+ animation: true
+ , placement: 'top'
+ , selector: false
+ , template: '<div class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'
+ , trigger: 'hover focus'
+ , title: ''
+ , delay: 0
+ , html: false
+ , container: false
+ }
+
+
+ /* TOOLTIP NO CONFLICT
+ * =================== */
+
+ $.fn.tooltip.noConflict = function () {
+ $.fn.tooltip = old
+ return this
+ }
+
+}(window.jQuery);
+/* ===========================================================
+ * bootstrap-popover.js v2.3.2
+ * http://getbootstrap.com/2.3.2/javascript.html#popovers
+ * ===========================================================
+ * Copyright 2013 Twitter, Inc.
+ *
+ * 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.
+ * =========================================================== */
+
+
+!function ($) {
+
+ "use strict"; // jshint ;_;
+
+
+ /* POPOVER PUBLIC CLASS DEFINITION
+ * =============================== */
+
+ var Popover = function (element, options) {
+ this.init('popover', element, options)
+ }
+
+
+ /* NOTE: POPOVER EXTENDS BOOTSTRAP-TOOLTIP.js
+ ========================================== */
+
+ Popover.prototype = $.extend({}, $.fn.tooltip.Constructor.prototype, {
+
+ constructor: Popover
+
+ , setContent: function () {
+ var $tip = this.tip()
+ , title = this.getTitle()
+ , content = this.getContent()
+
+ $tip.find('.popover-title')[this.options.html ? 'html' : 'text'](title)
+ $tip.find('.popover-content')[this.options.html ? 'html' : 'text'](content)
+
+ $tip.removeClass('fade top bottom left right in')
+ }
+
+ , hasContent: function () {
+ return this.getTitle() || this.getContent()
+ }
+
+ , getContent: function () {
+ var content
+ , $e = this.$element
+ , o = this.options
+
+ content = (typeof o.content == 'function' ? o.content.call($e[0]) : o.content)
+ || $e.attr('data-content')
+
+ return content
+ }
+
+ , tip: function () {
+ if (!this.$tip) {
+ this.$tip = $(this.options.template)
+ }
+ return this.$tip
+ }
+
+ , destroy: function () {
+ this.hide().$element.off('.' + this.type).removeData(this.type)
+ }
+
+ })
+
+
+ /* POPOVER PLUGIN DEFINITION
+ * ======================= */
+
+ var old = $.fn.popover
+
+ $.fn.popover = function (option) {
+ return this.each(function () {
+ var $this = $(this)
+ , data = $this.data('popover')
+ , options = typeof option == 'object' && option
+ if (!data) $this.data('popover', (data = new Popover(this, options)))
+ if (typeof option == 'string') data[option]()
+ })
+ }
+
+ $.fn.popover.Constructor = Popover
+
+ $.fn.popover.defaults = $.extend({} , $.fn.tooltip.defaults, {
+ placement: 'right'
+ , trigger: 'click'
+ , content: ''
+ , template: '<div class="popover"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>'
+ })
+
+
+ /* POPOVER NO CONFLICT
+ * =================== */
+
+ $.fn.popover.noConflict = function () {
+ $.fn.popover = old
+ return this
+ }
+
+}(window.jQuery);
+/* =============================================================
+ * bootstrap-scrollspy.js v2.3.2
+ * http://getbootstrap.com/2.3.2/javascript.html#scrollspy
+ * =============================================================
+ * Copyright 2013 Twitter, Inc.
+ *
+ * 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.
+ * ============================================================== */
+
+
+!function ($) {
+
+ "use strict"; // jshint ;_;
+
+
+ /* SCROLLSPY CLASS DEFINITION
+ * ========================== */
+
+ function ScrollSpy(element, options) {
+ var process = $.proxy(this.process, this)
+ , $element = $(element).is('body') ? $(window) : $(element)
+ , href
+ this.options = $.extend({}, $.fn.scrollspy.defaults, options)
+ this.$scrollElement = $element.on('scroll.scroll-spy.data-api', process)
+ this.selector = (this.options.target
+ || ((href = $(element).attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7
+ || '') + ' .nav li > a'
+ this.$body = $('body')
+ this.refresh()
+ this.process()
+ }
+
+ ScrollSpy.prototype = {
+
+ constructor: ScrollSpy
+
+ , refresh: function () {
+ var self = this
+ , $targets
+
+ this.offsets = $([])
+ this.targets = $([])
+
+ $targets = this.$body
+ .find(this.selector)
+ .map(function () {
+ var $el = $(this)
+ , href = $el.data('target') || $el.attr('href')
+ , $href = /^#\w/.test(href) && $(href)
+ return ( $href
+ && $href.length
+ && [[ $href.position().top + (!$.isWindow(self.$scrollElement.get(0)) && self.$scrollElement.scrollTop()), href ]] ) || null
+ })
+ .sort(function (a, b) { return a[0] - b[0] })
+ .each(function () {
+ self.offsets.push(this[0])
+ self.targets.push(this[1])
+ })
+ }
+
+ , process: function () {
+ var scrollTop = this.$scrollElement.scrollTop() + this.options.offset
+ , scrollHeight = this.$scrollElement[0].scrollHeight || this.$body[0].scrollHeight
+ , maxScroll = scrollHeight - this.$scrollElement.height()
+ , offsets = this.offsets
+ , targets = this.targets
+ , activeTarget = this.activeTarget
+ , i
+
+ if (scrollTop >= maxScroll) {
+ return activeTarget != (i = targets.last()[0])
+ && this.activate ( i )
+ }
+
+ for (i = offsets.length; i--;) {
+ activeTarget != targets[i]
+ && scrollTop >= offsets[i]
+ && (!offsets[i + 1] || scrollTop <= offsets[i + 1])
+ && this.activate( targets[i] )
+ }
+ }
+
+ , activate: function (target) {
+ var active
+ , selector
+
+ this.activeTarget = target
+
+ $(this.selector)
+ .parent('.active')
+ .removeClass('active')
+
+ selector = this.selector
+ + '[data-target="' + target + '"],'
+ + this.selector + '[href="' + target + '"]'
+
+ active = $(selector)
+ .parent('li')
+ .addClass('active')
+
+ if (active.parent('.dropdown-menu').length) {
+ active = active.closest('li.dropdown').addClass('active')
+ }
+
+ active.trigger('activate')
+ }
+
+ }
+
+
+ /* SCROLLSPY PLUGIN DEFINITION
+ * =========================== */
+
+ var old = $.fn.scrollspy
+
+ $.fn.scrollspy = function (option) {
+ return this.each(function () {
+ var $this = $(this)
+ , data = $this.data('scrollspy')
+ , options = typeof option == 'object' && option
+ if (!data) $this.data('scrollspy', (data = new ScrollSpy(this, options)))
+ if (typeof option == 'string') data[option]()
+ })
+ }
+
+ $.fn.scrollspy.Constructor = ScrollSpy
+
+ $.fn.scrollspy.defaults = {
+ offset: 10
+ }
+
+
+ /* SCROLLSPY NO CONFLICT
+ * ===================== */
+
+ $.fn.scrollspy.noConflict = function () {
+ $.fn.scrollspy = old
+ return this
+ }
+
+
+ /* SCROLLSPY DATA-API
+ * ================== */
+
+ $(window).on('load', function () {
+ $('[data-spy="scroll"]').each(function () {
+ var $spy = $(this)
+ $spy.scrollspy($spy.data())
+ })
+ })
+
+}(window.jQuery);/* ========================================================
+ * bootstrap-tab.js v2.3.2
+ * http://getbootstrap.com/2.3.2/javascript.html#tabs
+ * ========================================================
+ * Copyright 2013 Twitter, Inc.
+ *
+ * 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.
+ * ======================================================== */
+
+
+!function ($) {
+
+ "use strict"; // jshint ;_;
+
+
+ /* TAB CLASS DEFINITION
+ * ==================== */
+
+ var Tab = function (element) {
+ this.element = $(element)
+ }
+
+ Tab.prototype = {
+
+ constructor: Tab
+
+ , show: function () {
+ var $this = this.element
+ , $ul = $this.closest('ul:not(.dropdown-menu)')
+ , selector = $this.attr('data-target')
+ , previous
+ , $target
+ , e
+
+ if (!selector) {
+ selector = $this.attr('href')
+ selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7
+ }
+
+ if ( $this.parent('li').hasClass('active') ) return
+
+ previous = $ul.find('.active:last a')[0]
+
+ e = $.Event('show', {
+ relatedTarget: previous
+ })
+
+ $this.trigger(e)
+
+ if (e.isDefaultPrevented()) return
+
+ $target = $(selector)
+
+ this.activate($this.parent('li'), $ul)
+ this.activate($target, $target.parent(), function () {
+ $this.trigger({
+ type: 'shown'
+ , relatedTarget: previous
+ })
+ })
+ }
+
+ , activate: function ( element, container, callback) {
+ var $active = container.find('> .active')
+ , transition = callback
+ && $.support.transition
+ && $active.hasClass('fade')
+
+ function next() {
+ $active
+ .removeClass('active')
+ .find('> .dropdown-menu > .active')
+ .removeClass('active')
+
+ element.addClass('active')
+
+ if (transition) {
+ element[0].offsetWidth // reflow for transition
+ element.addClass('in')
+ } else {
+ element.removeClass('fade')
+ }
+
+ if ( element.parent('.dropdown-menu') ) {
+ element.closest('li.dropdown').addClass('active')
+ }
+
+ callback && callback()
+ }
+
+ transition ?
+ $active.one($.support.transition.end, next) :
+ next()
+
+ $active.removeClass('in')
+ }
+ }
+
+
+ /* TAB PLUGIN DEFINITION
+ * ===================== */
+
+ var old = $.fn.tab
+
+ $.fn.tab = function ( option ) {
+ return this.each(function () {
+ var $this = $(this)
+ , data = $this.data('tab')
+ if (!data) $this.data('tab', (data = new Tab(this)))
+ if (typeof option == 'string') data[option]()
+ })
+ }
+
+ $.fn.tab.Constructor = Tab
+
+
+ /* TAB NO CONFLICT
+ * =============== */
+
+ $.fn.tab.noConflict = function () {
+ $.fn.tab = old
+ return this
+ }
+
+
+ /* TAB DATA-API
+ * ============ */
+
+ $(document).on('click.tab.data-api', '[data-toggle="tab"], [data-toggle="pill"]', function (e) {
+ e.preventDefault()
+ $(this).tab('show')
+ })
+
+}(window.jQuery);/* =============================================================
+ * bootstrap-typeahead.js v2.3.2
+ * http://getbootstrap.com/2.3.2/javascript.html#typeahead
+ * =============================================================
+ * Copyright 2013 Twitter, Inc.
+ *
+ * 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.
+ * ============================================================ */
+
+
+!function($){
+
+ "use strict"; // jshint ;_;
+
+
+ /* TYPEAHEAD PUBLIC CLASS DEFINITION
+ * ================================= */
+
+ var Typeahead = function (element, options) {
+ this.$element = $(element)
+ this.options = $.extend({}, $.fn.typeahead.defaults, options)
+ this.matcher = this.options.matcher || this.matcher
+ this.sorter = this.options.sorter || this.sorter
+ this.highlighter = this.options.highlighter || this.highlighter
+ this.updater = this.options.updater || this.updater
+ this.source = this.options.source
+ this.$menu = $(this.options.menu)
+ this.shown = false
+ this.listen()
+ }
+
+ Typeahead.prototype = {
+
+ constructor: Typeahead
+
+ , select: function () {
+ var val = this.$menu.find('.active').attr('data-value')
+ this.$element
+ .val(this.updater(val))
+ .change()
+ return this.hide()
+ }
+
+ , updater: function (item) {
+ return item
+ }
+
+ , show: function () {
+ var pos = $.extend({}, this.$element.position(), {
+ height: this.$element[0].offsetHeight
+ })
+
+ this.$menu
+ .insertAfter(this.$element)
+ .css({
+ top: pos.top + pos.height
+ , left: pos.left
+ })
+ .show()
+
+ this.shown = true
+ return this
+ }
+
+ , hide: function () {
+ this.$menu.hide()
+ this.shown = false
+ return this
+ }
+
+ , lookup: function (event) {
+ var items
+
+ this.query = this.$element.val()
+
+ if (!this.query || this.query.length < this.options.minLength) {
+ return this.shown ? this.hide() : this
+ }
+
+ items = $.isFunction(this.source) ? this.source(this.query, $.proxy(this.process, this)) : this.source
+
+ return items ? this.process(items) : this
+ }
+
+ , process: function (items) {
+ var that = this
+
+ items = $.grep(items, function (item) {
+ return that.matcher(item)
+ })
+
+ items = this.sorter(items)
+
+ if (!items.length) {
+ return this.shown ? this.hide() : this
+ }
+
+ return this.render(items.slice(0, this.options.items)).show()
+ }
+
+ , matcher: function (item) {
+ return ~item.toLowerCase().indexOf(this.query.toLowerCase())
+ }
+
+ , sorter: function (items) {
+ var beginswith = []
+ , caseSensitive = []
+ , caseInsensitive = []
+ , item
+
+ while (item = items.shift()) {
+ if (!item.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item)
+ else if (~item.indexOf(this.query)) caseSensitive.push(item)
+ else caseInsensitive.push(item)
+ }
+
+ return beginswith.concat(caseSensitive, caseInsensitive)
+ }
+
+ , highlighter: function (item) {
+ var query = this.query.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&')
+ return item.replace(new RegExp('(' + query + ')', 'ig'), function ($1, match) {
+ return '<strong>' + match + '</strong>'
+ })
+ }
+
+ , render: function (items) {
+ var that = this
+
+ items = $(items).map(function (i, item) {
+ i = $(that.options.item).attr('data-value', item)
+ i.find('a').html(that.highlighter(item))
+ return i[0]
+ })
+
+ items.first().addClass('active')
+ this.$menu.html(items)
+ return this
+ }
+
+ , next: function (event) {
+ var active = this.$menu.find('.active').removeClass('active')
+ , next = active.next()
+
+ if (!next.length) {
+ next = $(this.$menu.find('li')[0])
+ }
+
+ next.addClass('active')
+ }
+
+ , prev: function (event) {
+ var active = this.$menu.find('.active').removeClass('active')
+ , prev = active.prev()
+
+ if (!prev.length) {
+ prev = this.$menu.find('li').last()
+ }
+
+ prev.addClass('active')
+ }
+
+ , listen: function () {
+ this.$element
+ .on('focus', $.proxy(this.focus, this))
+ .on('blur', $.proxy(this.blur, this))
+ .on('keypress', $.proxy(this.keypress, this))
+ .on('keyup', $.proxy(this.keyup, this))
+
+ if (this.eventSupported('keydown')) {
+ this.$element.on('keydown', $.proxy(this.keydown, this))
+ }
+
+ this.$menu
+ .on('click', $.proxy(this.click, this))
+ .on('mouseenter', 'li', $.proxy(this.mouseenter, this))
+ .on('mouseleave', 'li', $.proxy(this.mouseleave, this))
+ }
+
+ , eventSupported: function(eventName) {
+ var isSupported = eventName in this.$element
+ if (!isSupported) {
+ this.$element.setAttribute(eventName, 'return;')
+ isSupported = typeof this.$element[eventName] === 'function'
+ }
+ return isSupported
+ }
+
+ , move: function (e) {
+ if (!this.shown) return
+
+ switch(e.keyCode) {
+ case 9: // tab
+ case 13: // enter
+ case 27: // escape
+ e.preventDefault()
+ break
+
+ case 38: // up arrow
+ e.preventDefault()
+ this.prev()
+ break
+
+ case 40: // down arrow
+ e.preventDefault()
+ this.next()
+ break
+ }
+
+ e.stopPropagation()
+ }
+
+ , keydown: function (e) {
+ this.suppressKeyPressRepeat = ~$.inArray(e.keyCode, [40,38,9,13,27])
+ this.move(e)
+ }
+
+ , keypress: function (e) {
+ if (this.suppressKeyPressRepeat) return
+ this.move(e)
+ }
+
+ , keyup: function (e) {
+ switch(e.keyCode) {
+ case 40: // down arrow
+ case 38: // up arrow
+ case 16: // shift
+ case 17: // ctrl
+ case 18: // alt
+ break
+
+ case 9: // tab
+ case 13: // enter
+ if (!this.shown) return
+ this.select()
+ break
+
+ case 27: // escape
+ if (!this.shown) return
+ this.hide()
+ break
+
+ default:
+ this.lookup()
+ }
+
+ e.stopPropagation()
+ e.preventDefault()
+ }
+
+ , focus: function (e) {
+ this.focused = true
+ }
+
+ , blur: function (e) {
+ this.focused = false
+ if (!this.mousedover && this.shown) this.hide()
+ }
+
+ , click: function (e) {
+ e.stopPropagation()
+ e.preventDefault()
+ this.select()
+ this.$element.focus()
+ }
+
+ , mouseenter: function (e) {
+ this.mousedover = true
+ this.$menu.find('.active').removeClass('active')
+ $(e.currentTarget).addClass('active')
+ }
+
+ , mouseleave: function (e) {
+ this.mousedover = false
+ if (!this.focused && this.shown) this.hide()
+ }
+
+ }
+
+
+ /* TYPEAHEAD PLUGIN DEFINITION
+ * =========================== */
+
+ var old = $.fn.typeahead
+
+ $.fn.typeahead = function (option) {
+ return this.each(function () {
+ var $this = $(this)
+ , data = $this.data('typeahead')
+ , options = typeof option == 'object' && option
+ if (!data) $this.data('typeahead', (data = new Typeahead(this, options)))
+ if (typeof option == 'string') data[option]()
+ })
+ }
+
+ $.fn.typeahead.defaults = {
+ source: []
+ , items: 8
+ , menu: '<ul class="typeahead dropdown-menu"></ul>'
+ , item: '<li><a href="#"></a></li>'
+ , minLength: 1
+ }
+
+ $.fn.typeahead.Constructor = Typeahead
+
+
+ /* TYPEAHEAD NO CONFLICT
+ * =================== */
+
+ $.fn.typeahead.noConflict = function () {
+ $.fn.typeahead = old
+ return this
+ }
+
+
+ /* TYPEAHEAD DATA-API
+ * ================== */
+
+ $(document).on('focus.typeahead.data-api', '[data-provide="typeahead"]', function (e) {
+ var $this = $(this)
+ if ($this.data('typeahead')) return
+ $this.typeahead($this.data())
+ })
+
+}(window.jQuery);
+/* ==========================================================
+ * bootstrap-affix.js v2.3.2
+ * http://getbootstrap.com/2.3.2/javascript.html#affix
+ * ==========================================================
+ * Copyright 2013 Twitter, Inc.
+ *
+ * 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.
+ * ========================================================== */
+
+
+!function ($) {
+
+ "use strict"; // jshint ;_;
+
+
+ /* AFFIX CLASS DEFINITION
+ * ====================== */
+
+ var Affix = function (element, options) {
+ this.options = $.extend({}, $.fn.affix.defaults, options)
+ this.$window = $(window)
+ .on('scroll.affix.data-api', $.proxy(this.checkPosition, this))
+ .on('click.affix.data-api', $.proxy(function () { setTimeout($.proxy(this.checkPosition, this), 1) }, this))
+ this.$element = $(element)
+ this.checkPosition()
+ }
+
+ Affix.prototype.checkPosition = function () {
+ if (!this.$element.is(':visible')) return
+
+ var scrollHeight = $(document).height()
+ , scrollTop = this.$window.scrollTop()
+ , position = this.$element.offset()
+ , offset = this.options.offset
+ , offsetBottom = offset.bottom
+ , offsetTop = offset.top
+ , reset = 'affix affix-top affix-bottom'
+ , affix
+
+ if (typeof offset != 'object') offsetBottom = offsetTop = offset
+ if (typeof offsetTop == 'function') offsetTop = offset.top()
+ if (typeof offsetBottom == 'function') offsetBottom = offset.bottom()
+
+ affix = this.unpin != null && (scrollTop + this.unpin <= position.top) ?
+ false : offsetBottom != null && (position.top + this.$element.height() >= scrollHeight - offsetBottom) ?
+ 'bottom' : offsetTop != null && scrollTop <= offsetTop ?
+ 'top' : false
+
+ if (this.affixed === affix) return
+
+ this.affixed = affix
+ this.unpin = affix == 'bottom' ? position.top - scrollTop : null
+
+ this.$element.removeClass(reset).addClass('affix' + (affix ? '-' + affix : ''))
+ }
+
+
+ /* AFFIX PLUGIN DEFINITION
+ * ======================= */
+
+ var old = $.fn.affix
+
+ $.fn.affix = function (option) {
+ return this.each(function () {
+ var $this = $(this)
+ , data = $this.data('affix')
+ , options = typeof option == 'object' && option
+ if (!data) $this.data('affix', (data = new Affix(this, options)))
+ if (typeof option == 'string') data[option]()
+ })
+ }
+
+ $.fn.affix.Constructor = Affix
+
+ $.fn.affix.defaults = {
+ offset: 0
+ }
+
+
+ /* AFFIX NO CONFLICT
+ * ================= */
+
+ $.fn.affix.noConflict = function () {
+ $.fn.affix = old
+ return this
+ }
+
+
+ /* AFFIX DATA-API
+ * ============== */
+
+ $(window).on('load', function () {
+ $('[data-spy="affix"]').each(function () {
+ var $spy = $(this)
+ , data = $spy.data()
+
+ data.offset = data.offset || {}
+
+ data.offsetBottom && (data.offset.bottom = data.offsetBottom)
+ data.offsetTop && (data.offset.top = data.offsetTop)
+
+ $spy.affix(data)
+ })
+ })
+
+
+}(window.jQuery);
\ No newline at end of file
diff --git a/doc/etc/bootstrap/js/bootstrap.min.js b/doc/etc/bootstrap/js/bootstrap.min.js
new file mode 100644
index 0000000..848258d
--- /dev/null
+++ b/doc/etc/bootstrap/js/bootstrap.min.js
@@ -0,0 +1,6 @@
+/*!
+* Bootstrap.js by @fat & @mdo
+* Copyright 2013 Twitter, Inc.
+* http://www.apache.org/licenses/LICENSE-2.0.txt
+*/
+!function(e){"use strict";e(function(){e.support.transition=function(){var e=function(){var e=document.createElement("bootstrap"),t={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"},n;for(n in t)if(e.style[n]!==undefined)return t[n]}();return e&&{end:e}}()})}(window.jQuery),!function(e){"use strict";var t='[data-dismiss="alert"]',n=function(n){e(n).on("click",t,this.close)};n.prototype.close=funct [...]
\ No newline at end of file
diff --git a/doc/etc/fcm-icon.png b/doc/etc/fcm-icon.png
new file mode 100644
index 0000000..bdfa2bf
Binary files /dev/null and b/doc/etc/fcm-icon.png differ
diff --git a/doc/etc/fcm-terms-of-use.html b/doc/etc/fcm-terms-of-use.html
new file mode 100644
index 0000000..a7316bd
--- /dev/null
+++ b/doc/etc/fcm-terms-of-use.html
@@ -0,0 +1,95 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>FCM: Terms of Use</title>
+ <meta name="author" content="FCM team" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link rel="icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link rel="shortcut icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link href="../etc/bootstrap/css/bootstrap.min.css" rel="stylesheet" media="screen" />
+ <link href="../etc/fcm.css" rel="stylesheet" media="screen" />
+</head>
+<body>
+ <div class="navbar navbar-inverse">
+ <div class="navbar-inner">
+ <a class="brand" href=".."><span class="fcm-version">FCM</span></a>
+ <ul class="nav">
+ <li><a href="../installation/">Installation</a></li>
+
+ <li><a href="../user_guide/">User Guide</a></li>
+ </ul>
+ </div>
+ </div>
+
+ <div class="page-header">
+ <div class="fcm-page-content pull-right well well-small"></div>
+ <h1>FCM: Terms of Use</h1>
+ </div>
+
+ <div class="container">
+ <div class="row">
+ <div class="span12">
+
+ <h2 id="software.licence">FCM Software: Licence</h2>
+
+ <p>FCM is free software: you can redistribute it and/or modify it under the
+ terms of the <a href="http://www.gnu.org/licenses/gpl.html" rel="license">GNU
+ General Public License</a> as published by the Free Software Foundation,
+ either version 3 of the License, or (at your option) any later version.</p>
+
+ <p>FCM 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.</p>
+
+ <p>You should have received a copy of the GNU General Public License along
+ with FCM. If not, see
+ <a href="http://www.gnu.org/licenses/"
+ rel="license">http://www.gnu.org/licenses/</a>.</p>
+
+ <h2 id="doc.licence">FCM Documentation: Licence</h2>
+
+ <p>You may use and re-use Crown copyright information from any part of the
+ <a href="..">FCM Documentation</a> (not including logos) free of charge in
+ any format or medium, under the terms and conditions of the <a href=
+ "http://www.nationalarchives.gov.uk/doc/open-government-licence/" rel=
+ "license">Open Government Licence</a>, provided it is reproduced accurately
+ and not used in a misleading context. Where any of the Crown copyright
+ items on this website are being republished or copied to others, the source
+ of the material must be identified and the copyright status
+ acknowledged.</p>
+
+ <p>See the <a href=
+ "http://www.nationalarchives.gov.uk/doc/open-government-licence/" rel=
+ "license">Open Government Licence</a> for the full conditions of this
+ licence and for information on how to attribute the source of material.</p>
+
+ <h2 id="team">FCM: Maintenance</h2>
+
+ <p>The FCM software and <a href="..">FCM Documentation</a> are maintained
+ by the <a href="mailto:fcm-team at metoffice.gov.uk">FCM Team</a>, Met Office,
+ FitzRoy Road, Exeter, EX1 3PB, UK.</p>
+
+ </div>
+ </div>
+ </div>
+
+ <hr/>
+ <div class="container-fluid text-center">
+ <div class="row-fluid"><div class="span12">
+ <address><small>
+ © British Crown Copyright 2006-14
+ <a href="http://www.metoffice.gov.uk">Met Office</a>.<br />
+ This document is released under the British <a href=
+ "http://www.nationalarchives.gov.uk/doc/open-government-licence/" rel=
+ "license">Open Government Licence</a>.<br />
+ </small></address>
+ </div></div>
+ </div>
+
+ <script type="text/javascript" src="../etc/jquery.min.js"></script>
+ <script type="text/javascript" src="../etc/bootstrap/js/bootstrap.min.js"></script>
+ <script type="text/javascript" src="../etc/fcm.js"></script>
+ <script type="text/javascript" src="../etc/fcm-version.js"></script>
+</body>
+</html>
diff --git a/doc/etc/fcm-version.js b/doc/etc/fcm-version.js
new file mode 100644
index 0000000..ca077f8
--- /dev/null
+++ b/doc/etc/fcm-version.js
@@ -0,0 +1 @@
+FCM.VERSION="2014.09.0";
diff --git a/doc/etc/fcm.css b/doc/etc/fcm.css
new file mode 100644
index 0000000..c25e6c7
--- /dev/null
+++ b/doc/etc/fcm.css
@@ -0,0 +1,28 @@
+/**********************************************************************
+ * (C) British Crown Copyright 2006-14 Met Office.
+ *
+ * This file is part of FCM.
+ *
+ * FCM 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, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+ *
+ **********************************************************************/
+
+:hover > .sectionlink,
+.sectionlink:active,
+.sectionlink:hover {
+ display: inline;
+}
+.sectionlink {
+ display: none;
+}
diff --git a/doc/etc/fcm.js b/doc/etc/fcm.js
new file mode 100644
index 0000000..1a6ccef
--- /dev/null
+++ b/doc/etc/fcm.js
@@ -0,0 +1,166 @@
+/**********************************************************************
+ * (C) British Crown Copyright 2006-14 Met Office.
+ *
+ * This file is part of FCM.
+ *
+ * FCM 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, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+ *
+ **********************************************************************/
+
+FCM = {};
+$(function() {
+ var TITLE_COLLAPSE = "collapse";
+ var TITLE_EXPAND = "expand";
+ var IS_MAIN = true;
+
+ // Toggle a collapse/expand image.
+ function collapse_expand_icon_toggle(anchor) {
+ var icon = $("i", anchor);
+ if (icon.attr("title") == TITLE_EXPAND) {
+ icon.attr("title", TITLE_COLLAPSE);
+ icon.removeClass("icon-chevron-right");
+ icon.addClass("icon-chevron-down");
+ anchor.siblings().filter("ul").show();
+ }
+ else { // if (icon.attr("title") == "collapse")
+ icon.attr("title", TITLE_EXPAND);
+ icon.removeClass("icon-chevron-down");
+ icon.addClass("icon-chevron-right");
+ anchor.siblings().filter("ul").hide();
+ }
+ }
+
+ // Add collapse/expand anchor to a ul tree.
+ function ul_collapse_expand(ul, is_main) {
+ var nodes = $("li", ul);
+ nodes.each(function(i) {
+ var li = $(this);
+ var li_anchor = li.children().first();
+ if (!li_anchor.is("a")) {
+ return;
+ }
+ var li_ul = $("> ul", li);
+ li_ul.hide();
+ var icon = $("<i/>", {"title": TITLE_EXPAND});
+ icon.addClass("icon-chevron-right");
+ icon.css("opacity", 0.5);
+ var anchor = $("<a/>").append(icon);
+ li.prepend(anchor);
+ if (is_main) {
+ anchor.click(function() {
+ var href = li_anchor.attr("href");
+ $.get(
+ href,
+ function(data) {
+ collapse_expand_icon_toggle(anchor);
+ anchor.unbind("click");
+ if (content_gen(li, data, href)) {
+ ul_collapse_expand(li.children().filter("ul"));
+ anchor.click(function() {
+ collapse_expand_icon_toggle(anchor);
+ });
+ }
+ else {
+ icon.css("opacity", 0.1);
+ }
+ },
+ "html"
+ )
+ .error(function(b) {
+ alert(b);
+ anchor.unbind("click");
+ icon.css("opacity", 0.1);
+ });
+ });
+ }
+ else if (li_ul.length) {
+ anchor.click(function() {
+ collapse_expand_icon_toggle(anchor);
+ });
+ }
+ else {
+ icon.css("opacity", 0.1);
+ }
+ });
+ }
+
+ // Generate table of content of a document.
+ function content_gen(root, d, d_href) {
+ if (d == null) {
+ d = document;
+ }
+ var CONTENT_INDEX_OF = {"h2": 1, "h3": 2, "h4": 3, "h5": 4, "h6": 5};
+ var stack = [];
+ var done_something = false;
+ var headings = $("h2, h3, h4, h5, h6", $(d));
+ headings.each(function(i) {
+ if (this.id == null || this.id == "") {
+ return;
+ }
+ var tag_name = this.tagName.toLowerCase();
+ // Add to table of content
+ while (CONTENT_INDEX_OF[tag_name] < stack.length) {
+ stack.shift();
+ }
+ while (stack.length < CONTENT_INDEX_OF[tag_name]) {
+ var node = stack.length == 0 ? root : $("> :last-child", stack[0]);
+ stack.unshift($("<ul/>").appendTo(node).addClass("unstyled"));
+ }
+ var href = "#" + this.id;
+ if (d_href) {
+ href = d_href + href;
+ }
+ var padding = "";
+ for (var i = 0; i < stack.length; i++) {
+ padding += " ";
+ }
+ stack[0].append($("<li/>").html(padding).append(
+ $("<a/>", {"href": href}).html($(this).text())
+ ));
+
+ // Add a section link as well
+ if (d == document) {
+ var section_link_anchor = $("<a/>", {"href": "#" + this.id});
+ section_link_anchor.addClass("sectionlink");
+ section_link_anchor.append("\xb6");
+ $(this).append(section_link_anchor);
+ }
+
+ done_something = true;
+ });
+ return done_something;
+ }
+
+ var NODE;
+
+ // Top page table of content
+ NODE = $(".fcm-top-content");
+ if (NODE) {
+ ul_collapse_expand(NODE, IS_MAIN);
+ }
+
+ // Table of content
+ NODE = $(".fcm-page-content");
+ if (NODE) {
+ if (content_gen(NODE)) {
+ ul_collapse_expand(NODE);
+ }
+ }
+
+ // Display version information
+ NODE = $(".fcm-version");
+ if (NODE) {
+ NODE.text("FCM " + FCM.VERSION);
+ }
+});
diff --git a/doc/etc/fcm.png b/doc/etc/fcm.png
new file mode 100644
index 0000000..9677679
Binary files /dev/null and b/doc/etc/fcm.png differ
diff --git a/doc/etc/jquery.min.js b/doc/etc/jquery.min.js
new file mode 100644
index 0000000..198b3ff
--- /dev/null
+++ b/doc/etc/jquery.min.js
@@ -0,0 +1,4 @@
+/*! jQuery v1.7.1 jquery.com | jquery.org/license */
+(function(a,b){function cy(a){return f.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}function cv(a){if(!ck[a]){var b=c.body,d=f("<"+a+">").appendTo(b),e=d.css("display");d.remove();if(e==="none"||e===""){cl||(cl=c.createElement("iframe"),cl.frameBorder=cl.width=cl.height=0),b.appendChild(cl);if(!cm||!cl.createElement)cm=(cl.contentWindow||cl.contentDocument).document,cm.write((c.compatMode==="CSS1Compat"?"<!doctype html>":"")+"<html><body>"),cm.close();d=cm.createElement( [...]
+f.event={add:function(a,c,d,e,g){var h,i,j,k,l,m,n,o,p,q,r,s;if(!(a.nodeType===3||a.nodeType===8||!c||!d||!(h=f._data(a)))){d.handler&&(p=d,d=p.handler),d.guid||(d.guid=f.guid++),j=h.events,j||(h.events=j={}),i=h.handle,i||(h.handle=i=function(a){return typeof f!="undefined"&&(!a||f.event.triggered!==a.type)?f.event.dispatch.apply(i.elem,arguments):b},i.elem=a),c=f.trim(I(c)).split(" ");for(k=0;k<c.length;k++){l=A.exec(c[k])||[],m=l[1],n=(l[2]||"").split(".").sort(),s=f.event.special[m]| [...]
+{for(var a=0,b;(b=this[a])!=null;a++){b.nodeType===1&&f.cleanData(b.getElementsByTagName("*"));while(b.firstChild)b.removeChild(b.firstChild)}return this},clone:function(a,b){a=a==null?!1:a,b=b==null?a:b;return this.map(function(){return f.clone(this,a,b)})},html:function(a){if(a===b)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(W,""):null;if(typeof a=="string"&&!ba.test(a)&&(f.support.leadingWhitespace||!X.test(a))&&!bg[(Z.exec(a)||["",""])[1].toLowerCase()]){a=a.replac [...]
\ No newline at end of file
diff --git a/doc/etc/moment.min.js b/doc/etc/moment.min.js
new file mode 100644
index 0000000..62b1697
--- /dev/null
+++ b/doc/etc/moment.min.js
@@ -0,0 +1,6 @@
+// moment.js
+// version : 2.1.0
+// author : Tim Wood
+// license : MIT
+// momentjs.com
+!function(t){function e(t,e){return function(n){return u(t.call(this,n),e)}}function n(t,e){return function(n){return this.lang().ordinal(t.call(this,n),e)}}function s(){}function i(t){a(this,t)}function r(t){var e=t.years||t.year||t.y||0,n=t.months||t.month||t.M||0,s=t.weeks||t.week||t.w||0,i=t.days||t.day||t.d||0,r=t.hours||t.hour||t.h||0,a=t.minutes||t.minute||t.m||0,o=t.seconds||t.second||t.s||0,u=t.milliseconds||t.millisecond||t.ms||0;this._input=t,this._milliseconds=u+1e3*o+6e4*a+3 [...]
\ No newline at end of file
diff --git a/doc/index.html b/doc/index.html
new file mode 100644
index 0000000..6d73ef7
--- /dev/null
+++ b/doc/index.html
@@ -0,0 +1,93 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>FCM Documentation</title>
+ <meta name="author" content="FCM team" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link rel="icon" href="etc/fcm-icon.png" type="image/png" />
+ <link rel="shortcut icon" href="etc/fcm-icon.png" type="image/png" />
+ <link href="etc/bootstrap/css/bootstrap.min.css" rel="stylesheet" media="screen" />
+</head>
+<body>
+ <div class="navbar navbar-inverse">
+ <div class="navbar-inner">
+ <a class="active brand" href="#"><span class="fcm-version">FCM</span></a>
+ <ul class="nav">
+ <li><a href="installation/">Installation</a></li>
+
+ <li><a href="user_guide/">User Guide</a></li>
+ </ul>
+ </div>
+ </div>
+ <div class="container">
+ <div class="row">
+ <div class="span12">
+ <div class="hero-unit">
+ <div class="container-fluid">
+ <div class="row-fluid">
+ <div class="span12">
+ <h1>FCM</h1>
+ </div>
+ </div>
+
+ <div class="row-fluid">
+ <div class="span6 well">
+ <h2>Build</h2>
+
+ <p>A powerful build system for modern Fortran software
+ applications.</p>
+ </div>
+
+ <div class="span6 well">
+ <h2>Version Control</h2>
+
+ <p>Wrappers to the Subversion version control system,
+ usage conventions and processes for scientific software
+ development.</p>
+ </div>
+ </div>
+
+ <div class="row-fluid text-center">
+ <div class="span4">
+ <p><a class="btn btn-primary btn-block" href=
+ "https://github.com/metomi/fcm/releases/">
+ <i class="icon-download icon-white"></i>
+ Get FCM from Github</a></p>
+ </div>
+ <div class="span4">
+ <p><a class="btn btn-primary btn-block" href="installation/">
+ <i class="icon-wrench icon-white"></i>
+ Read the FCM Installation Guide</a></p>
+ </div>
+ <div class="span4">
+ <p><a class="btn btn-primary btn-block" href="user_guide/">
+ <i class="icon-user icon-white"></i>
+ Read the FCM User Guide</a></p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <hr/>
+ <div class="container-fluid text-center">
+ <div class="row-fluid"><div class="span12">
+ <address><small>
+ © British Crown Copyright 2006-14
+ <a href="http://www.metoffice.gov.uk">Met Office</a>.
+ See <a href="etc/fcm-terms-of-use.html">Terms of Use</a>.<br />
+ This document is released under the British <a href=
+ "http://www.nationalarchives.gov.uk/doc/open-government-licence/" rel=
+ "license">Open Government Licence</a>.<br />
+ </small></address>
+ </div></div>
+ </div>
+
+ <script type="text/javascript" src="etc/jquery.min.js"></script>
+ <script type="text/javascript" src="etc/bootstrap/js/bootstrap.min.js"></script>
+ <script type="text/javascript" src="etc/fcm.js"></script>
+ <script type="text/javascript" src="etc/fcm-version.js"></script>
+</body>
+</html>
diff --git a/doc/installation/index.html b/doc/installation/index.html
new file mode 100644
index 0000000..2b25aef
--- /dev/null
+++ b/doc/installation/index.html
@@ -0,0 +1,354 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>FCM: Installation</title>
+ <meta name="author" content="FCM team" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link rel="icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link rel="shortcut icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link href="../etc/bootstrap/css/bootstrap.min.css" type="text/css" rel="stylesheet" media="screen" />
+ <link href="../etc/fcm.css" type="text/css" rel="stylesheet" media="screen" />
+</head>
+<body>
+ <div class="navbar navbar-inverse">
+ <div class="navbar-inner">
+ <a class="brand" href=".."><span class="fcm-version">FCM</span></a>
+ <ul class="nav">
+ <li class="active"><a href="#">Installation</a></li>
+
+ <li><a href="../user_guide/">User Guide</a></li>
+ </ul>
+ </div>
+ </div>
+
+ <div class="page-header">
+ <div class="fcm-page-content pull-right well well-small"></div>
+ <h1>FCM: Installation</h1>
+ </div>
+
+ <div class="container">
+ <div class="row">
+ <div class="span12">
+ <h2 id="requirements">System Requirements</h2>
+
+ <p>FCM is intended to run on a Unix/Linux system. It is known to work on
+ RHEL-6 and some part of it on AIX-7.</p>
+
+ <p>FCM releases can be downloaded from <a href=
+ "https://github.com/metomi/fcm/releases">Github</a>. Download the tar.gz file
+ and un-pack it into an appropriate location on your system. Add the
+ <samp>bin/</samp> directory into your <var>PATH</var> environment variable.
+ Enable the configuration files in <samp>etc/fcm/</samp> directory and edit
+ them to meet the your requirements. Once you have done this you should now
+ have access to the FCM user utilities, assuming that you have met the
+ requirements described below:</p>
+
+ <dl class="well">
+ <dt><a href="http://www.perl.org/">Perl</a></dt>
+
+ <dd>
+ <p><dfn>used by:</dfn> <code>fcm</code>.</p>
+
+ <p><dfn>versions known to work:</dfn> 5.10.1.</p>
+
+ <p><dfn>remark:</dfn> We assume that all <em>core</em> Perl modules (as
+ documented by <a href="http://perldoc.perl.org/">perldoc.perl.org</a>) of
+ the <em>known to work versions</em> are installed on your system. (N.B. On
+ platforms based on RHEL, you may need the <em>perl-core</em> RPM instead
+ of just <em>perl</em>, see <a href=
+ "http://www.nntp.perl.org/group/perl.perl5.porters/2009/08/msg149891.html">this
+ discussion</a>.)</p>
+ </dd>
+
+ <dt>Perl module <a href=
+ "http://search.cpan.org/dist/Config-IniFiles/">Config::IniFiles</a></dt>
+
+ <dd>
+ <p><dfn>used by:</dfn> the admin commands.</p>
+
+ <p><dfn>versions known to work:</dfn> RHEL-6: 2.72.</p>
+ </dd>
+
+ <dt>Perl module <a href=
+ "http://search.cpan.org/dist/DBD-SQLite/">DBD::SQLite</a></dt>
+
+ <dd>
+ <p><dfn>used by:</dfn> the admin commands.</p>
+
+ <p><dfn>versions known to work:</dfn> RHEL-6: 1.29.</p>
+ </dd>
+
+ <dt>Perl module <a href=
+ "http://search.cpan.org/dist/Alien-SVN/">SVN::Client</a></dt>
+
+ <dd>
+ <p><dfn>used by:</dfn> the admin commands.</p>
+
+ <p><dfn>versions known to work:</dfn> RHEL-6: 1.8.5 (RPM from <a href=
+ "http://opensource.wandisco.com/rhel/6/svn-1.8/RPMS/x86_64/">http://opensource.wandisco.com/</a>).</p>
+ </dd>
+
+ <dt>Perl module <a href=
+ "http://search.cpan.org/dist/XML-Parser/">XML::Parser</a></dt>
+
+ <dd>
+ <p><dfn>used by:</dfn> the code management commands.</p>
+
+ <p><dfn>versions known to work:</dfn> RHEL-6: 2.36.</p>
+ </dd>
+
+ <dt>Perl module <a href=
+ "http://search.cpan.org/~srezic/Tk-804.028/">Tk</a></dt>
+
+ <dd>
+ <p><dfn>used by:</dfn> <code>fcm gui</code>.</p>
+
+ <p><dfn>versions known to work:</dfn> RHEL-6: 804.028.</p>
+ </dd>
+
+ <dt><a href="http://subversion.apache.org/">Subversion</a></dt>
+
+ <dd>
+ <p><dfn>used by:</dfn> the code management commands, the extract system
+ of <code>fcm make</code>, the deprecated <code>fcm extract</code>.</p>
+
+ <p><dfn>versions known to work:</dfn> AIX-7: 1.6.18, RHEL-6: 1.6.17,
+ RHEL-6: 1.8.8.</p>
+
+ <p><dfn>remark:</dfn> you can use the extract system to mirror code to a
+ remote platform for building. Therefore it is only necessary to have
+ Subversion installed on the platform where you do your code development.
+ If you use other platforms purely for building and running then you do
+ not need to have Subversion installed on these platforms.</p>
+ </dd>
+
+ <dt><a href="http://trac.edgewall.org/">Trac</a></dt>
+
+ <dd>
+ <p><dfn>used by:</dfn> (optional, but highly recommended as a companion
+ to Subversion)</p>
+
+ <p><dfn>versions known to work:</dfn> 0.11.7.</p>
+ </dd>
+
+ <dt><a href="http://furius.ca/xxdiff/">xxdiff</a></dt>
+
+ <dd>
+ <p><dfn>used by:</dfn> <code>fcm branch-diff --graphical</code>,
+ <code>fcm conflicts</code>, <code>fcm diff --graphical</code>.</p>
+
+ <p><dfn>versions known to work:</dfn> RHEL-6: 3.2.</p>
+
+ <p><dfn>remark:</dfn> The <code>fcm branch-diff --graphical</code> and
+ <code>fcm diff --graphical</code> commands use xxdiff by default but can
+ also use other graphical diff tools.</p>
+ </dd>
+
+ <dt><a href="http://www.gnu.org/software/diffutils/">GNU diffutils</a>:
+ diff3</dt>
+
+ <dd>
+ <p><dfn>used by:</dfn> the extract system of <code>fcm make</code>, the
+ deprecated <code>fcm extract</code>.</p>
+
+ <p><dfn>versions known to work:</dfn> RHEL-6: 2.8.1.</p>
+
+ <p><dfn>remark:</dfn>: used to merge changes to source files modified by
+ 2+ diff source trees (compared with the base).</p>
+ </dd>
+
+ <dt><a href="http://rsync.samba.org/">rsync</a></dt>
+
+ <dd>
+ <p><dfn>used by:</dfn> the mirror system of <code>fcm make</code>, the
+ deprecated <code>fcm extract</code>.</p>
+
+ <p><dfn>versions known to work:</dfn> AIX-7: 3.0.9, RHEL-6: 3.0.6.</p>
+
+ <p><dfn>remark:</dfn> used to mirror source file to another
+ <var>USER at HOST</var>.</p>
+ </dd>
+
+ <dt><a href="http://www.gnu.org/software/make/make.html">GNU make</a></dt>
+
+ <dd>
+ <p><dfn>used by:</dfn> the deprecated <code>fcm build</code>.</p>
+
+ <p><dfn>versions known to work:</dfn> AIX-7: 3.80, RHEL-6: 3.81.</p>
+ </dd>
+ </dl>
+ </div>
+ </div>
+
+ <div class="row">
+ <div class="span12">
+ <h2 id="distro">Content in the Distribution</h2>
+
+ <dl class="well">
+ <dt>ACKNOWLEDGEMENT.md<br />
+ CONTRIBUTING.md<br />
+ README.md<br />
+ COPYING</dt>
+
+ <dd>Terms of use and project information.</dd>
+
+ <dt>CHANGES.md</dt>
+
+ <dd>Contains highlight and noteworthy changes since release 2-3-1.</dd>
+
+ <dt>bin/</dt>
+
+ <dd>Contains the <code>fcm</code> user utilities.</dd>
+
+ <dt>doc/</dt>
+
+ <dd>FCM documentation.</dd>
+
+ <dt>doc/installation/</dt>
+
+ <dd>Contains this document.</dd>
+
+ <dt>doc/release_notes/</dt>
+
+ <dd>Contains release notes prior to and including release 2-3-1.</dd>
+
+ <dt>doc/user_guide/</dt>
+
+ <dd>Contains the <a href="../user_guide/">FCM User Guide</a>.</dd>
+
+ <dt>etc/</dt>
+
+ <dd>Miscellaneous items and example site configurations, including the
+ <samp>fcm/keyword.cfg.example</samp> file. If you wish to define keywords
+ for your site you will need to create the <samp>etc/fcm/keyword.cfg</samp>
+ file. An example file, <samp>fcm/keyword.cfg.example</samp>, is provided
+ which is a copy of the file currently used at the Met Office. For further
+ details please refer to the section <a
+ href="../user_guide/system_admin.html#fcm-keywords">FCM keywords</a> in the
+ System Admin chapter of the User Guide.</dd>
+
+ <dt>lib/</dt>
+
+ <dd>Contains the Perl library of FCM.</dd>
+
+ <dt>man/</dt>
+
+ <dd>Contains a basic manual page for <code>fcm</code>.</dd>
+
+ <dt>sbin/</dt>
+
+ <dd>Contains a selection of useful admin utility commands.</dd>
+
+ <dt>sbin/my-regular-update.example</dt>
+
+ <dd>An example of how you might set up a cron job to make use of the
+ <samp><repos>.latest</samp> file (see
+ <code>svn-hooks/post-commit-background</code>).</dd>
+
+ <dt>svn-hooks/</dt>
+
+ <dd>Contains a selection of useful hook scripts for Subversion.</dd>
+
+ <dt>svn-hooks/pre-commit</dt>
+
+ <dd>
+ This script restricts write-access to the repository by checking the
+ following:
+
+ <ul>
+ <li>It executes the Subversion utility <code>svnperms.py</code> if it,
+ and the associated <samp>svnperms.conf</samp> file, exist. This utility
+ checks whether the author of the current transaction has enough
+ permission to write to particular paths in the repository.</li>
+
+ <li>It checks the disk space required by the current transaction. It
+ fails the commit if it requires more than 10MB of disk space (or
+ whatever is specified in the
+ <code>pre-commit-size-threshold.conf</code> file.</li>
+ </ul>
+ </dd>
+
+ <dt>svn-hooks/post-commit</dt>
+
+ <dd>This script runs <code>post-commit-background</code> in the
+ background.</dd>
+
+ <dt>svn-hooks/post-commit-background</dt>
+
+ <dd>
+ This script runs in the background after each commit.
+
+ <ul>
+ <li>It updates a <samp><repos>.latest</samp> file with the latest
+ revision number.</li>
+
+ <li>It creates a dump of the new revision.</li>
+
+ <li>It calls <code>post-commit-background-custom</code> if it
+ exists.</li>
+ </ul>
+ </dd>
+
+ <dt>svn-hooks/pre-revprop-change</dt>
+
+ <dd>This script only allows the modification of <var>svn:log</var>.</dd>
+
+ <dt>svn-hooks/post-revprop-change</dt>
+
+ <dd>This script runs <code>post-revprop-change-background</code> in the
+ background.</dd>
+
+ <dt>svn-hooks/post-revprop-change-background</dt>
+
+ <dd>This script invokes the <code>trac-admin</code> command to
+ <code>resync</code> the revision property cache stored in the corresponding
+ Trac environment. If a user modifies the log message of a changeset and
+ he/she is not the original author of the changeset, this script will e-mail
+ the original author. If the file
+ <code>post-revprop-change-background-cc.list</code> exits, the script will
+ also e-mail those in the list.</dd>
+
+ <dt>t/</dt>
+
+ <dd>Contains functional test for FCM.</dd>
+
+ <dt>test/</dt>
+
+ <dd>Contains regression tests for FCM.</dd>
+
+ <dt>test/test_include/</dt>
+
+ <dd>Contains simple test code to check how your chosen compilers handle
+ include files.</dd>
+
+ <dt>tutorial/</dt>
+
+ <dd>Contains the files necessary to set up a Subversion repository for the
+ FCM tutorial. This will allow you to follow the <a href=
+ "../user_guide/getting_started.html#tutorial">tutorial section</a> in the
+ User Guide. See <samp>tutorial/README</samp> on how to set it up.</dd>
+ </dl>
+ </div>
+ </div>
+ </div>
+
+ <hr/>
+ <div class="container-fluid text-center">
+ <div class="row-fluid"><div class="span12">
+ <address><small>
+ © British Crown Copyright 2006-14
+ <a href="http://www.metoffice.gov.uk">Met Office</a>.
+ See <a href="../etc/fcm-terms-of-use.html">Terms of Use</a>.<br />
+ This document is released under the British <a href=
+ "http://www.nationalarchives.gov.uk/doc/open-government-licence/" rel=
+ "license">Open Government Licence</a>.<br />
+ </small></address>
+ </div></div>
+ </div>
+
+ <script type="text/javascript" src="../etc/jquery.min.js"></script>
+ <script type="text/javascript" src="../etc/bootstrap/js/bootstrap.min.js"></script>
+ <script type="text/javascript" src="../etc/fcm.js"></script>
+ <script type="text/javascript" src="../etc/fcm-version.js"></script>
+</body>
+</html>
diff --git a/doc/release_notes/1-1.html b/doc/release_notes/1-1.html
new file mode 100644
index 0000000..a66ab78
--- /dev/null
+++ b/doc/release_notes/1-1.html
@@ -0,0 +1,475 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>FCM 1.1 Release Notes</title>
+ <meta name="author" content="FCM team" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link rel="icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link rel="shortcut icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link href="../etc/bootstrap/css/bootstrap.min.css" rel="stylesheet" media="screen" />
+ <link href="../etc/fcm.css" rel="stylesheet" media="screen" />
+</head>
+<body>
+ <div class="navbar navbar-inverse">
+ <div class="navbar-inner">
+ <a class="brand" href=".."><span class="fcm-version">FCM</span></a>
+ <ul class="nav">
+ <li><a href="../installation/">Installation</a></li>
+
+ <li><a href="../user_guide/">User Guide</a></li>
+ </ul>
+ </div>
+ </div>
+
+ <div class="page-header">
+ <div class="fcm-page-content pull-right well well-small"></div>
+ <h1>FCM 1.1 Release Notes <small>06 November 2006</small></h1>
+ </div>
+
+ <div class="container">
+ <div class="row">
+ <div class="span12">
+
+ <p>These are the release notes for FCM release 1.1. You can use this release
+ of FCM freely under the terms of the FCM LICENSE, which you should receive
+ with the distribution of this release. Release 1.1 is the first external
+ release of FCM. (Release 1.0, 30 November 2005, was an internal Met Office
+ release which marked the start of the main migration of systems into FCM.)</p>
+
+ <p>FCM is maintained by the FCM team at the Met Office. Please feedback any
+ bug reports or feature requests to us by <a href=
+ "mailto:fcm-team at metoffice.gov.uk">e-mail</a>.</p>
+
+ <h2 id="new">What's New?</h2>
+
+ <p>Build system:</p>
+
+ <ul>
+ <li>Support building of Fortran <code>BLOCKDATA</code> program units.</li>
+
+ <li>Option to generate Fortran interface files in lower case names after
+ their program units.</li>
+
+ <li>Allow renaming of main program targets using the build configuration
+ file.</li>
+
+ <li>The <samp>fcm_env.ksh</samp> file now provides an environment variable
+ for the <samp>etc/</samp> sub-directory of the build which can be used if
+ you are <em>building</em> data files.</li>
+
+ <li>The build root directory is now locked while a build is running. This
+ prevents multiple instances of build running in the same directory.
+ However, you can bypass the lock if you specify the new
+ <code>--ignore-lock</code> option with <code>fcm build</code>.</li>
+ </ul>
+
+ <p>Extract system:</p>
+
+ <ul>
+ <li>The destination root directory is now locked while an extract is
+ running. This prevents multiple instances of extract running in the same
+ directory. However, you can bypass the lock if you specify the new
+ <code>--ignore-lock</code> option with <code>fcm extract</code>.</li>
+ </ul>
+
+ <p>Code management commands:</p>
+
+ <ul>
+ <li><code>fcm merge</code> now supports custom and reverse modes.</li>
+
+ <li><code>fcm merge</code> now allows automatic merges in sub-trees if it
+ is safe.</li>
+
+ <li><code>fcm merge</code> now handles automatic merges from sibling
+ branches that are created at different revisions of the parent.</li>
+
+ <li><code>fcm branch, fcm diff --branch</code> and <code>fcm merge</code>
+ can now handle creation, diff and automatic merge of a branch of a
+ branch.</li>
+
+ <li><code>fcm switch</code> is improved to allow safer switches of your
+ working copy to point to different branches in your project.</li>
+
+ <li><code>fcm commit</code> now displays your location in the branch, and
+ extra warning when you are committing to the trunk of a project.</li>
+
+ <li>New <code>fcm mkpatch</code> command.</li>
+ </ul>
+
+ <p>General:</p>
+
+ <ul>
+ <li>Error handling is improved.</li>
+
+ <li>The User Guide is now complete, with a much improved tutorial.</li>
+
+ <li>New document: Fortran coding standard for FCM.</li>
+ </ul>
+
+ <h2 id="fix">Minor Enhancements & Bug Fixes</h2>
+
+ <p>Build system:</p>
+
+ <ul>
+ <li>Ignore empty Fortran source files, so that alternate sections can be
+ handled correctly.</li>
+
+ <li>Handle recursive header file dependency correctly in pre-processing
+ stage.</li>
+
+ <li>Identify changes in pre-processed source file, the pre-processor
+ options and keys correctly in incremental builds.</li>
+
+ <li>Improve speed of pre-processing in incremental builds by not performing
+ any unnecessary null-action pre-processing.</li>
+
+ <li>Load archiver no longer includes main program objects.</li>
+
+ <li>It is now possible for a sub-package to exclude particular types of
+ dependencies.</li>
+ </ul>
+
+ <p>Extract system:</p>
+
+ <ul>
+ <li>Sub-directories are now extracted with the non-recursive mode of
+ <code>svn export</code>.</li>
+
+ <li>Peg revisions should now be handled correctly in <var>INC</var> extract
+ declarations.</li>
+
+ <li>The command should now fail if a declared source directory does not
+ exist or if the update of an extract destination fails.</li>
+ </ul>
+
+ <p>Code management commands:</p>
+
+ <ul>
+ <li><code>fcm branch --info</code> and <code>fcm diff</code> can now take a
+ <code>PATH</code> as an argument.</li>
+
+ <li>The <code>--ticket</code> option of <code>fcm branch --create</code>
+ can now accept multiple tickets.</li>
+
+ <li><code>fcm branch --info</code> and <code>fcm diff --branch</code>
+ should now work correctly in a sub-tree of a branch.</li>
+
+ <li><code>fcm branch --create</code> no longer offers to checkout the
+ branch.</li>
+
+ <li><code>fcm commit</code> no longer fails when adding a new symbolic
+ link.</li>
+
+ <li>The <code>--password</code> option is now supported by the <code>fcm
+ branch --create</code>, <code>fcm branch --delete</code>, <code>fcm
+ commit</code> and <code>fcm delete</code> commands.</li>
+
+ <li>Empty arguments to code management commands are now parsed
+ correctly.</li>
+
+ <li>The <code>fcm diff --graphical</code> option no longer fails with
+ binary files.</li>
+
+ <li>FCM will always set the environment variable <var>LANG=en_GB</var>
+ before running Subversion commands. This prevents failure of FCM when it
+ attempts to parse output from Subversion commands when a different
+ <var>LANG</var> setting is used.</li>
+ </ul>
+
+ <p>General:</p>
+
+ <ul>
+ <li>Various other very minor enhancements and bug fixes.</li>
+ </ul>
+
+ <h2 id="req">System Requirements</h2>
+
+ <h3 id="req_perl">Perl</h3>
+
+ <p>The core part of FCM is a set of Perl scripts and modules. For the build
+ system to work, you need the following modules installed:</p>
+
+ <ul>
+ <li>Carp</li>
+
+ <li>Cwd</li>
+
+ <li>File::Basename</li>
+
+ <li>File::Compare</li>
+
+ <li>File::Find</li>
+
+ <li>File::Path</li>
+
+ <li>File::Spec::Functions</li>
+
+ <li>File::Spec</li>
+
+ <li>FindBin</li>
+
+ <li>Getopt::Long</li>
+
+ <li>POSIX</li>
+ </ul>
+
+ <p>The code management commands and extract system need the following
+ additional modules installed:</p>
+
+ <ul>
+ <li>File::Temp</li>
+
+ <li>Getopt::Long</li>
+
+ <li>HTTP::Date</li>
+
+ <li>XML::DOM</li>
+ </ul>
+
+ <p>To use the simple GUI for some of the code management commands, you also
+ need the following modules:</p>
+
+ <ul>
+ <li>Tk::ROText</li>
+
+ <li>Tk</li>
+ </ul>
+
+ <p>At the Met Office we are currently using the complete FCM system with Perl
+ 5.8.x. In addition the build system is being used with Perl 5.6.x.</p>
+
+ <h3 id="req_svn">Subversion</h3>
+
+ <p>To use the code management commands (and relevant parts of the extract
+ system) you need to have <a href=
+ "http://subversion.tigris.org/">Subversion</a> installed.</p>
+
+ <ul>
+ <li>FCM makes extensive use of peg revisions in both the code management
+ and extract systems. This requires Subversion 1.2.0.</li>
+
+ <li>At the Met Office we are currently using Subversion 1.3.2 (although
+ 1.2.3 was used until very recently).</li>
+ </ul>
+
+ <p>Note that the extract system can mirror extracted code to a remote
+ platform for building. Therefore it is only necessary to have Subversion
+ installed on the platform where you do your code development. If you use
+ other platforms purely for building and running then you do not need to have
+ Subversion installed on these platforms.</p>
+
+ <h3 id="req_trac">Trac</h3>
+
+ <p>The use of <a href="http://trac.edgewall.org/">Trac</a> is entirely
+ optional (although highly recommended if you are using Subversion). At the
+ Met Office we are currently using version 0.9.6.</p>
+
+ <h3 id="req_other">Other Requirements</h3>
+
+ <p>The <code>fcm diff --graphical</code> and <code>fcm conflicts</code>
+ commands require <a href="http://furius.ca/xxdiff/">xxdiff</a>. At the Met
+ Office we are currently using version 3.1.</p>
+
+ <p>The build system requires <a href=
+ "http://www.gnu.org/software/make/make.html">GNU make</a>. At the Met Office
+ we are currently using version 3.79.x and 3.80.</p>
+
+ <p>Optionally, the build system can use <a href=
+ "http://www.ifremer.fr/ditigo/molagnon/fortran90/">f90aib</a> to generate
+ interface files. However, there is also a built in Perl based interface file
+ generator which is quicker and better in most cases so you are unlikely to
+ need f90aib unless you hit a problem with some particular code.</p>
+
+ <p>FCM is intended to run on a Unix/Linux system. It is currently used at the
+ Met Office on Linux (Red Hat 9 and Red Hat Enterprise 2.1 and 4.4) and HP-UX
+ 11.00.</p>
+
+ <h2 id="ins">Installation</h2>
+
+ <p>FCM is distributed in the form of a compressed tar file. Un-pack the tar
+ file into an appropriate location on your system. Then add the
+ <samp>bin/</samp> directory into your <var>PATH</var>. Once you have done this
+ you should now have full access to the FCM system, assuming that you have met
+ the requirements described in the previous section.</p>
+
+ <p>If you wish to define keywords for your systems you will need to create a
+ file <samp>etc/fcm.cfg</samp>. An example file, <samp>fcm.cfg.eg</samp>, is
+ provided which is a copy of the file currently used at the Met Office. For
+ further details please refer to the section <a href=
+ "../user_guide/system_admin.html#fcm-keywords">FCM keywords</a> in the System
+ Admin chapter of the User Guide.</p>
+
+ <p>The <samp>doc/</samp> directory contains all the system documentation.</p>
+
+ <ul>
+ <li><samp>doc/release_notes/</samp> contains these release notes. It also
+ contains the release notes for all previous versions which may be useful if
+ you have skipped any versions.</li>
+
+ <li><samp>doc/user_guide/</samp> contains the FCM User Guide in both
+ <a href="../user_guide/">HTML</a> and <a href=
+ "../user_guide/fcm-user-guide.pdf">PDF</a> form.</li>
+
+ <li><samp>doc/design/</samp> contains the <a href="../design/">FCM Detailed
+ Design</a> document (currently in draft form).</li>
+
+ <li><samp>doc/standards/</samp> contains the FCM <a href=
+ "../standards/perl_standard.html">Perl</a> and <a href=
+ "../standards/fortran_standard.html">Fortran</a> coding standards. The Perl
+ standard describes the standards followed by the FCM code. The Fortran
+ standard contains some <a href=
+ "../standards/fortran_standard.html#fcm">specific advice</a> on the best
+ way of writing Fortran code for use with FCM as well as more general advice
+ on good practise.</li>
+ </ul>
+
+ <p>The <samp>tutorial/</samp> directory contains the files necessary to set
+ up a tutorial repository. This will allow you to follow the <a href=
+ "../user_guide/getting_started.html#tutorial">tutorial section</a> in the
+ User Guide.</p>
+
+ <ul>
+ <li>The file <samp>tutorial/repos/tutorial.dump</samp> should be loaded
+ into an empty repository using the <code>svnadmin load</code> command.</li>
+
+ <li>The hook scripts in <samp>tutorial/hook/</samp> should then be
+ installed in this repository in order to prevent any commits to the trunk.
+ Note that the configuration file <samp>svnperms.conf</samp> assumes that
+ the tutorial repository is called <samp>tutorial_svn</samp>. Please edit
+ this file if you use a different name.</li>
+
+ <li>The repository should be configured to allow users write access. You
+ may find it easiest to simply allow anonymous access.</li>
+
+ <li>A Trac system should be configured associated with the Tutorial
+ repository. You then need to allow users write access. You may find it
+ easiest to set up a number of guest accounts for this purpose.</li>
+ </ul>
+
+ <p>The <samp>templates/</samp> directory contains various example scripts
+ which you may find useful. Note that these scripts are all specific to the
+ Met Office and may contain hard coded paths and email addresses. They are
+ provided in the hope that you may find them useful as templates for setting
+ up similar scripts of your own. However, they should only be used after
+ careful review to adapt them to your environment. The contents are as
+ follows:</p>
+
+ <dl>
+ <dt>templates/hook/pre-commit</dt>
+
+ <dd>
+ This script restricts write-access to the repository by checking the
+ following:
+
+ <ul>
+ <li>It executes the Subversion utility <code>svnperms.py</code> if it
+ exists. This utility checks whether the author of the current
+ transaction has enough permission to write to particular paths in the
+ repository.</li>
+
+ <li>It checks the disk space required by the current transaction. It
+ fails the commit if it requires more than 5Mb of disk space.</li>
+ </ul>
+ </dd>
+
+ <dt>templates/hook/post-commit</dt>
+
+ <dd>A simple post-commit hook script which runs the script
+ <code>post-commit-background</code> in the background.</dd>
+
+ <dt>templates/hook/post-commit-background</dt>
+
+ <dd>
+ This script runs in the background after each commit.
+
+ <ul>
+ <li>It updates a <samp><repos>.latest</samp> file with the latest
+ revision number.</li>
+
+ <li>It creates a dump of the new revision.</li>
+
+ <li>It calls the script <code>background_updates.pl</code> if it
+ exists.</li>
+ </ul>This script is installed as standard in all our repositories.
+ </dd>
+
+ <dt>templates/hook/background_updates.pl</dt>
+
+ <dd>An example of how you may want to set up a
+ <code>background_updates.pl</code> script to perform post-commit tasks for
+ a specific repository. This script uses a lock file to prevent multiple
+ commits in quick succession from causing problems.</dd>
+
+ <dt>templates/hook/pre-revprop-change</dt>
+
+ <dd>A simple pre-revprop-change hook script which runs the script
+ <code>pre-revprop-change.pl</code>.</dd>
+
+ <dt>templates/hook/pre-revprop-change.pl</dt>
+
+ <dd>If a user attempts to modify the log message of a changeset and he/she
+ is not the original author of the changeset, this script will e-mail the
+ original author. You can also set up a watch facility to monitor changes of
+ log messages that affect particular paths in the repository. For further
+ details please refer to the section <a href=
+ "../user_guide/system_admin.html#svn_watch">Watching changes in log
+ messages</a> in the System Admin chapter of the User Guide.</dd>
+
+ <dt>templates/hook/post-revprop-change</dt>
+
+ <dd>A simple post-revprop-change hook script which runs the script
+ <code>post-revprop-change.py</code>.</dd>
+
+ <dt>templates/hook/post-revprop-change.py</dt>
+
+ <dd>This hook script updates the Trac SQLite database following a
+ successful change in the log message.</dd>
+
+ <dt>templates/utils/cron_template.ksh</dt>
+
+ <dd>An example of how you might set up a cron job to make use of the
+ <samp><repos>.latest</samp> file.</dd>
+
+ <dt>templates/utils/daily_cron</dt>
+
+ <dd>The cron job which we run each night. It verifies and backs up each of
+ our repositories, housekeeps the revision dumps created by
+ <code>post-commit-background</code> and backs up each of our Trac systems.
+ It also handles the distribution of FCM to various platforms at the Met
+ Office.</dd>
+
+ <dt>templates/utils/fcm_add_trac.pl</dt>
+
+ <dd>This script sets up a new Trac system and applies some configuration
+ options which we use by default at the Met Office.</dd>
+
+ <dt>templates/utils/recover_svn.pl</dt>
+
+ <dd>This script allows us to recover all of our Subversion repositories by
+ using the nightly backups and the repository dumps.</dd>
+ </dl>
+
+ </div>
+ </div>
+ </div>
+
+ <hr/>
+ <div class="container-fluid text-center">
+ <div class="row-fluid"><div class="span12">
+ <address><small>
+ © British Crown Copyright 2006-14
+ <a href="http://www.metoffice.gov.uk">Met Office</a>.
+ See <a href="../etc/fcm-terms-of-use.html">Terms of Use</a>.<br />
+ This document is released under the British <a href=
+ "http://www.nationalarchives.gov.uk/doc/open-government-licence/" rel=
+ "license">Open Government Licence</a>.<br />
+ </small></address>
+ </div></div>
+ </div>
+
+ <script type="text/javascript" src="../etc/jquery.min.js"></script>
+ <script type="text/javascript" src="../etc/bootstrap/js/bootstrap.min.js"></script>
+ <script type="text/javascript" src="../etc/fcm.js"></script>
+ <script type="text/javascript" src="../etc/fcm-version.js"></script>
+</body>
+</html>
diff --git a/doc/release_notes/1-2.html b/doc/release_notes/1-2.html
new file mode 100644
index 0000000..e8824d7
--- /dev/null
+++ b/doc/release_notes/1-2.html
@@ -0,0 +1,400 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>FCM 1.2 Release Notes</title>
+ <meta name="author" content="FCM team" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link rel="icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link rel="shortcut icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link href="../etc/bootstrap/css/bootstrap.min.css" rel="stylesheet" media="screen" />
+ <link href="../etc/fcm.css" rel="stylesheet" media="screen" />
+</head>
+<body>
+ <div class="navbar navbar-inverse">
+ <div class="navbar-inner">
+ <a class="brand" href=".."><span class="fcm-version">FCM</span></a>
+ <ul class="nav">
+ <li><a href="../installation/">Installation</a></li>
+
+ <li><a href="../user_guide/">User Guide</a></li>
+ </ul>
+ </div>
+ </div>
+
+ <div class="page-header">
+ <div class="fcm-page-content pull-right well well-small"></div>
+ <h1>FCM 1.2 Release Notes <small>22 March 2007</small></h1>
+ </div>
+
+ <div class="container">
+ <div class="row">
+ <div class="span12">
+
+ <p>These are the release notes for FCM release 1.2. You can use this release
+ of FCM freely under the terms of the FCM LICENSE, which you should receive
+ with the distribution of this release.</p>
+
+ <p>FCM is maintained by the FCM team at the Met Office. Please feedback any
+ bug reports or feature requests to us by <a href=
+ "mailto:fcm-team at metoffice.gov.uk">e-mail</a>.</p>
+
+ <h2 id="new">What's New?</h2>
+
+ <p>Code management commands:</p>
+
+ <ul>
+ <li>New options <code>--trac</code> and <code>--wiki</code> for <code>fcm
+ diff --branch</code>.</li>
+
+ <li>Allow other graphical diff tools to be used in place of
+ <code>xxdiff</code>.</li>
+ </ul>
+
+ <p>General:</p>
+
+ <ul>
+ <li>New document <cite>External Distribution & Collaboration for FCM
+ Projects</cite>.</li>
+ </ul>
+
+ <h2 id="fix">Minor enhancements & Bug Fixes</h2>
+
+ <p>Build system:</p>
+
+ <ul>
+ <li>Extra warnings when multiple targets are detected in the source
+ tree.</li>
+
+ <li>Improved the patterns for detecting <code>recursive</code>,
+ <code>pure</code> and <code>elemental</code> Fortran subroutines and
+ functions.</li>
+ </ul>
+
+ <p>Code management commands:</p>
+
+ <ul>
+ <li><code>fcm branch --list</code> now prints the branches using FCM URL
+ keywords by default. Use the <code>--verbose</code> option to print
+ branches in full Subversion URLs.</li>
+ </ul>
+
+ <p>General:</p>
+
+ <ul>
+ <li>Enhanced <code>fcm cmp-ext-cfg</code> to link to tickets.</li>
+
+ <li>Improved handling of FCM URL keywords. The <code>SET::REPOS</code>
+ declaration in the central/user configuration file is deprecated in favour
+ of <code>SET::URL</code>. The keyword of the project with the standard
+ suffices (<code>_tr</code> or <code>-tr</code> for <em>trunk</em>,
+ <code>_br</code> or <code>-br</code> for <em>branches</em>, and
+ <code>_tg</code> or <code>-tg</code> for <em>tags</em>) are recognised
+ automatically.</li>
+
+ <li>Fix: full extract/build should no longer delete one another's cache if
+ they are run in the same directory.</li>
+
+ <li>Various other very minor enhancements and bug fixes.</li>
+ </ul>
+
+ <h2 id="req">System Requirements</h2>
+
+ <h3 id="req_perl">Perl</h3>
+
+ <p>The core part of FCM is a set of Perl scripts and modules. For the build
+ system to work, you need the following modules installed:</p>
+
+ <ul>
+ <li>Carp</li>
+
+ <li>Cwd</li>
+
+ <li>File::Basename</li>
+
+ <li>File::Compare</li>
+
+ <li>File::Find</li>
+
+ <li>File::Path</li>
+
+ <li>File::Spec::Functions</li>
+
+ <li>File::Spec</li>
+
+ <li>FindBin</li>
+
+ <li>Getopt::Long</li>
+
+ <li>POSIX</li>
+ </ul>
+
+ <p>The code management commands and extract system need the following
+ additional modules installed:</p>
+
+ <ul>
+ <li>File::Temp</li>
+
+ <li>Getopt::Long</li>
+
+ <li>HTTP::Date</li>
+
+ <li>XML::DOM</li>
+ </ul>
+
+ <p>To use the simple GUI for some of the code management commands, you also
+ need the following modules:</p>
+
+ <ul>
+ <li>Tk::ROText</li>
+
+ <li>Tk</li>
+ </ul>
+
+ <p>At the Met Office we are currently using the complete FCM system with Perl
+ 5.8.x. In addition the build system is being used with Perl 5.6.x.</p>
+
+ <h3 id="req_svn">Subversion</h3>
+
+ <p>To use the code management commands (and relevant parts of the extract
+ system) you need to have <a href=
+ "http://subversion.tigris.org/">Subversion</a> installed.</p>
+
+ <ul>
+ <li>FCM makes extensive use of peg revisions in both the code management
+ and extract systems. This requires Subversion 1.2.0.</li>
+
+ <li>At the Met Office we are currently using Subversion 1.3.2.</li>
+ </ul>
+
+ <p>Note that the extract system can mirror extracted code to a remote
+ platform for building. Therefore it is only necessary to have Subversion
+ installed on the platform where you do your code development. If you use
+ other platforms purely for building and running then you do not need to have
+ Subversion installed on these platforms.</p>
+
+ <h3 id="req_trac">Trac</h3>
+
+ <p>The use of <a href="http://trac.edgewall.org/">Trac</a> is entirely
+ optional (although highly recommended if you are using Subversion).</p>
+
+ <ul>
+ <li>The <code>--trac</code> and <code>--wiki</code> options to the
+ <code>fcm diff --branch</code> command allow you to view branch differences
+ using Trac. This requires Trac 0.10.</li>
+
+ <li>At the Met Office we are currently using Trac 0.10.3.</li>
+ </ul>
+
+ <h3 id="req_other">Other Requirements</h3>
+
+ <p>The <code>fcm conflicts</code> command requires <a href=
+ "http://furius.ca/xxdiff/">xxdiff</a>. At the Met Office we are currently
+ using version 3.1. The <code>fcm diff --graphical</code> command also uses
+ xxdiff by default although other graphical diff tools can also be used.</p>
+
+ <p>The build system requires <a href=
+ "http://www.gnu.org/software/make/make.html">GNU make</a>. At the Met Office
+ we are currently using version 3.79.x and 3.80.</p>
+
+ <p>Optionally, the build system can use <a href=
+ "http://www.ifremer.fr/ditigo/molagnon/fortran90/">f90aib</a> to generate
+ interface files. However, there is also a built in Perl based interface file
+ generator which is quicker and better in most cases so you are unlikely to
+ need f90aib unless you hit a problem with some particular code.</p>
+
+ <p>FCM is intended to run on a Unix/Linux system. It is currently used at the
+ Met Office on Linux (Red Hat Enterprise 2.1 and 4.4) and HP-UX 11.00.</p>
+
+ <h2 id="ins">Installation</h2>
+
+ <p>FCM is distributed in the form of a compressed tar file. Un-pack the tar
+ file into an appropriate location on your system. Then add the
+ <samp>bin/</samp> directory into your <var>PATH</var>. Once you have done
+ this you should now have full access to the FCM system, assuming that you
+ have met the requirements described in the previous section.</p>
+
+ <p>If you wish to define keywords for your systems you will need to create a
+ file <samp>etc/fcm.cfg</samp>. An example file, <samp>fcm.cfg.eg</samp>, is
+ provided which is a copy of the file currently used at the Met Office. For
+ further details please refer to the section <a href=
+ "../user_guide/system_admin.html#fcm-keywords">FCM keywords</a> in the System
+ Admin chapter of the User Guide.</p>
+
+ <p>The <samp>doc/</samp> directory contains all the system documentation.</p>
+
+ <ul>
+ <li><samp>doc/release_notes/</samp> contains these release notes. It also
+ contains the release notes for all previous versions which may be useful if
+ you have skipped any versions.</li>
+
+ <li><samp>doc/user_guide/</samp> contains the <a href="../user_guide/">FCM
+ User Guide</a>.</li>
+
+ <li><samp>doc/design/</samp> contains the <a href="../design/">FCM Detailed
+ Design</a> document (currently in draft form).</li>
+
+ <li><samp>doc/standards/</samp> contains the FCM <a href=
+ "../standards/perl_standard.html">Perl</a> and <a href=
+ "../standards/fortran_standard.html">Fortran</a> coding standards. The Perl
+ standard describes the standards followed by the FCM code. The Fortran
+ standard contains some <a href=
+ "../standards/fortran_standard.html#fcm">specific advice</a> on the best
+ way of writing Fortran code for use with FCM as well as more general advice
+ on good practise.</li>
+
+ <li><samp>doc/collaboration/</samp> contains the <a href=
+ "../collaboration/index.html">External Distribution & Collaboration for
+ FCM Projects</a> document which discusses how projects configured under FCM
+ can be distributed externally.</li>
+ </ul>
+
+ <p>The <samp>tutorial/</samp> directory contains the files necessary to set
+ up a tutorial repository. This will allow you to follow the <a href=
+ "../user_guide/getting_started.html#tutorial">tutorial section</a> in the
+ User Guide.</p>
+
+ <ul>
+ <li>The file <samp>tutorial/repos/tutorial.dump</samp> should be loaded
+ into an empty repository using the <code>svnadmin load</code> command.</li>
+
+ <li>The hook scripts in <samp>tutorial/hook/</samp> should then be
+ installed in this repository in order to prevent any commits to the trunk.
+ Note that the configuration file <samp>svnperms.conf</samp> assumes that
+ the tutorial repository is called <samp>tutorial_svn</samp>. Please edit
+ this file if you use a different name.</li>
+
+ <li>The repository should be configured to allow users write access. You
+ may find it easiest to simply allow anonymous access.</li>
+
+ <li>A Trac system should be configured associated with the Tutorial
+ repository. You then need to allow users write access. You may find it
+ easiest to set up a number of guest accounts for this purpose.</li>
+ </ul>
+
+ <p>The <samp>templates/</samp> directory contains various example scripts
+ which you may find useful. Note that these scripts are all specific to the
+ Met Office and may contain hard coded paths and email addresses. They are
+ provided in the hope that you may find them useful as templates for setting
+ up similar scripts of your own. However, they should only be used after
+ careful review to adapt them to your environment. The contents are as
+ follows:</p>
+
+ <dl>
+ <dt>templates/hook/pre-commit</dt>
+
+ <dd>
+ This script restricts write-access to the repository by checking the
+ following:
+
+ <ul>
+ <li>It executes the Subversion utility <code>svnperms.py</code> if it
+ exists. This utility checks whether the author of the current
+ transaction has enough permission to write to particular paths in the
+ repository.</li>
+
+ <li>It checks the disk space required by the current transaction. It
+ fails the commit if it requires more than 5Mb of disk space.</li>
+ </ul>
+ </dd>
+
+ <dt>templates/hook/post-commit</dt>
+
+ <dd>A simple post-commit hook script which runs the script
+ <code>post-commit-background</code> in the background.</dd>
+
+ <dt>templates/hook/post-commit-background</dt>
+
+ <dd>
+ This script runs in the background after each commit
+
+ <ul>
+ <li>It updates a <samp><repos>.latest</samp> file with the latest
+ revision number.</li>
+
+ <li>It creates a dump of the new revision.</li>
+
+ <li>It calls the script <code>background_updates.pl</code> if it
+ exists.</li>
+ </ul>This script is installed as standard in all our repositories.
+ </dd>
+
+ <dt>templates/hook/background_updates.pl</dt>
+
+ <dd>An example of how you may want to set up a
+ <code>background_updates.pl</code> script to perform post-commit tasks for
+ a specific repository. This script uses a lock file to prevent multiple
+ commits in quick succession from causing problems.</dd>
+
+ <dt>templates/hook/pre-revprop-change</dt>
+
+ <dd>A simple pre-revprop-change hook script which runs the script
+ <code>pre-revprop-change.pl</code>.</dd>
+
+ <dt>templates/hook/pre-revprop-change.pl</dt>
+
+ <dd>If a user attempts to modify the log message of a changeset and he/she
+ is not the original author of the changeset, this script will e-mail the
+ original author. You can also set up a watch facility to monitor changes of
+ log messages that affect particular paths in the repository. For further
+ details please refer to the section <a href=
+ "../user_guide/system_admin.html#svn_watch">Watching changes in log
+ messages</a> in the System Admin chapter of the User Guide.</dd>
+
+ <dt>templates/hook/post-revprop-change</dt>
+
+ <dd>A simple post-revprop-change hook script which runs the script
+ <code>post-revprop-change.py</code>.</dd>
+
+ <dt>templates/hook/post-revprop-change.py</dt>
+
+ <dd>This hook script updates the Trac SQLite database following a
+ successful change in the log message.</dd>
+
+ <dt>templates/utils/cron_template.ksh</dt>
+
+ <dd>An example of how you might set up a cron job to make use of the
+ <samp><repos>.latest</samp> file.</dd>
+
+ <dt>templates/utils/daily_cron</dt>
+
+ <dd>The cron job which we run each night. It verifies and backs up each of
+ our repositories, housekeeps the revision dumps created by
+ <code>post-commit-background</code> and backs up each of our Trac systems.
+ It also handles the distribution of FCM to various platforms at the Met
+ Office.</dd>
+
+ <dt>templates/utils/fcm_add_trac.pl</dt>
+
+ <dd>This script sets up a new Trac system and applies some configuration
+ options which we use by default at the Met Office.</dd>
+
+ <dt>templates/utils/recover_svn.pl</dt>
+
+ <dd>This script allows us to recover all of our Subversion repositories by
+ using the nightly backups and the repository dumps.</dd>
+ </dl>
+
+ </div>
+ </div>
+ </div>
+
+ <hr/>
+ <div class="container-fluid text-center">
+ <div class="row-fluid"><div class="span12">
+ <address><small>
+ © British Crown Copyright 2006-14
+ <a href="http://www.metoffice.gov.uk">Met Office</a>.
+ See <a href="../etc/fcm-terms-of-use.html">Terms of Use</a>.<br />
+ This document is released under the British <a href=
+ "http://www.nationalarchives.gov.uk/doc/open-government-licence/" rel=
+ "license">Open Government Licence</a>.<br />
+ </small></address>
+ </div></div>
+ </div>
+
+ <script type="text/javascript" src="../etc/jquery.min.js"></script>
+ <script type="text/javascript" src="../etc/bootstrap/js/bootstrap.min.js"></script>
+ <script type="text/javascript" src="../etc/fcm.js"></script>
+ <script type="text/javascript" src="../etc/fcm-version.js"></script>
+</body>
+</html>
diff --git a/doc/release_notes/1-3.html b/doc/release_notes/1-3.html
new file mode 100644
index 0000000..f9dc901
--- /dev/null
+++ b/doc/release_notes/1-3.html
@@ -0,0 +1,585 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>FCM 1.3 Release Notes</title>
+ <meta name="author" content="FCM team" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link rel="icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link rel="shortcut icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link href="../etc/bootstrap/css/bootstrap.min.css" rel="stylesheet" media="screen" />
+ <link href="../etc/fcm.css" rel="stylesheet" media="screen" />
+</head>
+<body>
+ <div class="navbar navbar-inverse">
+ <div class="navbar-inner">
+ <a class="brand" href=".."><span class="fcm-version">FCM</span></a>
+ <ul class="nav">
+ <li><a href="../installation/">Installation</a></li>
+
+ <li><a href="../user_guide/">User Guide</a></li>
+ </ul>
+ </div>
+ </div>
+
+ <div class="page-header">
+ <div class="fcm-page-content pull-right well well-small"></div>
+ <h1>FCM 1.3 Release Notes <small>30 January 2008</small></h1>
+ </div>
+
+ <div class="container">
+ <div class="row">
+ <div class="span12">
+
+ <p>These are the release notes for FCM release 1.3. You can use this release
+ of FCM freely under the terms of the FCM LICENSE, which you should receive
+ with the distribution of this release.</p>
+
+ <p>Note that FCM now requires Subversion 1.4.0 or later (previous releases
+ only required Subversion 1.2.0).</p>
+
+ <p>FCM is maintained by the FCM team at the Met Office. Please feedback any
+ bug reports or feature requests to us by <a href=
+ "mailto:fcm-team at metoffice.gov.uk">e-mail</a>.</p>
+
+ <h2 id="new">What's New?</h2>
+
+ <p>Build and extract:</p>
+
+ <ul>
+ <li>The syntax for declaring the extract/build destinations are unified.
+ The <code>DEST</code> (or <code>DEST::ROOTDIR</code>) declaration should be
+ used to declare the root of the extract/build destination. The
+ <code>DIR::ROOT</code> declaration is deprecated.</li>
+
+ <li>Users can no longer declare sub-directories in a destination.
+ Declarations such as <code>DEST::CFGDIR</code>, <code>DEST::SRCDIR</code>,
+ etc are no longer supported.</li>
+
+ <li>In configuration files, words or fields in a <em>label</em> can now be
+ delimited by a slash (<code>/</code>) as well as a double colon
+ (<code>::</code>). To improve readability, the convention is to only use
+ slash as the delimiter when referring to package names.</li>
+
+ <li>In a <code>true</code> or <code>false</code> declaration in a
+ configuration file, the system will now accept the following values as
+ false: <samp>0</samp>, <samp><empty string></samp>,
+ <samp>false</samp>, <samp>no</samp> and <samp>off</samp>. (It used to
+ accept <samp><any non empty string></samp> for <code>true</code> and
+ <samp>0</samp>, or <samp><empty string></samp> for
+ <code>false</code>.)</li>
+
+ <li>The build and extract caches are now located separately in
+ <samp>.cache/.bld/</samp> and <samp>.cache/.ext/</samp> respectively. In
+ most cases they will be upgraded automatically when you perform the next
+ incremental build/extract. However, if you use inherited build/extract, you
+ must upgrade the inherited build/extract before they can be used. To
+ upgrade an inherited extract, issue the following commands:
+ <pre>
+cd /path/to/inherited/extract/
+cd .cache/
+mkdir .ext/
+cp -r -p .config * .ext/
+</pre>
+
+ <p>To upgrade an inherited build, issue the following commands:</p>
+ <pre>
+cd /path/to/inherited/build/
+fcm bld
+</pre>
+ </li>
+
+ <li>Extra/common stages in the build/extract processes: <em>parse
+ configuration</em> and <em>set up destination</em>.</li>
+
+ <li>The build/extract destination (and the remote destination for extract)
+ will now be printed in verbose mode 1 or above.</li>
+
+ <li>A new command line option <code>--clean</code> for removing files
+ generated by previous extracts/builds.</li>
+
+ <li>An as-parsed configuration file will be generated for each run (if the
+ file differs from the previous run).</li>
+ </ul>
+
+ <p>Build:</p>
+
+ <ul>
+ <li>It is now more efficient to make <code>SRC</code> declarations for
+ files instead of the container directories. Container directories can still
+ be declared, but they will be expanded by the configuration file parser
+ into a list of source files within it. In addition, <code>SRC</code>
+ declarations no longer require the specification of package names - if it
+ is a relative path of the src/ sub-directory. If a relative path is
+ specified, it will be assumed a relative path of the src/
+ sub-directory.</li>
+
+ <li>For declarations such as <code>EXCL_DEP, PP, TOOL</code>, if a package
+ name is associated with the declaration, the system will fail if the
+ package is not declared or defined.</li>
+
+ <li>The system will now detect changes in <code>EXCL_DEP, INFILE_EXT and
+ OUTFILE_EXT</code> declarations in an incremental build.</li>
+
+ <li>It is now possible to make PP declarations down to the file level.
+ (Previously, it could only be done down to the level of the container
+ directories.)</li>
+
+ <li>The package name for a file used to be its root name (i.e. its basename
+ without the extension). It is now its basename, (although the system will
+ continue to support declarations down to the file's root name).</li>
+
+ <li>The declarations <code>TYPE</code> and <code>DEP</code> can now be used
+ to define the type and dependencies of a source file. (This replaces most
+ functionalities of the package configuration file.)</li>
+
+ <li>The build package configuration file is no longer supported.</li>
+
+ <li>The system now generates a single <samp>Makefile</samp> in the root
+ location of the destination. Hard coded directories should appear only once
+ at the top of the Makefile, provided that source files are only located in
+ the <samp>src/</samp> sub-directory of the build destination. (A build used
+ to have a top level <samp>Makefile</samp>, which included a
+ <samp>*.mk</samp> file for each source directory in the <samp>bld/</samp>
+ sub-directory of the destination.)</li>
+
+ <li>The <samp>fcm_env.ksh</samp> file is renamed <samp>fcm_env.sh</samp>,
+ which should work with the Bourne shell. (<samp>fcm_env.ksh</samp> is still
+ available via a symbolic link but is deprecated.)</li>
+
+ <li>The <code>--archive</code> or <code>-a</code> option will now archive
+ your build directories using the command <code>tar -czf FILE DIR</code>.
+ Consequently, the system will dearchive them using <code>tar -xzf
+ FILE</code>. If you have been using this option in your previous builds,
+ you should extract the archives manually using <code>tar -xf FILE</code>
+ before running <code>fcm build</code> in incremental mode. Remove the old
+ TAR files on success.</li>
+
+ <li>The linker command now defaults to the compiler of the language of the
+ program source file. (The default used to be <code>ld</code>.)</li>
+ </ul>
+
+ <p>Code management commands:</p>
+
+ <ul>
+ <li>Allow other graphical merge tools to be used in place of
+ <em>xxdiff</em> by modifying <code>bin/fcm_graphic_merge</code>.</li>
+
+ <li><code>fcm commit</code> will issue extra warning when a user attempts
+ to commit to or remove a branch belonging to another user.</li>
+
+ <li>The system has been modified to account for the improved support for
+ peg revisions using the <code>svn log</code> command in Subversion 1.4.
+ Unfortunately this means that Subversion 1.4.x clients are now
+ required.</li>
+
+ <li><code>fcm diff</code> now supports the <code>--summarize</code> option
+ which was introduced in Subversion 1.4.</li>
+
+ <li>Added alternate branching strategy in the <a href=
+ "../collaboration/">External Distribution & Collaboration for FCM
+ Projects</a> document.</li>
+
+ <li>A number of limitations with the <code>fcm mkpatch</code> command have
+ been fixed. It will also use unified diffs where possible in order to
+ reduce the size of the patch and to make it more readable.</li>
+ </ul>
+
+ <p>Extract:</p>
+
+ <ul>
+ <li>The <code>ROOTDIR</code> part of the <code>RDEST::ROOTDIR</code>
+ declaration is now optional.</li>
+
+ <li>The <code>MIRROR</code> declaration is deprecated, and replaced by the
+ <code>RDEST::MIRROR_CMD</code> declaration.</li>
+
+ <li>The build configuration file generated by extract should no longer
+ contain hard coded paths - except for <code>USE</code> declarations and
+ those protected by the <code>BLD</code> prefix.</li>
+
+ <li>The <code>VERSION</code> declaration is deprecated, and replaced by the
+ <code>REVISION</code> declaration.</li>
+
+ <li>Added a new <code>REVMATCH</code> declaration for the extract
+ configuration file. If you specify a revision (other than HEAD) for a
+ branch, and this revision is not associated with a changeset for this
+ branch, the system will normally inform you of this discrepancy. By setting
+ <code>REVMATCH</code> to "true", however, the discrepancy will cause
+ extract to fail.</li>
+
+ <li>Extract used to fail if the same file is modified by two different
+ branches (compared with the base branch). It now attempts to merge the
+ changes using <code>diff3 -E -m</code>. It only fails if there are
+ unresolved conflicts.</li>
+
+ <li>Consequently, the <code>OVERRIDE</code> declaration is deprecated, and
+ replaced by the <code>CONFLICT</code> declaration, which can be set to
+ <samp>fail</samp>, <samp>merge</samp> (default) or
+ <samp>override</samp>.</li>
+ </ul>
+
+ <h2 id="fix">Minor enhancements & Bug Fixes</h2>
+
+ <p>Build:</p>
+
+ <ul>
+ <li>If there is no source file to build, report an error at the beginning
+ of the build process.</li>
+ </ul>
+
+ <p>Code management commands:</p>
+
+ <ul>
+ <li>Fixed: FCM URL keyword not expanded when it is specified with an equal
+ sign in an option.</li>
+
+ <li>Fixed: typo in the output of <code>fcm branch --info</code>.</li>
+ </ul>
+
+ <p>Extract:</p>
+
+ <ul>
+ <li>Improved logic for better performance.</li>
+
+ <li>Allow mirroring to use <code>rsync</code> with an alternate remote
+ shell.</li>
+
+ <li><code>fcm cmp-ext-cfg</code>: Improved support for <a href=
+ "http://trac.edgewall.org/wiki/InterTrac">InterTrac</a> links.</li>
+ </ul>
+
+ <p>General:</p>
+
+ <ul>
+ <li>Fixed: problems parsing Subversion peg revision with the FCM URL
+ keywords.</li>
+
+ <li>The general shell used by FCM is changed from <code>/usr/bin/ksh</code>
+ to <code>/bin/sh</code> to improve portability.</li>
+
+ <li>Various other very minor enhancements and bug fixes.</li>
+ </ul>
+
+ <h2 id="issues">Known Issues</h2>
+
+ <p>The following are known issues with this release of FCM which we plan to
+ address in later releases:</p>
+
+ <ul>
+ <li>FCM build does not handle changes in an include file correctly in an
+ inherited build if the include file resides in the same directory as the
+ source file including it, and the source file remains unchanged. This is
+ due to the fact that most pre-processor/compiler commands search the
+ directory containing the source file for include files first before they
+ search elsewhere. We are hoping to find a solution to this problem before
+ the next release.</li>
+ </ul>
+
+ <h2 id="req">System Requirements</h2>
+
+ <h3 id="req_perl">Perl</h3>
+
+ <p>The core part of FCM is a set of Perl scripts and modules. For the build
+ system to work, you need the following modules installed:</p>
+
+ <ul>
+ <li>Carp</li>
+
+ <li>Cwd</li>
+
+ <li>File::Basename</li>
+
+ <li>File::Compare</li>
+
+ <li>File::Find</li>
+
+ <li>File::Path</li>
+
+ <li>File::Spec::Functions</li>
+
+ <li>File::Spec</li>
+
+ <li>FindBin</li>
+
+ <li>Getopt::Long</li>
+
+ <li>POSIX</li>
+ </ul>
+
+ <p>The code management commands and extract system need the following
+ additional modules installed:</p>
+
+ <ul>
+ <li>File::Temp</li>
+
+ <li>Getopt::Long</li>
+
+ <li>HTTP::Date</li>
+
+ <li>XML::DOM</li>
+ </ul>
+
+ <p>To use the simple GUI for some of the code management commands, you also
+ need the following modules:</p>
+
+ <ul>
+ <li>Tk::ROText</li>
+
+ <li>Tk</li>
+ </ul>
+
+ <p>At the Met Office we are currently using the complete FCM system with Perl
+ 5.8.x. In addition the build system is being used with Perl 5.6.x.</p>
+
+ <h3 id="req_svn">Subversion</h3>
+
+ <p>To use the code management commands (and relevant parts of the extract
+ system) you need to have <a href=
+ "http://subversion.tigris.org/">Subversion</a> installed.</p>
+
+ <ul>
+ <li>FCM makes extensive use of peg revisions in both the code management
+ and extract systems. This requires Subversion 1.4.0.</li>
+
+ <li>At the Met Office we are currently using Subversion 1.4.3.</li>
+ </ul>
+
+ <p>Note that the extract system can mirror extracted code to a remote
+ platform for building. Therefore it is only necessary to have Subversion
+ installed on the platform where you do your code development. If you use
+ other platforms purely for building and running then you do not need to have
+ Subversion installed on these platforms.</p>
+
+ <h3 id="req_trac">Trac</h3>
+
+ <p>The use of <a href="http://trac.edgewall.org/">Trac</a> is entirely
+ optional (although highly recommended if you are using Subversion).</p>
+
+ <ul>
+ <li>The <code>--trac</code> and <code>--wiki</code> options to the
+ <code>fcm diff --branch</code> command allow you to view branch differences
+ using Trac. This requires Trac 0.10.</li>
+
+ <li>At the Met Office we are currently using Trac 0.10.3.</li>
+ </ul>
+
+ <h3 id="req_other">Other Requirements</h3>
+
+ <p>The <code>fcm conflicts</code> command requires <a href=
+ "http://furius.ca/xxdiff/">xxdiff</a>. At the Met Office we are currently
+ using version 3.1. The <code>fcm diff --graphical</code> command also uses
+ xxdiff by default although other graphical diff tools can also be used.</p>
+
+ <p>The extract system can use diff3, which is part of <a href=
+ "http://www.gnu.org/software/diffutils/">GNU diffutils</a>, to merge together
+ changes where the same file is modified by two different branches (compared
+ with the base branch). At the Met Office we are currently using version
+ 2.8.1.</p>
+
+ <p>The build system requires <a href=
+ "http://www.gnu.org/software/make/make.html">GNU make</a>. At the Met Office
+ we are currently using version 3.79.x and 3.80.</p>
+
+ <p>Optionally, the build system can use <a href=
+ "http://www.ifremer.fr/ditigo/molagnon/fortran90/">f90aib</a> to generate
+ interface files. However, there is also a built in Perl based interface file
+ generator which is quicker and better in most cases so you are unlikely to
+ need f90aib unless you hit a problem with some particular code.</p>
+
+ <p>FCM is intended to run on a Unix/Linux system. It is currently used at the
+ Met Office on Linux (Red Hat Enterprise 2.1 and 4.5) and HP-UX 11.00.</p>
+
+ <h2 id="ins">Installation</h2>
+
+ <p>FCM is distributed in the form of a compressed tar file. Un-pack the tar
+ file into an appropriate location on your system. Then add the
+ <samp>bin/</samp> directory into your <var>PATH</var>. Once you have done
+ this you should now have full access to the FCM system, assuming that you
+ have met the requirements described in the previous section.</p>
+
+ <p>If you wish to define keywords for your systems you will need to create a
+ file <samp>etc/fcm.cfg</samp>. An example file, <samp>fcm.cfg.eg</samp>, is
+ provided which is a copy of the file currently used at the Met Office. For
+ further details please refer to the section <a href=
+ "../user_guide/system_admin.html#fcm-keywords">FCM keywords</a> in the System
+ Admin chapter of the User Guide.</p>
+
+ <p>The <samp>doc/</samp> directory contains all the system documentation.</p>
+
+ <ul>
+ <li><samp>doc/release_notes/</samp> contains these release notes. It also
+ contains the release notes for all previous versions which may be useful if
+ you have skipped any versions.</li>
+
+ <li><samp>doc/user_guide/</samp> contains the <a href="../user_guide/">FCM
+ User Guide</a>.</li>
+
+ <li><samp>doc/design/</samp> contains the <a href="../design/">FCM Detailed
+ Design</a> document (currently in draft form).</li>
+
+ <li><samp>doc/standards/</samp> contains the FCM <a href=
+ "../standards/perl_standard.html">Perl</a> and <a href=
+ "../standards/fortran_standard.html">Fortran</a> coding standards. The Perl
+ standard describes the standards followed by the FCM code. The Fortran
+ standard contains some <a href=
+ "../standards/fortran_standard.html#fcm">specific advice</a> on the best
+ way of writing Fortran code for use with FCM as well as more general advice
+ on good practise.</li>
+
+ <li><samp>doc/collaboration/</samp> contains the <a href=
+ "../collaboration/index.html">External Distribution & Collaboration for
+ FCM Projects</a> document which discusses how projects configured under FCM
+ can be distributed externally.</li>
+ </ul>
+
+ <p>The <samp>tutorial/</samp> directory contains the files necessary to set
+ up a tutorial repository. This will allow you to follow the <a href=
+ "../user_guide/getting_started.html#tutorial">tutorial section</a> in the
+ User Guide.</p>
+
+ <ul>
+ <li>The file <samp>tutorial/repos/tutorial.dump</samp> should be loaded
+ into an empty repository using the <code>svnadmin load</code> command.</li>
+
+ <li>The hook scripts in <samp>tutorial/hook/</samp> should then be
+ installed in this repository in order to prevent any commits to the trunk.
+ Note that the configuration file <samp>svnperms.conf</samp> assumes that
+ the tutorial repository is called <samp>tutorial_svn</samp>. Please edit
+ this file if you use a different name. You also need to install the
+ Subversion utility <code>svnperms.py</code> in order for this to work.</li>
+
+ <li>The repository should be configured to allow users write access. You
+ may find it easiest to simply allow anonymous access.</li>
+
+ <li>A Trac system should be configured associated with the Tutorial
+ repository. You then need to allow users write access. You may find it
+ easiest to set up a number of guest accounts for this purpose.</li>
+ </ul>
+
+ <p>The <samp>templates/</samp> directory contains various example scripts
+ which you may find useful. Note that these scripts are all specific to the
+ Met Office and may contain hard coded paths and email addresses. They are
+ provided in the hope that you may find them useful as templates for setting
+ up similar scripts of your own. However, they should only be used after
+ careful review to adapt them to your environment. The contents are as
+ follows:</p>
+
+ <dl>
+ <dt>templates/hook/pre-commit</dt>
+
+ <dd>
+ This script restricts write-access to the repository by checking the
+ following:
+
+ <ul>
+ <li>It executes the Subversion utility <code>svnperms.py</code> if it,
+ and the associated <code>svnperms.conf</code> file, exist. This utility
+ checks whether the author of the current transaction has enough
+ permission to write to particular paths in the repository.</li>
+
+ <li>It checks the disk space required by the current transaction. It
+ fails the commit if it requires more than 5Mb of disk space.</li>
+ </ul>
+ </dd>
+
+ <dt>templates/hook/post-commit</dt>
+
+ <dd>A simple post-commit hook script which runs the script
+ <code>post-commit-background</code> in the background.</dd>
+
+ <dt>templates/hook/post-commit-background</dt>
+
+ <dd>
+ This script runs in the background after each commit
+
+ <ul>
+ <li>It updates a <code><repos>.latest</code> file with the latest
+ revision number.</li>
+
+ <li>It creates a dump of the new revision.</li>
+
+ <li>It calls the script <code>background_updates.pl</code> if it
+ exists.</li>
+ </ul>This script is installed as standard in all our repositories.
+ </dd>
+
+ <dt>templates/hook/background_updates.pl</dt>
+
+ <dd>An example of how you may want to set up a
+ <code>background_updates.pl</code> script to perform post-commit tasks for
+ a specific repository. This script uses a lock file to prevent multiple
+ commits in quick succession from causing problems.</dd>
+
+ <dt>templates/hook/pre-revprop-change</dt>
+
+ <dd>A simple pre-revprop-change hook script which runs the script
+ <code>pre-revprop-change.pl</code>.</dd>
+
+ <dt>templates/hook/pre-revprop-change.pl</dt>
+
+ <dd>If a user attempts to modify the log message of a changeset and he/she
+ is not the original author of the changeset, this script will e-mail the
+ original author. You can also set up a watch facility to monitor changes of
+ log messages that affect particular paths in the repository. For further
+ details please refer to the section <a href=
+ "../user_guide/system_admin.html#svn_watch">Watching changes in log
+ messages</a> in the System Admin chapter of the User Guide.</dd>
+
+ <dt>templates/hook/post-revprop-change</dt>
+
+ <dd>A simple post-revprop-change hook script which runs the script
+ <code>post-revprop-change.py</code>.</dd>
+
+ <dt>templates/hook/post-revprop-change.py</dt>
+
+ <dd>This hook script updates the Trac SQLite database following a
+ successful change in the log message.</dd>
+
+ <dt>templates/utils/cron_template.sh</dt>
+
+ <dd>An example of how you might set up a cron job to make use of the
+ <samp><repos>.latest</samp> file.</dd>
+
+ <dt>templates/utils/daily_cron</dt>
+
+ <dd>The cron job which we run each night. It verifies and backs up each of
+ our repositories, housekeeps the revision dumps created by
+ <code>post-commit-background</code> and backs up each of our Trac systems.
+ It also handles the distribution of FCM to various platforms at the Met
+ Office.</dd>
+
+ <dt>templates/utils/fcm_add_trac.pl</dt>
+
+ <dd>This script sets up a new Trac system and applies some configuration
+ options which we use by default at the Met Office.</dd>
+
+ <dt>templates/utils/recover_svn.pl</dt>
+
+ <dd>This script allows us to recover all of our Subversion repositories by
+ using the nightly backups and the repository dumps.</dd>
+ </dl>
+
+ </div>
+ </div>
+ </div>
+
+ <hr/>
+ <div class="container-fluid text-center">
+ <div class="row-fluid"><div class="span12">
+ <address><small>
+ © British Crown Copyright 2006-14
+ <a href="http://www.metoffice.gov.uk">Met Office</a>.
+ See <a href="../etc/fcm-terms-of-use.html">Terms of Use</a>.<br />
+ This document is released under the British <a href=
+ "http://www.nationalarchives.gov.uk/doc/open-government-licence/" rel=
+ "license">Open Government Licence</a>.<br />
+ </small></address>
+ </div></div>
+ </div>
+
+ <script type="text/javascript" src="../etc/jquery.min.js"></script>
+ <script type="text/javascript" src="../etc/bootstrap/js/bootstrap.min.js"></script>
+ <script type="text/javascript" src="../etc/fcm.js"></script>
+ <script type="text/javascript" src="../etc/fcm-version.js"></script>
+</body>
+</html>
diff --git a/doc/release_notes/1-4.html b/doc/release_notes/1-4.html
new file mode 100644
index 0000000..7616f76
--- /dev/null
+++ b/doc/release_notes/1-4.html
@@ -0,0 +1,395 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>FCM 1.4 Release Notes</title>
+ <meta name="author" content="FCM team" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link rel="icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link rel="shortcut icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link href="../etc/bootstrap/css/bootstrap.min.css" rel="stylesheet" media="screen" />
+ <link href="../etc/fcm.css" rel="stylesheet" media="screen" />
+</head>
+<body>
+ <div class="navbar navbar-inverse">
+ <div class="navbar-inner">
+ <a class="brand" href=".."><span class="fcm-version">FCM</span></a>
+ <ul class="nav">
+ <li><a href="../installation/">Installation</a></li>
+
+ <li><a href="../user_guide/">User Guide</a></li>
+ </ul>
+ </div>
+ </div>
+
+ <div class="page-header">
+ <div class="fcm-page-content pull-right well well-small"></div>
+ <h1>FCM 1.4 Release Notes <small>12 February 2009</small></h1>
+ </div>
+
+ <div class="container">
+ <div class="row">
+ <div class="span12">
+
+ <p>These are the release notes for FCM release 1.4. You can use this release
+ of FCM freely under the terms of the FCM LICENSE, which you should receive
+ with the distribution of this release.</p>
+
+ <p>Note that FCM 1.4 requires Subversion 1.4.x (but it has not been tested on
+ Subversion 1.5.x or above).</p>
+
+ <p>FCM is maintained by the FCM team at the Met Office. Please feedback any
+ bug reports or feature requests to us by <a href=
+ "mailto:fcm-team at metoffice.gov.uk">e-mail</a>.</p>
+
+ <h2 id="fix">Minor enhancements & Bug Fixes</h2>
+
+ <p>Build:</p>
+
+ <ul>
+ <li>Fixed: ensure consistent behaviour for specifying <code>-D</code>,
+ <code>-I</code>, etc options for a preprocessor/compiler.</li>
+ </ul>
+
+ <p>Code management commands:</p>
+
+ <ul>
+ <li>Fixed: <code>fcm mkpatch</code>: Fix how property changes are
+ handled.</li>
+
+ <li>Fixed: <code>fcm mkpatch</code>: Fix how copied files and directories
+ are handled.</li>
+
+ <li>Fixed: <code>fcm mkpatch</code>: Prevent failures caused by the use of
+ Subversion keywords.</li>
+
+ <li>Fixed: <code>fcm mkpatch</code>: Prevent failures when used with
+ branches which do not follow the FCM naming convention.</li>
+ </ul>
+
+ <p>Extract:</p>
+
+ <ul>
+ <li>Allow extract config to define an alternate remote shell for
+ <code>rsync</code>.</li>
+ </ul>
+
+ <p>General:</p>
+
+ <ul>
+ <li>Some of the example scripts in the <samp>templates/</samp> directory
+ have been rewritten.</li>
+
+ <li>Various other very minor enhancements and bug fixes.</li>
+ </ul>
+
+ <h2 id="issues">Known Issues</h2>
+
+ <p>The following are known issues with this release of FCM which we plan to
+ address in later releases:</p>
+
+ <ul>
+ <li>FCM build does not handle changes in an include file correctly in an
+ inherited build if the include file resides in the same directory as the
+ source file including it, and the source file remains unchanged. This is
+ due to the fact that most pre-processor/compiler commands search the
+ directory containing the source file for include files first before they
+ search elsewhere.</li>
+ </ul>
+
+ <h2 id="req">System Requirements</h2>
+
+ <h3 id="req_perl">Perl</h3>
+
+ <p>The core part of FCM is a set of Perl scripts and modules. For the build
+ system to work, you need the following modules installed:</p>
+
+ <ul>
+ <li>Carp</li>
+
+ <li>Cwd</li>
+
+ <li>File::Basename</li>
+
+ <li>File::Compare</li>
+
+ <li>File::Find</li>
+
+ <li>File::Path</li>
+
+ <li>File::Spec::Functions</li>
+
+ <li>File::Spec</li>
+
+ <li>FindBin</li>
+
+ <li>Getopt::Long</li>
+
+ <li>POSIX</li>
+ </ul>
+
+ <p>The code management commands and extract system need the following
+ additional modules installed:</p>
+
+ <ul>
+ <li>File::Temp</li>
+
+ <li>Getopt::Long</li>
+
+ <li>HTTP::Date</li>
+
+ <li>XML::DOM</li>
+ </ul>
+
+ <p>To use the simple GUI for some of the code management commands, you also
+ need the following modules:</p>
+
+ <ul>
+ <li>Tk::ROText</li>
+
+ <li>Tk</li>
+ </ul>
+
+ <p>At the Met Office we are currently using the complete FCM system with Perl
+ 5.8.x. In addition the build system is being used with Perl 5.6.x.</p>
+
+ <h3 id="req_svn">Subversion</h3>
+
+ <p>To use the code management commands (and relevant parts of the extract
+ system) you need to have <a href=
+ "http://subversion.tigris.org/">Subversion</a> installed.</p>
+
+ <ul>
+ <li>FCM makes extensive use of peg revisions in both the code management
+ and extract systems. This requires Subversion 1.4.0.</li>
+
+ <li>At the Met Office we are currently using Subversion 1.4.3.</li>
+ </ul>
+
+ <p>Note that the extract system can mirror extracted code to a remote
+ platform for building. Therefore it is only necessary to have Subversion
+ installed on the platform where you do your code development. If you use
+ other platforms purely for building and running then you do not need to have
+ Subversion installed on these platforms.</p>
+
+ <h3 id="req_trac">Trac</h3>
+
+ <p>The use of <a href="http://trac.edgewall.org/">Trac</a> is entirely
+ optional (although highly recommended if you are using Subversion).</p>
+
+ <ul>
+ <li>The <code>--trac</code> and <code>--wiki</code> options to the
+ <code>fcm diff --branch</code> command allow you to view branch differences
+ using Trac. This requires Trac 0.10.</li>
+
+ <li>Some of the example scripts in the <samp>templates/</samp> directory
+ require Trac 0.11.</li>
+
+ <li>At the Met Office we are currently using Trac 0.11.2.1.</li>
+ </ul>
+
+ <h3 id="req_other">Other Requirements</h3>
+
+ <p>The <code>fcm conflicts</code> command requires <a href=
+ "http://furius.ca/xxdiff/">xxdiff</a>. At the Met Office we are currently
+ using version 3.1. The <code>fcm diff --graphical</code> command also uses
+ xxdiff by default although other graphical diff tools can also be used.</p>
+
+ <p>The extract system can use diff3, which is part of <a href=
+ "http://www.gnu.org/software/diffutils/">GNU diffutils</a>, to merge together
+ changes where the same file is modified by two different branches (compared
+ with the base branch). At the Met Office we are currently using version
+ 2.8.1.</p>
+
+ <p>The build system requires <a href=
+ "http://www.gnu.org/software/make/make.html">GNU make</a>. At the Met Office
+ we are currently using version 3.79.x and 3.80.</p>
+
+ <p>Optionally, the build system can use <a href=
+ "http://www.ifremer.fr/ditigo/molagnon/fortran90/">f90aib</a> to generate
+ interface files. However, there is also a built in Perl based interface file
+ generator which is quicker and better in most cases so you are unlikely to
+ need f90aib unless you hit a problem with some particular code.</p>
+
+ <p>FCM is intended to run on a Unix/Linux system. It is currently used at the
+ Met Office on Linux (Red Hat Enterprise 2.1 and 4.6) and HP-UX 11.00.</p>
+
+ <h2 id="ins">Installation</h2>
+
+ <p>FCM is distributed in the form of a compressed tar file. Un-pack the tar
+ file into an appropriate location on your system. Then add the
+ <samp>bin/</samp> directory into your <var>PATH</var>. Once you have done
+ this you should now have full access to the FCM system, assuming that you
+ have met the requirements described in the previous section.</p>
+
+ <p>If you wish to define keywords for your systems you will need to create a
+ file <samp>etc/fcm.cfg</samp>. An example file, <samp>fcm.cfg.eg</samp>, is
+ provided which is a copy of the file currently used at the Met Office. For
+ further details please refer to the section <a href=
+ "../user_guide/system_admin.html#fcm-keywords">FCM keywords</a> in the System
+ Admin chapter of the User Guide.</p>
+
+ <p>The <samp>doc/</samp> directory contains all the system documentation.</p>
+
+ <ul>
+ <li><samp>doc/release_notes/</samp> contains these release notes. It also
+ contains the release notes for all previous versions which may be useful if
+ you have skipped any versions.</li>
+
+ <li><samp>doc/user_guide/</samp> contains the <a href="../user_guide/">FCM
+ User Guide</a>.</li>
+
+ <li><samp>doc/design/</samp> contains the <a href="../design/">FCM Detailed
+ Design</a> document (currently in draft form).</li>
+
+ <li><samp>doc/standards/</samp> contains the FCM <a href=
+ "../standards/perl_standard.html">Perl</a> and <a href=
+ "../standards/fortran_standard.html">Fortran</a> coding standards. The Perl
+ standard describes the standards followed by the FCM code. The Fortran
+ standard contains some <a href=
+ "../standards/fortran_standard.html#fcm">specific advice</a> on the best
+ way of writing Fortran code for use with FCM as well as more general advice
+ on good practise.</li>
+
+ <li><samp>doc/collaboration/</samp> contains the <a href=
+ "../collaboration/index.html">External Distribution & Collaboration for
+ FCM Projects</a> document which discusses how projects configured under FCM
+ can be distributed externally.</li>
+ </ul>
+
+ <p>The <samp>tutorial/</samp> directory contains the files necessary to set
+ up a tutorial repository. This will allow you to follow the <a href=
+ "../user_guide/getting_started.html#tutorial">tutorial section</a> in the
+ User Guide.</p>
+
+ <ul>
+ <li>The file <samp>tutorial/svn.dump</samp> should be loaded into an empty
+ repository using the <code>svnadmin load</code> command.</li>
+
+ <li>The hook scripts in <samp>tutorial/hooks/</samp> should then be
+ installed in this repository in order to prevent any commits to the trunk.
+ Note that the configuration file <code>svnperms.conf</code> assumes that
+ the tutorial repository is called <samp>tutorial_svn</samp>. Please edit
+ this file if you use a different name.</li>
+
+ <li>The repository should be configured to allow users write access. You
+ may find it easiest to simply allow anonymous access.</li>
+
+ <li>A Trac system should be configured associated with the Tutorial
+ repository. You then need to allow users write access. You may find it
+ easiest to set up a number of guest accounts for this purpose.</li>
+ </ul>
+
+ <p>The <samp>templates/</samp> directory contains various example scripts
+ which you may find useful. Note that these scripts are all specific to the
+ Met Office and may contain hard coded paths and email addresses. They are
+ provided in the hope that you may find them useful as templates for setting
+ up similar scripts of your own. However, they should only be used after
+ careful review to adapt them to your environment. The contents are as
+ follows:</p>
+
+ <dl>
+ <dt>templates/hooks/pre-commit</dt>
+
+ <dd>
+ This script restricts write-access to the repository by checking the
+ following:
+
+ <ul>
+ <li>It executes the Subversion utility <code>svnperms.py</code> if it,
+ and the associated <samp>svnperms.conf</samp> file, exist. This utility
+ checks whether the author of the current transaction has enough
+ permission to write to particular paths in the repository.</li>
+
+ <li>It checks the disk space required by the current transaction. It
+ fails the commit if it requires more than 5Mb of disk space.</li>
+ </ul>
+ </dd>
+
+ <dt>templates/hooks/post-commit</dt>
+
+ <dd>A simple post-commit hook script which runs the script
+ <code>post-commit-background</code> in the background.</dd>
+
+ <dt>templates/hooks/post-commit-background</dt>
+
+ <dd>
+ This script runs in the background after each commit
+
+ <ul>
+ <li>It updates a <samp><repos>.latest</samp> file with the latest
+ revision number.</li>
+
+ <li>It creates a dump of the new revision.</li>
+
+ <li>It calls the script <code>background_updates.pl</code> if it
+ exists.</li>
+ </ul>This script is installed as standard in all our repositories.
+ </dd>
+
+ <dt>templates/hooks/background_updates.pl</dt>
+
+ <dd>An example of how you may want to set up a
+ <code>background_updates.pl</code> script to perform post-commit tasks for
+ a specific repository. This script uses a lock file to prevent multiple
+ commits in quick succession from causing problems.</dd>
+
+ <dt>templates/hooks/pre-revprop-change</dt>
+
+ <dd>A simple pre-revprop-change hook script which runs the script
+ <code>pre-revprop-change.pl</code>.</dd>
+
+ <dt>templates/hooks/pre-revprop-change.pl</dt>
+
+ <dd>If a user attempts to modify the log message of a changeset and he/she
+ is not the original author of the changeset, this script will e-mail the
+ original author. You can also set up a watch facility to monitor changes of
+ log messages that affect particular paths in the repository. For further
+ details please refer to the section <a href=
+ "../user_guide/system_admin.html#svn_watch">Watching changes in log
+ messages</a> in the System Admin chapter of the User Guide.</dd>
+
+ <dt>templates/hooks/post-revprop-change</dt>
+
+ <dd>A simple post-revprop-change hook script which invokes the
+ <code>trac-admin</code> command to <code>resync</code> the revision
+ property cache stored in the corresponding Trac environment.</dd>
+
+ <dt>templates/utils/cron_template.sh</dt>
+
+ <dd>An example of how you might set up a cron job to make use of the
+ <samp><repos>.latest</samp> file.</dd>
+
+ <dt>templates/utils/FCM/Admin/</dt>
+
+ <dd>A Perl library in the <code>FCM::Admin::*</code> name space, which
+ implements the functionalities of the FCM admin utility commands.</dd>
+
+ <dt>templates/utils/fcm-*</dt>
+
+ <dd>A selection of useful FCM admin utility commands.</dd>
+ </dl>
+
+ </div>
+ </div>
+ </div>
+
+ <hr/>
+ <div class="container-fluid text-center">
+ <div class="row-fluid"><div class="span12">
+ <address><small>
+ © British Crown Copyright 2006-14
+ <a href="http://www.metoffice.gov.uk">Met Office</a>.
+ See <a href="../etc/fcm-terms-of-use.html">Terms of Use</a>.<br />
+ This document is released under the British <a href=
+ "http://www.nationalarchives.gov.uk/doc/open-government-licence/" rel=
+ "license">Open Government Licence</a>.<br />
+ </small></address>
+ </div></div>
+ </div>
+
+ <script type="text/javascript" src="../etc/jquery.min.js"></script>
+ <script type="text/javascript" src="../etc/bootstrap/js/bootstrap.min.js"></script>
+ <script type="text/javascript" src="../etc/fcm.js"></script>
+ <script type="text/javascript" src="../etc/fcm-version.js"></script>
+</body>
+</html>
diff --git a/doc/release_notes/1-5.html b/doc/release_notes/1-5.html
new file mode 100644
index 0000000..b7de61b
--- /dev/null
+++ b/doc/release_notes/1-5.html
@@ -0,0 +1,483 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>FCM 1.5 Release Notes</title>
+ <meta name="author" content="FCM team" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link rel="icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link rel="shortcut icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link href="../etc/bootstrap/css/bootstrap.min.css" rel="stylesheet" media="screen" />
+ <link href="../etc/fcm.css" rel="stylesheet" media="screen" />
+</head>
+<body>
+ <div class="navbar navbar-inverse">
+ <div class="navbar-inner">
+ <a class="brand" href=".."><span class="fcm-version">FCM</span></a>
+ <ul class="nav">
+ <li><a href="../installation/">Installation</a></li>
+
+ <li><a href="../user_guide/">User Guide</a></li>
+ </ul>
+ </div>
+ </div>
+
+ <div class="page-header">
+ <div class="fcm-page-content pull-right well well-small"></div>
+ <h1>FCM 1.5 Release Notes <small>22 January 2010</small></h1>
+ </div>
+
+ <div class="container">
+ <div class="row">
+ <div class="span12">
+
+ <p>These are the release notes for FCM release 1.5. You can use this release
+ of FCM freely under the terms of the FCM LICENSE, which you should receive
+ with the distribution of this release.</p>
+
+ <p>FCM is maintained by the FCM team at the Met Office. Please feedback any
+ bug reports or feature requests to us by <a href=
+ "mailto:fcm-team at metoffice.gov.uk">e-mail</a>.</p>
+
+ <h2 id="new">What's New?</h2>
+
+ <dl>
+ <dt><code>fcm branch --list --show-all</code></dt>
+
+ <dd>New option to list all branches in a project.</dd>
+
+ <dt><code>fcm keyword-print</code>: new command</dt>
+
+ <dd>A command to print registered FCM keywords.</dd>
+
+ <dt>New method to manage revision keywords and other keywords related
+ settings.</dt>
+
+ <dd>See <a href="../user_guide/system_admin.html#fcm-keywords">FCM User
+ Guide > System Administration > FCM keywords</a> for detail. Note:
+ the <code>SET::TRAC</code> declaration is no longer supported.</dd>
+
+ <dt><code>fcm update</code>: improvement</dt>
+
+ <dd>The <code>fcm update</code> command applies to a whole working copy. If
+ the working copy contains local changes, the command will prompt the user
+ for confirmation.</dd>
+ </dl>
+
+ <h2 id="fix">Minor Enhancements & Bug Fixes</h2>
+
+ <dl>
+ <dt><code>fcm build</code>: new <code>NO_DEP</code> declaration to switch
+ off dependency checking</dt>
+
+ <dd>A new declaration to switch off dependency checking for a given
+ name-space. See the <a href=
+ "../user_guide/build.html#advanced_dependency">FCM User Guide > The
+ Build System > Further dependency features</a> for details.</dd>
+
+ <dt><code>fcm build</code>: incorrect behaviour when dealing with the
+ removal of TOOL declarations in incremental mode</dt>
+
+ <dd><code>fcm build</code> did not always handle the removal of TOOL
+ declarations from the build configuration correctly in incremental mode.
+ This has been fixed.</dd>
+
+ <dt><code>fcm build</code>: new <code>TOOL::FC_MODSEARCH</code>
+ declaration</dt>
+
+ <dd>While most Fortran compilers search for the compiled module definition
+ files (i.e. <samp>*.mod</samp> files) using the same option as the include
+ search path (i.e. <samp>-I</samp>), some require a special option such as
+ <samp>-M</samp>. The new <code>TOOL::FC_MODSEARCH</code> declaration allows
+ such an option to be specified in the build configuration file.</dd>
+
+ <dt><code>fcm build</code>: incorrect logic for handling
+ <code>INHERIT::SRC</code> declarations</dt>
+
+ <dd>The logic for handling this declaration was incorrect. This led to
+ deleted files being incorrectly inherited. This has been fixed.</dd>
+
+ <dt><code>fcm build</code>: incorrect logic for generating exclude
+ dependency files for directory-based libraries</dt>
+
+ <dd>These files were not being generated corectly. This has been fixed.</dd>
+
+ <dt><code>fcm build</code>: incorrect logic for handling
+ <code>SRC_TYPE</code></dt>
+
+ <dd>The system was unable to search for an include file whose type was
+ declared via a <code>SRC_TYPE</code> declaration. This has been fixed.</dd>
+
+ <dt><code>fcm build</code>: improvement to the Fortran interface file
+ generator</dt>
+
+ <dd>The logic to extract the calling interfaces of top level subroutines
+ and functions from Fortran source files has been rewritten, based on the
+ original logic developed by the <a href="http://www.ecmwf.int">European
+ Centre for Medium-Range Weather Forecasts (ECMWF)</a>. In particular, the
+ new logic will correctly handle 1) pre-processor directives with
+ continuation lines, 2) continuation and comment markers in quotes, 3)
+ BLOCKDATA program units in the source file, 4) TYPE components in variable
+ identifiers, and 5) multiple program units in the source file. There are
+ also improvements in the new logic to reduce the number of useless
+ declarations and module imports in the generated interface block.</dd>
+
+ <dt><code>fcm commit</code>: improvement to the commit message
+ delimiter</dt>
+
+ <dd>Some users found the old delimiter line confusing. This has been
+ improved.</dd>
+
+ <dt><code>fcm commit</code>: <code>svn:special</code> and
+ <code>svn:executable</code></dt>
+
+ <dd>A symbolic link pointing to an executable target can cause a subsequent
+ <code>svn checkout</code> to fail if the target is removed. To avoid the
+ potential problem, <code>fcm commit</code> has been altered to remove the
+ <code>svn:executable</code> property if a path is a symbolic link.</dd>
+
+ <dt><code>fcm extract</code>: handling of file permission changes in
+ incremental mode</dt>
+
+ <dd><code>fcm extract</code> did not handle file permission changes in
+ incremental mode. This has been fixed.</dd>
+
+ <dt><code>fcm extract</code>: handling of symbolic links</dt>
+
+ <dd>Symbolic links cannot be handled safely by <code>fcm extract</code>.
+ They are now removed from the extract.</dd>
+
+ <dt><code>fcm extract</code> and <code>fcm build</code>: machine
+ hostname</dt>
+
+ <dd>The machine hostname will now be printed with the destination in the
+ diagnostic output of these commands.</dd>
+
+ <dt><code>fcm extract</code>: missing <code>RDEST</code> in the on-success
+ configuration file</dt>
+
+ <dd>Some <code>RDEST</code> declarations were missing from the on-success
+ generated configuration file. This has been fixed.</dd>
+
+ <dt><code>fcm extract</code>: improved options for the mirror
+ sub-system</dt>
+
+ <dd>It is now possible to specify the options of the <code>rsync</code>
+ command in the extract configuration file. In addition, <code>ssh</code> is
+ now the default remote shell command.</dd>
+
+ <dt><code>fcm cmp-ext-cfg</code>: changed verbose option</dt>
+
+ <dd>The <code>--verbose</code> option now requires an argument.</dd>
+ </dl>
+
+ <h2 id="issues">Known Issues</h2>
+
+ <dl>
+ <dt>Build inheritance limitation: handling of include files</dt>
+
+ <dd>See the <a href="../user_guide/build.html#advanced_inherit">FCM User
+ Guide > The Build System > Inherit from a previous build</a> for
+ detail.</dd>
+ </dl>
+
+ <h2 id="req">System Requirements</h2>
+
+ <h3 id="req_perl">Perl</h3>
+
+ <p>The core part of FCM is a set of Perl scripts and modules. The following
+ core/CPAN Perl modules are required to invoke the <code>fcm</code>
+ command:</p>
+ <pre>
+Carp
+Cwd
+File::Basename
+File::Compare
+File::Copy
+File::Find
+File::Path
+File::Spec
+File::Spec::Functions
+File::Temp
+FindBin
+Getopt::Long
+HTTP::Date
+IO::File
+List::Util
+POSIX
+Pod::Usage
+Scalar::Util
+Sys::Hostname
+Text::ParseWords
+URI
+XML::DOM
+</pre>
+
+ <p>The following Perl modules are also required if you want to use the
+ <code>fcm gui</code> command:</p>
+ <pre>
+Tk
+Tk::ROText
+</pre>
+
+ <p>At the Met Office we are currently using FCM with Perl 5.8.2 on AIX 5.3
+ and Perl 5.8.5 on RHEL 4.</p>
+
+ <h3 id="req_svn">Subversion</h3>
+
+ <p>To use the code management commands (and relevant parts of the extract
+ system) you need to have <a href=
+ "http://subversion.tigris.org/">Subversion</a> installed.</p>
+
+ <p>At the Met Office we are currently using Subversion 1.4.3. Note: FCM 1.5
+ requires Subversion 1.4.x (but it has not been tested on Subversion 1.5.x or
+ above).</p>
+
+ <p>Note: you can use the extract system to mirror code to a remote platform
+ for building. Therefore it is only necessary to have Subversion installed on
+ the platform where you do your code development. If you use other platforms
+ purely for building and running then you do not need to have Subversion
+ installed on these platforms.</p>
+
+ <h3 id="req_trac">Trac</h3>
+
+ <p>The use of <a href="http://trac.edgewall.org/">Trac</a> is entirely
+ optional (although highly recommended if you are using Subversion).</p>
+
+ <p>At the Met Office we are currently using Trac 0.11.2.1. Note:</p>
+
+ <ul>
+ <li>The <code>--trac</code> and <code>--wiki</code> options to the
+ <code>fcm diff --branch</code> command allow you to view branch differences
+ using Trac. This requires Trac 0.10 or above.</li>
+
+ <li>Some of the example scripts in the <samp>examples/</samp> directory
+ require Trac 0.11.</li>
+ </ul>
+
+ <h3 id="req_other">Other Requirements</h3>
+
+ <p>The <code>fcm conflicts</code> command requires <a href=
+ "http://furius.ca/xxdiff/">xxdiff</a>. At the Met Office we are currently
+ using version 3.1. The <code>fcm diff --graphical</code> command also uses
+ xxdiff by default although other graphical diff tools can also be used.</p>
+
+ <p>The extract system uses <code>diff3</code>, (which is part of <a href=
+ "http://www.gnu.org/software/diffutils/">GNU diffutils</a>), to merge
+ together changes where the same file is modified by two different branches
+ (compared with the base branch). At the Met Office we are currently using
+ version 2.8.1.</p>
+
+ <p>The extract system uses <a href="http://rsync.samba.org/">rsync</a> to
+ mirror source file to another machine. At the Met Office we are currently
+ using version 2.6.3</p>
+
+ <p>The build system requires <a href=
+ "http://www.gnu.org/software/make/make.html">GNU make</a>. At the Met Office
+ we are currently using version 3.80.</p>
+
+ <p>FCM is intended to run on a Unix/Linux system. It is currently used at the
+ Met Office on Linux (RHEL 4.8) and AIX 5.3.</p>
+
+ <h2 id="ins">Installation</h2>
+
+ <p>FCM is distributed in the form of a compressed tar file. Un-pack the tar
+ file into an appropriate location on your system. Add the <samp>bin/</samp>
+ directory into your <var>PATH</var> environment variable. Once you have done
+ this you should now have full access to the FCM system, assuming that you
+ have met the requirements described in the previous section.</p>
+
+ <p>You should find the following contents in the distribution:</p>
+
+ <dl>
+ <dt>README</dt>
+
+ <dd>The README file contains the internal revision number of the release.</dd>
+
+ <dt>COPYRIGHT.txt<br />
+ LICENSE.html</dt>
+
+ <dd>The FCM license and other copyright information.</dd>
+
+ <dt>bin/</dt>
+
+ <dd>Contains the <code>fcm</code> command and other utilities.</dd>
+
+ <dt>doc/</dt>
+
+ <dd>System documentation.</dd>
+
+ <dt>doc/release_notes/</dt>
+
+ <dd>Contains these release notes. It also contains the release notes for
+ all previous versions which may be useful if you have skipped any
+ versions.</dd>
+
+ <dt>doc/user_guide/</dt>
+
+ <dd>Contains the <a href="../user_guide/">FCM User Guide</a>.</dd>
+
+ <dt>doc/standards/</dt>
+
+ <dd>Contains the FCM <a href="../standards/perl_standard.html">Perl</a> and
+ <a href="../standards/fortran_standard.html">Fortran</a> coding standards.
+ The Perl standard describes the standards followed by the FCM code. The
+ Fortran standard contains some <a href=
+ "../standards/fortran_standard.html#fcm">specific advice</a> on the best
+ way of writing Fortran code for use with FCM as well as more general advice
+ on good practise.</dd>
+
+ <dt>doc/collaboration/</dt>
+
+ <dd>Contains the <a href="../collaboration/index.html">External
+ Distribution & Collaboration for FCM Projects</a> document which
+ discusses how projects configured under FCM can be distributed
+ externally.</dd>
+
+ <dt>etc/</dt>
+
+ <dd>Miscellaneous items, including the <samp>fcm.cfg.eg</samp> file. If you
+ wish to define keywords for your systems you will need to create the
+ <samp>etc/fcm.cfg</samp> file. An example file, <samp>fcm.cfg.eg</samp>, is
+ provided which is a copy of the file currently used at the Met Office. For
+ further details please refer to the section <a href=
+ "../user_guide/system_admin.html#fcm-keywords">FCM keywords</a> in the
+ System Admin chapter of the User Guide.</dd>
+
+ <dt>examples/</dt>
+
+ <dd>Contains various example scripts which you may find useful. Note that
+ these scripts are all specific to the Met Office and may contain hard coded
+ paths and email addresses. They are provided in the hope that you may find
+ them useful as examples for setting up similar scripts of your own.
+ However, they should only be used after careful review to adapt them to
+ your environment.</dd>
+
+ <dt>examples/etc/regular-update.eg</dt>
+
+ <dd>An example of how you might set up a cron job to make use of the
+ <samp><repos>.latest</samp> file (see
+ <code>examples/svn-hooks/post-commit-background</code>).</dd>
+
+ <dt>examples/lib/</dt>
+
+ <dd>Contains the <code>FCM::Admin::*</code> Perl library, which implements
+ the functionalities of the FCM admin utility commands.</dd>
+
+ <dt>examples/sbin/</dt>
+
+ <dd>Contains a selection of useful admin utility commands.</dd>
+
+ <dt>examples/svn-hooks/pre-commit</dt>
+
+ <dd>
+ This script restricts write-access to the repository by checking the
+ following:
+
+ <ul>
+ <li>It executes the Subversion utility <code>svnperms.py</code> if it,
+ and the associated <samp>svnperms.conf</samp> file, exist. This utility
+ checks whether the author of the current transaction has enough
+ permission to write to particular paths in the repository.</li>
+
+ <li>It checks the disk space required by the current transaction. It
+ fails the commit if it requires more than 5Mb of disk space.</li>
+ </ul>
+ </dd>
+
+ <dt>examples/svn-hooks/post-commit</dt>
+
+ <dd>A simple post-commit hook script which runs the script
+ <code>post-commit-background</code> in the background.</dd>
+
+ <dt>examples/svn-hooks/post-commit-background</dt>
+
+ <dd>
+ This script runs in the background after each commit
+
+ <ul>
+ <li>It updates a <samp><repos>.latest</samp> file with the latest
+ revision number.</li>
+
+ <li>It creates a dump of the new revision.</li>
+
+ <li>It calls the script <code>background_updates.pl</code> if it
+ exists.</li>
+ </ul>This script is installed as standard in all our repositories.
+ </dd>
+
+ <dt>examples/svn-hooks/background_updates.pl</dt>
+
+ <dd>An example of how you may want to set up a
+ <code>background_updates.pl</code> script to perform post-commit tasks for
+ a specific repository. This script uses a lock file to prevent multiple
+ commits in quick succession from causing problems.</dd>
+
+ <dt>examples/svn-hooks/pre-revprop-change</dt>
+
+ <dd>A simple pre-revprop-change hook script which runs the script
+ <code>pre-revprop-change.pl</code>.</dd>
+
+ <dt>examples/svn-hooks/pre-revprop-change.pl</dt>
+
+ <dd>If a user attempts to modify the log message of a changeset and he/she
+ is not the original author of the changeset, this script will e-mail the
+ original author. You can also set up a watch facility to monitor changes of
+ log messages that affect particular paths in the repository. For further
+ details please refer to the section <a href=
+ "../user_guide/system_admin.html#svn_watch">Watching changes in log
+ messages</a> in the System Admin chapter of the User Guide.</dd>
+
+ <dt>examples/svn-hooks/post-revprop-change</dt>
+
+ <dd>A simple post-revprop-change hook script which invokes the
+ <code>trac-admin</code> command to <code>resync</code> the revision
+ property cache stored in the corresponding Trac environment.</dd>
+
+ <dt>lib/</dt>
+
+ <dd>Contains the Perl library of FCM.</dd>
+
+ <dt>man/</dt>
+
+ <dd>Contains a basic manual page for <code>fcm</code>.</dd>
+
+ <dt>t/</dt>
+
+ <dd>Contains unit test for FCM.</dd>
+
+ <dt>test/</dt>
+
+ <dd>Contains regression tests for FCM.</dd>
+
+ <dt>tutorial/</dt>
+
+ <dd>Contains the files necessary to set up a Subversion repository for the
+ FCM tutorial. This will allow you to follow the <a href=
+ "../user_guide/getting_started.html#tutorial">tutorial section</a> in the
+ User Guide. See <samp>tutorial/README</samp> on how to set it up.</dd>
+ </dl>
+
+ </div>
+ </div>
+ </div>
+
+ <hr/>
+ <div class="container-fluid text-center">
+ <div class="row-fluid"><div class="span12">
+ <address><small>
+ © British Crown Copyright 2006-14
+ <a href="http://www.metoffice.gov.uk">Met Office</a>.
+ See <a href="../etc/fcm-terms-of-use.html">Terms of Use</a>.<br />
+ This document is released under the British <a href=
+ "http://www.nationalarchives.gov.uk/doc/open-government-licence/" rel=
+ "license">Open Government Licence</a>.<br />
+ </small></address>
+ </div></div>
+ </div>
+
+ <script type="text/javascript" src="../etc/jquery.min.js"></script>
+ <script type="text/javascript" src="../etc/bootstrap/js/bootstrap.min.js"></script>
+ <script type="text/javascript" src="../etc/fcm.js"></script>
+ <script type="text/javascript" src="../etc/fcm-version.js"></script>
+</body>
+</html>
diff --git a/doc/release_notes/2-0.html b/doc/release_notes/2-0.html
new file mode 100644
index 0000000..976d4e5
--- /dev/null
+++ b/doc/release_notes/2-0.html
@@ -0,0 +1,754 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>FCM 2-0 Release Notes</title>
+ <meta name="author" content="FCM team" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link rel="icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link rel="shortcut icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link href="../etc/bootstrap/css/bootstrap.min.css" rel="stylesheet" media="screen" />
+ <link href="../etc/fcm.css" rel="stylesheet" media="screen" />
+</head>
+<body>
+ <div class="navbar navbar-inverse">
+ <div class="navbar-inner">
+ <a class="brand" href=".."><span class="fcm-version">FCM</span></a>
+ <ul class="nav">
+ <li><a href="../installation/">Installation</a></li>
+
+ <li><a href="../user_guide/">User Guide</a></li>
+ </ul>
+ </div>
+ </div>
+
+ <div class="page-header">
+ <div class="fcm-page-content pull-right well well-small"></div>
+ <h1>FCM 2-0 Release Notes <small>11 March 2011</small></h1>
+ </div>
+
+ <div class="container">
+ <div class="row">
+ <div class="span12">
+
+ <p>These are the release notes for FCM 2-0. You can use this release of FCM
+ freely under the terms of the FCM LICENSE, which you should receive with the
+ distribution of this release.</p>
+
+ <p>FCM is maintained by the FCM team at the Met Office. Please feedback any
+ bug reports or feature requests to us by <a href=
+ "mailto:fcm-team at metoffice.gov.uk">e-mail</a>.</p>
+
+ <h2 id="new">What's New?</h2>
+
+ <dl>
+ <dt><code><a href="../user_guide/command_ref.html#fcm-make">fcm
+ make</a></code>: new command</dt>
+
+ <dd>New extract/build system. Deprecates <code>fcm extract</code> and
+ <code>fcm build</code>. See <a href="../user_guide/make.html">User Guide
+ > FCM Make</a> for detail on how to use the new system. See also
+ <a href="#new.make">fcm make: key differences compared with fcm
+ extract/build</a>.</dd>
+
+ <dt><code><a href="../user_guide/command_ref.html#fcm-branch-create">fcm
+ branch-create</a></code>: new command</dt>
+
+ <dd>Deprecates <code>fcm branch --create</code>.</dd>
+
+ <dt><code><a href="../user_guide/command_ref.html#fcm-branch-delete">fcm
+ branch-delete</a></code>: new command</dt>
+
+ <dd>Deprecates <code>fcm branch --delete</code>.</dd>
+
+ <dt><code><a href="../user_guide/command_ref.html#fcm-branch-diff">fcm
+ branch-diff</a></code>: new command</dt>
+
+ <dd>Deprecates <code>fcm diff --branch</code>.</dd>
+
+ <dt><code><a href="../user_guide/command_ref.html#fcm-branch-info">fcm
+ branch-info</a></code>: new command</dt>
+
+ <dd>Deprecates <code>fcm branch --info</code>.</dd>
+
+ <dt><code><a href="../user_guide/command_ref.html#fcm-branch-list">fcm
+ branch-list</a></code>: new command</dt>
+
+ <dd>Deprecates <code>fcm branch --list</code>.</dd>
+
+ <dt><code><a href="../user_guide/command_ref.html#fcm-browse">fcm
+ browse</a></code></dt>
+
+ <dd>This is now the preferred name of <code>fcm trac</code> or <code>fcm
+ www</code>.</dd>
+
+ <dt><code><a href="../user_guide/command_ref.html#fcm-cfg-print">fcm
+ cfg-print</a></code></dt>
+
+ <dd>This is now the preferred name of <code>fcm cfg</code>.</dd>
+
+ <dt><code><a href="../user_guide/command_ref.html#fcm-export-items">fcm
+ export-items</a></code>: new command</dt>
+
+ <dd>Deprecates <code>fcm_update_version_dir.pl</code>.</dd>
+
+ <dt>Configuration for keywords</dt>
+
+ <dd>New syntax and location. See <a href=
+ "../user_guide/annex_cfg.html#keyword">User Guide > Annex: FCM
+ Configuration File > FCM Keyword Configuration</a> for detail.
+ Equivalent settings in <samp>$FCM/etc/fcm.cfg</samp> will no longer work.
+ Those in <samp>$HOME/.fcm</samp> should continue to work. We would
+ encourage users to migrate to the new syntax and location.</dd>
+
+ <dt>Configuration for external commands</dt>
+
+ <dd>New syntax and location. See <a href=
+ "../user_guide/annex_cfg.html#external">User Guide > Annex: FCM
+ Configuration File > FCM External Configuration</a> for detail.
+ Equivalent settings in <samp>$FCM/etc/fcm.cfg</samp> and
+ <samp>$HOME/.fcm</samp> will no longer work.</dd>
+ </dl>
+
+ <h3 id="new.make">fcm make: key differences compared with fcm
+ extract/build</h3>
+
+ <p>Single command and framework of a configurable chain of <dfn>steps</dfn>,
+ e.g. extract, mirror, preprocess, build, etc.</p>
+
+ <ul>
+ <li>as opposed to 2 separate commands with fixed steps.</li>
+
+ <li>possible to set up multiple builds with different configurations from
+ the same extract.</li>
+ </ul>
+
+ <p>New configuration file format, with more powerful syntax and declarations.
+ E.g.:</p>
+
+ <ul>
+ <li>Improved support for specifying <dfn>name-spaces</dfn> (previously
+ <dfn>package</dfn> or <dfn>sub-package</dfn>) for a declaration.</li>
+
+ <li>Improved support for declaring and referencing variables.</li>
+
+ <li>Improved support for space and other meta-characters.</li>
+ </ul>
+
+ <p>extract: automatically associates location keywords to source tree
+ locations. E.g.:</p>
+
+ <ul>
+ <li>
+ <code>fcm extract</code> configuration requires repeated declarations:
+ <pre>
+cfg::type ext
+repos::foo::base fcm:foo/trunk
+expsrc::foo::base
+repos::bar::base fcm:bar/trunk
+expsrc::bar::base
+</pre>
+ </li>
+
+ <li>
+ <code>fcm make</code> configuration is much simpler:
+ <pre>
+steps = extract
+extract.ns = foo bar
+</pre>
+ </li>
+ </ul>
+
+ <p>extract: clearly distinguishes a base source tree from the diff source
+ trees for each project. E.g.:</p>
+
+ <ul>
+ <li>
+ <code>fcm extract</code> configuration requires arbitrary IDs for each
+ source tree, separate revision declarations, and assumes that the first
+ declared tree for a project is the <dfn>base</dfn>:
+ <pre>
+cfg::type ext
+repos::foo::base fcm:foo/trunk
+revision::foo::base 1234
+expsrc::foo::base
+repos::foo::b1 fcm:foo/branches/dev/fred/r1234_b1
+revision::foo::b1 2345
+repos::foo::b2 fcm:foo/branches/dev/bob/r1234_b2
+repos::foo::b3 fcm:foo/branches/dev/alice/r1234_b3
+</pre>
+ </li>
+
+ <li>
+ <code>fcm make</code> configuration uses different declarations for the
+ location of the <dfn>base</dfn> source tree and the locations of the
+ <dfn>diff</dfn> source trees:
+ <pre>
+steps = extract
+extract.ns = foo
+extract.location[foo] = trunk at 1234
+extract.location{diff}[foo] = \
+ branches/dev/fred/r1234_b1 at 2345 \
+ branches/dev/bob/r1234_b2 \
+ branches/dev/alice/r1234_b3
+</pre>
+ </li>
+ </ul>
+
+ <p>extract: can easily filter parts of a project source tree, and/or change
+ the root of the extract tree. E.g.:</p>
+
+ <ul>
+ <li>
+ <code>fcm extract</code> configuration can cause confusion:
+ <pre>
+cfg::type ext
+repos::um::base fcm:um/trunk/src
+revision::foo::base vn7.7
+expsrc::um::base
+repos::um::b1 fcm:um/branches/dev/fred/vn7.7_b1/src
+repos::um::b2 fcm:um/branches/dev/bob/vn7.7_b2/src
+repos::um::b3 fcm:um/branches/dev/alice/vn7.7_b3/src
+</pre>
+ </li>
+
+ <li>
+ <code>fcm make</code> configuration is clearer and has more features:
+ <pre>
+steps = extract
+extract.ns = um
+extract.path-root[um] = src
+extract.path-excl[um] = configs scm
+extract.location[um] = trunk at vn7.7
+extract.location{diff}[um] = \
+ branches/dev/fred/vn7.7_b1 \
+ branches/dev/bob/vn7.7_b2 \
+ branches/dev/alice/vn7.7_b3
+</pre>
+ </li>
+ </ul>
+
+ <p>extract: works with project source trees as opposed to individual source
+ directories.</p>
+
+ <ul>
+ <li>fewer calls to the version control system servers.</li>
+
+ <li>deleted directories are now handled correctly.</li>
+ </ul>
+
+ <p>extract: can use multiple processes to retrieve source trees information
+ and to export source tree files from the version control system.</p>
+
+ <ul>
+ <li>extract of multiple projects and/or with multiple source trees can be
+ much faster.</li>
+ </ul>
+
+ <p>mirror: is now an independent step.</p>
+
+ <ul>
+ <li>can set up multiple mirror steps to mirror an extract to alternate
+ destinations.</li>
+ </ul>
+
+ <p>build: can use multiple processes to analyse the source files for
+ dependencies and other information.</p>
+
+ <ul>
+ <li>multi-process build is much faster.</li>
+ </ul>
+
+ <p>build: uses an internal task manager and runner - more efficient logic
+ possible:</p>
+
+ <ul>
+ <li>no longer requires GNU make.</li>
+
+ <li>no longer requires dummy files such as <samp>*.done
+ *.flags</samp>.</li>
+
+ <li>uses MD5 checksums to determine whether sources and targets are out of
+ date - as opposed to time stamps.</li>
+
+ <li>fails the build if duplicated targets are detected if those targets are
+ required by the build.</li>
+ </ul>
+
+ <p>build: has improved the logic for building Fortran program units.</p>
+
+ <ul>
+ <li>detects correctly multiple top program units in the same source
+ file.</li>
+
+ <li>sets up a module usage as an include dependency on the
+ <samp>*.mod</samp> file instead of the <samp>*.o</samp> file - reduces the
+ chance of module compile cascades in incremental mode.</li>
+
+ <li>only generates interface files on demand.</li>
+ </ul>
+
+ <p>build: has improved facilities for sources and targets selection.</p>
+
+ <ul>
+ <li>can now select targets by name-space, category and task.</li>
+
+ <li>has better documentation on the relationship between source files and
+ build targets.</li>
+
+ <li>note that target declarations are not cumulative and that targets are
+ inherited by default (unlike with <code>fcm build</code>).</li>
+ </ul>
+
+ <p>build: automatically uses the Fortran compiler to link Fortran executables
+ and the C compiler to link C executables.</p>
+
+ <p>build: has more diagnostics, e.g. on source dependencies, target build
+ tree, etc.</p>
+
+ <p>preprocess: is now an independent step, but shares all the logic of the
+ build system, e.g.:</p>
+
+ <ul>
+ <li>preprocessing dependency analysis and target update can be performed in
+ multiple processes.</li>
+
+ <li>note that file extensions are not modified by the preprocess step
+ unlike with <code>fcm build</code> which changed, for example
+ <samp>.F90</samp> extensions to <samp>.f90</samp>.</li>
+ </ul>
+
+ <p>Other notable changes:</p>
+
+ <ul>
+ <li>By default, <code>fcm make</code> will always rebuild link targets and
+ re-install scripts in inherited builds. Therefore, to use the executables
+ from a build all you need to do is set your <var>PATH</var> environment
+ variable to point to <samp>$DEST/build/bin/</samp> (where <var>$DEST</var>
+ is the destination of the make). Note that there is no
+ <samp>fcm_env.sh</samp> file produced by <code>fcm make</code>.</li>
+
+ <li><code>fcm extract</code> has the ability to fail if the declared
+ revision of a branch does not correspond to a changeset of that branch.
+ Furthermore, it can output the latest revision of a branch if the declared
+ revision is not the latest. <code>fcm make</code> does not support
+ this.</li>
+
+ <li>There is no equivalent of <code><a href=
+ "../user_guide/command_ref.html#fcm-cmp-ext-cfg">fcm cmp-ext-cfg</a></code>
+ for FCM make configurations.</li>
+
+ <li><code>fcm make</code> does not support defining a separate linker - it
+ always uses the compiler of the source file containing the main program
+ (which is the default with <code>fcm build</code>).</li>
+
+ <li><code>fcm make</code> does not recognise existing binaries as install
+ targets (unlike <code>fcm build</code>). This feature is currently used to
+ allow the Met Office's Suite Control System (SCS) to "build" suites but is
+ no longer considered the best method. SCS will continue to use <code>fcm
+ build</code> until such time as a better method is adopted or the system is
+ retired.</li>
+
+ <li><code>fcm make</code> recognises data files as install targets in a
+ similar way to <code>fcm build</code>. However, the default destination of
+ such targets is now the full name-space under the <samp>etc/</samp>
+ sub-directory.</li>
+
+ <li><code>fcm make</code> supports the building of libraries but does not
+ generate the relevant exclude dependency configuration as is done by
+ <code>fcm build</code>.</li>
+
+ <li><code>fcm make</code> has no equivalent of the <code>--archive</code>
+ nor the <code>--targets</code> options provided by <code>fcm
+ build</code>.</li>
+
+ <li><code>fcm build</code> recognises a file name without its extension as
+ a sub-package name. This is not true with <code>fcm make</code> which only
+ recognises the full file name as a namespace.</li>
+ </ul>
+
+ <p>In addition to the differences noted above, <code>fcm make</code> fixes
+ various subtle problems which can occur with <code>fcm extract</code> and
+ <code>fcm build</code> as a result of limitations in the internal design.
+ Overall, <code>fcm make</code> is much better and we recommend that all users
+ migrate to it. <code>fcm extract</code> and <code>fcm build</code> will
+ continue to be maintained for legacy systems but will not be developed
+ further.</p>
+
+ <h2 id="fix">Minor Changes and Bug Fixes</h2>
+
+ <dl>
+ <dt><code>fcm build</code></dt>
+
+ <dd>
+ <p>Handle directory names with a dot extension.</p>
+
+ <p>Correct search path for inherited configuration file.</p>
+
+ <p>Always export <var>OBJECTS</var> in generated
+ <samp>Makefile</samp>.</p>
+ </dd>
+
+ <dt><code>fcm cfg</code></dt>
+
+ <dd>
+ <p>Now an alias of <code>fcm cfg-print</code>.</p>
+
+ <p>The default behaviour is to parse FCM 2 configuration files. To parse
+ FCM 1 configuration files, use the <code>--fcm1</code> option.</p>
+
+ <p>The values in the output will no longer be lined up.</p>
+ </dd>
+
+ <dt><code>fcm extract</code></dt>
+
+ <dd>
+ <p>Fix double slashes in cache of extract with project root level
+ files.</p>
+
+ <p>Correct search path for inherited configuration file.</p>
+
+ <p>Fix incremental mode behaviour of targets with <dfn>deleted,
+ overriding inherited</dfn> status.</p>
+ </dd>
+
+ <dt><code>fcm keyword-print</code></dt>
+
+ <dd>Change in output format to match the new configuration file
+ format.</dd>
+
+ <dt><code>fcm mkpatch</code></dt>
+
+ <dd>
+ <p>Don't use patch file if PDF file detected.</p>
+
+ <p>Handle property changes to directories.</p>
+
+ <p>Handle copies within new directories.</p>
+
+ <p>Handle replaced directories.</p>
+
+ <p>Fix handling of symbolic links.</p>
+
+ <p>Fix pattern match used when checking for excluded or copied paths.</p>
+
+ <p>Use <code>--no-backup-if-mismatch</code> option to patch command to
+ ensure backup files not created if patch does not match exactly.</p>
+ </dd>
+
+ <dt><code>fcm</code> direct wrappers to <code>svn</code> commands</dt>
+
+ <dd>No longer prints <samp>=> svn ...</samp> on STDOUT.</dd>
+
+ <dt>Misc fixes</dt>
+
+ <dd>Misc fixes related to changes in Perl 5.10 and Subversion 1.6.</dd>
+ </dl>
+
+ <h2 id="issues">Known Issues</h2>
+
+ <dl>
+ <dt>Build inheritance limitation: handling of include files</dt>
+
+ <dd>See the <a href="../user_guide/make.html#build.inherit">User Guide >
+ FCM Make > Build > Build Inheritance</a> for detail.</dd>
+ </dl>
+
+ <h2 id="req">System Requirements</h2>
+
+ <h3 id="req.perl">Perl</h3>
+
+ <p>The core part of FCM is a set of Perl scripts and modules. At the Met
+ Office, FCM runs on:</p>
+
+ <dl>
+ <dt>Perl 5.8.2 on AIX 5.3</dt>
+
+ <dd>
+ <p><code>Text::ParseWords</code> (core Perl module) is upgraded to
+ version 3.22.</p>
+
+ <p>Met Office users do not use the code management commands and the
+ extract system on this platform.</p>
+ </dd>
+
+ <dt>Perl 5.8.5 on RHEL 4</dt>
+
+ <dd>
+ <p><a href=
+ "http://search.cpan.org/~gaas/libwww-perl/lib/HTTP/Date.pm">HTTP::Date</a>
+ in <a href="http://search.cpan.org/~gaas/libwww-perl/">libwww-perl</a> is
+ required by <code>fcm extract</code> and the extract system in <code>fcm
+ make</code>. (libwww-perl 5.79 installed.)</p>
+
+ <p><a href=
+ "http://search.cpan.org/~enno/libxml-enno/lib/XML/DOM.pm">XML::DOM</a> in
+ <a href="http://search.cpan.org/~enno/libxml-enno/">libxml-enno</a> is
+ required by the code management commands. (libxml-enno 1.02
+ installed.)</p>
+
+ <p><a href="http://search.cpan.org/~srezic/Tk/">Tk</a> is required by the
+ <code>fcm gui</code> command. (Tk 804.027 installed.)</p>
+ </dd>
+ </dl>
+
+ <h3 id="req.svn">Subversion</h3>
+
+ <p>To use the code management commands (and relevant parts of the extract
+ system) you need to have <a href=
+ "http://subversion.tigris.org/">Subversion</a> installed.</p>
+
+ <p>FCM requires Subversion 1.4.x or above. At the Met Office we are currently
+ using Subversion 1.4.3.</p>
+
+ <p>Note: you can use the extract system to mirror code to a remote platform
+ for building. Therefore it is only necessary to have Subversion installed on
+ the platform where you do your code development. If you use other platforms
+ purely for building and running then you do not need to have Subversion
+ installed on these platforms.</p>
+
+ <h3 id="req.trac">Trac</h3>
+
+ <p>The use of <a href="http://trac.edgewall.org/">Trac</a> is entirely
+ optional (although highly recommended if you are using Subversion).</p>
+
+ <p>At the Met Office we are currently using Trac 0.11.7. Note:</p>
+
+ <ul>
+ <li>The <code>--trac</code> and <code>--wiki</code> options to the
+ <code>fcm diff --branch</code> command allow you to view branch differences
+ using Trac. This requires Trac 0.10 or above.</li>
+
+ <li>Some of the example scripts in the <samp>examples/</samp> directory
+ require Trac 0.11.</li>
+ </ul>
+
+ <h3 id="req.other">Other Requirements</h3>
+
+ <p>The <code>fcm conflicts</code> command requires <a href=
+ "http://furius.ca/xxdiff/">xxdiff</a>. At the Met Office we are currently
+ using version 3.1. The <code>fcm diff --graphical</code> command also uses
+ xxdiff by default although other graphical diff tools can also be used.</p>
+
+ <p>The <code>fcm make</code> command uses <code>gzip</code>. At the Met
+ Office we are currently using gzip 1.2.4 on AIX 5.3 and gzip 1.3.3 on RHEL
+ 4.</p>
+
+ <p>The extract system uses <code>diff3</code>, (which is part of <a href=
+ "http://www.gnu.org/software/diffutils/">GNU diffutils</a>), to merge
+ together changes where the same file is modified by two different branches
+ (compared with the base branch). At the Met Office we are currently using
+ version 2.8.1.</p>
+
+ <p>The mirror system uses <a href="http://rsync.samba.org/">rsync</a> to
+ mirror source file to another machine. At the Met Office we are currently
+ using version 2.6.3</p>
+
+ <p>The deprecated <code>fcm build</code> requires <a href=
+ "http://www.gnu.org/software/make/make.html">GNU make</a>. At the Met Office
+ we are currently using version 3.80.</p>
+
+ <p>FCM is intended to run on a Unix/Linux system. It is currently used at the
+ Met Office on Linux (RHEL 4.8) and AIX 5.3.</p>
+
+ <h2 id="ins">Installation</h2>
+
+ <p>FCM is distributed in the form of a compressed tar file. Un-pack the tar
+ file into an appropriate location on your system. Add the <samp>bin/</samp>
+ directory into your <var>PATH</var> environment variable. Once you have done
+ this you should now have full access to the FCM system, assuming that you
+ have met the requirements described in the previous section.</p>
+
+ <p>You should find the following contents in the distribution:</p>
+
+ <dl>
+ <dt>README</dt>
+
+ <dd>The README file contains the internal revision number of the
+ release.</dd>
+
+ <dt>COPYRIGHT.txt<br />
+ LICENSE.html</dt>
+
+ <dd>The FCM license and other copyright information.</dd>
+
+ <dt>bin/</dt>
+
+ <dd>Contains the <code>fcm</code> command and other utilities.</dd>
+
+ <dt>doc/</dt>
+
+ <dd>System documentation.</dd>
+
+ <dt>doc/release_notes/</dt>
+
+ <dd>Contains these release notes. It also contains the release notes for
+ all previous versions which may be useful if you have skipped any
+ versions.</dd>
+
+ <dt>doc/user_guide/</dt>
+
+ <dd>Contains the <a href="../user_guide/">FCM User Guide</a>.</dd>
+
+ <dt>doc/standards/</dt>
+
+ <dd>Contains the FCM <a href="../standards/perl_standard.html">Perl</a> and
+ <a href="../standards/fortran_standard.html">Fortran</a> coding standards.
+ The Perl standard describes the standards followed by the FCM code. The
+ Fortran standard contains some <a href=
+ "../standards/fortran_standard.html#fcm">specific advice</a> on the best
+ way of writing Fortran code for use with FCM as well as more general advice
+ on good practice.</dd>
+
+ <dt>doc/collaboration/</dt>
+
+ <dd>Contains the <a href="../collaboration/index.html">External
+ Distribution & Collaboration for FCM Projects</a> document which
+ discusses how projects configured under FCM can be distributed
+ externally.</dd>
+
+ <dt>etc/</dt>
+
+ <dd>Miscellaneous items, including the <samp>fcm/keyword.cfg.eg</samp> file.
+ If you wish to define keywords for your site you will need to create the
+ <samp>etc/fcm/keyword.cfg</samp> file. An example file,
+ <samp>fcm/keyword.cfg.eg</samp>, is provided which is a copy of the file
+ currently used at the Met Office. For further details please refer to the
+ section <a href="../user_guide/system_admin.html#fcm-keywords">FCM
+ keywords</a> in the System Admin chapter of the User Guide.</dd>
+
+ <dt>examples/</dt>
+
+ <dd>Contains various example scripts which you may find useful. Note that
+ these scripts are all specific to the Met Office and may contain hard coded
+ paths and email addresses. They are provided in the hope that you may find
+ them useful as examples for setting up similar scripts of your own.
+ However, they should only be used after careful review to adapt them to
+ your environment.</dd>
+
+ <dt>examples/etc/regular-update.eg</dt>
+
+ <dd>An example of how you might set up a cron job to make use of the
+ <samp><repos>.latest</samp> file (see
+ <code>examples/svn-hooks/post-commit-background</code>).</dd>
+
+ <dt>examples/lib/</dt>
+
+ <dd>Contains the <code>FCM::Admin::*</code> Perl library, which implements
+ the functionalities of the FCM admin utility commands.</dd>
+
+ <dt>examples/sbin/</dt>
+
+ <dd>Contains a selection of useful admin utility commands.</dd>
+
+ <dt>examples/svn-hooks/pre-commit</dt>
+
+ <dd>
+ This script restricts write-access to the repository by checking the
+ following:
+
+ <ul>
+ <li>It executes the Subversion utility <code>svnperms.py</code> if it,
+ and the associated <samp>svnperms.conf</samp> file, exist. This utility
+ checks whether the author of the current transaction has enough
+ permission to write to particular paths in the repository.</li>
+
+ <li>It checks the disk space required by the current transaction. It
+ fails the commit if it requires more than 5Mb of disk space.</li>
+ </ul>
+ </dd>
+
+ <dt>examples/svn-hooks/post-commit</dt>
+
+ <dd>A simple post-commit hook script which runs the script
+ <code>post-commit-background</code> in the background.</dd>
+
+ <dt>examples/svn-hooks/post-commit-background</dt>
+
+ <dd>
+ This script runs in the background after each commit.
+
+ <ul>
+ <li>It updates a <samp><repos>.latest</samp> file with the latest
+ revision number.</li>
+
+ <li>It creates a dump of the new revision.</li>
+
+ <li>It calls <code>post-commit-background-custom</code> if it
+ exists.</li>
+ </ul>
+ </dd>
+
+ <dt>examples/svn-hooks/pre-revprop-change</dt>
+
+ <dd>A simple pre-revprop-change hook script which runs the script
+ <code>pre-revprop-change.pl</code>.</dd>
+
+ <dt>examples/svn-hooks/pre-revprop-change.pl</dt>
+
+ <dd>If a user attempts to modify the log message of a changeset and he/she
+ is not the original author of the changeset, this script will e-mail the
+ original author. You can also set up a watch facility to monitor changes of
+ log messages that affect particular paths in the repository. For further
+ details please refer to the section <a href=
+ "../user_guide/system_admin.html#svn_watch">Watching changes in log
+ messages</a> in the System Admin chapter of the User Guide.</dd>
+
+ <dt>examples/svn-hooks/post-revprop-change</dt>
+
+ <dd>A simple post-revprop-change hook script which invokes the
+ <code>trac-admin</code> command to <code>resync</code> the revision
+ property cache stored in the corresponding Trac environment.</dd>
+
+ <dt>lib/</dt>
+
+ <dd>Contains the Perl library of FCM.</dd>
+
+ <dt>man/</dt>
+
+ <dd>Contains a basic manual page for <code>fcm</code>.</dd>
+
+ <dt>t/</dt>
+
+ <dd>Contains unit test for FCM.</dd>
+
+ <dt>test/</dt>
+
+ <dd>Contains regression tests for FCM.</dd>
+
+ <dt>test/test_include/</dt>
+
+ <dd>Contains simple test code to check how your chosen compilers handle
+ include files (see <a href="#issues">Known Issues</a>).</dd>
+
+ <dt>tutorial/</dt>
+
+ <dd>Contains the files necessary to set up a Subversion repository for the
+ FCM tutorial. This will allow you to follow the <a href=
+ "../user_guide/getting_started.html#tutorial">tutorial section</a> in the
+ User Guide. See <samp>tutorial/README</samp> on how to set it up.</dd>
+ </dl>
+
+ </div>
+ </div>
+ </div>
+
+ <hr/>
+ <div class="container-fluid text-center">
+ <div class="row-fluid"><div class="span12">
+ <address><small>
+ © British Crown Copyright 2006-14
+ <a href="http://www.metoffice.gov.uk">Met Office</a>.
+ See <a href="../etc/fcm-terms-of-use.html">Terms of Use</a>.<br />
+ This document is released under the British <a href=
+ "http://www.nationalarchives.gov.uk/doc/open-government-licence/" rel=
+ "license">Open Government Licence</a>.<br />
+ </small></address>
+ </div></div>
+ </div>
+
+ <script type="text/javascript" src="../etc/jquery.min.js"></script>
+ <script type="text/javascript" src="../etc/bootstrap/js/bootstrap.min.js"></script>
+ <script type="text/javascript" src="../etc/fcm.js"></script>
+ <script type="text/javascript" src="../etc/fcm-version.js"></script>
+</body>
+</html>
diff --git a/doc/release_notes/2-1.html b/doc/release_notes/2-1.html
new file mode 100644
index 0000000..c33bf7d
--- /dev/null
+++ b/doc/release_notes/2-1.html
@@ -0,0 +1,457 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>FCM 2-1 Release Notes</title>
+ <meta name="author" content="FCM team" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link rel="icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link rel="shortcut icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link href="../etc/bootstrap/css/bootstrap.min.css" rel="stylesheet" media="screen" />
+ <link href="../etc/fcm.css" rel="stylesheet" media="screen" />
+</head>
+<body>
+ <div class="navbar navbar-inverse">
+ <div class="navbar-inner">
+ <a class="brand" href=".."><span class="fcm-version">FCM</span></a>
+ <ul class="nav">
+ <li><a href="../installation/">Installation</a></li>
+
+ <li><a href="../user_guide/">User Guide</a></li>
+ </ul>
+ </div>
+ </div>
+
+ <div class="page-header">
+ <div class="fcm-page-content pull-right well well-small"></div>
+ <h1>FCM 2-1 Release Notes <small>22 July 2011</small></h1>
+ </div>
+
+ <div class="container">
+ <div class="row">
+ <div class="span12">
+
+ <p>These are the release notes for FCM 2-1. You can use this release of FCM
+ freely under the terms of the FCM LICENSE, which you should receive with the
+ distribution of this release.</p>
+
+ <p>FCM is maintained by the FCM team at the Met Office. Please feedback any
+ bug reports or feature requests to us by <a href=
+ "mailto:fcm-team at metoffice.gov.uk">e-mail</a>.</p>
+
+ <h2 id="new">What's New?</h2>
+
+ <p>No major new features in this release.</p>
+
+ <h2 id="fix">Minor Changes and Bug Fixes</h2>
+
+ <dl>
+ <dt><code>fcm branch-create</code></dt>
+
+ <dd>
+ <p>if the <code>--ticket=N</code> option is not specified and
+ <var>NAME</var> contains only a list of positive integers separated by
+ <code>[_-]</code> (an underscore or a hyphen), the command will now assume
+ that <var>NAME</var> also specifies the related ticket numbers.</p>
+
+ <p>ticket numbers will now prefix the 1st line of the automatic commit log
+ message.</p>
+ </dd>
+
+ <dt><code>fcm cmp-ext-cfg</code></dt>
+
+ <dd>
+ <p>fixed <code>--wiki-format=TARGET</code> option, broken at <a href=
+ "2-0.html">2-0</a>.</p>
+ </dd>
+
+ <dt><code>fcm export-items</code></dt>
+
+ <dd>
+ <p>fixed <code>--new</code> option, broken at <a href=
+ "2-0.html">2-0</a>.</p>
+
+ <p>now generate real <samp>v*</samp> directories and revision directories
+ as symbolic links.</p>
+
+ <p><code>fcm_update_version_dir.pl</code> removed.</p>
+ </dd>
+
+ <dt><code>fcm make</code></dt>
+
+ <dd>
+ <p>build: allow space between <code>#</code> and <code>include</code> for
+ <code>#include</code> syntax in C or Fortran source files requiring
+ pre-processing.</p>
+
+ <p>build: will now fail when an invalid target key is specified.</p>
+
+ <p>build: correctly support compilers and linkers that require a space
+ between an option flag and its argument.</p>
+
+ <p>build: will no longer call <code>ar</code> for single file link.</p>
+
+ <p>build: improve error message if a target does not exist in the expected
+ location following an update.</p>
+
+ <p>build: <code>target-rename</code> declarations are now non-cumulative,
+ as documented.</p>
+
+ <p>build: <code>build.prop{file-name-option.f90-mod} = case=upper</code>
+ can now be specified for compilers that generate <samp>MODULE.mod</samp>
+ files for Fortran modules (as opposed to the more common
+ <samp>module.mod</samp> convention).</p>
+
+ <p>extract: <code>path-incl</code>, <code>path-excl</code>,
+ <code>path-root</code> declarations no longer require a name-space.</p>
+
+ <p>extract: fixed error handling when multiple processing.</p>
+ </dd>
+
+ <dt><code>fcm mkpatch</code></dt>
+
+ <dd>
+ <p>fixed CLI prompt to confirm removal of old output directory.</p>
+
+ <p>ensure initial branch creation is ignored.</p>
+ </dd>
+
+ <dt>commit message text editor</dt>
+
+ <dd>
+ <p><code>fcm branch-create</code>, <code>fcm
+ branch-delete</code> and <code>fcm commit</code> now support the
+ <var>[helpers] editor-cmd</var> option defined in
+ <samp>$HOME/.subversion/config</samp>.</p>
+
+ <p><code>nedit</code> is no longer the default external text editor.
+ <code>vi</code> is the default external text editor for the command line
+ interface. <code>gedit</code> is the default external text editor for
+ <code>fcm gui</code>.</p>
+ </dd>
+
+ <dt>configuration file syntax</dt>
+
+ <dd>
+ <p>the <code>inc=LOCATION</code> is no longer supported for configuration
+ files in the new part of the system.</p>
+
+ <p>incorrect variable assignment syntax will now trigger an exception.</p>
+
+ <p>leading and trailing spaces in modifiers and name-space are now
+ ignored without triggering Perl warnings.</p>
+ </dd>
+
+ <dt>installation dependency</dt>
+
+ <dd>
+ <p><code>fcm</code> no longer requires the <code>XML::DOM</code> Perl
+ module. It now uses <code>XML::Parser</code> instead. The latter is
+ normally installed by default on most Unix/Linux platforms.</p>
+ </dd>
+ </dl>
+
+ <h2 id="issues">Known Issues</h2>
+
+ <dl>
+ <dt>Build inheritance limitation: handling of include files</dt>
+
+ <dd>See the <a href="../user_guide/make.html#build.inherit">User Guide >
+ FCM Make > Build > Build Inheritance</a> for detail.</dd>
+ </dl>
+
+ <h2 id="req">System Requirements</h2>
+
+ <h3 id="req.perl">Perl</h3>
+
+ <p>The core part of FCM is a set of Perl scripts and modules. At the Met
+ Office, FCM runs on:</p>
+
+ <dl>
+ <dt>Perl 5.8.2 on AIX 5.3</dt>
+
+ <dd>
+ <p><code>Text::ParseWords</code> (core Perl module) is upgraded to
+ version 3.22.</p>
+
+ <p>Met Office users do not use the code management commands and the
+ extract system on this platform.</p>
+ </dd>
+
+ <dt>Perl 5.8.5 on RHEL 4</dt>
+
+ <dd>
+ <p><a href=
+ "http://search.cpan.org/~gaas/libwww-perl/lib/HTTP/Date.pm">HTTP::Date</a>
+ in <a href="http://search.cpan.org/~gaas/libwww-perl/">libwww-perl</a> is
+ required by <code>fcm extract</code> and the extract system in <code>fcm
+ make</code>. (libwww-perl 5.79 installed.)</p>
+
+ <p><a href="http://search.cpan.org/dist/XML-Parser/">XML::Parser</a> is
+ required by the code management commands. (2.34 installed.)</p>
+
+ <p><a href="http://search.cpan.org/~srezic/Tk/">Tk</a> is required by the
+ <code>fcm gui</code> command. (Tk 804.027 installed.)</p>
+ </dd>
+ </dl>
+
+ <h3 id="req.svn">Subversion</h3>
+
+ <p>To use the code management commands (and relevant parts of the extract
+ system) you need to have <a href=
+ "http://subversion.tigris.org/">Subversion</a> installed.</p>
+
+ <p>FCM requires Subversion 1.4.x or above. At the Met Office we are currently
+ using Subversion 1.4.3.</p>
+
+ <p>Note: you can use the extract system to mirror code to a remote platform
+ for building. Therefore it is only necessary to have Subversion installed on
+ the platform where you do your code development. If you use other platforms
+ purely for building and running then you do not need to have Subversion
+ installed on these platforms.</p>
+
+ <h3 id="req.trac">Trac</h3>
+
+ <p>The use of <a href="http://trac.edgewall.org/">Trac</a> is entirely
+ optional (although highly recommended if you are using Subversion).</p>
+
+ <p>At the Met Office we are currently using Trac 0.11.7. Note:</p>
+
+ <ul>
+ <li>The <code>--trac</code> and <code>--wiki</code> options to the
+ <code>fcm diff --branch</code> command allow you to view branch differences
+ using Trac. This requires Trac 0.10 or above.</li>
+
+ <li>Some of the example scripts in the <samp>examples/</samp> directory
+ require Trac 0.11.</li>
+ </ul>
+
+ <h3 id="req.other">Other Requirements</h3>
+
+ <p>The <code>fcm conflicts</code> command requires <a href=
+ "http://furius.ca/xxdiff/">xxdiff</a>. At the Met Office we are currently
+ using version 3.1. The <code>fcm diff --graphical</code> command also uses
+ xxdiff by default although other graphical diff tools can also be used.</p>
+
+ <p>The <code>fcm make</code> command uses <code>gzip</code>. At the Met
+ Office we are currently using gzip 1.2.4 on AIX 5.3 and gzip 1.3.3 on RHEL
+ 4.</p>
+
+ <p>The extract system uses <code>diff3</code>, (which is part of <a href=
+ "http://www.gnu.org/software/diffutils/">GNU diffutils</a>), to merge
+ together changes where the same file is modified by two different branches
+ (compared with the base branch). At the Met Office we are currently using
+ version 2.8.1.</p>
+
+ <p>The mirror system uses <a href="http://rsync.samba.org/">rsync</a> to
+ mirror source file to another machine. At the Met Office we are currently
+ using version 2.6.3</p>
+
+ <p>The deprecated <code>fcm build</code> requires <a href=
+ "http://www.gnu.org/software/make/make.html">GNU make</a>. At the Met Office
+ we are currently using version 3.80.</p>
+
+ <p>FCM is intended to run on a Unix/Linux system. It is currently used at the
+ Met Office on Linux (RHEL 4.8) and AIX 5.3.</p>
+
+ <h2 id="ins">Installation</h2>
+
+ <p>FCM is distributed in the form of a compressed tar file. Un-pack the tar
+ file into an appropriate location on your system. Add the <samp>bin/</samp>
+ directory into your <var>PATH</var> environment variable. Once you have done
+ this you should now have full access to the FCM system, assuming that you
+ have met the requirements described in the previous section.</p>
+
+ <p>You should find the following contents in the distribution:</p>
+
+ <dl>
+ <dt>README</dt>
+
+ <dd>The README file contains the internal revision number of the
+ release.</dd>
+
+ <dt>COPYRIGHT.txt<br />
+ LICENSE.html</dt>
+
+ <dd>The FCM license and other copyright information.</dd>
+
+ <dt>bin/</dt>
+
+ <dd>Contains the <code>fcm</code> command and other utilities.</dd>
+
+ <dt>doc/</dt>
+
+ <dd>System documentation.</dd>
+
+ <dt>doc/release_notes/</dt>
+
+ <dd>Contains these release notes. It also contains the release notes for
+ all previous versions which may be useful if you have skipped any
+ versions.</dd>
+
+ <dt>doc/user_guide/</dt>
+
+ <dd>Contains the <a href="../user_guide/">FCM User Guide</a>.</dd>
+
+ <dt>doc/standards/</dt>
+
+ <dd>Contains the FCM <a href="../standards/perl_standard.html">Perl</a> and
+ <a href="../standards/fortran_standard.html">Fortran</a> coding standards.
+ The Perl standard describes the standards followed by the FCM code. The
+ Fortran standard contains some <a href=
+ "../standards/fortran_standard.html#fcm">specific advice</a> on the best
+ way of writing Fortran code for use with FCM as well as more general advice
+ on good practice.</dd>
+
+ <dt>doc/collaboration/</dt>
+
+ <dd>Contains the <a href="../collaboration/index.html">External
+ Distribution & Collaboration for FCM Projects</a> document which
+ discusses how projects configured under FCM can be distributed
+ externally.</dd>
+
+ <dt>etc/</dt>
+
+ <dd>Miscellaneous items, including the <samp>fcm/keyword.cfg.eg</samp> file.
+ If you wish to define keywords for your site you will need to create the
+ <samp>etc/fcm/keyword.cfg</samp> file. An example file,
+ <samp>fcm/keyword.cfg.eg</samp>, is provided which is a copy of the file
+ currently used at the Met Office. For further details please refer to the
+ section <a href="../user_guide/system_admin.html#fcm-keywords">FCM
+ keywords</a> in the System Admin chapter of the User Guide.</dd>
+
+ <dt>examples/</dt>
+
+ <dd>Contains various example scripts which you may find useful. Note that
+ these scripts are all specific to the Met Office and may contain hard coded
+ paths and email addresses. They are provided in the hope that you may find
+ them useful as examples for setting up similar scripts of your own.
+ However, they should only be used after careful review to adapt them to
+ your environment.</dd>
+
+ <dt>examples/etc/regular-update.eg</dt>
+
+ <dd>An example of how you might set up a cron job to make use of the
+ <samp><repos>.latest</samp> file (see
+ <code>examples/svn-hooks/post-commit-background</code>).</dd>
+
+ <dt>examples/lib/</dt>
+
+ <dd>Contains the <code>FCM::Admin::*</code> Perl library, which implements
+ the functionalities of the FCM admin utility commands.</dd>
+
+ <dt>examples/sbin/</dt>
+
+ <dd>Contains a selection of useful admin utility commands.</dd>
+
+ <dt>examples/svn-hooks/pre-commit</dt>
+
+ <dd>
+ This script restricts write-access to the repository by checking the
+ following:
+
+ <ul>
+ <li>It executes the Subversion utility <code>svnperms.py</code> if it,
+ and the associated <samp>svnperms.conf</samp> file, exist. This utility
+ checks whether the author of the current transaction has enough
+ permission to write to particular paths in the repository.</li>
+
+ <li>It checks the disk space required by the current transaction. It
+ fails the commit if it requires more than 5Mb of disk space.</li>
+ </ul>
+ </dd>
+
+ <dt>examples/svn-hooks/post-commit</dt>
+
+ <dd>A simple post-commit hook script which runs the script
+ <code>post-commit-background</code> in the background.</dd>
+
+ <dt>examples/svn-hooks/post-commit-background</dt>
+
+ <dd>
+ This script runs in the background after each commit.
+
+ <ul>
+ <li>It updates a <samp><repos>.latest</samp> file with the latest
+ revision number.</li>
+
+ <li>It creates a dump of the new revision.</li>
+
+ <li>It calls <code>post-commit-background-custom</code> if it
+ exists.</li>
+ </ul>
+ </dd>
+
+ <dt>examples/svn-hooks/pre-revprop-change</dt>
+
+ <dd>A simple pre-revprop-change hook script which runs the script
+ <code>pre-revprop-change.pl</code>.</dd>
+
+ <dt>examples/svn-hooks/pre-revprop-change.pl</dt>
+
+ <dd>If a user attempts to modify the log message of a changeset and he/she
+ is not the original author of the changeset, this script will e-mail the
+ original author. You can also set up a watch facility to monitor changes of
+ log messages that affect particular paths in the repository. For further
+ details please refer to the section <a href=
+ "../user_guide/system_admin.html#svn_watch">Watching changes in log
+ messages</a> in the System Admin chapter of the User Guide.</dd>
+
+ <dt>examples/svn-hooks/post-revprop-change</dt>
+
+ <dd>A simple post-revprop-change hook script which invokes the
+ <code>trac-admin</code> command to <code>resync</code> the revision
+ property cache stored in the corresponding Trac environment.</dd>
+
+ <dt>lib/</dt>
+
+ <dd>Contains the Perl library of FCM.</dd>
+
+ <dt>man/</dt>
+
+ <dd>Contains a basic manual page for <code>fcm</code>.</dd>
+
+ <dt>t/</dt>
+
+ <dd>Contains unit test for FCM.</dd>
+
+ <dt>test/</dt>
+
+ <dd>Contains regression tests for FCM.</dd>
+
+ <dt>test/test_include/</dt>
+
+ <dd>Contains simple test code to check how your chosen compilers handle
+ include files (see <a href="#issues">Known Issues</a>).</dd>
+
+ <dt>tutorial/</dt>
+
+ <dd>Contains the files necessary to set up a Subversion repository for the
+ FCM tutorial. This will allow you to follow the <a href=
+ "../user_guide/getting_started.html#tutorial">tutorial section</a> in the
+ User Guide. See <samp>tutorial/README</samp> on how to set it up.</dd>
+ </dl>
+
+ </div>
+ </div>
+ </div>
+
+ <hr/>
+ <div class="container-fluid text-center">
+ <div class="row-fluid"><div class="span12">
+ <address><small>
+ © British Crown Copyright 2006-14
+ <a href="http://www.metoffice.gov.uk">Met Office</a>.
+ See <a href="../etc/fcm-terms-of-use.html">Terms of Use</a>.<br />
+ This document is released under the British <a href=
+ "http://www.nationalarchives.gov.uk/doc/open-government-licence/" rel=
+ "license">Open Government Licence</a>.<br />
+ </small></address>
+ </div></div>
+ </div>
+
+ <script type="text/javascript" src="../etc/jquery.min.js"></script>
+ <script type="text/javascript" src="../etc/bootstrap/js/bootstrap.min.js"></script>
+ <script type="text/javascript" src="../etc/fcm.js"></script>
+ <script type="text/javascript" src="../etc/fcm-version.js"></script>
+</body>
+</html>
diff --git a/doc/release_notes/2-2.html b/doc/release_notes/2-2.html
new file mode 100644
index 0000000..6198d6b
--- /dev/null
+++ b/doc/release_notes/2-2.html
@@ -0,0 +1,600 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>FCM 2-2 Release Notes</title>
+ <meta name="author" content="FCM team" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link rel="icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link rel="shortcut icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link href="../etc/bootstrap/css/bootstrap.min.css" rel="stylesheet" media="screen" />
+ <link href="../etc/fcm.css" rel="stylesheet" media="screen" />
+</head>
+<body>
+ <div class="navbar navbar-inverse">
+ <div class="navbar-inner">
+ <a class="brand" href=".."><span class="fcm-version">FCM</span></a>
+ <ul class="nav">
+ <li><a href="../installation/">Installation</a></li>
+
+ <li><a href="../user_guide/">User Guide</a></li>
+ </ul>
+ </div>
+ </div>
+
+ <div class="page-header">
+ <div class="fcm-page-content pull-right well well-small"></div>
+ <h1>FCM 2-2 Release Notes <small>15 June 2012</small></h1>
+ </div>
+
+ <div class="container">
+ <div class="row">
+ <div class="span12">
+
+ <p>These are the release notes for FCM 2-2. You can use this release of FCM
+ freely under the terms of the FCM LICENSE, which you should receive with the
+ distribution of this release.</p>
+
+ <p>FCM is maintained by the FCM team at the Met Office. Please feedback any
+ bug reports or feature requests to us by <a href=
+ "mailto:fcm-team at metoffice.gov.uk">e-mail</a>.</p>
+
+ <h2>Contents</h2>
+
+ <div id="fcm-content"></div>
+
+ <h2 id="changes-highlight">Highlight Changes</h2>
+
+ <dl>
+ <dt><code>Subversion 1.6</code></dt>
+
+ <dd>
+ <p>Code management commands and extract system tested with Subversion
+ 1.6. Note that Subversion 1.4 is no longer supported and Subversion 1.5
+ has not been tested.</p>
+
+ <p>We experienced some non-repeatable problems using Subversion 1.6 where
+ we got incorrect results from a merge. Running <code>svn cleanup</code>
+ appears to avoid the problem. Therefore this is now run prior to any
+ update, switch or merge command.</p>
+ </dd>
+
+ <dt><code>fcm branch-create</code></dt>
+
+ <dd>
+ <p><code>-rREV</code> option no longer supported - add a peg revision to
+ the source if necessary.</p>
+ </dd>
+
+ <dt><code>fcm branch-list</code></dt>
+
+ <dd>
+ <p>significant improvement to speed.</p>
+
+ <p>multiple projects can be specified as arguments.</p>
+
+ <p>more options for advanced listing.</p>
+
+ <p><code>-rREV</code> option no longer supported - can use peg revision
+ for each project in the argument list.</p>
+
+ <p>no longer returns 1 if 0 branch is found - non-zero value reserved for
+ fatal errors.</p>
+
+ <p>new <code>--quiet</code> option to print names of matched branches
+ only.</p>
+
+ <p><code>-v</code> option no longer supported - use new
+ <code>--url</code> option to print branch names as full URLs instead of
+ FCM location keywords.</p>
+
+ <p>minor change to output format.</p>
+ </dd>
+
+ <dt><code>fcm conflicts</code></dt>
+
+ <dd>
+ <p>handle resolution of common cases of tree conflicts.</p>
+ </dd>
+
+ <dt><code>fcm make</code></dt>
+
+ <dd>
+ <p>build: change in how the dependency tree is created. This fixes the
+ behaviour where the system incorrectly reports cyclic dependency. An
+ example situation is where a recursive <code>subroutine a</code> calls
+ <code>subroutine b</code> which calls <code>subroutine a</code>. The
+ object file target <samp>a.o</samp> has a dependency on
+ <samp>b.interface</samp> and the object file target <samp>b.o</samp> has a
+ dependency on <samp>a.interface</samp>. Previously,
+ <samp>b.interface</samp> would also depend on <samp>b.o</samp> which would
+ result in a cyclic dependency. This fix changes the internal data
+ structure for representing a build target. <strong>Use of <code>fcm
+ make</code> at release 2-2 to build incrementally or to inherit a build
+ created by <code>fcm make</code> at release 2-1 or before will result in an
+ incorrect behaviour or failure. Use the <code>--new</code> option to start
+ a new build if the build destination is known to contain an existing build
+ created by <code>fcm make</code> at release 2-1 or before.</strong></p>
+ </dd>
+
+ <dt>Flexible repository layout</dt>
+
+ <dd>
+ <p>FCM code management commands no longer insist on a rigid layout of its
+ Subversion repositories. Behaviours can now be configured per repository.
+ In its default setting, FCM code management commands no longer insist on
+ <samp>branches/</samp> and <samp>tags/</samp> being present in a
+ <em>project</em>.</p>
+ </dd>
+ </dl>
+
+ <h2 id="changes-minor">Minor Changes and Bug Fixes</h2>
+
+ <dl>
+ <dt><code>fcm branch-create</code></dt>
+
+ <dd>
+ <p>new <code>--switch</code> option to switch the current working copy to
+ point to the newly created branch.</p>
+ </dd>
+
+ <dt><code>fcm branch-delete</code></dt>
+
+ <dd>
+ <p>new <code>--switch</code> option to switch the current working copy to
+ point to the <em>trunk</em> after the branch deletion.</p>
+ </dd>
+
+ <dt><code>fcm branch-diff</code></dt>
+
+ <dd>
+ <p>fixed: the command will no longer fail when called with an unmodified
+ sub-tree of a branch.</p>
+ </dd>
+
+ <dt><code>fcm build/extract</code></dt>
+
+ <dd>
+ <p>fixed multi-level inheritance destination search path.</p>
+ </dd>
+
+ <dt><code>fcm commit</code></dt>
+
+ <dd>
+ <p>fixed incorrect behaviour on <code>svn commit</code> failure.</p>
+ </dd>
+
+ <dt><code>fcm make</code></dt>
+
+ <dd>
+ <p>build: fixed multi-level inheritance destination search path.</p>
+
+ <p>build: do not use relative path for inherited include paths and object
+ file paths in command lines.</p>
+
+ <p>build: fixed duplicated dependent targets being reported as missing
+ dependencies.</p>
+
+ <p>build: fixed incorrect behaviour with Fortran <samp>*.mod</samp>
+ target in incremental mode where only a build property (e.g. a compiler
+ flag) associated with the source file of the <samp>*.mod</samp> target is
+ modified.</p>
+
+ <p>extract: fixed handling of add/delete of files in file system
+ locations during incremental extract.</p>
+
+ <p>extract: fixed Perl warning when a file system location contains a
+ symbolic link with a non-existent target.</p>
+
+ <p>extract: fixed Perl warning when 2 diff locations both add a file of
+ the same name but with different contents.</p>
+
+ <p>extract: improved merge conflict diagnostics.</p>
+
+ <p>extract: allow unmodified location in the configuration to pass in an
+ inherited mode.</p>
+
+ <p>mirror: fixed: create configuration file in target even if there is no
+ source to mirror.</p>
+
+ <p>mirror: always expand a relative path in a mirror target to a full path
+ to allow it to be inherited. Some infrequently used modifiers in the
+ <code>mirror.prop</code> declarations are added/removed for this fix.</p>
+ </dd>
+
+ <dt><code>fcm merge</code></dt>
+
+ <dd>
+ <p>simplified prompts and diagnostic outputs.</p>
+
+ <p>new <code>--auto-log</code> option for automatic merge. If the option
+ is specified, the command will include the logs of the merged revisions in
+ the commit log message automatically.</p>
+ </dd>
+
+ <dt><code>fcm mkpatch</code></dt>
+
+ <dd>
+ <p>no longer uses patch files if they include a carrriage return in the
+ middle of a line.</p>
+ </dd>
+
+ <dt><code>fcm project-create</code></dt>
+
+ <dd>
+ <p>new command to create a new project and its trunk directory in a
+ repository.</p>
+ </dd>
+
+ <dt>code management command line</dt>
+
+ <dd>
+ <p>fixed logic for parsing <var>URL at REV</var> where <var>URL</var> is not
+ a location keyword but <var>REV</var> is a revision keyword.</p>
+ </dd>
+
+ <dt>revision keyword</dt>
+
+ <dd>
+ <p>fixed: <var>fcm:revision</var> property setting: trailing spaces in
+ each line will now be ignored.</p>
+ </dd>
+
+ <dt>user guide</dt>
+
+ <dd>
+ <p>tutorial: added section on tree conflict resolution.</p>
+
+ <p>tutorial: removed references to <code>fcm gui</code>.</p>
+
+ <p>added new annex: quick reference: tree conflict resolution.</p>
+ </dd>
+
+ <dt>Perl 5.12</dt>
+
+ <dd>
+ <p>fixed: <a href=
+ "http://search.cpan.org/~jesse/perl-5.12.0/pod/perl5120delta.pod#REGEXPs_are_now_first_class">
+ incompatibility problem</a>.</p>
+ </dd>
+ </dl>
+
+ <h2 id="issues">Known Issues</h2>
+
+ <dl>
+ <dt>Build inheritance limitation: handling of include files</dt>
+
+ <dd>See the <a href="../user_guide/make.html#build.inherit">User Guide >
+ FCM Make > Build > Build Inheritance</a> for detail.</dd>
+ </dl>
+
+ <h2 id="requirements">System Requirements</h2>
+
+ <p>FCM is intended to run on a Unix/Linux system. It is currently used at the
+ Met Office on AIX-5.3 and RHEL-6.1.</p>
+
+ <dl>
+ <dt><a href="http://www.perl.org/">Perl</a></dt>
+
+ <dd>
+ <p><dfn>used by:</dfn> <code>fcm</code>.</p>
+
+ <p><dfn>versions at Met Office:</dfn> AIX-5.3: 5.8.2 (see remark),
+ RHEL-6.1: 5.10.1.</p>
+
+ <p><dfn>remark:</dfn> 5.8.2 on AIX-5.3: code management commands, the
+ extract system of <code>fcm make</code> and the deprecated <code>fcm
+ extract</code> are not used by Met Office users on this platform.</p>
+
+ <p><dfn>remark:</dfn> 5.8.2 on AIX-5.3: <code>Text::ParseWords</code>
+ (core Perl module) is upgraded to version 3.22.</p>
+ </dd>
+
+ <dt>Perl module <a href=
+ "http://search.cpan.org/~gaas/libwww-perl-5.834/lib/HTTP/Date.pm">HTTP::Date</a></dt>
+
+ <dd>
+ <p><dfn>used by:</dfn> the extract system in <code>fcm make</code> and
+ the deprecated <code>fcm extract</code>.</p>
+
+ <p><dfn>versions at Met Office:</dfn> RHEL-6.1: 5.833.</p>
+ </dd>
+
+ <dt>Perl module <a href=
+ "http://search.cpan.org/~msergeant/XML-Parser-2.36/">XML::Parser</a></dt>
+
+ <dd>
+ <p><dfn>used by:</dfn> the code management commands.</p>
+
+ <p><dfn>versions at Met Office:</dfn> RHEL-6.1: 2.36.</p>
+ </dd>
+
+ <dt>Perl module <a href=
+ "http://search.cpan.org/~srezic/Tk-804.028/">Tk</a></dt>
+
+ <dd>
+ <p><dfn>used by:</dfn> <code>fcm gui</code>.</p>
+
+ <p><dfn>versions at Met Office:</dfn> RHEL-6.1: 804.028.</p>
+ </dd>
+
+ <dt><a href="http://subversion.apache.org/">Subversion</a></dt>
+
+ <dd>
+ <p><dfn>used by:</dfn> the code management commands, the extract system
+ of <code>fcm make</code>, the deprecated <code>fcm extract</code>.</p>
+
+ <p><dfn>versions at Met Office:</dfn> RHEL-6.1: 1.6.17.</p>
+
+ <p><dfn>remark:</dfn> you can use the extract system to mirror code to a
+ remote platform for building. Therefore it is only necessary to have
+ Subversion installed on the platform where you do your code development.
+ If you use other platforms purely for building and running then you do
+ not need to have Subversion installed on these platforms.</p>
+ </dd>
+
+ <dt><a href="http://trac.edgewall.org/">Trac</a></dt>
+
+ <dd>
+ <p><dfn>used by:</dfn> (optional, but highly recommended as a companion
+ to Subversion)</p>
+
+ <p><dfn>versions at Met Office:</dfn> 0.11.7.</p>
+ </dd>
+
+ <dt><a href="http://furius.ca/xxdiff/">xxdiff</a></dt>
+
+ <dd>
+ <p><dfn>used by:</dfn> <code>fcm branch-diff --graphical</code>,
+ <code>fcm conflicts</code>, <code>fcm diff --graphical</code>.</p>
+
+ <p><dfn>versions at Met Office:</dfn> RHEL-6.1: 3.2.</p>
+
+ <p><dfn>remark:</dfn> The <code>fcm branch-diff --graphical</code> and
+ <code>fcm diff --graphical</code> commands use xxdiff by default but can
+ also use other graphical diff tools.</p>
+ </dd>
+
+ <dt><a href="http://www.gzip.org/">gzip</a></dt>
+
+ <dd>
+ <p><dfn>used by:</dfn> <code>fcm make</code>.</p>
+
+ <p><dfn>versions at Met Office:</dfn> AIX-5.3: 1.2.4, RHEL-6.1: 1.3.12.</p>
+ </dd>
+
+ <dt><a href="http://www.gnu.org/software/diffutils/">GNU diffutils</a>:
+ diff3</dt>
+
+ <dd>
+ <p><dfn>used by:</dfn> the extract system of <code>fcm make</code>, the
+ deprecated <code>fcm extract</code>.</p>
+
+ <p><dfn>versions at Met Office:</dfn> RHEL-6.1: 2.8.1.</p>
+
+ <p><dfn>remark:</dfn>: used to merge changes to source files modified by
+ 2+ diff source trees (compared with the base).</p>
+ </dd>
+
+ <dt><a href="http://rsync.samba.org/">rsync</a></dt>
+
+ <dd>
+ <p><dfn>used by:</dfn> the mirror system of <code>fcm make</code>, the
+ deprecated <code>fcm extract</code>.</p>
+
+ <p><dfn>versions at Met Office:</dfn> AIX-5.3: 2.6.2, RHEL-6.1: 3.0.6.</p>
+
+ <p><dfn>remark:</dfn> used to mirror source file to another
+ <var>USER at HOST</var>.</p>
+ </dd>
+
+ <dt><a href="http://www.gnu.org/software/make/make.html">GNU make</a></dt>
+
+ <dd>
+ <p><dfn>used by:</dfn> the deprecated <code>fcm build</code>.</p>
+
+ <p><dfn>versions at Met Office:</dfn> AIX-5.3: 3.80, RHEL-6.1: 3.81.</p>
+ </dd>
+ </dl>
+
+ <h2 id="installation">Installation</h2>
+
+ <p>FCM is distributed in the form of a compressed tar file. Un-pack the tar
+ file into an appropriate location on your system. Add the <samp>bin/</samp>
+ directory into your <var>PATH</var> environment variable. Once you have done
+ this you should now have full access to the FCM system, assuming that you
+ have met the requirements described in the previous section.</p>
+
+ <p>You should find the following contents in the distribution:</p>
+
+ <dl>
+ <dt>README</dt>
+
+ <dd>The README file contains the internal revision number of the
+ release.</dd>
+
+ <dt>COPYRIGHT.txt<br />
+ LICENSE.html</dt>
+
+ <dd>The FCM license and other copyright information.</dd>
+
+ <dt>bin/</dt>
+
+ <dd>Contains the <code>fcm</code> command and other utilities.</dd>
+
+ <dt>doc/</dt>
+
+ <dd>System documentation.</dd>
+
+ <dt>doc/release_notes/</dt>
+
+ <dd>Contains these release notes. It also contains the release notes for
+ all previous versions which may be useful if you have skipped any
+ versions.</dd>
+
+ <dt>doc/user_guide/</dt>
+
+ <dd>Contains the <a href="../user_guide/">FCM User Guide</a>.</dd>
+
+ <dt>doc/standards/</dt>
+
+ <dd>Contains the FCM <a href="../standards/perl_standard.html">Perl</a> and
+ <a href="../standards/fortran_standard.html">Fortran</a> coding standards.
+ The Perl standard describes the standards followed by the FCM code. The
+ Fortran standard contains some <a href=
+ "../standards/fortran_standard.html#fcm">specific advice</a> on the best
+ way of writing Fortran code for use with FCM as well as more general advice
+ on good practice.</dd>
+
+ <dt>doc/collaboration/</dt>
+
+ <dd>Contains the <a href="../collaboration/index.html">External
+ Distribution & Collaboration for FCM Projects</a> document which
+ discusses how projects configured under FCM can be distributed
+ externally.</dd>
+
+ <dt>etc/</dt>
+
+ <dd>Miscellaneous items, including the <samp>fcm/keyword.cfg.eg</samp>
+ file. If you wish to define keywords for your site you will need to create
+ the <samp>etc/fcm/keyword.cfg</samp> file. An example file,
+ <samp>fcm/keyword.cfg.eg</samp>, is provided which is a copy of the file
+ currently used at the Met Office. For further details please refer to the
+ section <a href="../user_guide/system_admin.html#fcm-keywords">FCM
+ keywords</a> in the System Admin chapter of the User Guide.</dd>
+
+ <dt>examples/</dt>
+
+ <dd>Contains various example scripts which you may find useful. Note that
+ these scripts are all specific to the Met Office and may contain hard coded
+ paths and email addresses. They are provided in the hope that you may find
+ them useful as examples for setting up similar scripts of your own.
+ However, they should only be used after careful review to adapt them to
+ your environment.</dd>
+
+ <dt>examples/etc/regular-update.eg</dt>
+
+ <dd>An example of how you might set up a cron job to make use of the
+ <samp><repos>.latest</samp> file (see
+ <code>examples/svn-hooks/post-commit-background</code>).</dd>
+
+ <dt>examples/lib/</dt>
+
+ <dd>Contains the <code>FCM::Admin::*</code> Perl library, which implements
+ the functionalities of the FCM admin utility commands.</dd>
+
+ <dt>examples/sbin/</dt>
+
+ <dd>Contains a selection of useful admin utility commands.</dd>
+
+ <dt>examples/svn-hooks/pre-commit</dt>
+
+ <dd>
+ This script restricts write-access to the repository by checking the
+ following:
+
+ <ul>
+ <li>It executes the Subversion utility <code>svnperms.py</code> if it,
+ and the associated <samp>svnperms.conf</samp> file, exist. This utility
+ checks whether the author of the current transaction has enough
+ permission to write to particular paths in the repository.</li>
+
+ <li>It checks the disk space required by the current transaction. It
+ fails the commit if it requires more than 10MB of disk space (or
+ whatever is specified in the
+ <code>pre-commit-size-threshold.conf</code> file.</li>
+ </ul>
+ </dd>
+
+ <dt>examples/svn-hooks/post-commit</dt>
+
+ <dd>This script runs <code>post-commit-background</code> in the
+ background.</dd>
+
+ <dt>examples/svn-hooks/post-commit-background</dt>
+
+ <dd>
+ This script runs in the background after each commit.
+
+ <ul>
+ <li>It updates a <samp><repos>.latest</samp> file with the latest
+ revision number.</li>
+
+ <li>It creates a dump of the new revision.</li>
+
+ <li>It calls <code>post-commit-background-custom</code> if it
+ exists.</li>
+ </ul>
+ </dd>
+
+ <dt>examples/svn-hooks/pre-revprop-change</dt>
+
+ <dd>This script only allows the modification of <var>svn:log</var>.</dd>
+
+ <dt>examples/svn-hooks/post-revprop-change</dt>
+
+ <dd>This script runs <code>post-revprop-change-background</code> in the
+ background.</dd>
+
+ <dt>examples/svn-hooks/post-revprop-change-background</dt>
+
+ <dd>This script invokes the <code>trac-admin</code> command to
+ <code>resync</code> the revision property cache stored in the corresponding
+ Trac environment. If a user modifies the log message of a changeset and
+ he/she is not the original author of the changeset, this script will e-mail
+ the original author. If the file
+ <code>post-revprop-change-background-cc.list</code> exits, the script will
+ also e-mail those in the list.</dd>
+
+ <dt>lib/</dt>
+
+ <dd>Contains the Perl library of FCM.</dd>
+
+ <dt>man/</dt>
+
+ <dd>Contains a basic manual page for <code>fcm</code>.</dd>
+
+ <dt>t/</dt>
+
+ <dd>Contains unit test for FCM.</dd>
+
+ <dt>test/</dt>
+
+ <dd>Contains regression tests for FCM.</dd>
+
+ <dt>test/test_include/</dt>
+
+ <dd>Contains simple test code to check how your chosen compilers handle
+ include files (see <a href="#issues">Known Issues</a>).</dd>
+
+ <dt>tutorial/</dt>
+
+ <dd>Contains the files necessary to set up a Subversion repository for the
+ FCM tutorial. This will allow you to follow the <a href=
+ "../user_guide/getting_started.html#tutorial">tutorial section</a> in the
+ User Guide. See <samp>tutorial/README</samp> on how to set it up.</dd>
+ </dl>
+
+ </div>
+ </div>
+ </div>
+
+ <hr/>
+ <div class="container-fluid text-center">
+ <div class="row-fluid"><div class="span12">
+ <address><small>
+ © British Crown Copyright 2006-14
+ <a href="http://www.metoffice.gov.uk">Met Office</a>.
+ See <a href="../etc/fcm-terms-of-use.html">Terms of Use</a>.<br />
+ This document is released under the British <a href=
+ "http://www.nationalarchives.gov.uk/doc/open-government-licence/" rel=
+ "license">Open Government Licence</a>.<br />
+ </small></address>
+ </div></div>
+ </div>
+
+ <script type="text/javascript" src="../etc/jquery.min.js"></script>
+ <script type="text/javascript" src="../etc/bootstrap/js/bootstrap.min.js"></script>
+ <script type="text/javascript" src="../etc/fcm.js"></script>
+ <script type="text/javascript" src="../etc/fcm-version.js"></script>
+</body>
+</html>
diff --git a/doc/release_notes/2-3-1.html b/doc/release_notes/2-3-1.html
new file mode 100644
index 0000000..8b6e92c
--- /dev/null
+++ b/doc/release_notes/2-3-1.html
@@ -0,0 +1,117 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>FCM 2-3-1 Release Notes</title>
+ <meta name="author" content="FCM team" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link rel="icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link rel="shortcut icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link href="../etc/bootstrap/css/bootstrap.min.css" rel="stylesheet" media="screen" />
+ <link href="../etc/fcm.css" rel="stylesheet" media="screen" />
+</head>
+<body>
+ <div class="navbar navbar-inverse">
+ <div class="navbar-inner">
+ <a class="brand" href=".."><span class="fcm-version">FCM</span></a>
+ <ul class="nav">
+ <li><a href="../installation/">Installation</a></li>
+
+ <li><a href="../user_guide/">User Guide</a></li>
+ </ul>
+ </div>
+ </div>
+
+ <div class="page-header">
+ <div class="fcm-page-content pull-right well well-small"></div>
+ <h1>FCM 2-3-1 Release Notes <small>2013-04-05</small></h1>
+ </div>
+
+ <div class="container">
+ <div class="row">
+ <div class="span12">
+
+ <p>These are the release notes for FCM 2-3-1. This release of FCM is distributed
+ under the terms of the <a href="http://www.gnu.org/licenses/gpl.html"
+ rel="license">GNU General Public License</a>. See <a href=
+ "../etc/fcm-terms-of-use.html">Terms of Use</a> for detail.</p>
+
+ <p>If FCM is not yet installed at your site, please refer to <a href=
+ "../installation/">FCM: Installation</a> for detail.</p>
+
+ <h2>Contents</h2>
+
+ <div id="fcm-content"></div>
+
+ <h2 id="changes-highlight">Highlight Changes</h2>
+
+ <p>This is a bug fix release.</p>
+
+ <h2 id="changes-minor">Minor Changes and Bug Fixes</h2>
+
+ <dl>
+ <dt>Configuration file syntax</dt>
+
+ <dd>If a setting accepts a space delimited list of locations, it is now
+ possible to use <var>$HERE</var> in front of any of the locations in the
+ list.</dd>
+
+ <dt>User configuration file location</dt>
+
+ <dd>The user configuration file should now be placed at
+ <var>$HOME/.metomi/fcm/</var>. The old location
+ <var>$HOME/.met-um/fcm/</var> is still supported but is deprecated.</dd>
+
+ <dt><code>fcm make</code></dt>
+
+ <dd>
+ <p>Reduced run time memory footprint.</p>
+
+ <p>Build: Create the <var>include/</var> sub-directory for <var>*.o</var>
+ targets of Fortran source files containing modules. Some compilers will
+ put the <var>*.mod</var> files directly into the <var>include/</var>
+ sub-directory.</p>
+
+ <p>Build: <var>ns-dep.o</var> property improved. Its value will now accept
+ namespaces at the source file level. It will also accept <kbd>/</kbd> as
+ the root namespace. (Previously, it required a pair of quotes
+ <kbd>""</kbd>.) A bad namespace in the value will now trigger a missing
+ dependency error.</p>
+
+ <p>Build: handle <var>.</var> and <var>..</var> in value of
+ <var>source</var> declaration.</p>
+ </dd>
+ </dl>
+
+ <h2 id="issues">Known Issues</h2>
+
+ <dl>
+ <dt>Build inheritance limitation: handling of include files</dt>
+
+ <dd>See the <a href="../user_guide/make.html#build.inherit">User Guide >
+ FCM Make > Build > Build Inheritance</a> for detail.</dd>
+ </dl>
+
+ </div>
+ </div>
+ </div>
+
+ <hr/>
+ <div class="container-fluid text-center">
+ <div class="row-fluid"><div class="span12">
+ <address><small>
+ © British Crown Copyright 2006-14
+ <a href="http://www.metoffice.gov.uk">Met Office</a>.
+ See <a href="../etc/fcm-terms-of-use.html">Terms of Use</a>.<br />
+ This document is released under the British <a href=
+ "http://www.nationalarchives.gov.uk/doc/open-government-licence/" rel=
+ "license">Open Government Licence</a>.<br />
+ </small></address>
+ </div></div>
+ </div>
+
+ <script type="text/javascript" src="../etc/jquery.min.js"></script>
+ <script type="text/javascript" src="../etc/bootstrap/js/bootstrap.min.js"></script>
+ <script type="text/javascript" src="../etc/fcm.js"></script>
+ <script type="text/javascript" src="../etc/fcm-version.js"></script>
+</body>
+</html>
diff --git a/doc/release_notes/2-3.html b/doc/release_notes/2-3.html
new file mode 100644
index 0000000..da5e9b2
--- /dev/null
+++ b/doc/release_notes/2-3.html
@@ -0,0 +1,136 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>FCM 2-3 Release Notes</title>
+ <meta name="author" content="FCM team" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link rel="icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link rel="shortcut icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link href="../etc/bootstrap/css/bootstrap.min.css" rel="stylesheet" media="screen" />
+ <link href="../etc/fcm.css" rel="stylesheet" media="screen" />
+</head>
+<body>
+ <div class="navbar navbar-inverse">
+ <div class="navbar-inner">
+ <a class="brand" href=".."><span class="fcm-version">FCM</span></a>
+ <ul class="nav">
+ <li><a href="../installation/">Installation</a></li>
+
+ <li><a href="../user_guide/">User Guide</a></li>
+ </ul>
+ </div>
+ </div>
+
+ <div class="page-header">
+ <div class="fcm-page-content pull-right well well-small"></div>
+ <h1>FCM 2-3 Release Notes <small>26 October 2012</small></h1>
+ </div>
+
+ <div class="container">
+ <div class="row">
+ <div class="span12">
+
+ <p>These are the release notes for FCM 2-3. This release of FCM is distributed
+ under the terms of the <a href="http://www.gnu.org/licenses/gpl.html"
+ rel="license">GNU General Public License</a>. See <a href=
+ "../etc/fcm-terms-of-use.html">Terms of Use</a> for detail.</p>
+
+ <p>If FCM is not yet installed at your site, please refer to <a href=
+ "../installation/">FCM: Installation</a> for detail.</p>
+
+ <h2>Contents</h2>
+
+ <div id="fcm-content"></div>
+
+ <h2 id="changes-highlight">Highlight Changes</h2>
+
+ <dl>
+ <dt><a href="http://www.gnu.org/licenses/gpl.html" rel="license">GNU General
+ Public License</a></dt>
+
+ <dd>
+ <p>From this release onwards, FCM will be released under the terms of the
+ GNU General Public License.</p>
+ </dd>
+
+ <dt><code>fcm make</code></dt>
+
+ <dd>
+ <p>Build system now recognises all text files starting with a
+ <code>#!</code> line as scripts.</p>
+
+ <p>Build system target selection logic has been improved to work more
+ efficiently, and to work correctly with a complex case of non-fatal cyclic
+ dependency.</p>
+ </dd>
+ </dl>
+
+ <h2 id="changes-minor">Minor Changes and Bug Fixes</h2>
+
+ <dl>
+ <dt><code>fcm branch-create</code></dt>
+
+ <dd>
+ <p>Fixed: no longer issue an incorrect error when creating a branch for a
+ project that resides at the root level of a repository.</p>
+ </dd>
+
+ <dt><code>fcm make</code></dt>
+
+ <dd>
+ <p>The behaviour of the <code>--new</code> option is modified to remove
+ only step directories defined in the <code>steps</code> declaration
+ instead of all known steps in the configuration.</p>
+
+ <p>Mirror: write to the mirror target a <samp>fcm-make.cfg.orig</samp> file
+ containing the configuration of the original <code>fcm make</code>
+ invocation.</p>
+ </dd>
+
+ <dt><code>fcm loc-layout</code></dt>
+
+ <dd>
+ <p>New command to parse a Subversion target and print the FCM layout
+ information of its URL.</p>
+
+ <dt><code>fcm mkpatch</code></dt>
+
+ <dd>
+ <p>Import scripts now uses <code>/bin/bash</code> instead of
+ <code>/bin/sh</code>.</p>
+ </dd>
+ </dl>
+
+ <h2 id="issues">Known Issues</h2>
+
+ <dl>
+ <dt>Build inheritance limitation: handling of include files</dt>
+
+ <dd>See the <a href="../user_guide/make.html#build.inherit">User Guide >
+ FCM Make > Build > Build Inheritance</a> for detail.</dd>
+ </dl>
+
+ </div>
+ </div>
+ </div>
+
+ <hr/>
+ <div class="container-fluid text-center">
+ <div class="row-fluid"><div class="span12">
+ <address><small>
+ © British Crown Copyright 2006-14
+ <a href="http://www.metoffice.gov.uk">Met Office</a>.
+ See <a href="../etc/fcm-terms-of-use.html">Terms of Use</a>.<br />
+ This document is released under the British <a href=
+ "http://www.nationalarchives.gov.uk/doc/open-government-licence/" rel=
+ "license">Open Government Licence</a>.<br />
+ </small></address>
+ </div></div>
+ </div>
+
+ <script type="text/javascript" src="../etc/jquery.min.js"></script>
+ <script type="text/javascript" src="../etc/bootstrap/js/bootstrap.min.js"></script>
+ <script type="text/javascript" src="../etc/fcm.js"></script>
+ <script type="text/javascript" src="../etc/fcm-version.js"></script>
+</body>
+</html>
diff --git a/doc/release_notes/index.html b/doc/release_notes/index.html
new file mode 100644
index 0000000..a1ec0b9
--- /dev/null
+++ b/doc/release_notes/index.html
@@ -0,0 +1,105 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>FCM Release Notes (Old)</title>
+ <meta name="author" content="FCM team" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link rel="icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link rel="shortcut icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link href="../etc/bootstrap/css/bootstrap.min.css" rel="stylesheet" media="screen" />
+ <link href="../etc/fcm.css" rel="stylesheet" media="screen" />
+</head>
+<body>
+ <div class="navbar navbar-inverse">
+ <div class="navbar-inner">
+ <a class="brand" href=".."><span class="fcm-version">FCM</span></a>
+ <ul class="nav">
+ <li><a href="../installation/">Installation</a></li>
+
+ <li><a href="../user_guide/">User Guide</a></li>
+ </ul>
+ </div>
+ </div>
+
+ <div class="page-header">
+ <!--div class="fcm-page-content pull-right well well-small"></div-->
+ <h1>FCM Release Notes (Old)</h1>
+ </div>
+
+ <div class="container">
+ <div class="row">
+ <div class="span12">
+
+ <p>Current release notes can be found at
+ <a href="https://github.com/metomi/fcm/blob/master/CHANGES.md">FCM Changes</a>
+ or the <var>CHANGES.md</var> file of your local installation of FCM.</p>
+
+ <p>Old release notes:</p>
+
+ <dl>
+ <dt><a href="2-3-1.html">2-3-1</a></dt>
+
+ <dd>Released on 2013-04-05. Bug fix release.</dd>
+
+ <dt><a href="2-3.html">2-3</a></dt>
+
+ <dd>Released on 2012-10-26. GPL release.</dd>
+
+ <dt><a href="2-2.html">2-2</a></dt>
+
+ <dd>Released on 2012-06-15. Subversion 1.6 upgrade.</dd>
+
+ <dt><a href="2-1.html">2-1</a></dt>
+
+ <dd>Released on 2011-07-22. Bug fix release.</dd>
+
+ <dt><a href="2-0.html">2-0</a></dt>
+
+ <dd>Released on 2011-03-11. New extract and build system.</dd>
+
+ <dt><a href="1-5.html">1-5</a></dt>
+
+ <dd>Released on 2010-01-22. New features and bug fixes.</dd>
+
+ <dt><a href="1-4.html">1-4</a></dt>
+
+ <dd>Released on 2009-02-12. Bug fix release.</dd>
+
+ <dt><a href="1-3.html">1-3</a></dt>
+
+ <dd>Released on 2008-01-30. Major update to the extract and build
+ systems.</dd>
+
+ <dt><a href="1-2.html">1-2</a></dt>
+
+ <dd>Released on 2007-03-22. Bug fix release.</dd>
+
+ <dt><a href="1-1.html">1-1</a></dt>
+
+ <dd>Released on 2006-11-06. First external release of FCM.</dd>
+ </dl>
+
+ </div>
+ </div>
+ </div>
+
+ <hr/>
+ <div class="container-fluid text-center">
+ <div class="row-fluid"><div class="span12">
+ <address><small>
+ © British Crown Copyright 2006-14
+ <a href="http://www.metoffice.gov.uk">Met Office</a>.
+ See <a href="../etc/fcm-terms-of-use.html">Terms of Use</a>.<br />
+ This document is released under the British <a href=
+ "http://www.nationalarchives.gov.uk/doc/open-government-licence/" rel=
+ "license">Open Government Licence</a>.<br />
+ </small></address>
+ </div></div>
+ </div>
+
+ <script type="text/javascript" src="../etc/jquery.min.js"></script>
+ <script type="text/javascript" src="../etc/bootstrap/js/bootstrap.min.js"></script>
+ <script type="text/javascript" src="../etc/fcm.js"></script>
+ <script type="text/javascript" src="../etc/fcm-version.js"></script>
+</body>
+</html>
diff --git a/doc/user_guide/annex_bld_cfg.html b/doc/user_guide/annex_bld_cfg.html
new file mode 100644
index 0000000..9e15477
--- /dev/null
+++ b/doc/user_guide/annex_bld_cfg.html
@@ -0,0 +1,931 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>FCM: User Guide: Annex: Declarations in FCM 1 build configuration
+ file</title>
+ <meta name="author" content="FCM team" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link rel="icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link rel="shortcut icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link href="../etc/bootstrap/css/bootstrap.min.css" rel="stylesheet" media="screen" />
+ <link href="../etc/fcm.css" rel="stylesheet" media="screen" />
+</head>
+<body>
+ <div class="navbar navbar-inverse">
+ <div class="navbar-inner">
+ <a class="brand" href=".."><span class="fcm-version">FCM</span></a>
+ <ul class="nav">
+ <li><a href="../installation/">Installation</a></li>
+
+ <li class="active"><a href=".">User Guide</a></li>
+ </ul>
+ </div>
+ </div>
+
+ <div class="page-header">
+ <div class="fcm-page-content pull-right well well-small"></div>
+ <h1>FCM: User Guide: Annex: Declarations in FCM 1 build configuration
+ file</h1>
+ </div>
+
+ <div class="container">
+ <div class="row">
+ <div class="span12">
+
+ <p><em>The FCM 1 build system is deprecated. The documentation for the current
+ build system can be found at <a href="make.html">FCM Make</a>.</em></p>
+
+ <p>The following is a list of supported declarations for the configuration
+ file used by the FCM build system. Unless otherwise stated, the fields in all
+ declaration labels are not case sensitive. Build declarations can be made
+ either in a build configuration file or in an extract configuration file. In
+ the latter case, the prefix <code>BLD::</code> must be added at the beginning
+ of each label to inform the extract system that the declaration is a build
+ system declaration. (In a build configuration file, the prefix
+ <code>BLD::</code> is optional.)</p>
+
+ <dl>
+ <dt>CFG::TYPE</dt>
+
+ <dd>
+ <p>The configuration file type, the value should always be
+ <samp>bld</samp> for a build configuration file. This declaration is
+ compulsory for all build configuration files. (This declaration is
+ automatic when the extract system creates a build configuration
+ file.)</p>
+
+ <p>Example:</p>
+ <pre>
+cfg::type bld
+</pre>
+ </dd>
+
+ <dt>CFG::VERSION</dt>
+
+ <dd>
+ <p>The file format version, currently <samp>1.0</samp> - a version is
+ included so that we shall be able to read the configuration file
+ correctly should we decide to change its format in the future. (This
+ declaration is automatic when the extract system creates a build
+ configuration file.)</p>
+
+ <p>Example:</p>
+ <pre>
+cfg::version 1.0
+</pre>
+ </dd>
+
+ <dt>%<name></dt>
+
+ <dd>
+ <p><code>%<name></code> declares an internal variable
+ <var><name></var> that can later be re-used.</p>
+
+ <p>Example:</p>
+ <pre>
+%my_variable -foo -bar
+tool::fflags %my_variable
+tool::cflags %my_variable
+</pre>
+ </dd>
+
+ <dt>INC</dt>
+
+ <dd>
+ <p>This declares the name of a file containing build configuration. The
+ lines in the declared file will be included inline to the current
+ configuration file.</p>
+
+ <p>Example:</p>
+ <pre>
+inc ~frva/var_stable_22.0/cfg/bld.cfg
+# ... and then your changes ...
+</pre>
+ </dd>
+
+ <dt>
+ DEST[::ROOTDIR]<br />
+ <del>DIR::ROOT</del>
+ </dt>
+
+ <dd>
+ <p>The destination of the build. It must be declared for each build.
+ (This declaration is automatic when the extract system creates a build
+ configuration file. The value is normally the path of the extract
+ destination.)</p>
+
+ <p>Example:</p>
+ <pre>
+dest $HOME/my_build
+</pre>
+ </dd>
+
+ <dt>USE</dt>
+
+ <dd>
+ <p>This inherits settings from a previous build. The value must be must
+ be either the configuration file or the root directory of a successful
+ build. Output of the build, the tools, the exclude dependency
+ declarations, the file type registers declarations are automatically
+ inherited from the declared build. Source directories and build targets
+ declarations may be inherited depending on the INHERIT declarations. (If
+ you have a USE declaration in an extract, the resulting build
+ configuration file will contain an automatic USE declaration, which
+ expects an inherited build at the extract destination.)</p>
+
+ <p>Example:</p>
+ <pre>
+# Use VAR build 22.0
+USE ~frva/var_22.0
+</pre>
+ </dd>
+
+ <dt>INHERIT::<name>[::<pcks>]</dt>
+
+ <dd>
+ <p>This declares whether build targets (<name> =
+ <samp>target</samp>) or source directories (<name> =
+ <samp>src</samp>) can be inherited using the USE statement. By default,
+ source directories are inherited, while build targets are not. Use the
+ value <samp>true</samp> to switch on inheritance, or <samp>false</samp>
+ to switch off. For source directories declarations, the name of a
+ sub-package <pcks> can be specified. If a sub-package pcks is
+ specified, the declaration applies only to the files and directories
+ under the sub-package. Otherwise, the declaration applies globally.</p>
+
+ <p>Example:</p>
+ <pre>
+inherit::target true
+inherit::src false
+</pre>
+ </dd>
+
+ <dt>SRC[::<pcks>]</dt>
+
+ <dd>
+ <p>This declares a source file/directory. You must specify the
+ sub-package <pcks> if the source file/directory is located outside
+ of the <samp>src/</samp> sub-directory of the build destination or if you
+ want to redefine the sub-package name of the source file/directory. The
+ name of the sub-package <pcks> must be unique. Package names are
+ delimited by double colons <code>::</code> or double underscores
+ <code>__</code>. If you declare a relative path, it is assumed to be
+ relative to the <samp>src/</samp> sub-directory of the build destination.
+ (This declaration is automatic when the extract system creates the build
+ configuration file. The list of declared source directories will be the
+ list of extracted source directories.)</p>
+
+ <p>Example:</p>
+ <pre>
+src::var/code/VarMod_PF $HOME/var/src/code/VarMod_PF
+</pre>
+ </dd>
+
+ <dt>SEARCH_SRC</dt>
+
+ <dd>
+ <p>This declares a flag to determine whether the build system should
+ search the <samp>src/</samp> sub-directory of the build root for a list
+ of source files. The automatic search is useful if the build system is
+ invoked standalone and the <samp>src/</samp> sub-directory contains the
+ full source tree of the build. The default is to search
+ (<samp>true</samp>). Set the flag to <samp>false</samp> to switch off the
+ behaviour. (When the extract system creates a build configuration file,
+ it declares all source files. Searching of the source sub-directory
+ should not be required, and so this flag is automatically set to
+ <samp>false</samp>.)</p>
+
+ <p>Example:</p>
+ <pre>
+search_src false
+</pre>
+ </dd>
+
+ <dt>TARGET</dt>
+
+ <dd>
+ <p>Specify the targets for the build. Multiple targets can be declared in
+ one or more declarations. These targets become the dependencies of the
+ default <samp>all</samp> target in the <em>Makefile</em>. It is worth
+ noting that <code>TARGET</code> declarations are cumulative. A later
+ declaration does not override an earlier one - it simply adds more targets
+ to the list.</p>
+
+ <p>Example:</p>
+ <pre>
+target VarScr_AnalysePF VarScr_CovAccStats
+target VarScr_CovPFstats
+</pre>
+ </dd>
+
+ <dt>TOOL::<label>[::<pcks>]</dt>
+
+ <dd>
+ <p>This declaration is used to specify a build tool such as the Fortran
+ compiler or its flags. The <label> determines the tool you are
+ declaring. A TOOL declaration normally applies globally. However, where
+ it is sensible to do so, a sub-package <pcks> can be specified. In
+ which case, the declaration applies only to the files and directories
+ under the sub-package. A list of <label> fields is available
+ <a href="#tools-list">later in this annex</a>.</p>
+
+ <p>Example:</p>
+ <pre>
+tool::fc sxmpif90
+tool::fflags -Chopt -Pstack
+
+tool::cc sxmpic++
+tool::cflags -O nomsg -pvctl nomsg
+
+tool::ar sxar
+</pre>
+ </dd>
+
+ <dt>EXE_DEP[::<target>]</dt>
+
+ <dd>
+ <p>This declares an extra dependency for either all main program targets
+ or only <target> if it is specified. If <target> is
+ specified, it must be the name of a main program target. The value of the
+ declaration is a space delimited list. Each item in the list can either
+ be a valid name of a sub-package or the name of a valid object target. If
+ a sub-package name is used, the <em>make</em> rule for the main program
+ will be set to depend on all (non-program) object files within the
+ sub-package.</p>
+
+ <p>Example:</p>
+ <pre>
+# Only foo.exe to depend on the package foo::bar and egg.o
+exe_dep::foo.exe foo::bar egg.o
+
+# All executables to depend on the package foo::bar and egg.o
+exe_dep foo::bar egg.o
+
+# Only foo.exe to depend on all objects
+exe_dep::foo.exe
+
+# All executables to depend on all objects
+exe_dep
+</pre>
+ </dd>
+
+ <dt>BLOCKDATA[::<target>]</dt>
+
+ <dd>
+ <p>This declares a BLOCKDATA dependency for either all main program
+ targets or only <target> if it is specified. If <target> is
+ specified, it must be the name of a main program target. The value of the
+ declaration is a space delimited list. Each item in the list must be the
+ name of a valid object target containing a Fortran BLOCKDATA program
+ unit.</p>
+
+ <p>Example:</p>
+ <pre>
+# Only foo.exe to depend on blkdata.o
+blockdata::foo.exe blkdata.o
+
+# All executables to depend on fbd.o
+blockdata fbd.o
+</pre>
+ </dd>
+
+ <dt>EXCL_DEP[::<pcks>]</dt>
+
+ <dd>
+ <p>This declaration is used to specify whether a particular dependency
+ should be ignored during the automatic dependency scan. If a sub-package
+ <pcks> is specified, the declaration applies only to the files and
+ directories under the sub-package. Otherwise, the declaration applies
+ globally. The value of this declaration must contain one or two fields
+ (separated by the double colon <code>::</code>). The first field denotes
+ the dependency type, and the second field is the dependency target. If
+ the second field is specified, it will only exclude the dependency to the
+ specified target. Otherwise, it will exclude all dependency to the
+ specified type. The following dependency types are supported:</p>
+
+ <dl id="dependency-types">
+ <dt>USE</dt>
+
+ <dd>The dependency target is a Fortran module.</dd>
+
+ <dt>INTERFACE</dt>
+
+ <dd>The dependency target is a Fortran 9X interface block file.</dd>
+
+ <dt>INC</dt>
+
+ <dd>The dependency target is a Fortran INCLUDE file.</dd>
+
+ <dt>H</dt>
+
+ <dd>The dependency target is a pre-processor #include header file.</dd>
+
+ <dt>OBJ</dt>
+
+ <dd>The dependency target is a compiled binary object file.</dd>
+
+ <dt>EXE</dt>
+
+ <dd>The dependency target is an executable binary or script.</dd>
+ </dl>
+
+ <p>N.B. The following dependency targets are in the default list of
+ excluded dependencies:</p>
+
+ <dl>
+ <dt>Intrinsic Fortran modules:</dt>
+
+ <dd>
+ <ul>
+ <li>USE::ISO_C_BINDING</li>
+
+ <li>USE::IEEE_EXCEPTIONS</li>
+
+ <li>USE::IEEE_ARITHMETIC</li>
+
+ <li>USE::IEEE_FEATURES</li>
+ </ul>
+ </dd>
+
+ <dt>Intrinsic Fortran subroutines:</dt>
+
+ <dd>
+ <ul>
+ <li>OBJ::CPU_TIME</li>
+
+ <li>OBJ::GET_COMMAND</li>
+
+ <li>OBJ::GET_COMMAND_ARGUMENT</li>
+
+ <li>OBJ::GET_ENVIRONMENT_VARIABLE</li>
+
+ <li>OBJ::MOVE_ALLOC</li>
+
+ <li>OBJ::MVBITS</li>
+
+ <li>OBJ::RANDOM_NUMBER</li>
+
+ <li>OBJ::RANDOM_SEED</li>
+
+ <li>OBJ::SYSTEM_CLOCK</li>
+ </ul>
+ </dd>
+
+ <dt>Dummy declarations:</dt>
+
+ <dd>
+ <ul>
+ <li>OBJ::NONE</li>
+
+ <li>EXE::NONE</li>
+ </ul>
+ </dd>
+ </dl>
+
+ <p>Example:</p>
+ <pre>
+excl_dep USE::YourFortranMod
+excl_dep INTERFACE::HerFortran.interface
+excl_dep INC::HisFortranInc.inc
+excl_dep H::TheirHeader.h
+excl_dep OBJ
+excl_dep EXE
+</pre>
+ </dd>
+
+ <dt>DEP::<pcks></dt>
+
+ <dd>
+ <p>This declaration is used to specify a dependency for a source file in
+ <pcks>. The value of this declaration must contain two fields
+ (separated by the double colon <code>::</code>). The first field denotes
+ the dependency type, and the second field is the dependency target. The
+ dependency types are the same as those for EXCL_DEP described <a href=
+ "#dependency-types">above</a>.</p>
+
+ <p>Example:</p>
+ <pre>
+dep::foo/bar.f USE::your_fortran_mod
+dep::foo/bar.f INTERFACE::her_fortran.interface
+dep::foo/bar.f INC::his_fortran_inc.inc
+dep::foo/bar.f H::their_header.h
+dep::foo/bar.f OBJ::its_object.o
+dep::foo/egg EXE::ham
+</pre>
+ </dd>
+
+ <dt>NO_DEP::<pcks></dt>
+
+ <dd>
+ <p>This declaration is used to switch off/on dependency checking. If
+ <pcks> is specified in the label, the declaration applies to the
+ specified sub-package only.</p>
+
+ <p>Example:</p>
+ <pre>
+# Switch on dependency checking only for "foo"
+no_dep true
+no_dep::foo false
+</pre>
+ </dd>
+
+ <dt>EXE_NAME::<name></dt>
+
+ <dd>
+ <p>This renames the executable target of a main program source file
+ <name> to the specified value.</p>
+
+ <p>Example:</p>
+ <pre>
+# Rename executable target of foo.f90 from "foo.exe" to "bar"
+exe_name::foo bar
+</pre>
+ </dd>
+
+ <dt>LIB[::<pcks>]</dt>
+
+ <dd>
+ <p>This declares the name of a library archive target. If <pcks> is
+ specified in the label, the declaration applies to the library archive
+ target for that sub-package only. If set, the name of the library archive
+ target will be named <samp>lib<value>.a</samp>, where <value>
+ is the value of the declaration. If not specified, the default is to name
+ the global library <samp>libfcm_default.a</samp>. For a library archive
+ of a sub-package, the default is to name its library after the name of
+ the sub-package.</p>
+
+ <p>Example:</p>
+ <pre>
+# Rename the top level library "libfoo.a"
+lib foo
+
+# Rename the library for the sub-package "egg::ham"
+# from "libegg__ham.a" to "libegg-ham.a"
+lib::egg/ham egg-ham
+</pre>
+ </dd>
+
+ <dt>PP[::<pcks>]</dt>
+
+ <dd>
+ <p>This declares whether a pre-processing stage is required. To switch on
+ pre-processing, set the value to <samp>true</samp>. If <pcks> is
+ specified in the label, the flag applies to the files within that
+ sub-package only. Otherwise, the flag affects source directories in all
+ packages. The pre-processing stage is useful if the pre-processor changes
+ the dependency and/or the argument list of the source files. The default
+ behaviour is skip the pre-processing stage for all source.</p>
+
+ <p>Example:</p>
+ <pre>
+pp::gen true # switch on pre-processing for "gen" only
+pp true # switch on pre-processing globally
+</pre>
+ </dd>
+
+ <dt>SRC_TYPE::<pcks></dt>
+
+ <dd>
+ <p>This declaration is used to (re-)register the file type of the
+ sub-package <pcks> to associate with different file types. The
+ value of the declaration is a list of type flags delimited by the double
+ colon <code>::</code>. Each type flag is used internally to describe the
+ nature of the file. For example, a Fortran free source form containing a
+ main program is registered as
+ <code>FORTRAN::FORTRAN9X::SOURCE::PROGRAM</code>. A list of type flags is
+ available <a href="#infile-ext-types">later in this annex</a>.</p>
+
+ <p>Example:</p>
+ <pre>
+src_type::foo/bar.f FORTRAN::FORTRAN9X::SOURCE::PROGRAM
+</pre>
+ </dd>
+
+ <dt>INFILE_EXT::<ext></dt>
+
+ <dd>
+ <p>This declaration is used to re-register particular file name
+ extensions <ext> to associate with different file types. The value
+ of the declaration has a similar format to that of SRC_TYPE declaration
+ described above. A list of type flags is available <a href=
+ "#infile-ext-types">later in this annex</a>.</p>
+
+ <p>Example:</p>
+ <pre>
+infile_ext::h90 CPP::INCLUDE
+infile_ext::inc FORTRAN::FORTRAN9X::INCLUDE
+</pre>
+ </dd>
+
+ <dt>OUTFILE_EXT::<type></dt>
+
+ <dd>
+ <p>This declaration is used to re-register the output file extension for
+ a particular <type> of output files. The value must be a valid file
+ extension. The following is a list of output file types in-use by the
+ build system:</p>
+
+ <dl id="outfile-ext-types">
+ <dt>OBJ</dt>
+
+ <dd>compiled object files<br />
+ [default = .o]</dd>
+
+ <dt>MOD</dt>
+
+ <dd>compiled Fortran module information files<br />
+ [default = .mod]</dd>
+
+ <dt>EXE</dt>
+
+ <dd>binary executables<br />
+ [default = .exe]</dd>
+
+ <dt>DONE</dt>
+
+ <dd><em>done</em> files for compiled source<br />
+ [default = .done]</dd>
+
+ <dt>IDONE</dt>
+
+ <dd><em>done</em> files for included source<br />
+ [default = .idone]</dd>
+
+ <dt>FLAGS</dt>
+
+ <dd><em>flags</em> files, compiler flags config<br />
+ [default = .flags]</dd>
+
+ <dt>INTERFACE</dt>
+
+ <dd>interface files for F9X standalone subroutines/functions<br />
+ [default = .interface]</dd>
+
+ <dt>LIB</dt>
+
+ <dd>archive object library<br />
+ [default = .a]</dd>
+
+ <dt>TAR</dt>
+
+ <dd>TAR archive<br />
+ [default = .tar]</dd>
+ </dl>
+
+ <p>Example:</p>
+ <pre>
+# Output F9X interface files will now have ".foo" extension
+outfile_ext::interface .foo
+</pre>
+ </dd>
+ </dl>
+
+ <p id="tools-list">The following is a list of <label> fields that can
+ be used with a <code>TOOL</code> declaration. Those marked with an asterisk
+ (*) accept declarations at sub-package levels.</p>
+
+ <dl>
+ <dt>FC</dt>
+
+ <dd>The Fortran compiler.<br />
+ [default = <samp>f90</samp>]</dd>
+
+ <dt>FFLAGS *</dt>
+
+ <dd>Options used by the Fortran compiler.<br />
+ [default = ""]</dd>
+
+ <dt>FC_COMPILE</dt>
+
+ <dd>The option used by the Fortran compiler to suppress the linking
+ stage.<br />
+ [default = <samp>-c</samp>]</dd>
+
+ <dt>FC_INCLUDE</dt>
+
+ <dd>The option used by the Fortran compiler to specify the include search
+ path.<br />
+ [default = <samp>-I</samp>]</dd>
+
+ <dt>FC_MODSEARCH</dt>
+
+ <dd>The option used by the Fortran compiler to specify the search
+ path for the compiled module definition files. This option is often
+ unnecessary as it is normally covered by the include search path.<br />
+ [default = ""]</dd>
+
+ <dt>FC_DEFINE</dt>
+
+ <dd>The option used by the Fortran compiler to define a pre-processor
+ definition macro.<br />
+ [default = <samp>-D</samp>]</dd>
+
+ <dt>FC_OUTPUT</dt>
+
+ <dd>The option used by the Fortran compiler to specify the output file
+ name.<br />
+ [default = <samp>-o</samp>]</dd>
+
+ <dt>CC</dt>
+
+ <dd>The C compiler.<br />
+ [default = <samp>cc</samp>]</dd>
+
+ <dt>CFLAGS *</dt>
+
+ <dd>Options used by the C compiler.<br />
+ [default = ""]</dd>
+
+ <dt>CC_COMPILE</dt>
+
+ <dd>The option used by the C compiler to suppress the linking stage.<br />
+ [default = <samp>-c</samp>]</dd>
+
+ <dt>CC_INCLUDE</dt>
+
+ <dd>The option used by the C compiler to specify the include search
+ path.<br />
+ [default = <samp>-I</samp>]</dd>
+
+ <dt>CC_DEFINE</dt>
+
+ <dd>The option used by the C compiler to define a pre-processor definition
+ macro.<br />
+ [default = <samp>-D</samp>]</dd>
+
+ <dt>CC_OUTPUT</dt>
+
+ <dd>The option used by the C compiler to specify the output file
+ name.<br />
+ [default = <samp>-o</samp>]</dd>
+
+ <dt>LD *</dt>
+
+ <dd>Name of the linker or loader for linking object files into an
+ executable. If not set, use the compiler of the source file containing the
+ main program.<br />
+ [default = ""]</dd>
+
+ <dt>LDFLAGS *</dt>
+
+ <dd>The flags used by the linker or loader.<br />
+ [default = ""]</dd>
+
+ <dt>LD_OUTPUT</dt>
+
+ <dd>The option used by the linker or loader for the output file name (other
+ than the default <samp>a.out</samp>).<br />
+ [default = <samp>-o</samp>]</dd>
+
+ <dt>LD_LIBSEARCH</dt>
+
+ <dd>The option used by the linker or loader for specifying the search path
+ for link libraries.<br />
+ [default = <samp>-L</samp>]</dd>
+
+ <dt>LD_LIBLINK</dt>
+
+ <dd>The option used by the linker or loader command for linking with a
+ library.<br />
+ [default = <samp>-l</samp>]</dd>
+
+ <dt>AR</dt>
+
+ <dd>The archive command.<br />
+ [default = <samp>ar</samp>]</dd>
+
+ <dt>ARFLAGS</dt>
+
+ <dd>The options used for the archive command to create a library.<br />
+ [default = <samp>rs</samp>]</dd>
+
+ <dt>FPP</dt>
+
+ <dd>The Fortran pre-processor command.<br />
+ [default = <samp>cpp</samp>]</dd>
+
+ <dt>FPPKEYS *</dt>
+
+ <dd>The Fortran pre-processor will pre-define each word in this setting as
+ a macro.<br />
+ [default = ""]</dd>
+
+ <dt>FPPFLAGS *</dt>
+
+ <dd>The options used by the Fortran pre-processor.<br />
+ [default = <samp>-P -traditional</samp>]</dd>
+
+ <dt>FPP_DEFINE</dt>
+
+ <dd>The option used by the Fortran pre-processor to define a macro.<br />
+ [default = <samp>-D</samp>]</dd>
+
+ <dt>FPP_INCLUDE</dt>
+
+ <dd>The option used by the Fortran pre-processor to specify the include
+ search path.<br />
+ [default = <samp>-I</samp>]</dd>
+
+ <dt>CPP</dt>
+
+ <dd>The C pre-processor command.<br />
+ [default = <samp>cpp</samp>]</dd>
+
+ <dt>CPPKEYS *</dt>
+
+ <dd>The C pre-processor will pre-define each word in this setting as a
+ macro.<br />
+ [default = ""]</dd>
+
+ <dt>CPPFLAGS *</dt>
+
+ <dd>The options used by the C pre-processor.<br />
+ [default = <samp>-C</samp>]</dd>
+
+ <dt>CPP_DEFINE</dt>
+
+ <dd>The option used by the C pre-processor to define a macro.<br />
+ [default = <samp>-D</samp>]</dd>
+
+ <dt>CPP_INCLUDE</dt>
+
+ <dd>The option used by the C pre-processor to specify the include search
+ path.<br />
+ [default = <samp>-I</samp>]</dd>
+
+ <dt>MAKE</dt>
+
+ <dd>The <code>make</code> command.<br />
+ [default = <samp>make</samp>]</dd>
+
+ <dt>MAKEFLAGS</dt>
+
+ <dd>The options used by the <code>make</code> command.<br />
+ [default = ""]</dd>
+
+ <dt>MAKE_SILENT</dt>
+
+ <dd>The option used by the <code>make</code> command to specify silent
+ operation.<br />
+ [default = <samp>-s</samp>]</dd>
+
+ <dt>MAKE_JOB</dt>
+
+ <dd>The option used by the <code>make</code> command to specify the number
+ jobs to run simultaneously.<br />
+ [default = <samp>-j</samp>]</dd>
+
+ <dt>GENINTERFACE *</dt>
+
+ <dd>The command/method to extract the calling interfaces of top level
+ subroutines and functions in a Fortran 9X source. Supported values are
+ <samp>f90aib</samp> and <samp>none</samp> (to switch off interface
+ generation). If not specified, the system will use its own internal logic.
+ <br />
+ [default = (not specified)]</dd>
+
+ <dt>INTERFACE *</dt>
+
+ <dd>Generate Fortran 9X interface files with root names according to either
+ the root name of the source <samp>file</samp> or the name of the
+ <samp>program</samp> unit.<br />
+ [default = <samp>file</samp>]</dd>
+ </dl>
+
+ <p id="infile-ext-types">The following is a list of type flags that are
+ currently in-use (or <dfn>* reserved</dfn>) by the build system for TYPE and
+ INFILE_EXT declarations:</p>
+
+ <dl>
+ <dt>SOURCE</dt>
+
+ <dd>a source file containing program code of a supported language
+ (currently Fortran, FPP, C and CPP).</dd>
+
+ <dt>INCLUDE</dt>
+
+ <dd>an include file containing program code of a supported language
+ (currently Fortran, FPP, C and CPP).</dd>
+
+ <dt>FORTRAN</dt>
+
+ <dd>a file containing Fortran code.</dd>
+
+ <dt>FORTRAN9X</dt>
+
+ <dd>a file containing the Fortran free source form. This word must be used
+ in conjunction with the word <code>FORTRAN</code>.</dd>
+
+ <dt>FPP</dt>
+
+ <dd>a file containing Fortran code requiring pre-processing.</dd>
+
+ <dt>FPP9X</dt>
+
+ <dd>a file containing Fortran free source form requiring pre-processing.
+ This word must be used in conjunction with the word <code>FPP</code>.</dd>
+
+ <dt>C</dt>
+
+ <dd>a file containing C code.</dd>
+
+ <dt>CPP</dt>
+
+ <dd>a file containing CPP include header.</dd>
+
+ <dt>INTERFACE</dt>
+
+ <dd>a file containing a Fortran 9X interface block.</dd>
+
+ <dt>PROGRAM</dt>
+
+ <dd>a file containing a main program.</dd>
+
+ <dt>MODULE</dt>
+
+ <dd>a file containing a Fortran 9X module.</dd>
+
+ <dt>BINARY</dt>
+
+ <dd>a binary file.</dd>
+
+ <dt>EXE</dt>
+
+ <dd>an executable file. This word must be used in conjunction with the word
+ <code>BINARY</code>.</dd>
+
+ <dt>LIB</dt>
+
+ <dd>an archive library. This word must be used in conjunction with the word
+ <code>BINARY</code>.</dd>
+
+ <dt>SCRIPT</dt>
+
+ <dd>a file containing source code of a scripting language.</dd>
+
+ <dt>PVWAVE</dt>
+
+ <dd>a file containing executable PVWAVE scripts. This word must be used in
+ conjunction with the word <code>SCRIPT</code>.</dd>
+
+ <dt>SQL</dt>
+
+ <dd>a file containing SQL scripts. This word must be used in conjunction
+ with the word <code>SCRIPT</code>.</dd>
+
+ <dt>GENLIST</dt>
+
+ <dd>a GEN List file.</dd>
+
+ <dt>OBJ</dt>
+
+ <dd><dfn>(* reserved)</dfn> an object file. This word must be used in
+ conjunction with the word <code>BINARY</code>.</dd>
+
+ <dt>SHELL</dt>
+
+ <dd><dfn>(* reserved)</dfn> a file containing executable shell scripts.
+ This word must be used in conjunction with the word
+ <code>SCRIPT</code>.</dd>
+
+ <dt>PERL</dt>
+
+ <dd><dfn>(* reserved)</dfn> a file containing executable Perl scripts. This
+ word must be used in conjunction with the word <code>SCRIPT</code>.</dd>
+
+ <dt>PYTHON</dt>
+
+ <dd><dfn>(* reserved)</dfn> a file containing executable Python scripts.
+ This word must be used in conjunction with the word
+ <code>SCRIPT</code>.</dd>
+
+ <dt>TCL</dt>
+
+ <dd><dfn>(* reserved)</dfn> a file containing executable TCL scripts. This
+ word must be used in conjunction with the word <code>SCRIPT</code>.</dd>
+ </dl>
+
+ </div>
+ </div>
+ </div>
+
+ <hr/>
+ <div class="container-fluid text-center">
+ <div class="row-fluid"><div class="span12">
+ <address><small>
+ © British Crown Copyright 2006-14
+ <a href="http://www.metoffice.gov.uk">Met Office</a>.
+ See <a href="../etc/fcm-terms-of-use.html">Terms of Use</a>.<br />
+ This document is released under the British <a href=
+ "http://www.nationalarchives.gov.uk/doc/open-government-licence/" rel=
+ "license">Open Government Licence</a>.<br />
+ </small></address>
+ </div></div>
+ </div>
+
+ <script type="text/javascript" src="../etc/jquery.min.js"></script>
+ <script type="text/javascript" src="../etc/bootstrap/js/bootstrap.min.js"></script>
+ <script type="text/javascript" src="../etc/fcm.js"></script>
+ <script type="text/javascript" src="../etc/fcm-version.js"></script>
+</body>
+</html>
diff --git a/doc/user_guide/annex_cfg.html b/doc/user_guide/annex_cfg.html
new file mode 100644
index 0000000..8f6fb9b
--- /dev/null
+++ b/doc/user_guide/annex_cfg.html
@@ -0,0 +1,1433 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>FCM: User Guide: Annex: FCM Configuration File</title>
+ <meta name="author" content="FCM team" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link rel="icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link rel="shortcut icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link href="../etc/bootstrap/css/bootstrap.min.css" rel="stylesheet" media="screen" />
+ <link href="../etc/fcm.css" rel="stylesheet" media="screen" />
+</head>
+<body>
+ <div class="navbar navbar-inverse">
+ <div class="navbar-inner">
+ <a class="brand" href=".."><span class="fcm-version">FCM</span></a>
+ <ul class="nav">
+ <li><a href="../installation/">Installation</a></li>
+
+ <li class="active"><a href=".">User Guide</a></li>
+ </ul>
+ </div>
+ </div>
+
+ <div class="page-header">
+ <div class="fcm-page-content pull-right well well-small"></div>
+ <h1>FCM: User Guide: Annex: FCM Configuration File</h1>
+ </div>
+
+ <div class="container">
+ <div class="row">
+ <div class="span12">
+
+ <h2 id="syntax">Syntax</h2>
+
+ <h3 id="syntax.comment">Syntax: comment</h3>
+
+ <p>An empty line, a line with only space characters and a line beginning with
+ a <kbd>#</kbd> character is a comment line. For example:</p>
+ <pre>
+# This is a comment.
+ # This is also a comment.
+</pre>
+
+ <p>Also, if a line contains a space character followed by a <kbd>#</kbd>
+ character then this, and any following characters, are treated as an end of
+ line comment and are ignored.</p>
+
+ <h3 id="syntax.declaration">Syntax: declaration</h3>
+
+ <p>The general syntax of a declaration consists of a label, a set of
+ modifiers, a list of name-spaces, and a value:</p>
+ <pre>
+label{mod:mod-value}[ns] = value
+label{mod:mod-value} = value
+label[ns] = value
+label = value
+label = value\
+ value # "value value"
+label = value\
+ \value # "valuevalue"
+</pre>
+
+ <p>The label is compulsory. It defines the declaration, and may have one or
+ more characters in the alpha-numeric, the underscore, the minus sign and the
+ full stop. The label cannot be substituted with a variable.</p>
+
+ <p>The modifier is an optional part of the syntax, but may be a compulsory
+ part of some declarations. The modifiers are embedded between a pair of curly
+ braces and must come after the label, but before the name-spaces and the
+ equal sign. It should be a comma-separated list of colon-separated key-value
+ pairs, (i.e. <kbd>{key1: value1, key2: value2, ...}</kbd>). The value in each
+ modifier is optional - if a key is set without a value, its value is assumed
+ to be 1. The contents of the modifiers can be substituted using a
+ variable.</p>
+
+ <p>The name-space is an optional part of the syntax, but may be a compulsory
+ part of some declarations. The name-spaces are embedded between a pair of
+ square braces and must come after the label and/or modifiers but before the
+ equal sign. It should be a space-separated list of names. If a name contains
+ space, the space character can be escaped using a backslash character or the
+ name can be quoted. The contents of the name-spaces can be substituted using
+ a variable.</p>
+
+ <p>The first non-space character after the equal sign begins the value of the
+ declaration. If there is nothing after the equal sign, the value is an empty
+ string. The contents of the value can be substituted using a variable.
+ Trailing space characters are ignored. The end of line can be escaped using a
+ backslash, and the value of the declaration will continue from the next
+ non-comment line. If the first non-space character on that line is a
+ backslash then the line contents up to and including that backslash are
+ ignored.</p>
+
+ <h3 id="syntax.variable">Syntax: variable</h3>
+
+ <p>A variable is used to substitute commonly used values in a declaration. It
+ can refer to an environment variable or can be defined locally using the
+ syntax:</p>
+ <pre>
+$var = value # Sets "var" variable to "value"
+$var{?} = value # Sets "var" variable to "value" only if "var" is not defined
+# E.g.:
+$projects = foo bar baz
+$long_path = src/path/is/very/very/very/long
+$UMDIR{?} = /projects/um1
+# time passes...
+extract.ns = $projects
+extract.path-excl[$projects egg ham] = doc man ${long_path}_longer
+build.prop{cc.defs} = foo=\$foo # escape substitution
+</pre>
+
+ <p>The name of a variable must begin with an alphabet or an underscore and
+ followed by 0 or more alpha-numeric or underscore characters, (i.e. it must
+ match this regular expression: <var>^[A-Za-z_][A-Za-z0-9_]*$</var>).</p>
+
+ <p>Locally defined variables only apply in the context of the configuration
+ file and are not passed down, for instance, to the compiler. If there is an
+ environment variable with the same identifier, its value is normally
+ overridden (within the context of the configuration file) by any local
+ setting. However, if a <kbd>?</kbd> modifier is given, it assigns the value
+ to the variable only if the variable is not already defined (either as an
+ environment variable or locally). Substitution can be escaped using a
+ backslash character in front of the dollar sign. Any attempt to reference an
+ undefined variable will trigger an exception.</p>
+
+ <p>Note: <var>$HERE</var> is a reserved variable to indicate the parent
+ directory of the current configuration file. (The system ignores the $HERE
+ environment variable, if there is one.) Any attempt to assign a value to
+ <var>$HERE</var> will trigger an exception.</p>
+
+ <h3 id="syntax.include">Syntax: include</h3>
+
+ <p>An <code>include</code> declaration specifies the logical locations of a
+ list of configuration files, where contents are to be included inline:</p>
+ <pre>
+include{type:type} = location ...
+# E.g.:
+include = fcm:foo/path/to/config/file
+include = svn://server/path/to/config/file@1234
+include{type:svn} = http://server/path/to/config/file
+include = /path/to/config/file $HERE/another-config-file
+include = $HERE/another-config-file
+include = ~/path/to/config/file ~fred/path/to/config/file
+</pre>
+
+ <p>If the value of an <code>include</code> declaration is a relative path,
+ the system will search the directory containing the current configuration
+ file for the include file. More include search paths can be specified using
+ the <code>include-path</code> declaration. E.g.:</p>
+
+ <pre>
+# Define or replace include search paths
+include-path=/path/to/some/cfg /path/to/more/cfg
+# Append to include search paths
+include-path{+}=host2:/path/to/cfg
+include-path{type:svn,+}=https://host1/path/to/cfg
+# ... Include files can now be relative paths
+include=foo.cfg bar.cfg
+</pre>
+
+ <p>Some commands, e.g. <code>fcm make</code>, accept one or more
+ <code>--config-file-path=PATH</code> command line option, which can be used
+ to pre-define the include search paths when they read their configuration
+ files.</p>
+
+ <p>Currently, the location can be a path in the file system, a Subversion URL,
+ a FCM keyword pointing to a Subversion URL, or a location in the file system
+ of a remote host accessible via passphrase-less SSH. The system will attempt
+ to make an intelligent guess of the location type. To allow for future
+ expansion, both the <code>include</code> and the <code>include-path</code>
+ declarations accept a <var>type:TYPE</var> modifier (where TYPE can currently
+ be <samp>fs</samp> for a file system path or <samp>svn</samp> for a Subversion
+ location) to allow the location type to be defined where it is ambiguous.</p>
+
+ <h2 id="keyword">FCM Keyword Configuration</h2>
+
+ <p>The keyword configuration files are mainly used to define FCM location
+ keywords and related settings. The <code>fcm</code> command searches for
+ keyword configuration files from the following locations:</p>
+
+ <ol>
+ <li><dfn>(Deprecated)</dfn> <samp>$HOME/.fcm</samp> - expects FCM 1
+ configuration file format. See <a href="annex_fcm_cfg.html">Annex:
+ Declarations in FCM 1 central/user configuration file</a> for detail.</li>
+
+ <li><samp>$FCM/etc/fcm/keyword.cfg</samp> where <var>$FCM/bin/</var> is the
+ path at which <code>fcm</code> is installed.</li>
+
+ <li><samp>$HOME/.metomi/fcm/keyword.cfg</samp>.</li>
+ </ol>
+
+ <p>The following are declarations recognised by the keyword configuration
+ files:</p>
+
+ <dl>
+ <dt id="keyword.location">location</dt>
+
+ <dd>
+ <p><dfn>description</dfn>: Associates a namespace with a location, and
+ allows the use of <samp>fcm:namespace</samp> as a substitute of the
+ location.</p>
+
+ <p><dfn>modifier</dfn>: <var>primary</var>: Optional. If specified, the
+ location is a primary location, i.e. the system will create the
+ <samp>_tr</samp>, <samp>_br</samp>, <samp>_tg</samp>, <samp>-tr</samp>,
+ <samp>-br</samp> and <samp>-tg</samp> keywords for this location.</p>
+
+ <p><dfn>modifier</dfn>: <var>type</var>: Optional. Specifies the type of
+ the location. The system currently supports <samp>svn</samp> for a
+ Subversion location and <samp>fs</samp> for a file system location. If
+ not specified, the system will make an intelligent guess based on the
+ given value.</p>
+
+ <p><dfn>namespace</dfn>: The namespace to be associated with the
+ location.</p>
+
+ <p><dfn>value</dfn>: A valid location of a type supported by the
+ system.</p>
+
+ <p><dfn>example</dfn>:</p>
+ <pre>
+location{primary}[var] = svn://fcm5/VAR_svn/VAR
+location{primary, type:svn}[egg] = http://chicken/egg
+</pre>
+ </dd>
+
+ <dt id="keyword.revision">revision</dt>
+
+ <dd>
+ <p><dfn>description</dfn>: Associates a keyword with a revision of a
+ location in a version control system.</p>
+
+ <p><dfn>namespace</dfn>: The namespace of a location (already defined
+ using the <code>location</code> declaration), followed by a colon and the
+ namespace of the revision.</p>
+
+ <p><dfn>value</dfn>: A valid revision of the location.</p>
+
+ <p><dfn>example</dfn>:</p>
+ <pre>
+revision[um:vn7.5] = 18479
+revision[egg:free-range] = 1
+</pre>
+ </dd>
+
+ <dt id="keyword.browser.comp-pat">browser.comp-pat</dt>
+
+ <dd>
+ <p><dfn>description</dfn>: Specifies a regular expression to capture
+ components in the scheme-specific part of a version control system
+ location (already defined using the <code>location{primary}</code>
+ declaration). These components can then be used in the
+ <code>browser.loc-tmpl</code> template string.</p>
+
+ <p><dfn>namespace</dfn>: Optional. The namespace of a location. If not
+ specified, the declaration applies globally.</p>
+
+ <p><dfn>value</dfn>: A valid regular expression.</p>
+
+ <p><dfn>example</dfn>:</p>
+ <pre>
+browser.comp-pat = (?msx-i:\A // ([^/]+) /+ ([^/]+)_svn /*(.*) \z) # default
+browser.comp-pat[egg] = (?msx-i:\A//([^/]+)/(.*)\z)
+</pre>
+ </dd>
+
+ <dt id="keyword.browser.loc-tmpl">browser.loc-tmpl</dt>
+
+ <dd>
+ <p><dfn>description</dfn>: Specifies a template string, in which the
+ components captured by the <code>browser.comp.pat</code> regular
+ expression are used to fill in the numbered fields. The template should
+ have one more field than the number of components captured by
+ <code>browser.comp-pat</code>. The final field is used to place the
+ revision, which is generated via the <code>browser.rev-tmpl</code>.</p>
+
+ <p><dfn>namespace</dfn>: Optional. The namespace of a location. If not
+ specified, the declaration applies globally.</p>
+
+ <p><dfn>value</dfn>: A valid template.</p>
+
+ <p><dfn>example</dfn>:</p>
+ <pre>
+browser.loc-tmpl = http://{1}/projects/{2}/intertrac/source:/{3}{4} # default
+browser.loc-tmpl[egg] = http://{1}/intertrac/source:/{3}{4}
+</pre>
+ </dd>
+
+ <dt id="keyword.browser.rev-tmpl">browser.rev-tmpl</dt>
+
+ <dd>
+ <p><dfn>description</dfn>: Specifies a template string, which should have
+ a single numbered field for filling in the revision number.</p>
+
+ <p><dfn>namespace</dfn>: Optional. The namespace of a location. If not
+ specified, the declaration applies globally.</p>
+
+ <p><dfn>value</dfn>: A valid template.</p>
+
+ <p><dfn>example</dfn>:</p>
+ <pre>
+browser.rev-tmpl = @{1} # default
+</pre>
+ </dd>
+ </dl>
+
+ <h2 id="external">FCM External Configuration</h2>
+
+ <p>The external configuration files are used to define the name of external
+ commands used by the FCM system. The <code>fcm</code> command searches for
+ external configuration files from the following locations:</p>
+
+ <ol>
+ <li><samp>$FCM/etc/fcm/external.cfg</samp> where <var>$FCM/bin/</var> is
+ the path at which <code>fcm</code> is installed.</li>
+
+ <li><samp>$HOME/.metomi/fcm/external.cfg</samp>.</li>
+ </ol>
+
+ <p>The following are declarations recognised by the external configuration
+ files:</p>
+
+ <dl>
+ <dt id="external.browser">browser</dt>
+
+ <dd>
+ <p><dfn>description</dfn>: Specifies the command to invoke the web
+ browser. (default=<samp>firefox</samp>)</p>
+
+ <p><dfn>example</dfn>:</p>
+ <pre>
+browser = konqueror
+</pre>
+ </dd>
+
+ <dt id="external.diff3">diff3</dt>
+
+ <dd>
+ <p><dfn>description</dfn>: The shell command used by the extract system of
+ FCM Make to perform a 3-way merge. (default=<samp>diff3</samp>)</p>
+
+ <p><dfn>example</dfn>:</p>
+ <pre>
+diff3 = diff3
+</pre>
+ </dd>
+
+ <dt id="external.diff3.flags">diff3.flags</dt>
+
+ <dd>
+ <p><dfn>description</dfn>: The options used by the 3-way merge shell
+ command. (default=<samp>-E -m</samp>)</p>
+
+ <p><dfn>example</dfn>:</p>
+ <pre>
+diff3.flags = -E -m
+</pre>
+ </dd>
+
+ <dt id="external.graphic-diff">graphic-diff</dt>
+
+ <dd>
+ <p><dfn>description</dfn>: Specifies the command to invoke the graphic
+ diff tool. (default=<samp>xxdiff</samp>)</p>
+
+ <p><dfn>example</dfn>:</p>
+ <pre>
+graphic-diff = tkdiff
+</pre>
+ </dd>
+
+ <dt id="external.graphic-merge">graphic-merge</dt>
+
+ <dd>
+ <p><dfn>description</dfn>: Specifies the command to invoke the graphic
+ merge tool. (default=<samp>xxdiff</samp>)</p>
+
+ <p><dfn>example</dfn>:</p>
+ <pre>
+graphic-merge = xxdiff
+</pre>
+ </dd>
+
+ <dt id="external.ssh">ssh</dt>
+
+ <dd>
+ <p><dfn>description</dfn>: The secure remote shell command to execute
+ commands on a remote host. (default=<samp>ssh</samp>)</p>
+
+ <p><dfn>example</dfn>:</p>
+ <pre>
+ssh = ssh
+</pre>
+ </dd>
+
+ <dt id="external.ssh.flags">ssh.flags</dt>
+
+ <dd>
+ <p><dfn>description</dfn>: The options used by the secure shell command to
+ execute commands on a remote host. (default=<samp>-n
+ -oBatchMode=yes</samp>)</p>
+
+ <p><dfn>example</dfn>:</p>
+ <pre>
+ssh.flags = -n -oBatchMode=yes
+</pre>
+ </dd>
+
+ <dt id="external.rsync">rsync</dt>
+
+ <dd>
+ <p><dfn>description</dfn>: The <code>rsync</code> command.
+ (default=<samp>rsync</samp>)</p>
+
+ <p><dfn>example</dfn>:</p>
+ <pre>
+rsync = rsync
+</pre>
+ </dd>
+
+ <dt id="external.rsync.flags">rsync.flags</dt>
+
+ <dd>
+ <p><dfn>description</dfn>: The options used by the <code>rsync</code>
+ command. (default=<samp>-a --exclude=.* --delete-excluded --timeout=900
+ --rsh=ssh\ -oBatchMode=yes</samp>)</p>
+
+ <p><dfn>example</dfn>:</p>
+ <pre>
+rsync.flags = -a --exclude=.* --delete-excluded --timeout=900 --rsh=ssh\ -oBatchMode=yes
+</pre>
+ </dd>
+ </dl>
+
+ <h2 id="make">FCM Make Configuration</h2>
+
+ <p>A typical FCM make configuration consists of some top level declarations
+ to define the make, and specific declarations for each step. The top level
+ declarations are described below, and the specific declarations for each type
+ of step will be described in the sub-sections to follow.</p>
+
+ <p>Note:</p>
+
+ <ul>
+ <li>Unless stated otherwise, FCM make configuration declarations are
+ non-cumulative, i.e. if more than one declarations apply to the
+ <dfn>same</dfn> configuration, the value of the last declaration overrides
+ those of the earlier ones. E.g.:
+ <pre>
+# Sets the Fortran compiler flags for the root name-space
+build.prop{fc.flags} = -O3
+# build.prop{fc.flags}[/] = -O3
+
+# Sets the C/Fortran compiler flags for the "foo/bar" name-space
+build.prop{cc.flags, fc.flags}[foo/bar] = -g -C
+# build.prop{cc.flags}[foo/bar] = -g -C
+# build.prop{fc.flags}[/] = -O3
+# build.prop{fc.flags}[foo/bar] = -g -C
+
+# Sets the Fortran compiler flags for the root and "foo/bar" name-spaces
+build.prop{fc.flags}[/ foo/bar] = -O2
+# build.prop{cc.flags}[foo/bar] = -g -C
+# build.prop{fc.flags}[/] = -O2
+# build.prop{fc.flags}[foo/bar] = -O2
+
+# Sets the Fortran compiler flags for the "foo/bar" name-spaces
+build.prop{fc.flags}[foo/bar] = -O1
+# build.prop{cc.flags}[foo/bar] = -g -C
+# build.prop{fc.flags}[/] = -O2
+# build.prop{fc.flags}[foo/bar] = -O1
+</pre>
+ </li>
+
+ <li>Unless stated otherwise, declarations are inherited.</li>
+
+ <li>The default values of the property settings of each step class (e.g.
+ <code>build.prop{fc}</code>) can be modified in:
+
+ <ol>
+ <li>The site configuration file <samp>$FCM/etc/fcm/make.cfg</samp>
+ where <var>$FCM/bin/</var> is the path at which <code>fcm</code> is
+ installed.</li>
+
+ <li>The user configuration file
+ <samp>$HOME/.metomi/fcm/make.cfg</samp>.</li>
+
+ <li><samp>*.prop{class,*}</samp> declarations in the FCM make
+ configuration file of the current session.</li>
+ </ol>
+
+ <p>The syntax is identical to a normal FCM make configuration file
+ declaration except that:</p>
+
+ <ul>
+ <li>Only <samp>*.prop{*}</samp> declarations are accepted.</li>
+
+ <li>The settings apply to a step class, not an individual step.</li>
+
+ <li>Name-space is not allowed.</li>
+ </ul>
+ </li>
+ </ul>
+
+ <p>For further details, please refer to the chapter on <a href=
+ "make.html">FCM Make</a>.</p>
+
+ <dl>
+ <dt id="make.dest">dest</dt>
+
+ <dd>
+ <p><dfn>description</dfn>: Specifies the output destination of the make
+ system. If not specified, the system assumes the output destination to be
+ the current working directory. This setting is not inherited.</p>
+
+ <p><dfn>value</dfn>: A writable directory path.</p>
+
+ <p><dfn>example</dfn>:</p>
+ <pre>
+dest = path
+# E.g.:
+dest = $HERE
+</pre>
+ </dd>
+
+ <dt id="make.steps">steps</dt>
+
+ <dd>
+ <p><dfn>description</dfn>: Specifies a list of steps for the make system
+ to invoke. Each specified step can either be the ID of a known
+ sub-system, i.e. <samp>extract</samp>, <samp>mirror</samp>,
+ <samp>preprocess</samp> and <samp>build</samp>, or an ID defined using
+ the <code>step.class</code> declaration. (See below.)</p>
+
+ <p><dfn>value</dfn>: A list of steps for the make system to invoke.</p>
+
+ <p><dfn>example</dfn>:</p>
+ <pre>
+steps = id-1 id-2 ...
+# E.g.:
+steps = extract mirror
+</pre>
+ </dd>
+
+ <dt id="make.step.class">step.class</dt>
+
+ <dd>
+ <p><dfn>description</dfn>: Defines custom step IDs. This allows built-in
+ steps to be renamed, or re-used in multiple steps.</p>
+
+ <p><dfn>namespace</dfn>: Specifies the step IDs.</p>
+
+ <p><dfn>value</dfn>: An ID of a known sub-system, i.e.
+ <samp>extract</samp>, <samp>mirror</samp>, <samp>preprocess</samp> and
+ <samp>build</samp>.</p>
+
+ <p><dfn>example</dfn>:</p>
+ <pre>
+step.class[id ...] = id-of-class
+# E.g.:
+step.class[build-qxrecon build-ieee] = build
+</pre>
+ </dd>
+
+ <dt id="make.use">use</dt>
+
+ <dd>
+ <p><dfn>description</dfn>: Specifies inheritance locations. It is worth
+ noting that this declaration is cumulative, i.e. a <code>use</code>
+ declaration adds more inheritance locations rather than overrides the
+ inheritance locations of any previous <code>use</code> declarations.</p>
+
+ <p><dfn>value</dfn>: A list of paths where this make can inherit items
+ and properties from.</p>
+
+ <p><dfn>example</dfn>:</p>
+ <pre>
+use = path1 path2 ...
+E.g.:
+use = /path/to/inherit
+</pre>
+ </dd>
+ </dl>
+
+ <h3 id="make.extract">FCM Make Configuration: Extract</h3>
+
+ <p>All declarations are prefixed with <samp>extract.*</samp>. Where
+ appropriate, replace the leading <samp>extract</samp> with the appropriate
+ step ID. With the exception of the <code>extract.location{diff}</code>
+ declaration, any attempt to specify any extract declarations in an inherited
+ extract will result in an exception.</p>
+
+ <dl>
+ <dt id="make.extract.ns">extract.ns</dt>
+
+ <dd>
+ <p><dfn>description</dfn>: Specifies a list of names of the projects to
+ extract.</p>
+
+ <p><dfn>value</dfn>: A space-delimited list of names of the projects to
+ extract.</p>
+
+ <p><dfn>example</dfn>:</p>
+ <pre>
+extract.ns = gen ops um var ver
+</pre>
+ </dd>
+
+ <dt id="make.extract.location">extract.location</dt>
+
+ <dd>
+ <p><dfn>description</dfn>: Specifies the location of the base source tree
+ a project. If the base source tree is not specified for a Subversion
+ project, the system will try to use <samp>trunk at HEAD</samp>.</p>
+
+ <p><dfn>modifier</dfn>: <var>type</var>: Specifies the type of the
+ locations. The system currently supports <samp>svn</samp> for a Subversion
+ location, <samp>ssh</samp> for a file system on a remote host accessible
+ via SSH and RSYNC, and <samp>fs</samp> for a file system location.
+ (default=<samp>svn</samp> if the location is recognised as a Subversion
+ URL, <samp>ssh</samp> if the location has the form
+ <var>[USER@]HOST:PATH</var> and <var>HOST</var> is a valid remote host,
+ <samp>fs</samp> otherwise)</p>
+
+ <p><dfn>namespace</dfn>: A space-delimited list of project IDs.</p>
+
+ <p><dfn>value</dfn>: A location, which can be a full path in the file
+ system, or a relative location of the project root.</p>
+
+ <p><dfn>example</dfn>:</p>
+ <pre>
+extract.location[um] = trunk at vn7.5
+extract.location[gen ops var] = trunk at HEAD
+</pre>
+ </dd>
+
+ <dt id="make.extract.location.diff">extract.location{diff}</dt>
+
+ <dd>
+ <p><dfn>description</dfn>: Specifies the location(s) of one or more diff
+ source trees of a project. It is OK to add more diff source trees in an
+ inheriting extract.</p>
+
+ <p><dfn>modifier</dfn>: <var>type</var>: -as above-</p>
+
+ <p><dfn>namespace</dfn>: A project ID.</p>
+
+ <p><dfn>value</dfn>: A space-delimited list of locations. A location can
+ be a full path in the file system, or a relative location of the project
+ root.</p>
+
+ <p><dfn>example</dfn>:</p>
+ <pre>
+extract.location{diff}[gen] = \
+ branches/dev/fred/r12345_678 \
+ branches/dev/free/r12345_678 at 123
+</pre>
+ </dd>
+
+ <dt id="make.extract.location.primary">extract.location{primary}</dt>
+
+ <dd>
+ <p><dfn>description</dfn>: Specifies the project locations for an
+ extract, if they are not already defined in the keyword
+ configuration.</p>
+
+ <p><dfn>modifier</dfn>: <var>type</var>: -as above-</p>
+
+ <p><dfn>namespace</dfn>: A project ID.</p>
+
+ <p><dfn>value</dfn>: A project location. The location can be a path in
+ the file system, a Subversion URL or a FCM keyword pointing to a
+ Subversion URL.</p>
+
+ <p><dfn>example</dfn>:</p>
+ <pre>
+extract.location{primary}[egg] = svn://server2/egg
+extract.location{primary,type:svn}[foo] = http://server1/foo
+</pre>
+ </dd>
+
+ <dt id="make.extract.path-excl">extract.path-excl / <span id=
+ "make.extract.path-incl">extract.path-incl</span></dt>
+
+ <dd>
+ <p><dfn>description</dfn>: Specifies the paths in a project tree to
+ exclude/include. (A path-incl takes precedence over a path-excl
+ declaration.)</p>
+
+ <p><dfn>namespace</dfn>: A space-delimited list of project IDs.</p>
+
+ <p><dfn>value</dfn>: A space-delimited list of paths in a project
+ tree.</p>
+
+ <p><dfn>example</dfn>:</p>
+ <pre>
+extract.path-excl[foo] = / # everything
+extract.path-incl[foo] = bin lib src/code
+</pre>
+ </dd>
+
+ <dt id="make.extract.path-root">extract.path-root</dt>
+
+ <dd>
+ <p><dfn>description</dfn>: Specifies the root paths in the source trees
+ of the projects to be extracted. By default, the root path is the
+ root of a source tree. If this setting is specified, the system will only
+ extract source files under the specified root path. The system will also
+ adjust the name-space of each source file to be relative to the specified
+ root path. The <code>extract.path-excl</code> and
+ <code>extract.path-incl</code> declarations will apply from the level of
+ the specified root path.</p>
+
+ <p><dfn>namespace</dfn>: A space-delimited list of project IDs.</p>
+
+ <p><dfn>value</dfn>: A relative path in the project source trees.</p>
+
+ <p><dfn>example</dfn>:</p>
+ <pre>
+extract.path-root[gen] = src/code
+</pre>
+ </dd>
+
+ <dt id="make.extract.prop">extract.prop</dt>
+
+ <dd>
+ <p><dfn>description</dfn>: Sets or modifies a property.</p>
+
+ <p><dfn>modifier</dfn>: <var>diff3</var>: The shell command to perform a
+ 3-way merge. If not specified, use the <a href="#external.diff3">diff3</a>
+ setting of the FCM external configuration.</p>
+
+ <p><dfn>modifier</dfn>: <var>diff3.flags</var>: The options used by the
+ 3-way merge shell command. If not specified, use the <a href=
+ "#external.diff3.flags">diff3.flags</a> setting of the FCM external
+ configuration.</p>
+
+ <p><dfn>value</dfn>: The value of the property.</p>
+
+ <p><dfn>example</dfn>:</p>
+ <pre>
+extract.prop{diff3} = diff3
+extract.prop{diff3.flags} = -E -m
+</pre>
+ </dd>
+ </dl>
+
+ <h3 id="make.mirror">FCM Make Configuration: Mirror</h3>
+
+ <p>All declarations are prefixed with <samp>mirror.*</samp>. Where
+ appropriate, replace the leading <samp>mirror</samp> with the appropriate
+ ID.</p>
+
+ <dl>
+ <dt id="make.mirror.target">mirror.target</dt>
+
+ <dd>
+ <p><dfn>description</dfn>: Specifies the mirror target. This setting is
+ not inherited.</p>
+
+ <p><dfn>value</dfn>: The mirror target.</p>
+
+ <p><dfn>example</dfn>:</p>
+ <pre>
+mirror.target = fred at server:/path/of/dest
+# Or if the current user ID is "fred"
+mirror.target = server:/path/of/dest
+# Or if the current user ID is "fred" and local host is "server"
+mirror.target = /path/of/dest
+</pre>
+ </dd>
+
+ <dt id="make.mirror.prop">mirror.prop</dt>
+
+ <dd>
+ <p><dfn>description</dfn>: Sets or modifies a property.</p>
+
+ <p><dfn>modifier</dfn>: See below.</p>
+
+ <p><dfn>value</dfn>: The value of the property.</p>
+
+ <p><dfn>example</dfn>:</p>
+ <pre>
+mirror.prop{rsync} = cnysr
+</pre>
+ </dd>
+ </dl>
+
+ <p>The following is a list of properties accepted by the
+ <code>mirror.prop</code> declaration:</p>
+
+ <dl>
+ <dt id="make.mirror.prop.config-file.steps">config-file.steps</dt>
+
+ <dd>The steps to be included in the generated configuration file in the
+ mirror destination.</dd>
+
+ <dt id="make.mirror.prop.no-config-file">no-config-file</dt>
+
+ <dd>If specified, do not generate a <samp>fcm-make.cfg</samp> in the mirror
+ target.</dd>
+
+ <dt id="make.mirror.prop.ssh">ssh</dt>
+
+ <dd>The secure remote shell command to execute commands on a remote machine.
+ If not specified, use the <a href= "#external.ssh">ssh</a> setting of the
+ FCM external configuration.</dd>
+
+ <dt id="make.mirror.prop.ssh.flags">ssh.flags</dt>
+
+ <dd>The flags used by the remote shell command to execute commands on a
+ remote machine. If not specified, use the <a href=
+ "#external.ssh.flags">ssh.flags</a> setting of the FCM external
+ configuration.</dd>
+
+ <dt id="make.mirror.prop.rsync">rsync</dt>
+
+ <dd>The <code>rsync</code> command. If not specified, use the <a href=
+ "#external.rsync">rsync</a> setting of the FCM external configuration.</dd>
+
+ <dt id="make.mirror.prop.rsync.flags">rsync.flags</dt>
+
+ <dd>The options used by the <code>rsync</code> command. If not specified,
+ use the <a href= "#external.rsync.flags">rsync.flags</a> setting of the FCM
+ external configuration.</dd>
+ </dl>
+
+ <h3 id="make.build">FCM Make Configuration: Build</h3>
+
+ <p>All declarations are prefixed with <samp>build.*</samp>. Where
+ appropriate, replace the leading <samp>build</samp> with the appropriate
+ ID.</p>
+
+ <dl>
+ <dt id="make.build.source">build.source</dt>
+
+ <dd>
+ <p><dfn>description</dfn>: Specifies one or more source files and/or
+ directories.</p>
+
+ <p><dfn>namespace</dfn>: Optional. Specifies the namespace of the
+ specified paths.</p>
+
+ <p><dfn>value</dfn>: A list of space-delimited file system paths.</p>
+
+ <p><dfn>example</dfn>:</p>
+ <pre>
+build.source[foo] = $HOME/foo $DATADIR/foo
+</pre>
+ </dd>
+
+ <dt id="make.build.ns-excl">build.ns-excl / <span id=
+ "make.build.ns-incl">build.ns-incl</span></dt>
+
+ <dd>
+ <p><dfn>description</dfn>: Specifies the name-spaces in the source tree
+ to exclude/include. (A <code>build.ns-incl</code> takes precedence over a
+ <code>build.ns-excl</code> declaration.)</p>
+
+ <p><dfn>value</dfn>: A list of space-delimited items. Each item is a
+ name-space to exclude/include.</p>
+
+ <p><dfn>example</dfn>:</p>
+ <pre>
+build.ns-excl = / # exclude everything
+build.ns-incl = foo bar/baz # include items in these name-spaces
+</pre>
+ </dd>
+
+ <dt id="make.build.target">build.target</dt>
+
+ <dd>
+ <p><dfn>description</dfn>: Selects targets to build according to their
+ keys, categories, name-spaces and tasks.</p>
+
+ <p><dfn>modifier</dfn>: <samp>key</samp> (default if no modifier
+ specified), <samp>category</samp>, <samp>ns</samp> (i.e. name-space) or
+ <samp>task</samp></p>
+
+ <p><dfn>value</dfn>: A list of space-delimited items (according to the
+ modifier).</p>
+
+ <p><dfn>example</dfn>:</p>
+ <pre>
+build.target{category} = bin lib
+build.target{ns} = egg/fried ham
+build.target{task} = archive install link
+build.target = egg.sh mince.py
+</pre>
+ </dd>
+
+ <dt id="make.build.target-rename">build.target-rename</dt>
+
+ <dd>
+ <p><dfn>description</dfn>: Rename targets.</p>
+
+ <p><dfn>value</dfn>: A list of space-delimited items. Each item should be
+ the (automatic) key of a target, followed by a colon and its preferred
+ key.</p>
+
+ <p><dfn>example</dfn>:</p>
+ <pre>
+build.target-rename = hello.exe:hello
+</pre>
+ </dd>
+
+ <dt id="make.build.prop">build.prop</dt>
+
+ <dd>
+ <p><dfn>description</dfn>: Sets or modifies a property.</p>
+
+ <p><dfn>modifier</dfn>: The name of one (or more) property. See
+ below.</p>
+
+ <p><dfn>namespace</dfn>: Optional. A list of space-delimited namespaces to
+ which this setting applies. The namespaces can be target names, when used
+ with target properties (dependency properties are regarded as source
+ properties), or the hierarchical namespaces of source files. If not
+ specified, the setting applies to the global/root namespace. N.B. Not all
+ build properties accept a namespace. See below for detail.</p>
+
+ <p><dfn>value</dfn>: The value of the property.</p>
+
+ <p><dfn>example</dfn>:</p>
+ <pre>
+build.prop{fc} = gfortran
+build.prop{cc.flags,fc.flags} = -C -g
+build.prop{fc.flags}[foo/bar] = -C -g -W
+build.prop{fc.flags}[egg ham bacon] = -C -W
+build.prop{fc.flag-define} = -D%s
+build.prop{cc.defs,fc.defs}[gen] = LOWERCASE UNDERSCORE
+build.prop{fc.libs}[myprog.exe] = netcdf grib
+</pre>
+ </dd>
+ </dl>
+
+ <p>The following is a list of properties accepted by the
+ <code>build.prop</code> declaration. The default value is an empty string
+ unless given otherwise. Properties that do not accept a namespace are marked
+ with an asterisk (*).</p>
+
+ <dl>
+ <dt id="make.build.prop.ar">ar</dt>
+
+ <dd>The archive command. (default=<samp>ar</samp>)</dd>
+
+ <dt id="make.build.prop.ar.flags">ar.flags</dt>
+
+ <dd>The options used by the archive command. (default=<samp>rs</samp>)</dd>
+
+ <dt id="make.build.prop.cc">cc</dt>
+
+ <dd>The C compiler and linker. (default=<samp>gcc</samp>)</dd>
+
+ <dt id="make.build.prop.cc.defs">cc.defs</dt>
+
+ <dd>The C compiler will pre-define each word in this setting as a
+ macro.</dd>
+
+ <dt id="make.build.prop.cc.flags">cc.flags</dt>
+
+ <dd>The options used by the C compiler at compile time.</dd>
+
+ <dt id="make.build.prop.cc.flags-ld">cc.flags-ld</dt>
+
+ <dd>The options used by the C compiler at link time.</dd>
+
+ <dt id="make.build.prop.cc.flag-compile">cc.flag-compile</dt>
+
+ <dd>The option used by the C compiler to suppress the linking stage.
+ (default=<samp>-c</samp>)</dd>
+
+ <dt id="make.build.prop.cc.flag-define">cc.flag-define</dt>
+
+ <dd>The option used by the C compiler to define a macro.
+ (default=<samp>-D%s</samp>)</dd>
+
+ <dt id="make.build.prop.cc.flag-include">cc.flag-include</dt>
+
+ <dd>The option used by the C compiler to specify an include search
+ path. (default=<samp>-I%s</samp>)</dd>
+
+ <dt id="make.build.prop.cc.flag-lib">cc.flag-lib</dt>
+
+ <dd>The option used by the C compiler at link time to specify a
+ library. (default=<samp>-l%s</samp>)</dd>
+
+ <dt id="make.build.prop.cc.flag-lib-path">cc.flag-lib-path</dt>
+
+ <dd>The option used by the C compiler at link time to specify a library
+ search path. (default=<samp>-L%s</samp>)</dd>
+
+ <dt id="make.build.prop.cc.flag-omp">cc.flag-omp</dt>
+
+ <dd>The option used by the C compiler to switch on OpenMP.</dd>
+
+ <dt id="make.build.prop.cc.flag-output">cc.flag-output</dt>
+
+ <dd>The option used by the C compiler to specify the output file name.
+ (default=<samp>-o%s</samp>)</dd>
+
+ <dt id="make.build.prop.cc.include-paths">cc.include-paths</dt>
+
+ <dd>The C compiler will add each directory in this setting as an include
+ search path.</dd>
+
+ <dt id="make.build.prop.cc.libs">cc.libs</dt>
+
+ <dd>The C linker will add each item in this setting as a link
+ library.</dd>
+
+ <dt id="make.build.prop.cc.lib-paths">cc.lib-paths</dt>
+
+ <dd>The C linker will add each directory in this setting as a library
+ search path.</dd>
+
+ <dt id="make.build.prop.cxx">cxx</dt>
+
+ <dd>The C++ compiler and linker. (default=<samp>g++</samp>)</dd>
+
+ <dt id="make.build.prop.cxx.defs">cxx.defs</dt>
+
+ <dd>The C++ compiler will pre-define each word in this setting as a
+ macro.</dd>
+
+ <dt id="make.build.prop.cxx.flags">cxx.flags</dt>
+
+ <dd>The options used by the C++ compiler at compile time.</dd>
+
+ <dt id="make.build.prop.cxx.flags-ld">cxx.flags-ld</dt>
+
+ <dd>The options used by the C++ compiler at link time.</dd>
+
+ <dt id="make.build.prop.cxx.flag-compile">cxx.flag-compile</dt>
+
+ <dd>The option used by the C++ compiler to suppress the linking stage.
+ (default=<samp>-c</samp>)</dd>
+
+ <dt id="make.build.prop.cxx.flag-define">cxx.flag-define</dt>
+
+ <dd>The option used by the C++ compiler to define a macro.
+ (default=<samp>-D%s</samp>)</dd>
+
+ <dt id="make.build.prop.cxx.flag-include">cxx.flag-include</dt>
+
+ <dd>The option used by the C++ compiler to specify an include search
+ path. (default=<samp>-I%s</samp>)</dd>
+
+ <dt id="make.build.prop.cxx.flag-lib">cxx.flag-lib</dt>
+
+ <dd>The option used by the C++ compiler at link time to specify a
+ library. (default=<samp>-l%s</samp>)</dd>
+
+ <dt id="make.build.prop.cxx.flag-lib-path">cxx.flag-lib-path</dt>
+
+ <dd>The option used by the C++ compiler at link time to specify a library
+ search path. (default=<samp>-L%s</samp>)</dd>
+
+ <dt id="make.build.prop.cxx.flag-omp">cxx.flag-omp</dt>
+
+ <dd>The option used by the C++ compiler to switch on OpenMP.</dd>
+
+ <dt id="make.build.prop.cxx.flag-output">cxx.flag-output</dt>
+
+ <dd>The option used by the C++ compiler to specify the output file name.
+ (default=<samp>-o%s</samp>)</dd>
+
+ <dt id="make.build.prop.cxx.include-paths">cxx.include-paths</dt>
+
+ <dd>The C++ compiler will add each directory in this setting as an include
+ search path.</dd>
+
+ <dt id="make.build.prop.cxx.libs">cxx.libs</dt>
+
+ <dd>The C++ linker will add each item in this setting as a link
+ library.</dd>
+
+ <dt id="make.build.prop.cxx.lib-paths">cxx.lib-paths</dt>
+
+ <dd>The C++ linker will add each directory in this setting as a library
+ search path.</dd>
+
+ <dt id="make.build.prop.dep.bin">dep.bin</dt>
+
+ <dd>Specifies a list of manual dependencies on external executable
+ commands.</dd>
+
+ <dt id="make.build.prop.dep.f.module">dep.f.module</dt>
+
+ <dd>Specifies a list of manual Fortran module dependencies.</dd>
+
+ <dt id="make.build.prop.dep.include">dep.include</dt>
+
+ <dd>Specifies a list of manual include dependencies.</dd>
+
+ <dt id="make.build.prop.dep.o">dep.o</dt>
+
+ <dd>Specifies a list of manual object dependencies.</dd>
+
+ <dt id="make.build.prop.dep.o.special">dep.o.special</dt>
+
+ <dd>Specifies a list of manual object dependencies, which must appear on
+ the command line of the linker, (e.g. an object containing a Fortran
+ blockdata program unit).</dd>
+
+ <dt id="make.build.prop.fc">fc</dt>
+
+ <dd>The Fortran compiler and linker. (default=<samp>gfortran</samp>)</dd>
+
+ <dt id="make.build.prop.fc.defs">fc.defs</dt>
+
+ <dd>The Fortran compiler will pre-define each word in this setting as a
+ macro.</dd>
+
+ <dt id="make.build.prop.fc.flags">fc.flags</dt>
+
+ <dd>The options used by the Fortran compiler at compile time.</dd>
+
+ <dt id="make.build.prop.fc.flags-ld">fc.flags-ld</dt>
+
+ <dd>The options used by the Fortran compiler at link time.</dd>
+
+ <dt id="make.build.prop.fc.flag-compile">fc.flag-compile</dt>
+
+ <dd>The option used by the Fortran compiler to suppress the linking stage.
+ (default=<samp>-c</samp>)</dd>
+
+ <dt id="make.build.prop.fc.flag-define">fc.flag-define</dt>
+
+ <dd>The option used by the Fortran compiler to define a macro.
+ (default=<samp>-D%s</samp>)</dd>
+
+ <dt id="make.build.prop.fc.flag-include">fc.flag-include</dt>
+
+ <dd>The option used by the Fortran compiler to specify an include search
+ path. (default=<samp>-I%s</samp>)</dd>
+
+ <dt id="make.build.prop.fc.flag-lib">fc.flag-lib</dt>
+
+ <dd>The option used by the Fortran compiler at link time to specify a
+ library. (default=<samp>-l%s</samp>)</dd>
+
+ <dt id="make.build.prop.fc.flag-lib-path">fc.flag-lib-path</dt>
+
+ <dd>The option used by the Fortran compiler at link time to specify a
+ library search path. (default=<samp>-L%s</samp>)</dd>
+
+ <dt id="make.build.prop.fc.flag-module">fc.flag-module</dt>
+
+ <dd>The option used by the Fortran compiler to specify a module search
+ path.</dd>
+
+ <dt id="make.build.prop.fc.flag-omp">fc.flag-omp</dt>
+
+ <dd>The option used by the Fortran compiler to switch on OpenMP. If
+ specified, the build system will treat <code>USE</code> and
+ <code>INCLUDE</code> statements that are protected by <code>!$ </code>
+ sentinels as normal dependency statements.</dd>
+
+ <dt id="make.build.prop.fc.flag-output">fc.flag-output</dt>
+
+ <dd>The option used by the Fortran compiler to specify the output file
+ name. (default=<samp>-o%s</samp>)</dd>
+
+ <dt id="make.build.prop.fc.include-paths">fc.include-paths</dt>
+
+ <dd>The Fortran compiler will add each directory in this setting as an
+ include search path.</dd>
+
+ <dt id="make.build.prop.fc.libs">fc.libs</dt>
+
+ <dd>The Fortran linker will add each item in this setting as a link
+ library.</dd>
+
+ <dt id="make.build.prop.fc.lib-paths">fc.lib-paths</dt>
+
+ <dd>The Fortran linker will add each directory in this setting as a library
+ search path.</dd>
+
+ <dt id="make.build.prop.file-ext.a">file-ext.a *</dt>
+
+ <dd>Specifies the extension of an object archive file.
+ (default=<samp>.a</samp>)</dd>
+
+ <dt id="make.build.prop.file-ext.bin">file-ext.bin *</dt>
+
+ <dd>Specifies the extension of a binary executable file.
+ (default=<samp>.exe</samp>)</dd>
+
+ <dt id="make.build.prop.file-ext.c">file-ext.c *</dt>
+
+ <dd>Specifies the extensions of a C source file.
+ (default=<samp>.c .i .m .mi</samp>)</dd>
+
+ <dt id="make.build.prop.file-ext.cxx">file-ext.cxx *</dt>
+
+ <dd>Specifies the extensions of a C++ source file.
+ (default=<samp>.cc .cp .cxx .cpp .CPP .c++ .C .mm .M .mii</samp>)</dd>
+
+ <dt id="make.build.prop.file-ext.fortran">file-ext.fortran *</dt>
+
+ <dd>Specifies the extensions of a Fortran source file. (default=<samp>.F
+ .FOR .FTN .F90 .F95 .f .for .ftn .f90 .f95 .inc</samp>)</dd>
+
+ <dt id="make.build.prop.file-ext.f90-interface">file-ext.f90-interface
+ *</dt>
+
+ <dd>Specifies the extension of a Fortran interface file.
+ (default=<samp>.interface</samp>)</dd>
+
+ <dt id="make.build.prop.file-ext.f90-mod">file-ext.f90-mod *</dt>
+
+ <dd>Specifies the extension of a compiled Fortran module file.
+ (default=<samp>.mod</samp>)</dd>
+
+ <dt id="make.build.prop.file-ext.h">file-ext.h *</dt>
+
+ <dd>Specifies the extensions of a C/C++ header file.
+ (default=<samp>.h</samp>)</dd>
+
+ <dt id="make.build.prop.file-ext.o">file-ext.o *</dt>
+
+ <dd>Specifies the extension of a compiled object file.
+ (default=<samp>.o</samp>)</dd>
+
+ <dt id="make.build.prop.file-ext.script">file-ext.script *</dt>
+
+ <dd>Specifies the extensions of script files.</dd>
+
+ <dt id="make.build.prop.file-name-option.f90-mod">file-name-option.f90-mod
+ *</dt>
+
+ <dd>Specifies other options for naming a compiled Fortran module file.
+ Accepts <samp>case=upper</samp> or <samp>case=lower</samp> (default).</dd>
+
+ <dt id="make.build.prop.file-pat.script">file-pat.script *</dt>
+
+ <dd>Specifies a regular expression for matching the name of script
+ files.</dd>
+
+ <dt id="make.build.prop.ignore-missing-dep-ns">ignore-missing-dep-ns *</dt>
+
+ <dd>Specifies a list of source name-spaces, in which targets can ignore
+ missing dependencies.</dd>
+
+ <dt id="make.build.prop.keep-lib-o">keep-lib-o</dt>
+
+ <dd>Relevant when linking a binary executable. If <kbd>true</kbd>, create
+ and keep the dependent object library as <samp>lib/libNAME.a</samp>, where
+ <var>NAME</var> is the root name of the executable. The normal behaviour is
+ to create the dependent object library in a temporary directory, which is
+ removed after the linker command is completed.</dd>
+
+ <dt id="make.build.prop.ld">ld</dt>
+
+ <dd>The linker command. If not specified, use the compiler of the source
+ file.</dd>
+
+ <dt id="make.build.prop.no-dep.bin">no-dep.bin</dt>
+
+ <dd>Switches off a list of automatic dependencies on external executable.
+ If the value is a <samp>*</samp>, switches off all automatic external
+ executable dependencies.</dd>
+
+ <dt id="make.build.prop.no-dep.f.module">no-dep.f.module</dt>
+
+ <dd>Switches off a list of automatic Fortran module dependencies. If the
+ value is a <samp>*</samp>, switches off all automatic Fortran module
+ dependencies.</dd>
+
+ <dt id="make.build.prop.no-dep.include">no-dep.include</dt>
+
+ <dd>Switches off a list of automatic include dependencies. If the value is
+ a <samp>*</samp>, switches off all automatic include dependencies.</dd>
+
+ <dt id="make.build.prop.no-dep.o">no-dep.o</dt>
+
+ <dd>Switches off a list of automatic object dependencies. If the value is a
+ <samp>*</samp>, switches off all automatic object dependencies.</dd>
+
+ <dt id="make.build.prop.no-inherit-source">no-inherit-source *</dt>
+
+ <dd>If a list of values is specified, the system will not inherit sources
+ with the name-spaces matching the specified values. If the value is a
+ <samp>*</samp>, the system will not inherit any sources.</dd>
+
+ <dt id="make.build.prop.no-inherit-target-category">
+ no-inherit-target-category *</dt>
+
+ <dd>If a list of values is specified, the system will not inherit a target
+ in a category matching a specified value. (default=<samp>bin etc
+ lib</samp>)</dd>
+
+ <dt id="make.build.prop.no-step-source">no-step-source *</dt>
+
+ <dd>If a list of make steps is specified, the system will not search for
+ source files from the specified make steps.</dd>
+
+ <dt id="make.build.prop.ns-dep.o">ns-dep.o</dt>
+
+ <dd>Specifies a list of link-time object dependencies on all objects in the
+ list of name-spaces in the value.</dd>
+ </dl>
+
+ <h3 id="make.preprocess">FCM Make Configuration: Preprocess</h3>
+
+ <p>The preprocess system uses the same declarations as the build system (see
+ <a href="#make.build">FCM Make Configuration: Build</a>), although their
+ prefixes should be replaced with <samp>preprocess.*</samp> or the appropriate
+ step ID. The following is a list of modifiers accepted by the
+ <code>preprocess.prop</code> declaration. The default value of a property is
+ an empty string unless given otherwise. Properties that do not accept a
+ namespace are marked with an asterisk (*).</p>
+
+ <dl>
+ <dt id="make.preprocess.prop.cpp">cpp</dt>
+
+ <dd>The command of the C/C++ pre-processor. (default=<samp>cpp</samp>)</dd>
+
+ <dt id="make.preprocess.prop.cpp.defs">cpp.defs</dt>
+
+ <dd>The C/C++ pre-processor will pre-define each word in this setting as a
+ macro.</dd>
+
+ <dt id="make.preprocess.prop.cpp.flags">cpp.flags</dt>
+
+ <dd>The options used by the C/C++ pre-processor.</dd>
+
+ <dt id="make.preprocess.prop.cpp.flag-define">cpp.flag-define</dt>
+
+ <dd>The option used by the C/C++ pre-processor to define a macro.
+ (default=<samp>-D%s</samp>)</dd>
+
+ <dt id="make.preprocess.prop.cpp.flag-include">cpp.flag-include</dt>
+
+ <dd>The option used by the C/C++ pre-processor to specify the include
+ search path. (default=<samp>-I%s</samp>)</dd>
+
+ <dt id="make.preprocess.prop.cpp.include-paths">cpp.include-paths</dt>
+
+ <dd>The C/C++ preprocessor will add each directory in this setting as an
+ include search path.</dd>
+
+ <dt id="make.preprocess.prop.dep.include">dep.include</dt>
+
+ <dd>Specifies a list of manual include dependencies.</dd>
+
+ <dt id="make.preprocess.prop.file-ext.cpp">file-ext.cpp *</dt>
+
+ <dd>Specifies the extensions of C/C++ source file.
+ (default=<samp>.c .m .cc .cp .cxx .cpp .CPP .c++ .C .mm .M</samp>)</dd>
+
+ <dt id="make.preprocess.prop.file-ext.fpp">file-ext.fpp *</dt>
+
+ <dd>Specifies the extensions of Fortran source file requiring
+ preprocessing. (default=<samp>.F .FOR .FTN .F90 .F95</samp>)</dd>
+
+ <dt id="make.preprocess.prop.file-ext.h">file-ext.h *</dt>
+
+ <dd>Specifies the extensions of a C/C++ header file.
+ (default=<samp>.h</samp>)</dd>
+
+ <dt id="make.preprocess.prop.fpp">fpp</dt>
+
+ <dd>The command of the Fortran pre-processor.
+ (default=<samp>cpp</samp>)</dd>
+
+ <dt id="make.preprocess.prop.fpp.defs">fpp.defs</dt>
+
+ <dd>The Fortran pre-processor will pre-define each word in this setting as
+ a macro.</dd>
+
+ <dt id="make.preprocess.prop.fpp.flags">fpp.flags</dt>
+
+ <dd>The options used by the Fortran pre-processor. (default=<samp>-P
+ -traditional</samp>)</dd>
+
+ <dt id="make.preprocess.prop.fpp.flag-define">fpp.flag-define</dt>
+
+ <dd>The option used by the Fortran pre-processor to define a macro.
+ (default=<samp>-D%s</samp>)</dd>
+
+ <dt id="make.preprocess.prop.fpp.flag-include">fpp.flag-include</dt>
+
+ <dd>The option used by the Fortran pre-processor to specify the include
+ search path. (default=<samp>-I%s</samp>)</dd>
+
+ <dt id="make.preprocess.prop.fpp.include-paths">fpp.include-paths</dt>
+
+ <dd>The Fortran preprocessor will add each directory in this setting as an
+ include search path.</dd>
+
+ <dt id="make.preprocess.prop.no-dep.include">no-dep.include</dt>
+
+ <dd>Switches off a list of automatic include dependencies. If the value is
+ a <samp>*</samp>, it switches off all automatic include dependencies.</dd>
+
+ <dt id="make.preprocess.prop.no-inherit-source">no-inherit-source *</dt>
+
+ <dd>Same as the build property of the same name.</dd>
+
+ <dt id="make.preprocess.prop.no-inherit-target-category">
+ no-inherit-target-category *</dt>
+
+ <dd>Same as the build property of the same name.</dd>
+
+ <dt id="make.preprocess.prop.no-step-source">no-step-source *</dt>
+
+ <dd>Same as the build property of the same name.</dd>
+ </dl>
+
+ </div>
+ </div>
+ </div>
+
+ <hr/>
+ <div class="container-fluid text-center">
+ <div class="row-fluid"><div class="span12">
+ <address><small>
+ © British Crown Copyright 2006-14
+ <a href="http://www.metoffice.gov.uk">Met Office</a>.
+ See <a href="../etc/fcm-terms-of-use.html">Terms of Use</a>.<br />
+ This document is released under the British <a href=
+ "http://www.nationalarchives.gov.uk/doc/open-government-licence/" rel=
+ "license">Open Government Licence</a>.<br />
+ </small></address>
+ </div></div>
+ </div>
+
+ <script type="text/javascript" src="../etc/jquery.min.js"></script>
+ <script type="text/javascript" src="../etc/bootstrap/js/bootstrap.min.js"></script>
+ <script type="text/javascript" src="../etc/fcm.js"></script>
+ <script type="text/javascript" src="../etc/fcm-version.js"></script>
+</body>
+</html>
diff --git a/doc/user_guide/annex_ext_cfg.html b/doc/user_guide/annex_ext_cfg.html
new file mode 100644
index 0000000..f56fca7
--- /dev/null
+++ b/doc/user_guide/annex_ext_cfg.html
@@ -0,0 +1,397 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>FCM: User Guide: Annex: Declarations in FCM 1 extract configuration
+ file</title>
+ <meta name="author" content="FCM team" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link rel="icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link rel="shortcut icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link href="../etc/bootstrap/css/bootstrap.min.css" rel="stylesheet" media="screen" />
+ <link href="../etc/fcm.css" rel="stylesheet" media="screen" />
+</head>
+<body>
+ <div class="navbar navbar-inverse">
+ <div class="navbar-inner">
+ <a class="brand" href=".."><span class="fcm-version">FCM</span></a>
+ <ul class="nav">
+ <li><a href="../installation/">Installation</a></li>
+
+ <li class="active"><a href=".">User Guide</a></li>
+ </ul>
+ </div>
+ </div>
+
+ <div class="page-header">
+ <div class="fcm-page-content pull-right well well-small"></div>
+ <h1>FCM: User Guide: Annex: Declarations in FCM 1 extract configuration
+ file</h1>
+ </div>
+
+ <div class="container">
+ <div class="row">
+ <div class="span12">
+
+ <p><em>The FCM 1 extract system is deprecated. The documentation for the
+ current extract system can be found at <a href="make.html">FCM
+ Make</a>.</em></p>
+
+ <p>The following is a list of supported declarations for the configuration
+ file used by the FCM extract system. Unless otherwise stated, the fields in
+ all declaration labels are not case sensitive.</p>
+
+ <dl>
+ <dt>CFG::TYPE</dt>
+
+ <dd>
+ <p>The configuration file type, the value should always be
+ <samp>ext</samp> for an extract configuration file. This declaration is
+ compulsory for all extract configuration files.</p>
+
+ <p>Example:</p>
+ <pre>
+cfg::type ext
+</pre>
+ </dd>
+
+ <dt>CFG::VERSION</dt>
+
+ <dd>
+ <p>The file format version, currently <samp>1.0</samp> - a version is
+ included so that we shall be able to read the configuration file
+ correctly should we decide to change its format in the future.</p>
+
+ <p>Example:</p>
+ <pre>
+cfg::version 1.0
+</pre>
+ </dd>
+
+ <dt>%<name></dt>
+
+ <dd>
+ <p><code>%<name></code> declares an internal variable
+ <var><name></var> that can later be re-used.</p>
+
+ <p>Example:</p>
+ <pre>
+%my_variable foo
+src::bar::base %my_variable
+src::egg::base %my_variable
+src::ham::base %my_variable
+</pre>
+ </dd>
+
+ <dt>INC</dt>
+
+ <dd>
+ <p>This declares the name of a file containing extract configuration. The
+ lines in the declared file will be included inline to the current
+ configuration file.</p>
+
+ <p>Example:</p>
+ <pre>
+inc ~frva/var_stable_22.0/cfg/ext.cfg
+# ... and then your changes ...
+</pre>
+ </dd>
+
+ <dt>DEST[::ROOTDIR]</dt>
+
+ <dd>
+ <p>The <em>root</em> path of the destination of this extract. This
+ declaration is compulsory for all extract configuration files.</p>
+
+ <p>Example:</p>
+ <pre>
+dest $HOME/project/my_project
+</pre>
+ </dd>
+
+ <dt>USE</dt>
+
+ <dd>
+ <p>This declares the location of a previous successful extract, which the
+ current extract will inherit from. If the previous extract is also a
+ build, the subsequent invocation of the build system on the current
+ extract will automatically trigger an inherited incremental build based
+ on that build.</p>
+
+ <p>Example:</p>
+ <pre>
+use ~frva/var_stable_22.0
+# ... and then the settings for your current extract ...
+</pre>
+ </dd>
+
+ <dt>RDEST[::ROOTDIR]</dt>
+
+ <dd>
+ <p>The alternate destination of this extract. This declaration is
+ compulsory if this extract requires mirroring to an alternate
+ destination.</p>
+
+ <p>Example:</p>
+ <pre>
+rdest /home/nwp/da/frva/project/my_project
+</pre>
+ </dd>
+
+ <dt>RDEST::LOGNAME</dt>
+
+ <dd>
+ <p>The login name of the user on the alternate destination machine. If
+ not specified, the current login name of the user on the local platform
+ is assumed.</p>
+
+ <p>Example:</p>
+ <pre>
+rdest::logname frva
+</pre>
+ </dd>
+
+ <dt>RDEST::MACHINE</dt>
+
+ <dd>
+ <p>The destination machine for this extract. If not specified, the current
+ host name is assumed.</p>
+
+ <p>Example:</p>
+ <pre>
+rdest::machine tx01
+</pre>
+ </dd>
+
+ <dt>
+ RDEST::MIRROR_CMD<br />
+ <del>MIRROR</del>
+ </dt>
+
+ <dd>
+ <p>The extract system can mirror the extracted source to an alternate
+ machine. Currently, it does this using either the <code>rdist</code> or
+ the <code>rsync</code> command. The default is <samp>rsync</samp>. This
+ declaration can be used to switch to using <samp>rdist</samp>.</p>
+
+ <p>Example:</p>
+ <pre>
+rdest::mirror_cmd rdist
+</pre>
+ </dd>
+
+ <dt>
+ RDEST::RSH_MKDIR_RSH (<del>RDEST::REMOTE_SHELL</del>)<br />
+ RDEST::RSH_MKDIR_RSHFLAGS<br />
+ RDEST::RSH_MKDIR_MKDIR<br />
+ RDEST::RSH_MKDIR_MKDIRFLAGS
+ </dt>
+
+ <dd>
+ <p>If <code>rsync</code> is used to mirror an extract, the system needs to
+ issue a separate remote shell command to create the container directory of
+ the mirror destination. The default is to issue a shell command in the
+ form <samp>ssh -n -oBatchMode=yes LOGNAME at MACHINE mkdir -p DEST</samp>.
+ These declarations can be used to modify the command.</p>
+
+ <p>Example:</p>
+ <pre>
+# Examples using the default settings:
+rdest::rsh_mkdir_rsh ssh
+rdest::rsh_mkdir_rshflags -n -oBatchMode=yes
+rdest::rsh_mkdir_mkdir mkdir
+rdest::rsh_mkdir_mkdirflags -p
+</pre>
+ </dd>
+
+ <dt>
+ RDEST::RSYNC<br />
+ RDEST::RSYNCFLAGS
+ </dt>
+
+ <dd>
+ <p>These declarations are only useful if <code>rsync</code> is used to
+ mirror an extract. By default, the system issues the shell command
+ <samp>rsync -a --exclude='.*' --delete-excluded --timeout=900 --rsh='ssh
+ -oBatchMode=yes' SOURCE DEST</samp>. These declarations can be used to
+ modify the command.</p>
+
+ <p>Example:</p>
+ <pre>
+# Examples using the default settings:
+rdest::rsync rsync
+rdest::rsyncflags -a --exclude='.*' --delete-excluded --timeout=900 \
+ --rsh='ssh -oBatchMode=yes'
+</pre>
+ </dd>
+
+ <dt>REPOS::<pck>::<branch></dt>
+
+ <dd>
+ <p>This declares a URL or a local file system path for the container
+ <em>repository</em> of a branch named <branch> in a package named
+ <pck>. The package name <pck> must be the name of a top-level
+ package (i.e. it must not contain the double colon <code>::</code>
+ delimiter). The name <branch> is used internally within the extract
+ system, and so is independent of the branch name of the code management
+ system. However, it is usually desirable to use the same name of the
+ actual branch in the code management system. For declaration of a local
+ file system path, the convention is to name the branch <samp>user</samp>.
+ Please note that both <pck> and <branch> fields are case
+ sensitive. The declared URL must be a valid Subversion URL or a valid FCM
+ URL keyword.</p>
+
+ <p>Example:</p>
+ <pre>
+repos::var::base fcm:var_tr
+repos::var::branch1 fcm:var_br/frsn/r4790_foobar
+repos::var::user $HOME/var
+</pre>
+ </dd>
+
+ <dt>
+ REVISION::<pck>::<branch><br />
+ <del>VERSION::<pck>::<branch></del>
+ </dt>
+
+ <dd>
+ <p>The revision to be used for the URL of <branch> in the package
+ <pck>. If specified, the revision must be a revision where the
+ branch exists. If not specified, the revision defaults to last changed
+ revision at the HEAD of the branch. Please note that if the declared
+ <em>branch</em> is in the local file system, this declaration must not be
+ used. The value of the declaration can be a FCM revision keyword or any
+ revision argument acceptable by Subversion. You can use a valid revision
+ number, a date between a pair of curly brackets (e.g. <samp>{"2005-05-01
+ 12:00"}</samp>) or the keyword HEAD. However, please do not use the
+ keywords BASE, COMMITTED or PREV as these are reserved for working copies
+ only. Again, please note that both <pck> and <branch> fields
+ are case sensitive.</p>
+
+ <p>Example:</p>
+ <pre>
+# Declare the revision with the FCM revision keyword "vn22.0"
+revision::var::base vn22.0
+# Declare the revision with a {date}
+revision::var::branch1 {2006-01-01}
+</pre>
+ </dd>
+
+ <dt>REVMATCH</dt>
+
+ <dd>
+ <p>If set to true, the declared revision of a branch must be a changed
+ revision of that branch, (unless the keyword HEAD is used).</p>
+
+ <p>Example:</p>
+ <pre>
+revmatch true
+</pre>
+ </dd>
+
+ <dt>SRC::<pcks>::<branch></dt>
+
+ <dd>
+ <p>This declares a source directory for the sub-package <pcks> of
+ <branch>. If the repository is declared as a URL, the source
+ directory must be quoted as a relative path to the URL. If the repository
+ is declared as a path in the local file system, the source directory can
+ be declared as either a relative path to the <em>repository</em> or a
+ full path. If the source directory is a relative path and <pcks> is
+ a top-level package, the full name of the sub-package will be determined
+ automatically using the directory names of the relative path as the names
+ of the sub-packages. If the source directory is a full path, the full
+ sub-package name must be specified. The name of the sub-package
+ determines the destination path of the source directory in the
+ extract.</p>
+
+ <p>Example:</p>
+ <pre>
+src::var::base code/VarMod_PF
+src::var/code/VarMod_PF::user $HOME/var/code/VarMod_PF
+</pre>
+ </dd>
+
+ <dt>EXPSRC::<pcks>::<branch></dt>
+
+ <dd>
+ <p>This declares an expandable source directory for the sub-package
+ <pcks> of <branch>. This declaration is essentially the same
+ as the SRC declaration, except that the system will attempt to search
+ recursively for sub-directories within the declared source directory.</p>
+
+ <p>Example:</p>
+ <pre>
+expsrc::var::base code
+expsrc::var::user code
+</pre>
+ </dd>
+
+ <dt>
+ CONFLICT<br />
+ <del>OVERRIDE</del>
+ </dt>
+
+ <dd>
+ <p>This declaration can be used to specify the conflict mode, which is
+ relevant when a file is modified by two different branches (or more)
+ relative to the base branch. The conflict mode can be <samp>fail</samp>,
+ <samp>merge</samp> (default) or <samp>override</samp> (or 0, 1 and 2
+ respectively). If <samp>fail</samp> is specified, the extract fails when
+ a file is modified by two branches (or more) relative to the base branch.
+ If <samp>merge</samp> is specified, the system will attempt to merge the
+ changes. It will fail only on unresolved conflicts. If
+ <samp>override</samp> is specified, the changes in the last branch takes
+ precedence and the changes in the earlier branches will be ignored. Note:
+ the old <code>override true|false</code> declaration is deprecated. If
+ declared, <code>override true</code> will be equivalent to <code>conflict
+ override</code>, and <code>override false</code> will be equivalent to
+ <code>conflict fail</code>.</p>
+
+ <p>Example:</p>
+ <pre>
+conflict override
+</pre>
+ </dd>
+
+ <dt>BLD::<fields></dt>
+
+ <dd>
+ <p>Declare a build configuration file declaration. The label
+ <fields> is the label of the declaration. On a successful extract,
+ <fields> will be added to the build configuration file. Please note
+ that some of the <fields> may be case sensitive.</p>
+
+ <p>Example:</p>
+ <pre>
+bld::target VarScr_AnalysePF
+bld::tool::fc sxmpif90
+bld::tool::cc sxmpic++
+# ... and so on ...
+</pre>
+ </dd>
+ </dl>
+
+ </div>
+ </div>
+ </div>
+
+ <hr/>
+ <div class="container-fluid text-center">
+ <div class="row-fluid"><div class="span12">
+ <address><small>
+ © British Crown Copyright 2006-14
+ <a href="http://www.metoffice.gov.uk">Met Office</a>.
+ See <a href="../etc/fcm-terms-of-use.html">Terms of Use</a>.<br />
+ This document is released under the British <a href=
+ "http://www.nationalarchives.gov.uk/doc/open-government-licence/" rel=
+ "license">Open Government Licence</a>.<br />
+ </small></address>
+ </div></div>
+ </div>
+
+ <script type="text/javascript" src="../etc/jquery.min.js"></script>
+ <script type="text/javascript" src="../etc/bootstrap/js/bootstrap.min.js"></script>
+ <script type="text/javascript" src="../etc/fcm.js"></script>
+ <script type="text/javascript" src="../etc/fcm-version.js"></script>
+</body>
+</html>
diff --git a/doc/user_guide/annex_fcm_cfg.html b/doc/user_guide/annex_fcm_cfg.html
new file mode 100644
index 0000000..f5d25b9
--- /dev/null
+++ b/doc/user_guide/annex_fcm_cfg.html
@@ -0,0 +1,198 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>FCM: User Guide: Annex: Declarations in FCM 1 central/user configuration
+ file</title>
+ <meta name="author" content="FCM team" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link rel="icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link rel="shortcut icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link href="../etc/bootstrap/css/bootstrap.min.css" rel="stylesheet" media="screen" />
+ <link href="../etc/fcm.css" rel="stylesheet" media="screen" />
+</head>
+<body>
+ <div class="navbar navbar-inverse">
+ <div class="navbar-inner">
+ <a class="brand" href=".."><span class="fcm-version">FCM</span></a>
+ <ul class="nav">
+ <li><a href="../installation/">Installation</a></li>
+
+ <li class="active"><a href=".">User Guide</a></li>
+ </ul>
+ </div>
+ </div>
+
+ <div class="page-header">
+ <div class="fcm-page-content pull-right well well-small"></div>
+ <h1>FCM: User Guide: Annex: Declarations in FCM 1 central/user configuration
+ file</h1>
+ </div>
+
+ <div class="container">
+ <div class="row">
+ <div class="span12">
+
+ <p>The deprecated FCM 1 commands, i.e. <code><a href=
+ "command_ref.html#fcm-build">fcm build</a></code>, <code><a href=
+ "command_ref.html#fcm-cmp-ext-cfg">fcm cmp-ext-cfg</a></code> and
+ <code><a href="command_ref.html#fcm-extract">fcm extract</a></code>, can
+ modify their settings using the central and user configuration files at:</p>
+
+ <ul>
+ <li><samp>$FCM/etc/fcm.cfg</samp> where <var>$FCM/bin/</var> is the path at
+ which <code>fcm</code> is installed.</li>
+
+ <li><samp>$HOME/.fcm</samp>.</li>
+ </ul>
+
+ <p>Note:</p>
+
+ <ul>
+ <li>The detail of the above settings will remain undocumented. For further
+ information, please refer to the Perl module <code>FCM1::Config</code>,
+ which should be located at <samp>lib/FCM1/Config.pm</samp> in your FCM
+ distribution.</li>
+
+ <li>Setting labels in both files are case insensitive.</li>
+ </ul>
+
+ <p>The following deprecated settings related to FCM keywords will no longer
+ work in <samp>$FCM/etc/fcm.cfg</samp> but will continue to work in
+ <samp>$HOME/.fcm</samp>. However, it would be desirable to migrate these
+ settings to their equivalents in <samp>$HOME/.metomi/fcm/keyword.cfg</samp>
+ as documented in <a href="annex_cfg.html#keyword">Annex: FCM Configuration
+ File > FCM Keyword Configuration</a>.</p>
+
+ <dl>
+ <dt>SET::URL::<pck><br />
+ SET::REPOS::<pck></dt>
+
+ <dd>
+ <p><samp>$HOME/.metomi/fcm/keyword.cfg</samp> equivalent: <code><a href=
+ "annex_cfg.html#keyword.location">location</a></code> or <code><a href=
+ "annex_cfg.html#keyword.location">location{primary}</a></code>.</p>
+
+ <p>This declares a URL keyword for the package <pck>. The value of
+ the declaration must be a valid Subversion <URL>. Once declared,
+ the URL keyword <pck> will be associated with the specified URL. In
+ subsequent invocations of the <code>fcm</code> command, the following
+ expansion may take place:</p>
+
+ <ul>
+ <li><samp>fcm:<pck></samp>: replaced by
+ <samp><URL></samp>.</li>
+
+ <li><samp>fcm:<pck>_tr</samp> or <samp>fcm:<pck>-tr</samp>:
+ replaced by <samp><URL>/trunk</samp></li>
+
+ <li><samp>fcm:<pck>_br</samp> or <samp>fcm:<pck>-br</samp>:
+ replaced by <samp><URL>/branches</samp></li>
+
+ <li><samp>fcm:<pck>_tg</samp> or <samp>fcm:<pck>-tg</samp>:
+ replaced by <samp><URL>/tags</samp></li>
+ </ul>
+
+ <p>Example:</p>
+ <pre>
+# Associate "var" with "svn://server/VAR_svn/var"
+set::url::var svn://server/VAR_svn/var
+
+# "fcm:var" is now the same as "svn://server/VAR_svn/var"
+</pre>
+ </dd>
+
+ <dt>SET::REVISION::<pck>::<keyword></dt>
+
+ <dd>
+ <p><samp>$HOME/.metomi/fcm/keyword.cfg</samp> equivalent: <code><a href=
+ "annex_cfg.html#keyword.revision">revision</a></code>.</p>
+
+ <p>This declares <keyword> to be the revision number for the
+ package <pck>. The <keyword> string can contain any
+ characters except spaces. It must not contain only digits (as digits are
+ treated as revision numbers). It must not be the Subversion revision
+ keywords HEAD, BASE, COMMITTED and PREV. It cannot begin and end with a
+ pair of curly brackets (as this will be parsed as a revision date). The
+ package <pck> must be associated with a URL using the
+ SET::URL::<pck> declaration described above before this declaration
+ can make sense. Once defined, <keyword> can be used anywhere in
+ place the defined revision number.</p>
+
+ <p>Example:</p>
+ <pre>
+set::revision::var::v22.0 8410
+
+# E.g. "fcm list -r v22.0 fcm:var" is now the same as
+# "fcm list -r 8410 fcm:var".
+</pre>
+ </dd>
+
+ <dt>SET::URL_BROWSER_MAPPING_DEFAULT::<key></dt>
+
+ <dd>
+ <p><samp>$HOME/.metomi/fcm/keyword.cfg</samp> equivalent: <code><a href=
+ "annex_cfg.html#keyword.browser.comp-pat">browser.comp-pat</a></code>,
+ <code><a href=
+ "annex_cfg.html#keyword.browser.loc-tmpl">browser.loc-tmpl</a></code>,
+ and <code><a href=
+ "annex_cfg.html#keyword.browser.rev-tmpl">browser.rev-tmpl</a></code>.</p>
+
+ <p>These declarations are used to change the global default for mapping a
+ version control system URL to its corresponding web browser URL.
+ <key> can be LOCATION_COMPONENT_PATTERN, BROWSER_URL_TEMPLATE or
+ BROWSER_REV_TEMPLATE.</p>
+
+ <p>Example:</p>
+ <pre>
+set::url_browser_mapping_default::location_component_pattern ^//([^/]+)/(.*)$
+set::url_browser_mapping_default::browser_url_template http://{1}/intertrac/source:{2}{3}
+set::url_browser_mapping_default::browser_rev_template @{1}
+</pre>
+ </dd>
+
+ <dt>SET::URL_BROWSER_MAPPING::<pck>::<key></dt>
+
+ <dd>
+ <p><samp>$HOME/.metomi/fcm/keyword.cfg</samp> equivalent: <code><a href=
+ "annex_cfg.html#keyword.browser.comp-pat">browser.comp-pat</a></code>,
+ <code><a href=
+ "annex_cfg.html#keyword.browser.loc-tmpl">browser.loc-tmpl</a></code>,
+ and <code><a href=
+ "annex_cfg.html#keyword.browser.rev-tmpl">browser.rev-tmpl</a></code>.</p>
+
+ <p>Similar to SET::URL_BROWSER_MAPPING_DEFAULT::<key>, but settings
+ only apply to the specified <pck>.</p>
+
+ <p>Example:</p>
+ <pre>
+set::url_browser_mapping::var::location_component_pattern ^//([^/]+)/(.*)$
+set::url_browser_mapping::var::browser_url_template http://{1}/intertrac/source:{2}{3}
+set::url_browser_mapping::var::browser_rev_template @{1}
+</pre>
+ </dd>
+ </dl>
+
+ </div>
+ </div>
+ </div>
+
+ <hr/>
+ <div class="container-fluid text-center">
+ <div class="row-fluid"><div class="span12">
+ <address><small>
+ © British Crown Copyright 2006-14
+ <a href="http://www.metoffice.gov.uk">Met Office</a>.
+ See <a href="../etc/fcm-terms-of-use.html">Terms of Use</a>.<br />
+ This document is released under the British <a href=
+ "http://www.nationalarchives.gov.uk/doc/open-government-licence/" rel=
+ "license">Open Government Licence</a>.<br />
+ </small></address>
+ </div></div>
+ </div>
+
+ <script type="text/javascript" src="../etc/jquery.min.js"></script>
+ <script type="text/javascript" src="../etc/bootstrap/js/bootstrap.min.js"></script>
+ <script type="text/javascript" src="../etc/fcm.js"></script>
+ <script type="text/javascript" src="../etc/fcm-version.js"></script>
+</body>
+</html>
diff --git a/doc/user_guide/annex_quick_ref.html b/doc/user_guide/annex_quick_ref.html
new file mode 100644
index 0000000..be921cc
--- /dev/null
+++ b/doc/user_guide/annex_quick_ref.html
@@ -0,0 +1,256 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>FCM: User Guide: Annex: Quick Reference</title>
+ <meta name="author" content="FCM team" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link rel="icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link rel="shortcut icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link href="../etc/bootstrap/css/bootstrap.min.css" rel="stylesheet" media="screen" />
+ <link href="../etc/fcm.css" rel="stylesheet" media="screen" />
+</head>
+<body>
+ <div class="navbar navbar-inverse">
+ <div class="navbar-inner">
+ <a class="brand" href=".."><span class="fcm-version">FCM</span></a>
+ <ul class="nav">
+ <li><a href="../installation/">Installation</a></li>
+
+ <li class="active"><a href=".">User Guide</a></li>
+ </ul>
+ </div>
+ </div>
+
+ <div class="page-header">
+ <div class="fcm-page-content pull-right well well-small"></div>
+ <h1>FCM: User Guide: Annex: Quick Reference</h1>
+ </div>
+
+ <div class="container">
+ <div class="row">
+ <div class="span12">
+
+ <p>Note: some sub-commands can be invoked with alternate names. For example,
+ <code>fcm help</code> is the same as <code>fcm ?</code>. In this annex, some
+ favourite alternate names are listed, separated by a pipe, i.e. the above
+ example will be given as <samp>fcm help|?</samp>.</p>
+
+ <h2 id="help">Getting help</h2>
+
+ <dl>
+ <dt><code>fcm help|?</code></dt>
+
+ <dd>get list of subcommands</dd>
+
+ <dt><code>fcm help|? SUBCOMMAND</code></dt>
+
+ <dd>get help on SUBCOMMAND</dd>
+ </dl>
+
+ <h2 id="maintaining-wc">Maintaining the working copy</h2>
+
+ <dl>
+ <dt><code>fcm checkout|co [OPTIONS] URL [DEST]</code></dt>
+
+ <dd>Checkout URL (and create a working copy at DEST)</dd>
+
+ <dt><code>fcm checkout|co -r N URL [DEST]</code></dt>
+
+ <dd>Checkout revision N of URL (and create a working copy at DEST)</dd>
+
+ <dt><code>fcm info</code></dt>
+
+ <dd>Print working copy information</dd>
+
+ <dt><code>fcm status|st [OPTIONS]</code></dt>
+
+ <dd>Print status of working copy</dd>
+
+ <dt><code>fcm status|st -u</code></dt>
+
+ <dd>Show update information</dd>
+
+ <dt><code>fcm status|st -v</code></dt>
+
+ <dd>Show verbose information</dd>
+
+ <dt><code>fcm update|up</code></dt>
+
+ <dd>Update working copy with repository changes</dd>
+
+ <dt><code>fcm switch|sw URL</code></dt>
+
+ <dd>Switch your working copy to point to a branch specified by URL</dd>
+
+ <dt><code>fcm commit|ci</code></dt>
+
+ <dd>Commit local changes back into the repository</dd>
+ </dl>
+
+ <h2 id="preparing-changes">Preparing changes</h2>
+
+ <dl>
+ <dt><code>fcm diff|di [OPTIONS]</code></dt>
+
+ <dd>Display working copy changes in unified diff format</dd>
+
+ <dt><code>fcm branch-diff|bdiff|bdi [OPTIONS]</code></dt>
+
+ <dd>Show differences relative to the base of the branch</dd>
+
+ <dt><code>fcm diff|di -g</code></dt>
+
+ <dd>Display working copy changes with a graphical diff tool</dd>
+
+ <dt><code>fcm diff|di -r N</code></dt>
+
+ <dd>Display working copy changes against revision N</dd>
+
+ <dt><code>fcm diff|di -t</code></dt>
+
+ <dd>Display differences in Trac, (with -b only)</dd>
+
+ <dt><code>fcm revert [OPTIONS] PATH</code></dt>
+
+ <dd>Restore the file PATH to the pristine copy</dd>
+
+ <dt><code>fcm revert -R PATH</code></dt>
+
+ <dd>Descend PATH recursively, restoring any modified files to the pristine
+ copy</dd>
+
+ <dt><code>fcm mkdir [PATH]</code></dt>
+
+ <dd>Add a directory PATH under revision control</dd>
+
+ <dt><code>fcm add [OPTIONS] PATH ...</code></dt>
+
+ <dd>Add PATH under revision control</dd>
+
+ <dt><code>fcm add -c [PATH]</code></dt>
+
+ <dd>Check for items not under revision control and add them</dd>
+
+ <dt><code>fcm delete|del|rm [OPTIONS] PATH ...</code></dt>
+
+ <dd>Remove PATH from revision control</dd>
+
+ <dt><code>fcm delete|del|rm -c [PATH]</code></dt>
+
+ <dd>Check for missing items and remove them</dd>
+
+ <dt><code>fcm copy|cp SRC DST</code></dt>
+
+ <dd>Duplicate SRC to DST, remembering history</dd>
+
+ <dt><code>fcm move|mv SRC DST</code></dt>
+
+ <dd>Move or rename SRC to DST, remembering history</dd>
+
+ <dt><code>fcm propset|ps svn:executable ON FILE</code></dt>
+
+ <dd>Indicate that FILE will have executable permission when checked out to a
+ Unix file system.</dd>
+
+ <dt><code>fcm propdel|pd svn:executable FILE</code></dt>
+
+ <dd>Reverse of the above.</dd>
+
+ <dt><code>fcm propset|ps svn:special ON FILE</code></dt>
+
+ <dd>Indicate that FILE is a symbolic link rather than a regular file.</dd>
+
+ <dt><code>fcm propdel|pd svn:special FILE</code></dt>
+
+ <dd>Reverse of the above.</dd>
+ </dl>
+
+ <h2 id="browse">Browsing</h2>
+
+ <dl>
+ <dt><code>fcm log [OPTIONS] [TARGET]</code></dt>
+
+ <dd>Show the log message of a TARGET that can either be working copy or
+ URL</dd>
+
+ <dt><code>fcm log -r N[:M] [TARGET]</code></dt>
+
+ <dd>Show the log message of a range of reivsions</dd>
+
+ <dt><code>fcm propedit|pe --revprop svn:log -r N [TARGET]</code></dt>
+
+ <dd>Edit the commit log message of revision N.</dd>
+
+ <dt><code>fcm list|ls [OPTIONS] [TARGET]</code></dt>
+
+ <dd>List directory entries in TARGET</dd>
+
+ <dt><code>fcm list|ls -r N [TARGET]</code></dt>
+
+ <dd>List directory entries of revision N</dd>
+
+ <dt><code>fcm list|ls -v [TARGET]</code></dt>
+
+ <dd>List directory entries in verbose mode</dd>
+
+ <dt><code>fcm list|ls -R [TARGET]</code></dt>
+
+ <dd>List directory entries recursively down the directories</dd>
+
+ <dt><code>fcm browse [TARGET]</code></dt>
+
+ <dd>Open a WWW browser to browse TARGET with Trac</dd>
+ </dl>
+
+ <h2 id="branch">Branching</h2>
+
+ <dl>
+ <dt><code>fcm branch-info|binfo [OPTIONS] [URL]</code></dt>
+
+ <dd>Show branch information of URL or local working copy</dd>
+
+ <dt><code>fcm branch-delete|bdel [URL]</code></dt>
+
+ <dd>Show branch information and delete the branch</dd>
+
+ <dt><code>fcm branch-create|bcreate NAME [URL]</code></dt>
+
+ <dd>Create a branch</dd>
+
+ <dt><code>fcm branch-list|blist|bls [--show-all|-a] [URL]</code></dt>
+
+ <dd>Lists branches</dd>
+
+ <dt><code>fcm merge [SOURCE]</code></dt>
+
+ <dd>Merge changes from SOURCE to your working copy</dd>
+
+ <dt><code>fcm conflicts|cf</code></dt>
+
+ <dd>Use xxdiff to resolve conflicts in your working copy</dd>
+ </dl>
+
+ </div>
+ </div>
+ </div>
+
+ <hr/>
+ <div class="container-fluid text-center">
+ <div class="row-fluid"><div class="span12">
+ <address><small>
+ © British Crown Copyright 2006-14
+ <a href="http://www.metoffice.gov.uk">Met Office</a>.
+ See <a href="../etc/fcm-terms-of-use.html">Terms of Use</a>.<br />
+ This document is released under the British <a href=
+ "http://www.nationalarchives.gov.uk/doc/open-government-licence/" rel=
+ "license">Open Government Licence</a>.<br />
+ </small></address>
+ </div></div>
+ </div>
+
+ <script type="text/javascript" src="../etc/jquery.min.js"></script>
+ <script type="text/javascript" src="../etc/bootstrap/js/bootstrap.min.js"></script>
+ <script type="text/javascript" src="../etc/fcm.js"></script>
+ <script type="text/javascript" src="../etc/fcm-version.js"></script>
+</body>
+</html>
diff --git a/doc/user_guide/annex_quick_ref_tree_conflicts.html b/doc/user_guide/annex_quick_ref_tree_conflicts.html
new file mode 100644
index 0000000..3cd2f99
--- /dev/null
+++ b/doc/user_guide/annex_quick_ref_tree_conflicts.html
@@ -0,0 +1,283 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>FCM: User Guide: Annex: Quick Reference: Tree Conflict Resolution</title>
+ <meta name="author" content="FCM team" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link rel="icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link rel="shortcut icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link href="../etc/bootstrap/css/bootstrap.min.css" rel="stylesheet" media="screen" />
+ <link href="../etc/fcm.css" rel="stylesheet" media="screen" />
+</head>
+<body>
+ <div class="navbar navbar-inverse">
+ <div class="navbar-inner">
+ <a class="brand" href=".."><span class="fcm-version">FCM</span></a>
+ <ul class="nav">
+ <li><a href="../installation/">Installation</a></li>
+
+ <li class="active"><a href=".">User Guide</a></li>
+ </ul>
+ </div>
+ </div>
+
+ <div class="page-header">
+ <div class="fcm-page-content pull-right well well-small"></div>
+ <h1>FCM: User Guide: Annex: Quick Reference: Tree Conflict Resolution</h1>
+ </div>
+
+ <div class="container">
+ <div class="row">
+ <div class="span12">
+
+ <h2 id="tree:reference:intro">Introduction</h2>
+
+ <p>A tree conflict appears in <code>fcm status</code> like this:</p>
+ <pre>
+! C subroutine/hello_sub_dummy.h
+ > local missing, incoming edit upon merge
+</pre>
+
+ <p>and can require complex action to solve, based on the situation. Happily,
+ <code>fcm conflicts</code> automates the resolution of most ordinary tree
+ conflicts.</p>
+
+ <p>This page is intended to give more information about the meaning of the
+ common tree conflicts, and to give guidance on those that aren't handled
+ automatically, such as directory tree conflicts.</p>
+
+ <p>The list below is ordered by output from <code>fcm status</code>. There
+ are two choices that the user must make:</p>
+
+ <ul>
+ <li><em>keep local</em>: Choose the local branch or revision version of a
+ file or directory. Answer <samp>y</samp> in <code>fcm
+ conflicts</code>.</li>
+
+ <li><em>discard local</em>: Choose the merge branch or other revision
+ version of a file or directory. Answer <samp>n</samp> in <code>fcm
+ conflicts</code>.</li>
+ </ul>
+
+ <p>For example, if you are merging the trunk into a working copy of a branch,
+ <em>keep local</em> would refer to <strong>keeping</strong> the changes as
+ they existed on the branch; <em>discard local</em> would refer to accepting
+ the trunk changes and <strong>discarding</strong> the branch ones.</p>
+
+ <p>In this page, we use the word <em>local</em> for your local working copy,
+ and <em>external</em> for the outside source you are updating or merging in
+ from. In the example above, <em>local</em> would mean your working copy of
+ the branch; <em>external</em> would mean the trunk.</p>
+
+ <p>Subversion implements rename as a copy-and-delete operation. This means a
+ rename can show up as a delete (or <em>missing</em>) in the tree conflict
+ information.</p>
+
+ <p>It's very important to find out if your tree conflict arises from a
+ rename, but this information has to be dug out of <code>fcm log</code>. A
+ rename can have occurred locally or externally. An external rename would show
+ up in <code>fcm status</code> as an addition with history (<samp>A</samp>
+ with <samp>+</samp>): for example:</p>
+ <pre>
+A + FILENAME
+</pre>
+
+ <p><code>fcm log -v FILENAME</code> can be used to examine if this is really
+ just a rename - it will show up as FILENAME (from ORIGINAL_FILENAME).</p>
+
+ <p>Local renames that have been committed won't show up in <code>fcm
+ status</code>. These can still be found using <code>fcm log -v</code> for
+ each filename, or you can try to remember what happened!</p>
+
+ <h2 id="tree:reference:list">Resolution List</h2>
+
+ <p>This section contains specific help on different types of tree
+ conflict.</p>
+
+ <p>Find the relevant section below by running <code>fcm status</code> and
+ looking up the information below the file in conflict - e.g.:</p>
+ <pre>
+! C subroutine/hello_sub_dummy.h
+ > local delete, incoming delete upon merge
+</pre>
+
+ <p>in this case the <samp>local delete, incoming delete upon merge</samp> is
+ the correct section header.</p>
+
+ <p>There are some situations not covered below - <samp>local
+ obstruction</samp> is not covered here, as it is a case of the user
+ corrupting the working copy - try a new checkout. Similarly, <samp>local
+ unversioned</samp> is just a case of a problem with something in the working
+ copy - an unversioned file or directory exists where Subversion wants to put
+ the new stuff. Delete or move it, and try the merge again.</p>
+
+ <p>If you know that a rename has happened, use the <samp>(renaming)</samp>
+ suffix for your section below. Otherwise, choose the <samp>(no
+ renaming)</samp> suffix.</p>
+
+ <p><code>fcm resolve</code> always takes the form <code>fcm resolve --accept
+ working FILENAME</code> for tree conflicts.</p>
+
+ <dl>
+ <dt id="add:add"><samp>local add, incoming add upon merge</samp></dt>
+
+ <dd>
+ <p><dfn>what it means:</dfn>: files or directories added with the same
+ name independently</p>
+
+ <p><dfn>what keep local does</dfn>: uses rename to shuffle the old file
+ to a different name, copies the new file in, renames the new file to the
+ original name but with a temporary-style suffix (e.g. hello.F90 ->
+ hello.F90.xD4r), and again renames the old file to the original name.
+ (Then runs <code>fcm resolve</code>).</p>
+
+ <p><dfn>what discarding local does</dfn>: renames the old file to give it
+ a temporary-style suffix (e.g. hello.F90 -> hello.F90.r6Ys), and
+ copies the new file into the original name. (Then runs <code>fcm
+ resolve</code>).</p>
+ </dd>
+
+ <dt id="edit:delete:no_rename"><samp>local edit, incoming delete upon merge
+ (no renaming)</samp></dt>
+
+ <dd>
+ <p><dfn>meaning</dfn>: file or directory modified on the branch locally,
+ but deleted on the merge branch</p>
+
+ <p><dfn>what keep local does</dfn>:just runs <code>fcm
+ resolve</code>.</p>
+
+ <p><dfn>what discarding local does</dfn>:deletes the file or directory
+ and runs <code>fcm resolve</code>.</p>
+ </dd>
+
+ <dt id="edit:delete:rename"><samp>local edit, incoming delete upon merge
+ (renaming)</samp></dt>
+
+ <dd>
+ <p><dfn>meaning</dfn>: file modified on branch locally, but renamed on
+ merge branch</p>
+
+ <p><dfn>what keep local does</dfn>: copies over the renamed file, and the
+ common ancestor of the file on the branches, and uses them for a text
+ conflict style merge into the old (local) filename. It then removes the
+ renamed file and runs <code>fcm resolve</code>.</p>
+
+ <p><dfn>what discarding local does</dfn>: copies over the renamed file,
+ and the common ancestor of the file on the branches, and uses them for a
+ text conflict style merge into the new renamed file. It then deletes the
+ old file and runs <code>fcm resolve</code>.</p>
+ </dd>
+
+ <dt id="delete:delete:no_rename"><samp>local delete, incoming delete upon
+ merge (no renaming)</samp></dt>
+
+ <dd>
+ <p><dfn>meaning</dfn>: same filename deleted both locally and
+ externally.</p>
+
+ <p><dfn>in both cases</dfn>: just runs <code>fcm resolve</code>.</p>
+ </dd>
+
+ <dt id="delete:rename:rename"><samp>local delete, incoming delete upon
+ merge (just external renaming)</samp></dt>
+
+ <dd>
+ <p><dfn>meaning</dfn>: file deleted locally, but renamed externally</p>
+
+ <p><dfn>what keep local does</dfn>: just runs <code>fcm
+ resolve</code>.</p>
+
+ <p><dfn>what discarding local does</dfn>: just deletes the new renamed
+ file and runs <code>fcm resolve</code>.</p>
+ </dd>
+
+ <dt id="rename:delete:rename"><samp>local delete, incoming delete upon
+ merge (just local renaming)</samp></dt>
+
+ <dd>
+ <p><dfn>meaning</dfn>: file renamed locally, but deleted externally</p>
+
+ <p><dfn>what keep local does</dfn>: just runs <code>fcm
+ resolve</code>.</p>
+
+ <p><dfn>what discarding local does</dfn>: deletes the local renamed file
+ and runs <code>fcm resolve</code>.</p>
+ </dd>
+
+ <dt id="delete:delete:rename"><samp>local delete, incoming delete upon
+ merge (local renaming AND external renaming)</samp></dt>
+
+ <dd>
+ <p><dfn>meaning</dfn>: same file renamed locally AND externally, to two
+ different names.</p>
+
+ <p><dfn>what keep local does</dfn>: copies in the external file and
+ common ancestor file to construct a text-style merge using
+ <code>xxdiff</code> into the locally-renamed filename. Removes the
+ external rename and runs <code>fcm resolve</code>.</p>
+
+ <p><dfn>what discarding local does</dfn>: copies in the external file and
+ common ancestor file to construct a text-style merge using
+ <code>xxdiff</code> into the externally-renamed filename. Removes the
+ local rename and runs <code>fcm resolve</code>.</p>
+ </dd>
+
+ <dt id="missing:edit:no_rename"><samp>local missing, incoming edit upon
+ merge (no renaming)</samp></dt>
+
+ <dd>
+ <p><dfn>meaning</dfn>: locally deleted file, add newer file from merge
+ branch?</p>
+
+ <p><dfn>what keep local does</dfn>: just runs <code>fcm
+ resolve</code>.</p>
+
+ <p><dfn>what discarding local does</dfn>: copies in the external file
+ using its URL and runs <code>fcm resolve</code>.</p>
+ </dd>
+
+ <dt id="missing:edit:rename"><samp>local missing, incoming edit upon merge
+ (renaming)</samp></dt>
+
+ <dd>
+ <p><dfn>meaning</dfn>: locally renamed file, but external changes to the
+ old filename</p>
+
+ <p><dfn>what keep local does</dfn>: copies in the external file and
+ common ancestor to construct a text-style merge using
+ <code>xxdiff</code>, into the locally-renamed filename. Runs <code>fcm
+ resolve</code>.</p>
+
+ <p><dfn>what discarding local does</dfn>: copies in the external file and
+ common ancestor to construct a text-style merge using
+ <code>xxdiff</code>, into the original filename. Deletes the
+ locally-renamed file and adds the original filename, then runs <code>fcm
+ resolve</code>.</p>
+ </dd>
+ </dl>
+
+ </div>
+ </div>
+ </div>
+
+ <hr/>
+ <div class="container-fluid text-center">
+ <div class="row-fluid"><div class="span12">
+ <address><small>
+ © British Crown Copyright 2006-14
+ <a href="http://www.metoffice.gov.uk">Met Office</a>.
+ See <a href="../etc/fcm-terms-of-use.html">Terms of Use</a>.<br />
+ This document is released under the British <a href=
+ "http://www.nationalarchives.gov.uk/doc/open-government-licence/" rel=
+ "license">Open Government Licence</a>.<br />
+ </small></address>
+ </div></div>
+ </div>
+
+ <script type="text/javascript" src="../etc/jquery.min.js"></script>
+ <script type="text/javascript" src="../etc/bootstrap/js/bootstrap.min.js"></script>
+ <script type="text/javascript" src="../etc/fcm.js"></script>
+ <script type="text/javascript" src="../etc/fcm-version.js"></script>
+</body>
+</html>
diff --git a/doc/user_guide/api.html b/doc/user_guide/api.html
new file mode 100644
index 0000000..9913835
--- /dev/null
+++ b/doc/user_guide/api.html
@@ -0,0 +1,222 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>FCM: User Guide: A Brief Introduction to the FCM Perl API</title>
+ <meta name="author" content="FCM team" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link rel="icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link rel="shortcut icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link href="../etc/bootstrap/css/bootstrap.min.css" rel="stylesheet" media="screen" />
+ <link href="../etc/fcm.css" rel="stylesheet" media="screen" />
+</head>
+<body>
+ <div class="navbar navbar-inverse">
+ <div class="navbar-inner">
+ <a class="brand" href=".."><span class="fcm-version">FCM</span></a>
+ <ul class="nav">
+ <li><a href="../installation/">Installation</a></li>
+
+ <li class="active"><a href=".">User Guide</a></li>
+ </ul>
+ </div>
+ </div>
+
+ <div class="page-header">
+ <div class="fcm-page-content pull-right well well-small"></div>
+ <h1>FCM: User Guide: A Brief Introduction to the FCM Perl API</h1>
+ </div>
+
+ <div class="container">
+ <div class="row">
+ <div class="span12">
+
+ <p>The majority of FCM functionalities are provided by a set of Perl modules.
+ Old modules developed prior to <a href="../release_notes/2-0.html">release
+ 2-0</a> reside in the <code>FCM1::*</code> name-space. Modules developed
+ thereafter reside in the <code>FCM::*</code> name-space. These are
+ sub-divided into the following name-spaces:</p>
+
+ <dl>
+ <dt><code>FCM::Class</code></dt>
+
+ <dd>
+ <p>Provides an internal object class framework.</p>
+
+ <p>The majority of the classes in the <code>FCM::*</code> name-space are
+ sub-classes of either <code>FCM::Class::CODE</code> or
+ <code>FCM::Class::HASH</code>. The former creates classes that are
+ blessed <code>CODE</code> references, and is intended for configurable
+ functional (i.e. mostly <em>stateless</em>) objects. The latter creates
+ classes that are blessed <code>HASH</code> references, and is intended
+ for data objects.</p>
+
+ <p>Note: In theory, we could use the standard module <code><a href=
+ "http://search.cpan.org/~rjbs/perl-5.12.3/lib/Class/Struct.pm">Class::Struct</a></code>
+ or the modern <a href="http://www.iinteractive.com/moose/">MOOSE</a>
+ framework. The problem is that the former is not powerful enough to give
+ us what we need, and the latter is not a standard module and is too heavy
+ weight for our intends and purposes. Instead, the developer decides that
+ it is easier to go for a light weight and in house solution.</p>
+ </dd>
+
+ <dt><code>FCM::CLI</code></dt>
+
+ <dd>
+ <p>Provides the logic and configuration of the command line interface
+ (CLI).</p>
+
+ <p>It is made up of the following components:</p>
+
+ <dl>
+ <dt><code>FCM::CLI</code></dt>
+
+ <dd>Logic to provide help and invoke functions of
+ <code>FCM::System</code>.</dd>
+
+ <dt><code>FCM::CLI::Exception</code></dt>
+
+ <dd>CLI exception.</dd>
+
+ <dt><code>FCM::CLI::Parser</code></dt>
+
+ <dd>CLI option parser and configuration.</dd>
+
+ <dt><samp>fcm-*.pod</samp></dt>
+
+ <dd>Help files for the CLI commands.</dd>
+ </dl>
+ </dd>
+
+ <dt><code>FCM::Context::*</code></dt>
+
+ <dd>
+ <p>Provides the data structures for storing the run time contexts.</p>
+
+ <p>The objects of these classes do very little, but they provide the data
+ structures that define the <em>states</em> of the program at run
+ time.</p>
+ </dd>
+
+ <dt><code>FCM::Exception</code></dt>
+
+ <dd>
+ <p>Provides the base class for exceptions.</p>
+ </dd>
+
+ <dt><code>FCM::System</code></dt>
+
+ <dd>
+ <p>Provides a façade to the functionalities of the FCM system.</p>
+
+ <p>The actual implementation is delegated to the following:</p>
+
+ <dl>
+ <dt><code>FCM::System::CM</code></dt>
+
+ <dd>The code management system. Currently a thin adapter to
+ <code>FCM1::Cm</code>.</dd>
+
+ <dt><code>FCM::System::Misc</code></dt>
+
+ <dd>Miscellaneous functions, e.g. <code>browse</code>,
+ <code>cfg-print</code>, <code>keyword-print</code>.</dd>
+
+ <dt><code>FCM::System::Old</code></dt>
+
+ <dd>Thin adapter to the old extract and build systems.</dd>
+
+ <dt><code>FCM::System::Make</code></dt>
+
+ <dd>The logic of the FCM make system.</dd>
+
+ <dt><code>FCM::System::Make::Build</code></dt>
+
+ <dd>FCM make: build system logic.</dd>
+
+ <dt><code>FCM::System::Make::Build::*</code></dt>
+
+ <dd>FCM make: build system components: File type and task specific
+ logic.</dd>
+
+ <dt><code>FCM::System::Make::Extract</code></dt>
+
+ <dd>FCM make: extract system logic.</dd>
+
+ <dt><code>FCM::System::Make::Mirror</code></dt>
+
+ <dd>FCM make: mirror system logic.</dd>
+
+ <dt><code>FCM::System::Make::Preprocess</code></dt>
+
+ <dd>FCM make: preprocess system logic, actually a configuration of
+ <code>FCM::System::Make::Build</code>.</dd>
+
+ <dt><code>FCM::System::Make::Share::*</code></dt>
+
+ <dd>Shared logic between all subsystems in
+ <code>FCM::System::Make::*</code>.</dd>
+ </dl>
+ </dd>
+
+ <dt><code>FCM::Util</code></dt>
+
+ <dd>
+ <p>Provides supporting utilities.</p>
+
+ <p>Functionalities include:</p>
+
+ <ul>
+ <li>abstract utilities for SVN URLs and file system paths.</li>
+
+ <li>configuration file reader.</li>
+
+ <li>event handler.</li>
+
+ <li>file utilities.</li>
+
+ <li>message report.</li>
+
+ <li>name space utilities.</li>
+
+ <li>shell invocation.</li>
+
+ <li>multi-process task runner.</li>
+
+ <li>timer.</li>
+ </ul>
+
+ <p>The logic of the more complex utilities are delegated to modules in
+ the <code>FCM::Util::*</code> name space.</p>
+ </dd>
+ </dl>
+
+ <p>Note: The majority of modules in the old <code>FCM1::*</code> name space
+ are considered deprecated, with the exception of <code>FCM1::Cm</code> and
+ those providing support functionalities for it. The functionalities of these
+ modules will eventually be absorbed into the <code>FCM::System::CM</code>
+ framework.</p>
+
+ </div>
+ </div>
+ </div>
+
+ <hr/>
+ <div class="container-fluid text-center">
+ <div class="row-fluid"><div class="span12">
+ <address><small>
+ © British Crown Copyright 2006-14
+ <a href="http://www.metoffice.gov.uk">Met Office</a>.
+ See <a href="../etc/fcm-terms-of-use.html">Terms of Use</a>.<br />
+ This document is released under the British <a href=
+ "http://www.nationalarchives.gov.uk/doc/open-government-licence/" rel=
+ "license">Open Government Licence</a>.<br />
+ </small></address>
+ </div></div>
+ </div>
+
+ <script type="text/javascript" src="../etc/jquery.min.js"></script>
+ <script type="text/javascript" src="../etc/bootstrap/js/bootstrap.min.js"></script>
+ <script type="text/javascript" src="../etc/fcm.js"></script>
+ <script type="text/javascript" src="../etc/fcm-version.js"></script>
+</body>
+</html>
diff --git a/doc/user_guide/build.html b/doc/user_guide/build.html
new file mode 100644
index 0000000..12ec032
--- /dev/null
+++ b/doc/user_guide/build.html
@@ -0,0 +1,1646 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>FCM: User Guide: Annex: The FCM 1 Build System</title>
+ <meta name="author" content="FCM team" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link rel="icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link rel="shortcut icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link href="../etc/bootstrap/css/bootstrap.min.css" rel="stylesheet" media="screen" />
+ <link href="../etc/fcm.css" rel="stylesheet" media="screen" />
+</head>
+<body>
+ <div class="navbar navbar-inverse">
+ <div class="navbar-inner">
+ <a class="brand" href=".."><span class="fcm-version">FCM</span></a>
+ <ul class="nav">
+ <li><a href="../installation/">Installation</a></li>
+
+ <li class="active"><a href=".">User Guide</a></li>
+ </ul>
+ </div>
+ </div>
+
+ <div class="page-header">
+ <div class="fcm-page-content pull-right well well-small"></div>
+ <h1>FCM: User Guide: Annex: The FCM 1 Build System</h1>
+ </div>
+
+ <div class="container">
+ <div class="row">
+ <div class="span12">
+
+ <h2 id="introduction">Introduction</h2>
+
+ <p><em>The FCM 1 build system is deprecated. The documentation for the current
+ build system can be found at <a href="make.html">FCM Make</a>.</em></p>
+
+ <p>The build system analyses the directory tree containing a set of source
+ code, processes the configuration, and invokes <code>make</code> to
+ compile/build the source code into the project executables. In this chapter,
+ we shall use many examples to explain how to use the build system. At the end
+ of this chapter, you should be able to use the build system, either by
+ defining the build configuration file directly or by using the extract system
+ to generate a suitable build configuration file.</p>
+
+ <h2 id="command">The Build Command</h2>
+
+ <p>To invoke the build system, simply issue the command:</p>
+ <pre>
+fcm build
+</pre>
+
+ <p>By default, the build system searches for a build configuration file
+ <samp>bld.cfg</samp> in <samp>$PWD</samp> and then <samp>$PWD/cfg</samp>. If
+ a build configuration file is not found in these directories, the command
+ fails with an error. If a build configuration file is found, the system will
+ use the configuration specified in the file to perform the build. If you use
+ the extract system to extract your source tree, a build configuration should
+ be written for you automatically at the <samp>cfg/</samp> sub-directory of
+ the destination root directory.</p>
+
+ <p>If the root directory of the build does not exist, the system performs a
+ new full build at this directory. If a previous build already exists at this
+ directory, the system performs an incremental build. If a full (fresh) build
+ is required for whatever reason, you can invoke the build system using the
+ <code>-f</code> option, (i.e. the command becomes <code>fcm build -f</code>).
+ If you simply want to remove all the items generated by a previous build in
+ the destination, you can invoke the build system using the
+ <code>--clean</code> option.</p>
+
+ <p>The build system uses GNU <code>make</code> to perform the majority of the
+ build. GNU <code>make</code> has a <code>-j jobs</code> option to specify the
+ number of <var>jobs</var> to run simultaneously. Invoking the build system
+ with the same option triggers this option when the build system invokes the
+ <code>make</code> command. The argument to the option <var>jobs</var> must be
+ an integer. The default is <samp>1</samp>. For example, the command <code>fcm
+ build -j 4</code> will allow <code>make</code> to perform 4 jobs
+ simultaneously.</p>
+
+ <p>For further information on the build command, please see <a href=
+ "command_ref.html#fcm-build">FCM Command Reference > fcm build</a>.</p>
+
+ <h2 id="basic">Basic Features</h2>
+
+ <p>The build configuration file is the user interface of the build system. It
+ is a line based text file. You can create your own build configuration file
+ or you can use the extract system to create one for you. For a complete set
+ of build configuration file declarations, please refer to the <a href=
+ "annex_bld_cfg.html">Annex: Declarations in FCM build configuration
+ file</a>.</p>
+
+ <h3 id="basic_build">Basic build configuration</h3>
+
+ <p>Suppose we have a directory at <samp>$HOME/example</samp>. Its
+ sub-directory at <samp>$HOME/example/src</samp> contains a source tree to be
+ built. You may want to have a build configuration file
+ <samp>$HOME/example/cfg/bld.cfg</samp>, which may contain:</p>
+ <pre id="example_1">
+# Example 1
+# ----------------------------------------------------------------------
+cfg::type bld # line 1
+cfg::version 1.0 # line 2
+
+dest $HOME/example # line 4
+
+target foo.exe bar.exe # line 6
+
+tool::fc ifort # line 8
+tool::fflags -O3 # line 9
+tool::cc gcc # line 10
+tool::cflags -O3 # line 11
+
+tool::ldflags -O3 -L$(HOME)/lib -legg -lham # line 13
+</pre>
+
+ <p>Here is an explanation of what each line does:</p>
+
+ <ul>
+ <li><dfn>line 1</dfn>: the label <code>CFG::TYPE</code> declares the type
+ of the configuration file. The value <samp>bld</samp> tells the system that
+ it is a build configuration file.</li>
+
+ <li><dfn>line 2</dfn>: the label <code>CFG::VERSION</code> declares the
+ version of the build configuration file. The current default is
+ <samp>1.0</samp>. Although it is not currently used, if we have to change
+ the format of the configuration file at a later stage, we shall be able to
+ use this number to determine whether we are reading a file with an older
+ format or one with a newer format.</li>
+
+ <li><dfn>line 4</dfn>: the label <code>DEST</code> declares the root
+ directory of the current build.</li>
+
+ <li><dfn>line 6</dfn>: the label <code>TARGET</code> declares a list of
+ <em>default</em> targets. The default targets of the current build will be
+ <samp>foo.exe</samp> and <samp>bar.exe</samp>.</li>
+
+ <li><dfn>line 8</dfn>: the label <code>TOOL::FC</code> declares the Fortran
+ compiler command.</li>
+
+ <li><dfn>line 9</dfn>: the label <code>TOOL::FFLAGS</code> declares the
+ options to be used when invoking the Fortran compiler command.</li>
+
+ <li><dfn>line 10</dfn>: the label <code>TOOL::CC</code> declares the C
+ compiler command.</li>
+
+ <li><dfn>line 11</dfn>: the label <code>TOOL::CFLAGS</code> declares the
+ options to be used when invoking the C compiler command.</li>
+
+ <li><dfn>line 13</dfn>: the label <code>TOOL::LDFLAGS</code> declares the
+ options to be used when invoking the linker command.</li>
+ </ul>
+
+ <p>When we invoke the build system, it reads the above configuration file. It
+ will go through various internal processes, such as dependency generations,
+ to obtain the required information to prepare the <samp>Makefile</samp> of
+ the build. (All of which will be described in later sections.) The
+ <samp>Makefile</samp> of the build will be placed at
+ <samp>$HOME/example/bld</samp>. The system will then invoke <code>make</code>
+ to build the targets specified in line 6, i.e. <samp>foo.exe</samp> and
+ <samp>bar.exe</samp> using the build tools specified between line 8 to line
+ 13. On a successful build, the target executables will be sent to
+ <samp>$HOME/example/bin/</samp>. The build system also creates a shell script
+ called <samp>fcm_env.sh</samp> in <samp>$HOME/example/</samp>. If you source
+ the shell script, it will export your <var>PATH</var> environment variable to
+ search the <samp>$HOME/example/bin/</samp> directory for executables.</p>
+
+ <p>N.B. You may have noticed that the <code>-c</code> (compile to object file
+ only) option is missing from the compiler flags declarations. This is because
+ the option is inserted automatically by the build system, unless it is
+ already declared.</p>
+
+ <p>N.B. You can declare the linker using <code>TOOL::LD</code>. If it is not
+ specified, the default is to use the compiler command for the source file
+ containing the main program.</p>
+
+ <dl>
+ <dt>Note - declaration of source files for build</dt>
+
+ <dd>
+ <p>Source files do not have to reside in the <samp>src/</samp>
+ sub-directory of the build root directory. They can be anywhere, but you
+ will have to declare them using the label <code>SRC::<pcks></code>,
+ where <code><pcks></code> is the sub-package name in which the
+ source belongs. If a directory is specified then the build system
+ automatically searches for all source files in this directory. E.g.</p>
+ <pre>
+# Declare a source in the sub-package "foo/bar"
+src::foo/bar $HOME/foo/bar
+</pre>
+
+ <p>By default, the build system searches the <samp>src/</samp>
+ sub-directory of the build root directory for source files. If all source
+ files are already declared explicitly, you can switch off the automatic
+ directory search by setting the <code>SEARCH_SRC</code> flag to false.
+ E.g.</p>
+ <pre>
+search_src false
+</pre>
+
+ <p>As mentioned in the previous chapter, the name of a sub-package
+ <pcks> provides a unique namespace for a file. The name of a
+ sub-package is a list of words delimited by a slash <code>/</code>. (The
+ system uses the double colons <code>::</code> and the double underscores
+ <code>__</code> internally. Please avoid using <code>::</code> and
+ <code>__</code> for naming your files and directories.)</p>
+
+ <p>Currently, the build system only supports non-space characters in the
+ package name, as the space character is used as a delimiter between the
+ declaration label and its value. If there are spaces in the path name to
+ a file or directory, you should explicity re-define the package name of
+ that path to a package name with no space using the above method.
+ However, we recommend that only non-space characters are used for naming
+ directories and files to make life simple.</p>
+
+ <p>In the build system, the sub-package name also provides an
+ <em>inheritance</em> relationship for sub-packages. For instance, we may
+ have a sub-package called <samp>foo/bar/egg</samp>, which belongs to the
+ sub-package <samp>foo/bar</samp>, which belongs to the package
+ <samp>foo</samp>.</p>
+
+ <ul>
+ <li>If we declare a global build tool, it applies to all packages.</li>
+
+ <li>If we declare a build tool for <samp>foo</samp>, it applies also to
+ the sub-package <samp>foo/bar</samp> and <samp>foo/bar/egg</samp>.</li>
+
+ <li>If we declare a build tool for <samp>foo/bar</samp>, it applies
+ also to <samp>foo/bar/egg</samp>, but not to other sub-packages in
+ <samp>foo</samp>.</li>
+ </ul>
+ </dd>
+ </dl>
+
+ <h3 id="basic_extract">Build configuration via the extract system</h3>
+
+ <p>As mentioned earlier, you can obtain a build configuration file through
+ the extract system. The following example is what you may have in your
+ extract configuration in order to obtain a similar configuration as <a href=
+ "#example_1">example 1</a>:</p>
+ <pre id="example_2">
+# Example 2
+# ----------------------------------------------------------------------
+cfg::type ext # line 1
+cfg::version 1.0 # line 2
+
+dest $HOME/example # line 4
+
+bld::target foo.exe bar.exe # line 6
+
+bld::tool::fc ifort # line 8
+bld::tool::fflags -O3 # line 9
+bld::tool::cc gcc # line 10
+bld::tool::cflags -O3 # line 11
+
+bld::tool::ldflags -O3 -L$(HOME)/lib -legg -lham # line 13
+
+# ... and other declarations for source locations ...
+</pre>
+
+ <p>It is easy to note the similarities and differences between <a href=
+ "#example_1">example 1</a> and <a href="#example_2">example 2</a>. <a href=
+ "#example_2">Example 2</a> is an extract configuration file. It extracts to a
+ destination root directory that will become the root directory of the build.
+ Line 6 to line 13 are the same declarations, except that they are now
+ prefixed with <code>BLD::</code>. In an extract configuration file, any lines
+ prefixed with <code>BLD::</code> means that they are build configuration
+ setting. These lines are ignored by the extract system but are parsed down to
+ the output build configuration file, with the <code>BLD::</code> prefix
+ removed. (Note: the <code>BLD::</code> prefix is optional for declarations in
+ a build configuration file.)</p>
+
+ <p>N.B. If you use the extract system to mirror an extract to an alternate
+ location, the extract system will assume that the root directory of the
+ alternate destination is the root directory of the build, and that the build
+ will be carried out in that destination.</p>
+
+ <h3 id="basic_exename">Naming of executables</h3>
+
+ <p>If a source file called <samp>foo.f90</samp> contains a main program, the
+ default behaviour of the system is to name its executable
+ <samp>foo.exe</samp>. The root name of the executable is the same as the
+ original file name, but its file extension is replaced with
+ <samp>.exe</samp>. The output extension can be altered by re-registering the
+ extension for output EXE files. How this can be done will be discussed later
+ in the sub-section <a href="#advanced_file-type">File Type</a>.</p>
+
+ <p>If you need to alter the full name of the executable, you can use the
+ <code>EXE_NAME::</code> declaration. For example, the declaration:</p>
+ <pre>
+bld::exe_name::foo bar
+</pre>
+
+ <p>will rename the executable of <samp>foo.f90</samp> from
+ <samp>foo.exe</samp> to <samp>bar</samp>.</p>
+
+ <p>Note: the declaration label is <code>bld::exe_name::foo</code> (not
+ <code>bld::exe_name::foo.exe</code>) and the executable will be named
+ <samp>bar</samp> (not <samp>bar.exe</samp>).</p>
+
+ <h3 id="basic_flags">Setting the compiler flags</h3>
+
+ <p>As discussed in the first example, the compiler commands and their flags
+ can be set via the <code>TOOL::</code> declarations. A simple
+ <code>TOOL::FFLAGS</code> declaration, for example, alters the compiler
+ options for compiling all Fortran source files in the build. If you need to
+ alter the compiler options only for the source files in a particular
+ sub-package, it is possible to do so by adding the sub-package name to the
+ declaration label. For example, the declaration label
+ <code>TOOL::FFLAGS::foo/bar</code> will ensure that the declaration only
+ applies to the code in the sub-package <samp>foo/bar</samp>. You can even
+ make declarations down to the individual source file level. For example, the
+ declaration label <code>TOOL::FFLAGS::foo/bar/egg.f90</code> will ensure that
+ the declaration applies only for the file <samp>foo/bar/egg.f90</samp>.</p>
+
+ <p>N.B. Although the prefix <code>TOOL::</code> and the tool names are
+ case-insensitive, sub-package names are case sensitive in the declarations.
+ Internally, tool names are turned into uppercase, and the sub-package
+ delimiters are changed from the slash <code>/</code> (or double colons
+ <code>::</code>) to the double underscores <code>__</code>. When the system
+ generates the <samp>Makefile</samp> for the build, each <code>TOOL</code>
+ declaration will be exported as an environment variable. For example, the
+ declaration <code>tool::fflags/foo/bar</code> will be exported as
+ <samp>FFLAGS__foo__bar</samp>.</p>
+
+ <p>N.B. <code>TOOL</code> declarations for sub-packages are only accepted by
+ the system when it is sensible to do so. For example, it allows you to
+ declare different compiler flags, linker commands and linker flags for
+ different sub-packages, but it does not accept different compilers for
+ different sub-packages. If you attempt to make a <code>TOOL</code>
+ declaration for a sub-package that does not exist, the build system will exit
+ with an error.</p>
+
+ <p>The following is an example setting in an extract configuration file based
+ on <a href="#example_2">example 2</a>:</p>
+ <pre id="example_3">
+# Example 3
+# ----------------------------------------------------------------------
+cfg::type ext
+cfg::version 1.0
+
+dest $HOME/example
+
+bld::target foo.exe bar.exe
+
+bld::tool::fc ifort
+bld::tool::fflags -O3 # line 9
+bld::tool::cc gcc
+bld::tool::cflags -O3
+
+bld::tool::ldflags -L$(HOME)/lib -legg -lham
+
+bld::tool::fflags::ops -O1 -C # line 15
+bld::tool::fflags::gen -O2 # line 16
+
+# ... and other declarations for repositories and source directories ...
+</pre>
+
+ <p>In the example above, line 15 alters the Fortran compiler flags for
+ <samp>ops</samp>, so that all source files in <samp>ops</samp> will be
+ compiled with optimisation level 1 and will have runtime error checking
+ switched on. Line 16, alters the Fortran compiler flags for <samp>gen</samp>,
+ so that all source files in <samp>gen</samp> will be compiled with
+ optimisation level 2. All other Fortran source files will use the global
+ setting declared at line 9, so they they will all be compiled with
+ optimisation level 3.</p>
+
+ <dl>
+ <dt>Note - changing compiler flags in incremental builds</dt>
+
+ <dd>
+ <p>Suppose you have performed a successful build using the configuration
+ in <a href="#example_3">example 3</a>, and you have decided to change
+ some of the compiler flags, you can do so by altering the appropriate
+ flags in the build configuration file. When you trigger an incremental
+ build, the system will detect changes in compiler flags automatically,
+ and update only the required targets. The following hierarchy is
+ followed:</p>
+
+ <ul>
+ <li>If the compiler flags for a particular source file change, only
+ that source file and any targets depending on that source file are
+ re-built.</li>
+
+ <li>If the compiler flags for a container package change, only source
+ files within that container package and any targets depending on those
+ source files are re-built.</li>
+
+ <li>If the global compiler flags change, all source files are
+ re-built.</li>
+
+ <li>If the compiler command changes, all source files are
+ re-built.</li>
+ </ul>
+ </dd>
+ </dl>
+
+ <p>N.B. For a full list of build tools declarations, please see <a href=
+ "annex_bld_cfg.html#tools-list">Annex: Declarations in FCM build
+ configuration file > list of tools</a>.</p>
+
+ <h3 id="basic_interface">Automatic Fortran 9X interface block</h3>
+
+ <p>For each Fortran 9X source file containing standalone subroutines and/or
+ functions, the system generates an interface file and sends it to the
+ <samp>inc/</samp> sub-directory of the build root. An interface file contains
+ the interface blocks for the subroutines and functions in the original source
+ file. In an incremental build, if you have modified a Fortran 9X source file,
+ its interface file will only be re-generated if the content of the interface
+ has changed.</p>
+
+ <p>Consider a source file <samp>foo.f90</samp> containing a subroutine called
+ <samp>foo</samp>. In a normal operation, the system writes the interface file
+ to <samp>foo.interface</samp> in the <samp>inc/</samp> sub-directory of the
+ build root. By default, the root name of the interface file is the same as
+ that of the source file, and is case sensitive. You can change this behaviour
+ using a <code>TOOL::INTERFACE</code> declaration. E.g.:</p>
+ <pre>
+bld::tool::interface program # The default is "file"
+</pre>
+
+ <p>In such case, the root name of the interface file will be named in lower
+ case after the first program unit in the file.</p>
+
+ <p>The default extension for an interface file is <samp>.interface</samp>.
+ This can be modified through the input and output file type register, which
+ will be discussed in a later section on <a href="#advanced_file-type">File
+ Type</a>.</p>
+
+ <p>In most cases, we modify procedures without altering their calling
+ interfaces. Consider another source file <samp>bar.f90</samp> containing a
+ subroutine <samp>bar</samp>. If <samp>bar</samp> calls <samp>foo</samp>, it
+ is good practice for <samp>bar</samp> to have an explicit interface for
+ <samp>foo</samp>. This can be achieved if the subroutine <samp>bar</samp> has
+ the following within its declaration section:</p>
+ <pre>
+INCLUDE 'foo.interface'
+</pre>
+
+ <p>The source file <samp>bar.f90</samp> is now dependent on the interface
+ file <samp>foo.interface</samp>. This can make incremental build very
+ efficient, as changes in the <samp>foo.f90</samp> file will not normally
+ trigger the re-compilation of <samp>bar.f90</samp>, provided that the
+ interface of the <code>subroutine foo</code> remains unchanged. (However, the
+ system is clever enough to know that it needs to re-link any executables that
+ are dependent on the object file for the <code>subroutine bar</code>.)</p>
+
+ <p>By default, the system uses its own internal logic to extract the calling
+ interfaces of top level subroutines and functions in a Fortran source file to
+ generate an interface block. However, the system can also work with the
+ interface generator <code>f90aib</code>, which is a freeware obtained from
+ <a href=
+ "http://www.ifremer.fr/ditigo/molagnon/fortran90/contenu.html">Fortran 90
+ texts and programs, assembled by Michel Olagnon</a> at the French Research
+ Institute for Exploitation of the Sea. To do so, you need to make a
+ declaration in the build configuration file using the label
+ <code>TOOL::GENINTERFACE</code>. As for any other <code>TOOL</code>
+ declarations, you can attach a sub-package name to the label. The change will
+ then apply only to source files within that sub-package. If
+ <code>TOOL::GENINTERFACE</code> is declared to have the value
+ <code>NONE</code>, interface generation will be switched off. The following
+ are some examples:</p>
+ <pre id="example_4">
+# Example 4
+# ----------------------------------------------------------------------
+# This is an EXTRACT configuration file ...
+
+# ... some other declarations ...
+
+bld::tool::geninterface f90aib # line 5
+bld::tool::geninterface::bar none # line 6
+
+# ... some other declarations ...
+</pre>
+
+ <p>In line 5, the global interface generator is now set to
+ <code>f90aib</code>. In line 6, by setting the interface generator for the
+ package <samp>bar</samp> to the <code>none</code> keyword, no interface file
+ will be generated for source files under the package <samp>bar</samp>.</p>
+
+ <p>Switching off the interface block generator can be useful in many
+ circumstances. For example, if the interface block is already provided
+ manually within the source tree, or if the interface block is never used by
+ other program units, it is worth switching off the interface generator for
+ the source file to speed up the build process.</p>
+
+ <h3 id="basic_dependency">Automatic dependency</h3>
+
+ <p>The build system has a built-in dependency scanner, which works out the
+ dependency relationship between source files, so that they can be built in
+ the correct order. The system scans all source files of known types for all
+ supported dependency patterns. Dependencies of source files in a sub-package
+ are written in a cache, which can be retrieved for incremental builds. (In an
+ incremental build, only changed source files need to be re-scanned for
+ dependency information. Dependency information for other files are retrieved
+ from the cache.) The dependency information is passed to the
+ <code>make</code> rule generator, which writes the <samp>Makefile</samp>.</p>
+
+ <p>The <code>make</code> rule generator generates different <code>make</code>
+ rules for different dependency types. The following dependency patterns are
+ automatically detected by the current system:</p>
+
+ <ul>
+ <li>The <code>USE <module></code> statement in a Fortran source file
+ is the first pattern. The statement has two implications: 1) The current
+ file compiles only if the module has been successfully compiled, and needs
+ to be re-compiled if the module has changed. 2) The executable depending on
+ the current file can only resolve all its externals by linking with the
+ object file of the compiled module. The executable needs to be re-linked if
+ the module and its dependencies has changed.</li>
+
+ <li>The <code>INCLUDE '<name>.interface'</code> statement in a
+ Fortran source file is the second pattern. (The default extension for an
+ interface file is <samp>.interface</samp>. This can be modified through the
+ input and output file type register, which will be discussed in a later
+ section on <a href="#advanced_file-type">File Type</a>.) It has two
+ implications: 1) The current file compiles only if the included interface
+ file is in the INCLUDE search path, and needs to be re-compiled if the
+ interface file changes. 2) The executable depending on the current file can
+ only resolve all its externals by linking with the object file of the
+ source file that generates the interface file. The executable needs to be
+ re-linked if the source file (and its dependencies) associated with the
+ interface file has changed. It is worth noting that for this dependency to
+ work, the root <name> of the interface file should match with that of
+ the source file associated with the interface file. (Please note that you
+ can use pre-processor [#include "<name>.interface] instead of Fortran
+ INCLUDE, but it will not work if you switch on the <a href=
+ "#advanced_pp">pre-processing</a> stage, which will be discussed in a later
+ section.)</li>
+
+ <li>The <code>INCLUDE '<file>'</code> statement (excluding the
+ INCLUDE interface file statement) in a Fortran source file is the third
+ pattern. It has two implications: 1) The current file compiles only if the
+ included file is in the INCLUDE search path, and needs to be re-compiled if
+ the include file changes. 2) The executable needs to be linked with any
+ objects the include file is dependent on. It needs to be re-linked if these
+ objects have changed.</li>
+
+ <li>The <code>#include '<file>'</code> statement in a Fortran/C
+ source or header file is the fourth pattern. It has similar implications as
+ the Fortran INCLUDE statement. However, they have to be handled differently
+ because <code>#include</code> statements are processed by the
+ pre-processor, which may be performed in a separate stage of the FCM build
+ process. This will be further discussed in a later sub-section on <a href=
+ "#advanced_pp">Pre-processing</a>.</li>
+ </ul>
+
+ <p>If you want your code to be built automatically by the FCM build system,
+ you should also design your code to conform to the following rules:</p>
+
+ <ol>
+ <li>Single compilable program unit, (i.e. program, subroutine, function or
+ module), per file.</li>
+
+ <li>Unique name for each compilable program unit.</li>
+
+ <li>Always supply an interface for subroutines and functions, i.e.:
+
+ <ul>
+ <li>Put them in modules.</li>
+
+ <li>Put them in the CONTAINS section within the main program unit.</li>
+
+ <li>Use interface files.</li>
+ </ul>
+ </li>
+
+ <li>If interface files are used, it is good practice to name each source
+ file after the program unit it contains. It will make life a lot simpler
+ when using the <a href="#basic_interface">Automatic Fortran 9X interface
+ block</a> feature, which has already been discussed in the previous
+ section.
+
+ <ul>
+ <li>The problem is that, by default, the root name of the interface
+ file is the same as that of the source file rather than the program
+ unit. If they differ then the build system will create a dependency on
+ the wrong object file (since the object files are named according to
+ the program unit).</li>
+
+ <li>This problem can be avoided by changing the behaviour of the
+ interface file generator to use the name of the program unit instead
+ (using a <code>TOOL::INTERFACE</code> declaration).</li>
+ </ul>
+ </li>
+ </ol>
+
+ <dl>
+ <dt>Note - setting build targets</dt>
+
+ <dd>
+ <p>The <samp>Makefile</samp> generated by the build system contains a
+ list of targets that can be built. The build system allows you to build
+ (or perform the actions of) any targets that are present in the generated
+ <samp>Makefile</samp>. There are two ways to specify the targets to be
+ built.</p>
+
+ <p>Firstly, you can use the <code>TARGET</code> declarations in your
+ build configuration file to specify the default targets to be built.
+ These targets will be set as dependencies of the <samp>all</samp> target
+ in the generated <samp>Makefile</samp>, which is the default target to be
+ built when <code>make</code> is invoked by FCM. It is worth noting that
+ <code>TARGET</code> declarations are cumulative. A later declaration does
+ not override an earlier one - it simply adds more targets to the
+ list.</p>
+
+ <p>Alternatively, you can use the <code>-t</code> option when you invoke
+ the <code>fcm build</code> command. The option takes an argument, which
+ should be a colon <code>:</code> separated list of targets to be built.
+ When the <code>-t</code> option is set, FCM invokes <code>make</code> to
+ build these targets instead. (E.g. if we invoke the build system with the
+ command <code>fcm build -t foo.exe:bar.exe</code>, it will invoke
+ <code>make</code> to build <samp>foo.exe</samp> and
+ <samp>bar.exe</samp>.)</p>
+
+ <p>If you do not specify any explicit targets, the system will search
+ your source tree for main programs:</p>
+
+ <ul>
+ <li>If there are main programs in your source tree, they will be set as
+ the default targets automatically.</li>
+
+ <li>Otherwise, the default is to build the top level library archive
+ containing objects compiled from the source files in the current source
+ tree. (For more information on building library archives, please see
+ the section on <a href="#advanced_library">Creating library
+ archives</a>.)</li>
+ </ul>
+ </dd>
+ </dl>
+
+ <h2 id="advanced">Advanced Features</h2>
+
+ <h3 id="advanced_dependency">Further dependency features</h3>
+
+ <p>Apart from the usual dependency patterns described in the previous
+ sub-section, the automatic dependency scanner also recognises two special
+ directives when they are inserted into a source file:</p>
+
+ <ul>
+ <li>The directive <code>DEPENDS ON: <object></code> in a comment line
+ of a Fortran/C source file: It states that the current file is dependent on
+ the declared external object. The executable depending on the current file
+ needs to link with this external object in order to resolve all its
+ external references. It needs to be re-linked if the declared external
+ object (and its dependencies) has changed.</li>
+
+ <li>The directive <code>CALLS: <executable></code> in a comment line
+ of a script: It states that the current script is dependent on the declared
+ executable file, which can be another script or a binary executable. The
+ current script can only function correctly if the declared executable is
+ found in the search path. This directive is useful to ensure that all
+ dependent executables are built or copied to the correct path.</li>
+ </ul>
+
+ <p>Another way to specify external dependency is to use the
+ <code>EXE_DEP</code> declaration to declare extra dependencies. The
+ declaration normally applies to all main programs, but if the form
+ <code>EXE_DEP::<target></code> is used, it will only apply to
+ <target>, (which must be the name of a main program target). If the
+ declaration is made without a value, the main programs will be set to depend
+ on all object files. Otherwise, the value can be supplied as a space
+ delimited list of items. Each item can be either the name of a sub-package or
+ an object target. For the former, the main programs will be set to depend on
+ all object files within the sub-package. For the latter, the main programs
+ will be set to depend on the object target. The following are some
+ examples:</p>
+ <pre id="example_5">
+# Example 5
+# ----------------------------------------------------------------------
+cfg::type ext
+cfg::version 1.0
+
+bld::exe_dep::foo.exe foo/bar egg.o # line 4
+bld::exe_dep # line 5
+# ... some other declarations ...
+</pre>
+
+ <p>Here is an explanation of what each line does:</p>
+
+ <ul>
+ <li><dfn>line 4</dfn>: this line declares the dependency on the sub-package
+ <samp>foo/bar</samp> and the object target <samp>egg.o</samp> for building
+ the main program target <samp>foo.exe</samp>. The target
+ <samp>foo.exe</samp> will now depends on all object files in the
+ <samp>foo/bar</samp> sub-package as well as the object target
+ <samp>egg.o</samp>.</li>
+
+ <li><dfn>line 5</dfn>: this line declares that all other main program
+ targets will depend on all (non-program) object files in the build.</li>
+ </ul>
+
+ <dl>
+ <dt>Note - naming of object files</dt>
+
+ <dd>
+ <p>By default, object files are named with the suffix <samp>.o</samp>.
+ For a Fortran source file, the build system uses the lower case name of
+ the first program unit within the file to name its object file. For
+ example, if the first program unit in the Fortran source file
+ <samp>foo.f90</samp> is <code>PROGRAM Bar</code>, the object file will be
+ <samp>bar.o</samp>. For a C source file, the build system uses the lower
+ case root name of the source file to name its object file. For example, a
+ C source file called <samp>egg.c</samp> will have its object file named
+ <samp>egg.o</samp>.</p>
+
+ <p>The reason for using lower case to name the object files is because
+ Fortran is a case insensitive language. Its symbols can either be in
+ lower or upper case. E.g. the <code>SUBROUTINE Foo</code> is the same as
+ the <code>SUBROUTINE foo</code>. It can be rather confusing if the
+ subroutines are stored in different files. When they are compiled and
+ archived into a library, there will be a clash of namespace, as the
+ Fortran compiler thinks they are the same. However, this type of error
+ does not normally get reported. If <samp>Foo</samp> and <samp>foo</samp>
+ are very different code, the user may end up using the wrong subroutine,
+ which may lead to a very long debugging session. By naming all object
+ files in lower case, this type of situation can be avoided. If there is a
+ clash in names due to the use of upper/lower cases, it will be reported
+ as warnings by the build system, (as <em>duplicated targets</em> for
+ building <samp>foo.o</samp>).</p>
+ </dd>
+ </dl>
+
+ <p>It is realised that there are situations when an automatically detected
+ dependency should not be written into the <samp>Makefile</samp>. For example,
+ the dependency may be a standard module provided by the Fortran compiler, and
+ does not need to be built in the usual way. In such case, we need to have a
+ way to exclude this module during an automatic dependency scan.</p>
+
+ <p>The <code>EXCL_DEP</code> declaration can be used to do just that. The
+ following extract configuration contains some examples of the basic usage of
+ the <code>EXCL_DEP</code> declaration:</p>
+ <pre id="example_6">
+# Example 6
+# ----------------------------------------------------------------------
+cfg::type ext
+cfg::version 1.0
+
+bld::excl_dep USE::YourFortranMod # line 4
+bld::excl_dep INTERFACE::HerFortran.interface # line 5
+bld::excl_dep INC::HisFortranInc.inc # line 6
+bld::excl_dep H::TheirHeader.h # line 7
+bld::excl_dep OBJ::ItsObject.o # line 8
+
+# ... some other declarations ...
+</pre>
+
+ <p>Here is an explanation of what each line does:</p>
+
+ <ul>
+ <li><dfn>line 4</dfn>: this line declares that the Fortran module
+ <samp>YourFortranMod</samp> should be excluded. The value of each
+ <code>EXCL_DEP</code> declaration has two parts. The first part is a label
+ that is used to define the type of dependency to be excluded. For a full
+ list of these labels, please see the <a href=
+ "annex_bld_cfg.html#dependency-types">dependency types table</a> in the
+ <a href="annex_bld_cfg.html">Annex: Declarations in FCM build configuration
+ file</a>. The label <code>USE</code> denotes a Fortran module. The second
+ part of the label is the dependency itself. For instance, if a Fortran
+ source file contains the line: <code>USE YourFortranMod</code>, the
+ dependency scanner will ignore it.</li>
+
+ <li><dfn>line 5</dfn>: this line declares that the include statement for
+ the Fortran 9X interface file <samp>HerFortran.interface</samp> should be
+ excluded. The label <code>INTERFACE</code> denotes a Fortran INCLUDE
+ statement for a Fortran 9X interface block file. For example, if a Fortran
+ source file contains the line: <code>INCLUDE 'HerFortran.interface'</code>,
+ the dependency scanner will ignore it.</li>
+
+ <li><dfn>line 6</dfn>: this line declares that the include statement for
+ <samp>HisFortranInc.inc</samp> should be excluded. The label
+ <code>INC</code> denotes a Fortran INCLUDE statement other than an INCLUDE
+ statement for an interface block file. For example, if a Fortran source
+ file contains the line: <code>INCLUDE 'HisFortranInc.inc'</code>, the
+ dependency scanner will ignore it.</li>
+
+ <li><dfn>line 7</dfn>: this line declares that the header include statement
+ <samp>TheirHeader.h</samp> should be excluded. The label <code>H</code>
+ denotes a pre-processing #include statement. For example, if a source file
+ contains the line: <code>#include 'TheirHeader.h'</code>, the dependency
+ scanner will ignore it.</li>
+
+ <li><dfn>line 8</dfn>: this line declares that the external dependency for
+ <samp>ItsObject.o</samp> should be excluded. The label <code>OBJ</code>
+ denotes a compiled binary object. These dependencies are normally inserted
+ into the source files as special comments. For example, if a source file
+ contains the line: <code>! depends on: ItsObject.o</code>, the dependency
+ scanner will ignore it.</li>
+ </ul>
+
+ <p>An <code>EXCL_DEP</code> declaration normally applies to all files in the
+ build. However, you can suffix it with the name of a sub-package, i.e.
+ <code>EXCL_DEP::<pcks></code>. In such case, the declaration will only
+ apply while scanning for dependencies in the source files in the sub-package
+ named <pcks>.</p>
+
+ <p>You can also exclude all dependency scan of a particular type. To do so,
+ simply declare the type in the value. For example, if you do not want the
+ build system to scan for the <code>CALLS: <executable></code> directive
+ in the comment lines of your scripts, you can make the following
+ declaration:</p>
+ <pre>
+bld::excl_dep EXE
+</pre>
+
+ <p>The opposite of the <code>EXCL_DEP</code> declaration is the
+ <code>DEP::<pcks></code> declaration, which you can use to add a
+ dependency to a source file (in the package name <code><pcks></code>).
+ The syntax of the declaration is similar to that of <code>EXCL_DEP</code>,
+ but you must specify the package name of a source file for DEP declarations.
+ Please also note that a <code>DEP</code> declaration only works if the
+ particular dependency is supported for the particular source file - as it
+ makes no sense, for example, to specify a USE dependency for a shell
+ script.</p>
+
+ <p>If you need to switch off dependency checking completely, you can use the
+ <code>NO_DEP</code> declaration. For example, to switch off dependency
+ checking for all but the <samp>foo/bar</samp> sub-package, you can do:</p>
+ <pre>
+bld::no_dep true
+bld::no_dep::foo/bar false
+</pre>
+
+ <h3 id="advanced_blockdata">Linking a Fortran executable with a BLOCKDATA
+ program unit</h3>
+
+ <p>If it is required to link Fortran executables with BLOCKDATA program
+ units, you must declare the executable targets and the objects containing the
+ BLOCKDATA program units using the <code>BLOCKDATA::<target></code>
+ declarations. For example, if <samp>foo.exe</samp> is an executable target
+ depending on the objects of the BLOCKDATA program units
+ <samp>blkdata.o</samp> and <samp>fbk.o</samp>, you will make the following
+ declarations:</p>
+ <pre>
+bld::blockdata::foo.exe blkdata fbk
+</pre>
+
+ <p>If all your executables are dependent on <samp>blkdata.o</samp> and
+ <samp>fbk.o</samp>, you will make the following declarations:</p>
+ <pre>
+bld::blockdata blkdata fbk
+</pre>
+
+ <h3 id="advanced_library">Creating library archives</h3>
+
+ <p>If you are interested in building library archives, the build system
+ allows you to do it in a relatively simple way. For each sub-package in the
+ source tree, there is a target to build a library containing all the objects
+ compiled from the source files (that are not main programs) within the
+ sub-package. If the sub-package contains children sub-packages, the object
+ files of the children will also be included recursively. By default, the
+ library archive is named after the sub-package, in the format
+ <code>lib<pcks>.a</code>. (For example, the library archive for the
+ package <samp>foo/bar/egg</samp> will be named
+ <samp>libfoo__bar__egg.a</samp> by default.) If you do not like the default
+ name for the sub-package library, you can use the
+ <code>LIB::<pcks></code> declaration to rename it, as long as the new
+ name does not clash with other targets. For example, to rename
+ <samp>libfoo__bar__egg.a</samp> to <samp>libham.a</samp>, you will make the
+ following declaration in your extract configuration file:</p>
+ <pre>
+bld::lib::foo/bar/egg ham
+</pre>
+
+ <p>In addition to sub-package libraries, you can also build a global library
+ archive for the whole source tree. By default, the library is named
+ <samp>libfcm_default.a</samp>, but you can rename it using the
+ <code>LIB</code> declaration as above. For example, to rename the library to
+ <samp>libmy-lib.a</samp>, you will make the following declaration in your
+ extract configuration file:</p>
+ <pre>
+bld::lib my-lib
+</pre>
+
+ <p>When a library archive is created successfully, the build system will
+ automatically generate the relevant exclude dependency configurations in the
+ <samp>etc/</samp> sub-directory of the build root. You will be able to
+ include these configurations in subsequent builds that utilise the library.
+ The root names of the configuration files match those of the library archives
+ that you can create in the current build, but the extension <samp>*.a</samp>
+ is replaced with <samp>*.cfg</samp>. For example, the exclude dependency
+ configuration for <samp>libmy-lib.a</samp> is <samp>libmy-lib.cfg</samp>.</p>
+
+ <h3 id="advanced_pp">Pre-processing</h3>
+
+ <p>As most modern compilers can handle pre-processing, the build system
+ leaves pre-processing to the compiler by default. However, it is recognised
+ that there are code written with pre-processor directives that can alter the
+ argument list of procedures and/or their dependencies. If a source file
+ requires pre-processing in such a way, we have to pre-process before running
+ the interface block generator and the dependency scanner. The <code>PP</code>
+ declaration can be used to switch on this pre-processing stage. The
+ pre-processing stage can be switched on globally or for individual
+ sub-packages only. The following is an example, using an extract
+ configuration file:</p>
+ <pre id="example_7">
+# Example 7
+# ----------------------------------------------------------------------
+cfg::type ext
+cfg::version 1.0
+
+bld::pp::gen true # line 4
+bld::pp::var/foo true # line 5
+
+bld::tool::cppkeys GOOD WEATHER FORECAST # line 7
+bld::tool::fppkeys FOO BAR EGG HAM # line 8
+
+# ... some other declarations ...
+</pre>
+
+ <p>Here is an explanation of what each line does:</p>
+
+ <ul>
+ <li><dfn>line 4-5</dfn>: these switches on the pre-processing stage for all
+ sub-packages under <samp>gen</samp> and <samp>var/foo</samp>.</li>
+
+ <li><dfn>line 7</dfn>: this declares a list of pre-defined macros
+ <samp>GOOD</samp>, <samp>WEATHER</samp> and <samp>FORECAST</samp> for
+ pre-processing all C files.</li>
+
+ <li><dfn>line 8</dfn>: this declares a list of pre-defined macros
+ <samp>FOO</samp>, <samp>BAR</samp>, <samp>EGG</samp> and <samp>HAM</samp>
+ for pre-processing all Fortran files that require processing.</li>
+ </ul>
+
+ <p>Source files requiring pre-processing may contain <code>#include</code>
+ statements to include header files. For including a local file, its name
+ should be embedded within a pair of quotes, i.e. <samp>'file.h'</samp> or
+ <samp>"file.h"</samp>. If the header file is embedded within a pair of
+ <file.h> angle brackets, the system will assume that the file can be
+ found in a standard location.</p>
+
+ <p>The build system allows header files to be placed anywhere within the
+ declared source tree. The system uses the dependency scanner, as described in
+ the previous sub-section to scan for any header file dependencies. All source
+ files requiring pre-processing and all header files are scanned. Header files
+ that are required are copied to the <samp>inc/</samp> subdirectory of the
+ build root, which is automatically added to the pre-processor search path via
+ the <code>-I<dir></code> option. The build system uses an internal
+ logic similar to <code>make</code> to perform pre-processing. Header files
+ are only copied to the <samp>inc/</samp> sub-directory if they are used in
+ <code>#include</code> statements.</p>
+
+ <p>Unlike <code>make</code>, which only uses the timestamp to determine
+ whether an item is out of date, the internal logic of the build system does
+ this by inspecting the content of the file as well. In an incremental build,
+ the pre-processed file is only updated if its content has changed. This
+ avoids unnecessary updates (and hence unnecessary re-compilation) in an
+ incremental build if the changed section of the code does not affect the
+ output file.</p>
+
+ <p>Pre-processed code generated during the pre-processing stage are sent to
+ the <samp>ppsrc/</samp> sub-directory of the build root. It will have a
+ relative path that reflects the name of the declared sub-package. The
+ pre-processed source file will have the same root name as the original source
+ file. For C files, the same extension <samp>.c</samp> will be used. For
+ Fortran files, the case of the extension will normally be dropped, e.g. from
+ <samp>.F90</samp> to <samp>.f90</samp>.</p>
+
+ <p>Following pre-processing, the system will use the pre-processed source
+ file as if it is the original source file. The interface generator will
+ generate the interface file using the pre-processed file, the dependency
+ scanner will scan the pre-processed file for dependencies, and the compiler
+ will compile the pre-processed source.</p>
+
+ <p>The <code>TOOL::CPPKEYS</code> and <code>TOOL::FPPKEYS</code> declarations
+ are used to pre-define macros in the C and Fortran pre-processor
+ respectively. This is implemented by the build system using the pre-processor
+ <code>-D</code> option on each word in the list. The use of these
+ declarations are not confined to the pre-process stage. If any source files
+ requiring pre-processing are left to the compiler, the declarations will be
+ used to set up the commands for compiling these source files.</p>
+
+ <p>The <code>TOOL::CPPKEYS</code> and <code>TOOL::FPPKEYS</code> declarations
+ normally applies globally, but like any other <code>TOOL</code> declarations,
+ they can be suffixed with sub-package names. In such cases, the declarations
+ will apply only to the specified sub-packages.</p>
+
+ <dl>
+ <dt>Note - changing pre-processor flags</dt>
+
+ <dd>
+ <p>As for compiler flags, the build system detects changes in
+ pre-processor flags (<code>TOOL::CPPFLAGS</code> and
+ <code>TOOL::FPPFLAGS</code>) and macro definitions
+ (<code>TOOL::CPPKEYS</code> and <code>TOOL::FPPKEYS</code>). If the
+ pre-processor flags or the macro definitions have changed in an
+ incremental build, the system will re-do all the necessary
+ pre-processing. The following hierarchy is followed:</p>
+
+ <ul>
+ <li>If the pre-processor flags or macro definitions for a particular
+ source file change, only that source file will be pre-processed
+ again.</li>
+
+ <li>If the pre-processor flags or macro definitions for a particular
+ container package change, only source files within that container will
+ be pre-processed again.</li>
+
+ <li>If the global pre-processor flags or macro definitions change, all
+ source files will be pre-processed again.</li>
+
+ <li>If the pre-processor command changes, all source files are
+ pre-processed again.</li>
+ </ul>
+ </dd>
+ </dl>
+
+ <h3 id="advanced_file-type">File type</h3>
+
+ <p>The build system only knows what to do with an input source file if it
+ knows what type of file it is. The type of a source file is normally
+ determined automatically using one of the following three methods (in
+ order):</p>
+
+ <ol>
+ <li>If the file is named with an extension, its extension will be matched
+ against a set of registered file extensions. If a match is found, the file
+ type will be set according to the register.</li>
+
+ <li>If a file does not have an extension or does not match with a
+ registered extension, its name is compared with a set of pre-defined
+ patterns. If a match is found, the file type will be set according to the
+ file type associated with the pattern.</li>
+
+ <li>If the above two methods failed and if the file is a text file, the
+ system will attempt to read the first line of the file. If the first line
+ begins with a <code>#!</code> pattern, the line will be compared with a set
+ of pre-defined patterns. If a match is found, the file type will be set
+ according to the file type associated with the pattern.</li>
+ </ol>
+
+ <p>In addition to the above, if a file is a Fortran or C source file, the
+ system will attempt to open the source file to determine whether it contains
+ a main program, module (Fortran only) or just standalone procedures. All
+ these information will be used later by the build system to process the
+ source file.</p>
+
+ <p>The build system registers a file type with a set of type flags delimited
+ by the double colons <code>::</code>. For example, a Fortran 9X source file
+ is registered as <code>FORTRAN::FORTRAN9X::SOURCE</code>. (Please note that
+ the order of the type flags in the list is insignificant. For example,
+ <code>FORTRAN::SOURCE</code> is the same as <code>SOURCE::FORTRAN</code>.)
+ For a list of all the type flags used by the build system, please see the
+ <a href="annex_bld_cfg.html#infile-ext-types">input file extension type flags
+ table</a> in the <a href="annex_bld_cfg.html">Annex: Declarations in FCM
+ build configuration file</a>.</p>
+
+ <p>The following is a list of default input file extensions and their
+ associated types:</p>
+
+ <dl>
+ <dt><samp>.f .for .ftn .f77</samp></dt>
+
+ <dd><code>FORTRAN::SOURCE</code> Fortran 77 source file (assumed to be
+ fixed format)</dd>
+
+ <dt><samp>.f90 .f95</samp></dt>
+
+ <dd><code>FORTRAN::FORTRAN9X::SOURCE</code> Fortran 9X source file (assumed
+ to be free format)</dd>
+
+ <dt><samp>.F .FOR .FTN .F77</samp></dt>
+
+ <dd><code>FPP::SOURCE</code> Fortran 77 source file (assumed to be fixed
+ format) that requires pre-processing</dd>
+
+ <dt><samp>.F90 .F95</samp></dt>
+
+ <dd><code>FPP::FPP9X::SOURCE</code> Fortran 9X source file (assumed to be
+ free format) that requires pre-processing</dd>
+
+ <dt><samp>.c</samp></dt>
+
+ <dd><code>C::SOURCE</code> C source file</dd>
+
+ <dt><samp>.h .h90</samp></dt>
+
+ <dd><code>CPP::INCLUDE</code> Pre-processor <code>#include</code> header
+ file</dd>
+
+ <dt><samp>.o .obj</samp></dt>
+
+ <dd><code>BINARY::OBJ</code> Compiled binary object</dd>
+
+ <dt><samp>.exe</samp></dt>
+
+ <dd><code>BINARY::EXE</code> Binary executable</dd>
+
+ <dt><samp>.a</samp></dt>
+
+ <dd><code>BINARY::LIB</code> Binary object library archive</dd>
+
+ <dt><samp>.sh .ksh .bash .csh</samp></dt>
+
+ <dd><code>SHELL::SCRIPT</code> Unix shell script</dd>
+
+ <dt><samp>.pl .pm</samp></dt>
+
+ <dd><code>PERL::SCRIPT</code> Perl script</dd>
+
+ <dt><samp>.py</samp></dt>
+
+ <dd><code>PYTHON::SCRIPT</code> Python script</dd>
+
+ <dt><samp>.tcl</samp></dt>
+
+ <dd><code>TCL::SCRIPT</code> Tcl/Tk script</dd>
+
+ <dt><samp>.pro</samp></dt>
+
+ <dd><code>PVWAVE::SCRIPT</code> IDL/PVWave program</dd>
+
+ <dt><samp>.cfg</samp></dt>
+
+ <dd><code>CFGFILE</code> FCM configuration file</dd>
+
+ <dt><samp>.inc</samp></dt>
+
+ <dd><code>FORTRAN::FORTRAN9X::INCLUDE</code> Fortran INCLUDE file</dd>
+
+ <dt><samp>.interface</samp></dt>
+
+ <dd><code>FORTRAN::FORTRAN9X::INCLUDE::INTERFACE</code> Fortran 9X INCLUDE
+ interface block file</dd>
+ </dl>
+
+ <p>N.B. The extension must be unique. For example, the system does not
+ support the use of <samp>.inc</samp> files for both <code>#include</code> and
+ Fortran <code>INCLUDE</code>.</p>
+
+ <p>The following is a list of supported file name patterns and their
+ associated types:</p>
+
+ <dl>
+ <dt><samp>*Scr_* *Comp_* *IF_* *Suite_* *Interface_*</samp></dt>
+
+ <dd><code>SHELL::SCRIPT</code> Unix shell script, GEN-based project naming
+ conventions</dd>
+
+ <dt><samp>*List_*</samp></dt>
+
+ <dd><code>SHELL::SCRIPT::GENLIST</code> Unix shell script, GEN list
+ file</dd>
+
+ <dt><samp>*Sql_*</samp></dt>
+
+ <dd><code>SCRIPT::SQL</code> SQL script, GEN-based project naming
+ conventions</dd>
+ </dl>
+
+ <p>The following is a list of supported <code>#!</code> line patterns and
+ their associated types:</p>
+
+ <dl>
+ <dt><samp>*sh* *ksh* *bash* *csh*</samp></dt>
+
+ <dd><code>SHELL::SCRIPT</code> Unix shell script</dd>
+
+ <dt><samp>*perl*</samp></dt>
+
+ <dd><code>PERL::SCRIPT</code> Perl script</dd>
+
+ <dt><samp>*python*</samp></dt>
+
+ <dd><code>PYTHON::SCRIPT</code> Python script</dd>
+
+ <dt><samp>*tclsh* *wish*</samp></dt>
+
+ <dd><code>TCL::SCRIPT</code> Tcl/Tk script</dd>
+ </dl>
+
+ <p>The build system allows you to add or modify the register for input file
+ extensions and their associated type using the
+ <code>INFILE_EXT::<ext></code> declaration, where <ext> is a file
+ name extension without the leading dot. If file extension alone is
+ insufficient for defining the type of your source file, you can use the
+ <code>SRC_TYPE::<pcks></code> declaration, (where <pcks> is the
+ package name of the source file). For example, in an extract configuration
+ file, you may have:</p>
+ <pre id="example_8">
+# Example 8
+# ----------------------------------------------------------------------
+cfg::type ext
+cfg::version 1.0
+
+bld::infile_ext::foo CPP::INCLUDE # line 4
+bld::infile_ext::bar FORTRAN::FORTRAN9X::INCLUDE # line 5
+bld::src_type::egg/ham.f FORTRAN::FORTRAN9X::INCLUDE # line 6
+
+# ... some other declarations ...
+</pre>
+
+ <p>Here is an explanation of what each line does:</p>
+
+ <ul>
+ <li><dfn>line 4</dfn>: this line registers the extension <samp>.foo</samp>
+ to be of type <code>CPP::INCLUDE</code>. This means that any input files
+ with <samp>.foo</samp> extension will be treated as if they are
+ pre-processor header files.</li>
+
+ <li><dfn>line 5</dfn>: this line registers the extension <samp>.bar</samp>
+ to be of type <code>FORTRAN::FORTRAN9X::INCLUDE</code>. This means that any
+ input file with <samp>.bar</samp> extension will be treated as if they are
+ Fortran 9X INCLUDE files.</li>
+
+ <li><dfn>line 6</dfn>: this line declares the type for the source file in
+ the package <samp>egg::ham.f</samp> to be
+ <code>FORTRAN::FORTRAN9X::INCLUDE</code>. Without this declaration, this
+ file would normally be given the type <code>FORTRAN::SOURCE</code>.</li>
+ </ul>
+
+ <p>The <code>INFILE_EXT</code> declarations deal with extensions of input
+ files. There is also a <code>OUTFILE_EXT::<type></code> declaration
+ that deals with extensions of output files. The declaration is opposite that
+ of <code>INFILE_EXT</code>. The file <type> is now declared with the
+ label, and the extension is declared as the value. It is worth noting that
+ <code>OUTFILE_EXT</code> declarations use very different syntax for
+ <type>, and the declared extension must include the leading dot. For a
+ list of output types used by the build system, please see the <a href=
+ "annex_bld_cfg.html#outfile-ext-types">output file extension types table</a>
+ in the <a href="annex_bld_cfg.html">Annex: Declarations in FCM build
+ configuration file</a>. An example is given below:</p>
+ <pre id="example_9">
+# Example 9
+# ----------------------------------------------------------------------
+cfg::type ext
+cfg::version 1.0
+
+bld::outfile_ext::mod .MOD # line 4
+bld::outfile_ext::interface .intfb # line 5
+
+# ... some other declarations ...
+</pre>
+
+ <p>Here is an explanation of what each line does:</p>
+
+ <ul>
+ <li><dfn>line 4</dfn>: this line modifies the extension of compiled Fortran
+ 9X module information files from the default <samp>.mod</samp> to
+ <samp>.MOD</samp>.</li>
+
+ <li><dfn>line 5</dfn>: this line modifies the extension of INCLUDE Fortran
+ 9X interface block files from the default <samp>.interface</samp> to
+ <samp>.intfb</samp>.</li>
+ </ul>
+
+ <p>N.B. If you have made changes to the file type registers, whether it is
+ for input files or output files, it is always worth re-building your code in
+ full-build mode to avoid unexpected behaviour.</p>
+
+ <h3 id="advanced_inherit">Inherit from a previous build</h3>
+
+ <p>As you can inherit from previous extracts, you can inherit from previous
+ builds. The very same <code>USE</code> statement can be used to declare a
+ build, which the current build will depend on. The only difference is that
+ the declared location must contain a valid build configuration file. In fact,
+ if you use the extract system to obtain your build configuration file, any
+ <code>USE</code> declarations in the extract configuration file will also be
+ <code>USE</code> declarations in the output build configuration file.</p>
+
+ <p>By declaring a previous build with a <code>USE</code> statement, the
+ current build automatically inherits settings from it. The following points
+ are worth noting:</p>
+
+ <ul>
+ <li>Build targets are not normally inherited. However, you can switch on
+ inheritance of build targets using an <code>INHERIT::TARGET</code>
+ declaration, such as:
+ <pre>
+inherit::target true
+</pre>
+ </li>
+
+ <li>The build root directory and its sub-directories of the inherited build
+ are placed into the search paths. For example, if we have an inherited
+ build at <samp>/path/to/inherited</samp>, and it is used by a build at
+ <samp>/path/to/my_build</samp>, the search path of executable files will
+ become <samp>/path/to/my_build/bin:/path/to/inherited/bin</samp>, so that
+ the <samp>bin/</samp> sub-directory of the current build is searched before
+ the <samp>bin/</samp> sub-directory of the inherited build. If two or more
+ <code>USE</code> statements are declared, the <code>USE</code> statement
+ declared last will have higher priority. For example, if the current build
+ is <var>C</var>, and it USEs build <var>A</var> before build <var>B</var>,
+ the search path will be <samp>C:B:A</samp>.</li>
+
+ <li>Source files are inherited by default. If a source file is declared in
+ the current build that has the same package name as a source file of the
+ inherited build, it will override that in the inherited build. Any source
+ files missing from the current build will be taken from the inherited
+ build.
+
+ <p>You can switch off inheritance of source files using an
+ <code>INHERIT::SRC</code> declaration. This declaration can be suffixed
+ with the name of a sub-package. In such case, the declaration applies
+ only to the inheritance of the sub-package. Otherwise, it applies
+ globally. For example:</p>
+ <pre>
+# Switch off inheritance of source files in the <samp>gen</samp> sub-package
+inherit::src::gen false
+</pre>
+ </li>
+
+ <li><code>BLOCKDATA</code>, <code>DEP</code>, <code>EXCL_DEP</code>,
+ <code>EXE_DEP</code>, <code>INFILE_EXT</code>, <code>LIB</code>,
+ <code>OUTFILE_EXT</code>, <code>PP</code>, <code>TOOL</code> and
+ <code>SRC_TYPE</code> declarations are automatically inherited. If the same
+ setting is declared in the current incremental build, it overrides the
+ inherited declaration.</li>
+ </ul>
+
+ <p>As an example, suppose we have already performed an extract and build
+ based on the configuration in <a href="#example_2">example 2</a>, we can set
+ up an extract configuration file as follows:</p>
+ <pre id="example_10">
+# Example 10
+# ----------------------------------------------------------------------
+cfg::type ext
+cfg::version 1.0
+
+use $HOME/example # line 4
+
+dest $HOME/example10 # line 6
+
+bld::inherit::target true # line 8
+bld::target ham.exe egg.exe # line 9
+
+bld::tool::fflags -O2 -w # line 11
+bld::tool::cflags -O2 # line 12
+
+# ... and other declarations for repositories and source directories ...
+</pre>
+
+ <p>Here is an explanation of what each line does:</p>
+
+ <ul>
+ <li><dfn>line 4</dfn>: this line declares a previous extract at
+ <samp>$HOME/example</samp> which the current extract will inherit from. The
+ same line will be written to the output build configuration file. The
+ subsequent build will then inherit from the build at
+ <samp>$HOME/example</samp>.</li>
+
+ <li><dfn>line 6</dfn>: this declares the destination root directory of the
+ current extract, which will become the root directory of the current build.
+ Search paths of the build sub-directories will be set automatically. For
+ example, the search path for executable files created by the current build
+ will be <samp>$HOME/example10/bin:$HOME/example/bin</samp>.</li>
+
+ <li><dfn>line 8</dfn>: this line switches on inheritance of build targets.
+ The build targets in <a href="#example_1">example 1</a>, i.e.
+ <samp>foo.exe</samp> and <samp>bar.exe</samp> will be built as part of the
+ current build.</li>
+
+ <li><dfn>line 9</dfn>: this declares two new build targets
+ <samp>ham.exe</samp> and <samp>egg.exe</samp> to be added to the inherited
+ ones. The default build targets of the current build will now be
+ <samp>foo.exe</samp>, <samp>bar.exe</samp>, <samp>ham.exe</samp> and
+ <samp>egg.exe</samp>.</li>
+
+ <li><dfn>line 11-12</dfn>: these lines modify options used by the Fortran
+ and the C compilers, overriding those inherited from <a href=
+ "#example_1">example 1</a>.</li>
+ </ul>
+
+ <dl>
+ <dt>Build inheritance limitation: handling of include files</dt>
+
+ <dd>
+ <p>The build system uses the compiler/pre-processor's <code>-I</code>
+ option to specify the search path for include files. For example, it uses
+ the option to specify the <samp>inc/</samp> sub-directories of the
+ current build and its inherited build.</p>
+
+ <p>However, some compilers/pre-processors (e.g. <code>cpp</code>) search
+ for include files from the container directory of the source file before
+ searching for the paths specified by the <code>-I</code> options. This
+ behaviour may cause the build to behave incorrectly.</p>
+
+ <p>Consider a source file <samp>egg/hen.c</samp> that includes
+ <samp>fried.h</samp>. If the directory structure looks like:</p>
+ <pre>
+# Sources in inherited build:
+egg/hen.c
+egg/fried.h
+
+# Sources in current build:
+egg/fried.h
+</pre>
+
+ <p>The system will correctly identify that <samp>fried.h</samp> is out of
+ date, and trigger a re-compilation of <samp>egg/hen.c</samp>. However, if
+ the compiler searches for the include files from the container directory
+ of the source file first, it will wrongly use the include file in the
+ inherited build instead of the current one.</p>
+
+ <p>Some compilers (e.g. <code>gfortran</code>) do not behave this way and
+ others (e.g. <code>ifort</code>) have options to prevent include file
+ search in the container directory of the source file. If you are using
+ such a compiler you can avoid the problem for Fortran compilation
+ although this does not fix the problem entirely if you have switched on
+ the pre-processing stage. Otherwise you may have to work around the
+ problem, (e.g. by making a comment change in the source file, or by not
+ using an inherited build at all).</p>
+ </dd>
+ </dl>
+
+ <h3 id="advanced_data">Building data files</h3>
+
+ <p>While the usual targets to be built are the executables associated with
+ source files containing main programs, libraries or scripts, the build system
+ also allows you to build <em>data</em> files. All files with no registered
+ type are considered to be <em>data</em> files. For each container
+ sub-package, there is an automatic target for copying all <em>data</em> files
+ to the <samp>etc/</samp> sub-directory of the build root. The name of the
+ target has the form <code><pcks>.etc</code>, where <pcks> is the
+ name of the sub-package (with package names delimited by the double
+ underscore <code>__</code>). For example, the target name for sub-package
+ <samp>foo/bar</samp> is <samp>foo__bar.etc</samp>. This target is
+ particularly useful for copying, say, all namelists in a sub-package to the
+ <samp>etc/</samp> sub-directory of the build root.</p>
+
+ <p>At the end of a successful build, if the <samp>etc/</samp> sub-directory
+ is not empty, the <samp>fcm_env.sh</samp> script will export the environment
+ variable <var>FCM_ETCDIR</var> to point to the <samp>etc/</samp>
+ sub-directory. You should be able to use this environment variable to locate
+ your data files.</p>
+
+ <h2 id="verbose">Diagnostic verbose level</h2>
+
+ <p>The amount of diagnostic messages generated by the build system is
+ normally set to a level suitable for normal everyday operation. This is the
+ default diagnostic verbose level 1. If you want a minimum amount of
+ diagnostic messages, you should set the verbose level to 0. If you want more
+ diagnostic messages, you can set the verbose level to 2 or 3. You can modify
+ the verbose level in two ways. The first way is to set the environment
+ variable <var>FCM_VERBOSE</var> to the desired verbose level. The second way
+ is to invoke the build system with the <code>-v <level></code> option.
+ (If set, the command line option overrides the environment variable.)</p>
+
+ <p>The following is a list of diagnostic output at each verbose level:</p>
+
+ <dl>
+ <dt>Level 0</dt>
+
+ <dd>
+ <ul>
+ <li>Report the time taken at the end of each stage of the build
+ process.</li>
+
+ <li>Run the <code>make</code> command in silent mode.</li>
+ </ul>
+ </dd>
+
+ <dt>Level 1</dt>
+
+ <dd>
+ <ul>
+ <li>Everything at verbose level 0.</li>
+
+ <li>Report the name of the build configuration file.</li>
+
+ <li>Report the location of the build destination.</li>
+
+ <li>Report date/time at the beginning of each stage of the build
+ process.</li>
+
+ <li>Report removed directories.</li>
+
+ <li>Report number of pre-processed files.</li>
+
+ <li>Report number of generated F9X interface files.</li>
+
+ <li>Report number of source files scanned for dependencies.</li>
+
+ <li>Report name of updated <samp>Makefile</samp>.</li>
+
+ <li>Print compiler/linker commands.</li>
+
+ <li>Report total time.</li>
+ </ul>
+ </dd>
+
+ <dt>Level 2</dt>
+
+ <dd>
+ <ul>
+ <li>Everything at verbose level 1.</li>
+
+ <li>For incremental build in archive mode, report the commands used to
+ extract the archives.</li>
+
+ <li>Report creation and removal of directories.</li>
+
+ <li>Report pre-processor commands.</li>
+
+ <li>Print compiler/linker commands with timestamps.</li>
+ </ul>
+ </dd>
+
+ <dt>Level 3</dt>
+
+ <dd>
+ <ul>
+ <li>Everything at verbose level 2.</li>
+
+ <li>Report update of dummy files.</li>
+
+ <li>Report all shell commands.</li>
+
+ <li>Report pre-processor commands with timestamps.</li>
+
+ <li>Report any F9X interface files generated.</li>
+
+ <li>Report number of lines and number of automatic dependencies for
+ each source file which is scanned.</li>
+
+ <li>Run <code>make</code> on normal mode (as opposed to silent
+ mode).</li>
+
+ <li>Report start date/time and time taken of <code>make</code>
+ commands.</li>
+ </ul>
+ </dd>
+ </dl>
+
+ <h2 id="overview">Overview of the build process</h2>
+
+ <p>The FCM build process can be summarised in five stages. Here is a summary
+ of what is done in each stage:</p>
+
+ <ol>
+ <li><dfn>Parse configuration and setup destination</dfn>: in this
+ pre-requisite stage, the build system parses the configuration file. The
+ <samp>src/</samp> sub-directory is searched recursively for source files.
+ For full builds, it ensures that the sub-directories and files created by
+ the build system are removed. If you invoke <code>fcm build</code> with a
+ <code>--clean</code> option, the system will not go any further.</li>
+
+ <li><dfn>Setup build</dfn>: in this first stage, the system determines
+ whether any settings have changed by using the cache. If so, the cache is
+ updated with the current settings.</li>
+
+ <li><dfn>Pre-process</dfn>: if any files in any source files require
+ pre-processing, they will be pre-processed at this stage. The resulting
+ pre-processed source files will be sent to the <samp>ppsrc/</samp>
+ sub-directory of the build root.</li>
+
+ <li><dfn>Generate dependency</dfn>: the system scans source files of
+ registered types for dependency information. For an incremental build, the
+ information is only updated if a source file is changed. The system then
+ uses the information to write a <samp>Makefile</samp> for the main
+ build.</li>
+
+ <li><dfn>Generate interface</dfn>: if there are Fortran 9X source files
+ with standalone subroutines and functions, the build system generates
+ interface blocks for them. The result of which will be written to the
+ interface files in the <samp>inc/</samp> sub-directory of the build
+ root.</li>
+
+ <li>
+ <dfn>Make</dfn>: the system invokes <code>make</code> on the
+ <samp>Makefile</samp> generated in the previous stage to perform the main
+ build. Following a build, the <em>root</em> directory of the build may
+ contain the following sub-directories (empty ones are removed
+ automatically at the end of the build process):
+
+ <dl>
+ <dt><samp>.cache/.bld/</samp></dt>
+
+ <dd>Cache files, used internally by FCM.</dd>
+
+ <dt><samp>bin/</samp></dt>
+
+ <dd>Executable binaries and scripts.</dd>
+
+ <dt><samp>cfg/</samp></dt>
+
+ <dd>Configuration files.</dd>
+
+ <dt><samp>done/</samp></dt>
+
+ <dd>Dummy <em>done</em> files used internally by the
+ <samp>Makefile</samp> generated by FCM.</dd>
+
+ <dt><samp>etc/</samp></dt>
+
+ <dd>Miscellaneous data files.</dd>
+
+ <dt><samp>flags/</samp></dt>
+
+ <dd>Dummy <em>flags</em> files used internally by the
+ <samp>Makefile</samp> generated by FCM.</dd>
+
+ <dt><samp>inc/</samp></dt>
+
+ <dd>Include files, such as <samp>*.h</samp>, <samp>*.inc</samp>,
+ <samp>*.interface</samp>, and <samp>*.mod</samp>.</dd>
+
+ <dt><samp>lib/</samp></dt>
+
+ <dd>Object library archives.</dd>
+
+ <dt><samp>obj/</samp></dt>
+
+ <dd>Compiled object files.</dd>
+
+ <dt><samp>ppsrc/</samp></dt>
+
+ <dd>Source directories with pre-processed files.</dd>
+
+ <dt><samp>src/</samp></dt>
+
+ <dd>Source directories. This directory is not changed by the build
+ system.</dd>
+
+ <dt><samp>tmp/</samp></dt>
+
+ <dd>Temporary objects and binaries. Files generated by the
+ compiler/linker may be left here.</dd>
+ </dl>
+ </li>
+ </ol>
+
+ </div>
+ </div>
+ </div>
+
+ <hr/>
+ <div class="container-fluid text-center">
+ <div class="row-fluid"><div class="span12">
+ <address><small>
+ © British Crown Copyright 2006-14
+ <a href="http://www.metoffice.gov.uk">Met Office</a>.
+ See <a href="../etc/fcm-terms-of-use.html">Terms of Use</a>.<br />
+ This document is released under the British <a href=
+ "http://www.nationalarchives.gov.uk/doc/open-government-licence/" rel=
+ "license">Open Government Licence</a>.<br />
+ </small></address>
+ </div></div>
+ </div>
+
+ <script type="text/javascript" src="../etc/jquery.min.js"></script>
+ <script type="text/javascript" src="../etc/bootstrap/js/bootstrap.min.js"></script>
+ <script type="text/javascript" src="../etc/fcm.js"></script>
+ <script type="text/javascript" src="../etc/fcm-version.js"></script>
+</body>
+</html>
diff --git a/doc/user_guide/changeset.png b/doc/user_guide/changeset.png
new file mode 100644
index 0000000..67c3437
Binary files /dev/null and b/doc/user_guide/changeset.png differ
diff --git a/doc/user_guide/code_management.html b/doc/user_guide/code_management.html
new file mode 100644
index 0000000..20352a3
--- /dev/null
+++ b/doc/user_guide/code_management.html
@@ -0,0 +1,1243 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>FCM: User Guide: Code Management</title>
+ <meta name="author" content="FCM team" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link rel="icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link rel="shortcut icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link href="../etc/bootstrap/css/bootstrap.min.css" rel="stylesheet" media="screen" />
+ <link href="../etc/fcm.css" rel="stylesheet" media="screen" />
+</head>
+<body>
+ <div class="navbar navbar-inverse">
+ <div class="navbar-inner">
+ <a class="brand" href=".."><span class="fcm-version">FCM</span></a>
+ <ul class="nav">
+ <li><a href="../installation/">Installation</a></li>
+
+ <li class="active"><a href=".">User Guide</a></li>
+ </ul>
+ </div>
+ </div>
+
+ <div class="page-header">
+ <div class="fcm-page-content pull-right well well-small"></div>
+ <h1>FCM: User Guide: Code Management</h1>
+ </div>
+
+ <div class="container">
+ <div class="row">
+ <div class="span12">
+
+ <h2 id="svn">Using Subversion</h2>
+
+ <p>One of the key strengths of Subversion is its documentation. <a href=
+ "http://svnbook.red-bean.com/en/1.8/">Version Control with Subversion</a>
+ (which we'll just refer to as the <cite>Subversion book</cite> from now on)
+ is an excellent book which explains in detail how to use Subversion and also
+ provides a good introduction to all the basic concepts of version control.
+ Rather than trying to write our own explanations (and not doing as good a
+ job) we will simply refer you to the <cite>Subversion book</cite>, where
+ appropriate, for the relevant information.</p>
+
+ <p>In general, the approach taken in this section is to make sure that you
+ first understand how to perform a particular action using the Subversion
+ tools and then describe how this differs using FCM.</p>
+
+ <h3 id="svn_concepts">Basic Concepts</h3>
+
+ <p>In order to use FCM you need to have a basic understanding of version
+ control. If you're not already familiar with Subversion or CVS then please
+ read the chapter <a href=
+ "http://svnbook.red-bean.com/en/1.8/svn.basic.html">Fundamental Concepts</a>
+ from the <cite>Subversion book</cite>. In particular, make sure that you
+ understand:</p>
+
+ <ul>
+ <li>The <q title=
+ "http://svnbook.red-bean.com/en/1.8/svn.basic.version-control-basics.html#svn.basic.vsn-models.copy-merge">
+ Copy-Modify-Merge</q> approach to file sharing.</li>
+
+ <li>Global Revision Numbers.</li>
+ </ul>
+
+ <p>Note that this chapter states that <q title=
+ "http://svnbook.red-bean.com/en/1.8/svn.basic.in-action.html#svn.basic.in-action.wc">
+ working copies do not always correspond to any single revision in the
+ repository</q>. However, the FCM working practices do not encourage this and
+ the wrapper scripts provided by FCM should ensure that your working copy (a
+ local copy of the repository's files and directories where you can prepare
+ changes) always corresponds to exactly one revision.</p>
+
+ <p><acronym title="Concurrent Versions System">CVS</acronym> users should
+ already be familiar with all the basic concepts. This is not surprising since
+ Subversion was designed as a replacement for CVS and it uses the same
+ development model. However, there are some important differences which may
+ confuse those more familiar with CVS. Fortunately, the appendix in the
+ <cite>Subversion book</cite> <a href=
+ "http://svnbook.red-bean.com/en/1.8/svn.forcvs.html">Subversion for CVS
+ Users</a> is specifically written for those moving from CVS to Subversion and
+ you should read this if you are a CVS user.</p>
+
+ <h3 id="svn_basic">Basic Command Line Usage</h3>
+
+ <p>Before we discuss the FCM system you need to have a good understanding of
+ how to perform most of the normal day-to-day tasks using Subversion.
+ Therefore, unless you are already familiar with Subversion, please read the
+ chapter <a href="http://svnbook.red-bean.com/en/1.8/svn.tour.html">Basic
+ Usage</a> from the <cite>Subversion book</cite>.</p>
+
+ <p>So, now you have an understanding of how to do basic tasks using
+ Subversion (you did read the <cite>Basic Usage</cite> chapter didn't you?),
+ how is using FCM different? Well, the key thing to remember is that, instead
+ of using the command <code>svn</code> you need to use the command
+ <code>fcm</code>. The advantages of this are as follows:</p>
+
+ <ul>
+ <li><code>fcm</code> implements all of the commands that <code>svn</code>
+ does (including all the command abbreviations).</li>
+
+ <li>In some cases <code>fcm</code> does very little and basically passes on
+ the command to <code>svn</code>.</li>
+
+ <li>In other cases <code>fcm</code> has a lot of additional functionality
+ compared with the equivalent <code>svn</code> command.</li>
+
+ <li><code>fcm</code> also implements several commands not provided by
+ <code>svn</code>.</li>
+
+ <li><code>fcm</code> provides support for URL and revision keywords.</li>
+
+ <li>Most of the additional features and commands are discussed later in
+ this section or in the following sections.</li>
+ </ul>
+
+ <p>Full details of all the <code>fcm</code> commands available are provided
+ in the <a href="command_ref.html">FCM Command Reference</a> section.</p>
+
+ <h4 id="svn_basic_keywords">URL And Revision Keywords</h4>
+
+ <p>URL keywords can be used to specify URLs in <code>fcm</code> commands. The
+ syntax is <code>fcm:<keyword></code>. Keywords can be defined in the
+ FCM keyword configuration file (i.e. <samp>$FCM/etc/fcm/keyword.cfg</samp>
+ and <samp>$HOME/.metomi/fcm/keyword.cfg</samp>).</p>
+
+ <p>For example, if you define a keyword in your configuration file as
+ follows:</p>
+ <pre>
+location{primary}[um] = svn://fcm2/UM_svn/UM
+</pre>
+
+ <p>then you can abbreviate the URL as in the following examples:</p>
+ <pre>
+# fcm ls svn://fcm2/UM_svn/UM
+fcm ls fcm:um
+
+# fcm ls svn://fcm2/UM_svn/UM/trunk
+fcm ls fcm:um_tr # OR: fcm ls fcm:um-tr
+
+# fcm ls svn://fcm2/UM_svn/UM/branches
+fcm ls fcm:um_br # OR: fcm ls fcm:um-br
+
+# fcm ls svn://fcm2/UM_svn/UM/tags
+fcm ls fcm:um_tg # OR: fcm ls fcm:um-tg
+</pre>
+
+ <p>Using URL keywords has two advantages.</p>
+
+ <ul>
+ <li>They are shorter and easier to remember.</li>
+
+ <li>If the repository needs to be moved then only the keyword definitions
+ need to be updated (although any working copies you have will still need to
+ be <em>relocated</em> by issuing a <a href=
+ "http://svnbook.red-bean.com/en/1.8/svn.ref.svn.c.switch.html"><code>fcm
+ switch --relocate</code></a> command).</li>
+ </ul>
+
+ <p>In a similar way, revision keywords can be used to specify revision
+ numbers in <code>fcm</code> commands. The keyword can be used anywhere a
+ revision number can be used. Each keyword is associated with a URL keyword
+ and can only be used when referring to that repository.</p>
+
+ <p>For example, if you define a keyword in your configuration file as
+ follows:</p>
+ <pre>
+revision[fcm:vn1.0] = 112
+</pre>
+
+ <p>then the following commands are equivalent:<br />
+ <code>fcm log -r 112 svn://fcm1/FCM_svn/trunk</code><br />
+ <code>fcm log -r vn1.0 fcm:fcm_tr</code></p>
+
+ <p>You can use the <code>fcm keyword-print</code> command to print all
+ registered location keywords. You can also print the location keyword and the
+ revision keywords of a particular project. For example, to print the keywords
+ for the <samp>UM</samp> project, you can type <code>fcm keyword-print
+ fcm:um</code>.</p>
+
+ <h4 id="svn_basic_diff">Examining Changes</h4>
+
+ <p>Code differences can be displayed graphically using <code>xxdiff</code> by
+ using the <code>--graphical</code> (or <code>-g</code>) option to <code>fcm
+ diff</code>. This option can be used in combination with any other options
+ which are accepted by <code>svn diff</code>.</p>
+
+ <p>An example display from <code>xxdiff</code> is shown below.</p>
+
+ <p class="image"><img src="xxdiff1.png" alt="xxdiff 2-way display" /><br />
+ <code>xxdiff</code> 2-way display</p>
+
+ <p>Points to note:</p>
+
+ <ul>
+ <li>By default <code>xxdiff</code> is configured to show horizontal
+ differences. This means that the parts of the line which have changed are
+ highlighted (e.g. the text <samp>useful</samp> is highlighted in the
+ example above).</li>
+
+ <li>The number shown to the right of each file name shows the current line
+ number. The number on the far right is the number of differences found (2
+ in the example above).</li>
+
+ <li>You may find the following keyboard shortcuts useful.
+
+ <ul>
+ <li><kbd>N</kbd> - move to the next difference</li>
+
+ <li><kbd>P</kbd> - move to the previous difference</li>
+
+ <li><kbd>Ctrl-Q</kbd> - exit</li>
+ </ul>
+ </li>
+
+ <li>If you want to use another diff tool instead of <code>xxdiff</code> to
+ examine changes, you can define the <code>graphic-diff</code> setting in a
+ FCM external configuration file (i.e.<samp>$FCM/etc/fcm/external.cfg</samp>
+ or <samp>$HOME/.metomi/fcm/external.cfg</samp>). For example, to use <code>
+ tkdiff</code>, you can do:
+ <pre>
+# in your site's $FCM/etc/fcm/external.cfg:
+# OR: in your $HOME/.metomi/fcm/external.cfg:
+graphic-diff = tkdiff
+</pre>
+ </li>
+ </ul>
+
+ <h4 id="svn_basic_conflicts">Resolving Conflicts</h4>
+
+ <p>Your working copy may contain files or directories <em>in conflict</em> as
+ a result of an update or a merge (covered later). Conflicts arise from the
+ situation where two changes being applied to a file <em>overlap</em>. These
+ can be text-based, as in two changes to the same line of text in a file, or
+ filesystem-based, as in two different renamings of the same file.</p>
+
+ <p>For conflicts in normal (text) files, the command <code>fcm
+ conflicts</code> can be used to help resolve them. (A discussion on binary
+ files is given in the section <a href="working_practices.html#binary">Working
+ with Binary Files</a> later in this document.). For each file in <em>text
+ conflict</em>, the <code>fcm conflicts</code> command calls a graphical merge
+ tool (i.e. <code>xxdiff</code> by default) to display a 3-way diff.</p>
+
+ <p>An example display from <code>xxdiff</code> is shown below.</p>
+
+ <p class="image"><img src="xxdiff2.png" alt="xxdiff 3-way display" /><br />
+ <code>xxdiff</code> 3-way display</p>
+
+ <p>Points to note:</p>
+
+ <ul>
+ <li>The file in the middle is the common ancestor from the merge. The file
+ on the left is your original file and the file on the right is the file
+ containing the changes which you are merging in.</li>
+
+ <li><code>xxdiff</code> is configured to automatically select regions that
+ would end up being selected by an automatic merge (e.g. there are only
+ changes in one of the files). Any difference <em>hunks</em> which cannot be
+ resolved automatically are left <em>unselected</em>.</li>
+
+ <li>Before you can save a merged version you need to go through each
+ unselected difference hunk and decide which text you wish to use.
+
+ <ul>
+ <li>Selecting a diff hunk can be carried out by clicking on it with the
+ left mouse button (or refer to the keyboard shortcuts shown under the
+ <kbd>Region</kbd> menu). The colours update to display which side is
+ selected for output. You can select individual lines with the middle
+ mouse button.</li>
+
+ <li>If you want to select more than one side, you have to invoke the
+ <kbd>Region->Split/swap/join</kbd> command (keyboard shortcut:
+ <kbd>S</kbd>). This will split the current diff hunk so you can select
+ the pieces you want from both sides. Further invocations of this
+ command will cause swapping of the regions, looping through all the
+ different ordering possibilities, and finally joining the regions again
+ (preserving selections where it is possible).</li>
+ </ul>
+ </li>
+
+ <li>The number on the far right is the number of unselected difference
+ hunks (1 in the example above). Once this number is 0 then you are ready to
+ save the merged file.</li>
+
+ <li>If you want to see how the merged file will look with the current
+ selections then select <kbd>Windows->Toggle Merged View</kbd> (keyboard
+ shortcut: <kbd>Alt+Y</kbd>). An extra window then appears showing the
+ merged output that updates interactively as you make selections.</li>
+
+ <li>You may find the following keyboard shortcuts useful.
+
+ <ul>
+ <li><kbd>B</kbd> - move to the next unselected hunk</li>
+
+ <li><kbd>O</kbd> - move to the previous unselected hunk</li>
+ </ul>
+ </li>
+
+ <li>There are several different ways to exit the 3-way diff (available from
+ the <kbd>File</kbd> menu):
+
+ <ul>
+ <li>Exit with MERGE (keyboard shortcut: <kbd>M</kbd>) - This saves the
+ merge result. If there are any unselected difference hunks remaining
+ then you will be warned and given the option of saving the file with
+ conflict markers.</li>
+
+ <li>Exit with ACCEPT (keyboard shortcut: <kbd>A</kbd>) - This saves the
+ file you are merging in (i.e. the right one) as the merge result (i.e.
+ you have <em>accepted</em> all the changes).</li>
+
+ <li>Exit with REJECT (keyboard shortcut: <kbd>R</kbd>) - This saves the
+ original working copy file (i.e. the left one) as the merge result
+ (i.e. you have <em>rejected</em> all the changes).</li>
+ </ul>
+
+ <p>If you just want to exit without making any decisions you can also
+ just close the window.</p>
+ </li>
+
+ <li>For further details please read the <a href=
+ "http://furius.ca/xxdiff/doc/xxdiff-doc.html"><code>xxdiff</code> users
+ manual</a> (available from the <kbd>Help</kbd> menu). In particular, read
+ the section <a href=
+ "http://furius.ca/xxdiff/doc/xxdiff-doc.html#merging-files-and-resolving-conflicts">
+ <em>Merging files and resolving conflicts</em></a>.</li>
+ </ul>
+
+ <p>If you have resolved all the conflicts in a file then you will be prompted
+ on whether to run <code>svn resolved</code> on the file to signal that the
+ file is no longer in conflict.</p>
+ <pre>
+(SHELL PROMPT)$ fcm conflicts
+Conflicts in file: Gen_setup_local1.proc
+You have chosen to ACCEPT all the changes
+Would you like to run "svn resolved"?
+Enter "y" or "n" (or just press <return> for "n"): y
+Resolved conflicted state of 'Gen_setup_local1.proc'
+Conflicts in file: Gen_setup_remote2.proc
+Merge conflicts were not all resolved
+Conflicts in file: Gen_setup_remote3.proc
+All merge conflicts resolved
+Would you like to run "svn resolved"?
+Enter "y" or "n" (or just press <return> for "n"): y
+Resolved conflicted state of 'Gen_setup_remote3.proc'
+</pre>
+
+ <p>It is important to realise that there are some types of merge that
+ <code>xxdiff</code> will not be able to help you with.</p>
+
+ <ul>
+ <li>It you have 2 versions of a file, both with substantial changes to the
+ same piece of code, then the <code>xxdiff</code> display will be extremely
+ colourful and not very helpful.</li>
+
+ <li>In these cases it is often easier to start with one version of the file
+ and manually re-apply the changes from the other version. It might not be
+ obvious how to do this and you may need to speak to the author of the other
+ change to agree how this can be done. Fortunately this situation should be
+ very rare.</li>
+
+ <li>For a more detailed discussion please refer to <a href=
+ "http://software.ericsink.com/scm/scm_file_merge.html">Chapter 3: File
+ Merge</a> in the online book called <a href=
+ "http://software.ericsink.com/scm/source_control.html">Source Control
+ HOWTO</a>.</li>
+ </ul>
+
+ <p>For files in <em>tree conflict</em>, which is otherwise known as a
+ structural or filesystem-based conflict, the command <code>fcm
+ conflicts</code> will manually resolve the problem by prompting you to choose
+ a course of action. You can either keep the file as it was before the merge
+ (<em>keep local</em>), or accept the external changes to the file.</p>
+
+ <p>The most common way to generate a tree conflict after a merge is when a
+ file has been deleted or renamed on one branch, and modified on another.
+ These are incompatible changes, to Subversion, and it doesn't know which
+ action to take. This is the cause of the tree conflict dilemma which the user
+ must solve.</p>
+
+ <p>In the following example, the branch in the working copy has had a
+ deletion of a file. The branch that is being merged in has subsequently
+ modified the file, which means that you may want to incorporate these
+ changes. A tree conflict is therefore flagged up.</p>
+ <pre>
+(SHELL PROMPT)$ fcm merge fcm:tutorial_br/dev/bfitz/r1_366
+Merge(s) available from /tutorial/branches/dev/bfitz/r1_366: 1257
+About to merge in changes from /tutorial/branches/dev/bfitz/r1_366 at 1257 compared with /tutorial/trunk at 1
+This merge will result in the following change:
+--------------------------------------------------------------------------------
+--- Merging r2 through r1257 into '.':
+ C src/subroutine/hello_sub.f90
+Summary of conflicts:
+ Tree conflicts: 1
+--------------------------------------------------------------------------------
+Would you like to go ahead with the merge?
+Enter "y" or "n" (or just press <return> for "n"): y
+Performing merge ...
+--- Merging r2 through r1257 into '.':
+ C src/subroutine/hello_sub.f90
+Summary of conflicts:
+ Tree conflicts: 1
+(SHELL PROMPT)$ fcm status
+ M .
+! C src/subroutine/hello_sub.f90
+ > local missing, incoming edit upon merge
+(SHELL PROMPT)$ fcm info src/subroutine/hello_sub.f90
+Path: src/subroutine/hello_sub.f90
+Name: hello_sub.f90
+Node Kind: none
+Tree conflict: local missing, incoming edit upon merge
+ Source left: (file) svn://fcm1/tutorial_svn/tutorial/trunk/src/subroutine/hello_sub.f90@1
+ Source right: (file) svn://fcm1/tutorial_svn/tutorial/branches/dev/bfitz/r1_366/src/subroutine/hello_sub.f90@1257
+</pre>
+
+ <p>In this example, running <code>fcm conflicts</code> would give:</p>
+ <pre>
+(SHELL PROMPT)$ fcm conflicts
+[info] src/subroutine/hello_sub.f90: in tree conflict.
+Locally: deleted
+Externally: modified.
+Answer (y) to leave the file deleted.
+Answer (n) to add the file with the changes.
+Keep the local version?
+Enter "y" or "n" (or just press <return> for "n")
+</pre>
+
+ <p>In this example, to keep the file as it was before (in a deleted state),
+ enter <samp>y</samp>.</p>
+
+ <p>Otherwise, to accept the merge branch version of the file (adding it with
+ the edited changes), enter <samp>n</samp>.</p>
+
+ <p>There are many other types of tree conflicts that can occur, and <code>fcm
+ conflicts</code> does not cover all of them. Tree conflicts arising from
+ updates and switches are not covered (which should be rare under FCM working
+ practice). More importantly, tree conflicts on directories are not covered,
+ because of the potential nesting of conflicts within the directories. It can
+ often be difficult to identify the problem and figure out the solution in the
+ case of directory conflicts, and the easiest solution may be to try to
+ resolve the discrepancy before the merge.</p>
+
+ <p>For further details, see the <a href=
+ "annex_quick_ref_tree_conflicts.html">Tree Conflict</a> annex</p>
+
+ <h4 id="svn_basic_check">Adding and Removing Files</h4>
+
+ <p>If your working copy contains files which are not under version control
+ then you can use the command <code>fcm add --check</code> to add them. This
+ will go through each of the files and prompt to see if you wish to put that
+ file under version control using <code>svn add</code>. For each file you can
+ enter <kbd>y</kbd> for yes, <kbd>n</kbd> for no or <kbd>a</kbd> to assume yes
+ for all following files.</p>
+ <pre>
+(SHELL PROMPT)$ fcm add -c
+? xxdiff1.png
+? xxdiff2.png
+? xxdiff3.png
+? xxdiff4.png
+Add file 'xxdiff1.png'?
+Enter "y", "n" or "a" (or just press <return> for "n"): y
+A xxdiff1.png
+Add file 'xxdiff2.png'?
+Enter "y", "n" or "a" (or just press <return> for "n"): n
+Add file 'xxdiff3.png'?
+Enter "y", "n" or "a" (or just press <return> for "n"): a
+A xxdiff3.png
+A xxdiff4.png
+</pre>
+
+ <p>Similarly, if your working copy contains files which are missing (i.e. you
+ have deleted them without using <code>svn delete</code>) then you can use the
+ command <code>fcm delete --check</code> to delete them. This will go through
+ each of the files and prompt to see if you wish to remove that file from
+ version control using <code>svn delete</code>.</p>
+
+ <p>As noted in the <a href=
+ "http://subversion.apache.org/faq.html#wc-change-detection">Subversion
+ FAQ</a>, it can be dangerous using these commands. If you have moved or
+ copied a file then simply adding them would cause the history to be lost.
+ Therefore take care to only use these commands on files which really are new
+ or deleted.</p>
+
+ <h4 id="svn_basic_commit">Committing Changes</h4>
+
+ <p>The command <code>fcm commit</code> should be used for committing changes
+ back to the repository. It differs from the <code>svn commit</code> command
+ in a number of important ways:</p>
+
+ <ul>
+ <li>Your working copy <em>must</em> be up to date. <code>fcm commit</code>
+ will abort if it finds that any files are out of date with respect to the
+ repository. This ensures that your working copy reflects how the repository
+ will be after you have committed your changes.
+
+ <ul>
+ <li>This helps to ensure that any tests you have done prior to
+ committing are valid.</li>
+
+ <li><code>fcm commit</code> is not suitable if you need to commit
+ changes from a working copy containing mixed revisions. However, you
+ are very unlikely to need to do this.</li>
+
+ <li>Actually there is a small chance that your working copy might not
+ be up to date when you commit if someone else is committing some
+ changes at the same time. However, this should very seldom happen and,
+ even if it does, the commit would fail if any of the files being
+ changed became out of date (i.e. it is not possible to lose any
+ changes).</li>
+ </ul>
+ </li>
+
+ <li>If it discovers a file named <samp>#commit_message#</samp> in the top
+ level of your working copy it uses this to provide a template commit
+ message (which you can then edit).
+
+ <ul>
+ <li>If you have performed a merge then a message describing the merge
+ will have been added to this file. It is important that you leave this
+ included in the commit message and do not change its format, as it is
+ used by the <code>fcm branch-info</code> command.</li>
+
+ <li>You can, if you wish, add entries to this file as you go along to
+ record what changes you have prepared in your working copy. You can
+ also use the command <code>fcm commit --dry-run</code> to allow you to
+ edit the commit message without committing any changes.</li>
+
+ <li><samp>#commit_message#</samp> is ignored by Subversion (so you
+ won't see it show up as an unversioned files when you run <code>fcm
+ status</code>).</li>
+ </ul>
+ </li>
+
+ <li>It always operates from the top of your working copy. If you issue the
+ <code>fcm commit</code> command from a sub-directory of your working copy
+ then it will automatically work out the top directory and work from there.
+
+ <ul>
+ <li>This ensures that any template commit message gets picked up and
+ that you do not, for example, accidently commit a partial set of
+ changes from a merge.</li>
+ </ul>
+ </li>
+
+ <li>It always commits <em>all</em> the changes in your working copy (it
+ does not accept a list of files to commit).
+
+ <ul>
+ <li>Once again, this avoids any danger of accidently committing a
+ partial set of changes.</li>
+
+ <li>You should only work on one change within a working copy. If you
+ need to prepare another, unrelated change then use a separate working
+ copy.</li>
+ </ul>
+ </li>
+
+ <li>It runs <code>svn update</code> after the commit to ensure that your
+ working copy is at the latest revision and to avoid any confusion caused by
+ your working copy containing mixed revisions.</li>
+ </ul>
+ <pre>
+(SHELL PROMPT)$ fcm commit
+Starting editor to create commit message ...
+Change summary:
+------------------------------------------------------------------------
+[Project: GEN]
+[Branch : branches/test/frsn/r123_foo_bar]
+[Sub-dir: <top>]
+
+M src/code/GenMod_Control/GenMod_Control.f90
+M src/code/GenMod_Control/Gen_SetupControl.f90
+------------------------------------------------------------------------
+Commit message is as follows:
+------------------------------------------------------------------------
+An example commit.
+------------------------------------------------------------------------
+Would you like to commit this change?
+Enter "y" or "n" (or just press <return> for "n"): y
+Sending src/code/GenMod_Control/GenMod_Control.f90
+Sending src/code/GenMod_Control/Gen_SetupControl.f90
+Transmitting file data ..
+Committed revision 170.
+=> svn update
+At revision 170.
+</pre>
+
+ <h3 id="svn_branching">Branching And Merging</h3>
+
+ <p>Branching is a fundamental concept common to most version control systems.
+ For a good introduction please read the chapter <a href=
+ "http://svnbook.red-bean.com/en/1.8/svn.branchmerge.html">Branching and
+ Merging</a> from the <cite>Subversion book</cite>. Even if you are already
+ familiar with branching using other version control systems you should still
+ read this chapter to see how branching is implemented in Subversion.</p>
+
+ <p>Having read this chapter from the <cite>Subversion book</cite> you should
+ understand:</p>
+
+ <ul>
+ <li>Why each project directory has sub-directories called <em>trunk</em>,
+ <em>branches</em> and <em>tags</em>. This structure is assumed by
+ <code>fcm</code> (Subversion recommends it but doesn't insist on it).</li>
+
+ <li>That when you make a branch you are taking a copy of the entire project
+ file tree. Fortunately, the design of the Subversion repository means that
+ these copies are <q title=
+ "http://svnbook.red-bean.com/en/1.8/svn.branchmerge.using.html#svn.branchmerge.using.create">
+ cheap</q> - they are quick to create and take very little space.</li>
+
+ <li>That Subversion has only implemented merge tracking recently,
+ long after FCM has implemented its own solution optimised for our
+ recommended working practice.</li>
+
+ <li>That each revision of your repository can also be thought of as a
+ <em>changeset</em>.</li>
+
+ <li>That once a change is committed to a repository it cannot be removed
+ (only reversed). Therefore you must take care not to committ a sensitive
+ document or a large data file unintentionally.</li>
+ </ul>
+
+ <p>FCM provides various commands which make working with branches easier (as
+ described in the following sections).</p>
+
+ <h4 id="svn_branching_create">Creating Branches</h4>
+
+ <p>The command <code>fcm branch-create</code> (or simply <code>fcm
+ bcreate</code> or even <code>fcm bc</code>) should be used for creating new
+ branches. It provides a number of features:</p>
+
+ <ul>
+ <li>It applies a standard naming convention for branches. The branch name
+ is automatically constructed for you depending on the option(s) supplied to
+ the command. The full detail of these options are described in the <a href=
+ "command_ref.html#fcm-branch-create">FCM Command Reference > fcm
+ branch-create</a> section.</li>
+
+ <li>By default, it assumes that you are branching from the last changed
+ revision of the <em>trunk</em>.
+
+ <ul>
+ <li>You can use the <code>--branch-of-branch</code> option if you need
+ to create a branch of a branch. A branch of a branch can be useful in
+ many situations. For example, consider a shared branch used by several
+ members of your team to develop, say, a new science scheme, and you
+ have come up with some different ideas of implementing the scheme. You
+ may want to create a branch of the shared branch to develop your idea
+ before merging it back to the shared branch. Note that you can only
+ merge a branch of a branch with it's parent or with another branch
+ created from the same parent. You can't, for example, merge it with the
+ trunk.</li>
+
+ <li>You can do <code>fcm bc NAME SOURCE at REV</code> if you
+ need to create a branch from an earlier revision of the SOURCE.</li>
+ </ul>
+ </li>
+
+ <li>Each branch always contains a full copy of the trunk (or its parent
+ branch) - you cannot create a branch from a sub-tree.
+
+ <ul>
+ <li>There would be no reason to only include a sub-tree in a
+ branch.</li>
+ </ul>
+ </li>
+
+ <li>It applies a standard commit message which defines how the branch has
+ been created. If a Trac ticket is specified using the <code>--ticket
+ <number></code> option, it is added to the commit log message. If you
+ need to add anything to the commit log message, please do so
+ <strong>above</strong> the line that says <samp>--Add your commit message
+ ABOVE - do not alter this line or those below--</samp>.</li>
+ </ul>
+
+ <p>The following is a list of the different types of branches available:</p>
+
+ <dl>
+ <dt>User development branches</dt>
+
+ <dd><samp>branches/dev/<Userid>/<Branch_Name></samp> These are
+ for changes which are intended to be merged back to the trunk once they are
+ complete. Most branches will belong to this type. e.g.
+ branches/dev/frdm/vn6.1_ImprovedDeepConvection,
+ branches/dev/frdm/r2134_NewBranchNamingConvention.</dd>
+
+ <dt>Shared development branches</dt>
+
+ <dd><samp>branches/dev/Share/<Branch_Name></samp></dd>
+
+ <dt>User test branches</dt>
+
+ <dd><samp>branches/test/<Userid>/<Branch_Name></samp> These are
+ for changes which are <em>not</em> intended for the trunk. e.g. Proof of
+ concept work, temporary code written for dealing with a one-off problem,
+ etc.</dd>
+
+ <dt>Shared test branches</dt>
+
+ <dd><samp>branches/test/Share/<Branch_Name></samp></dd>
+
+ <dt>User packages</dt>
+
+ <dd><samp>branches/pkg/<Userid>/<Branch_Name></samp> These are
+ branches which combine together a number of different development branches.
+ Sometimes this will simply be for testing purposes (i.e. for testing a
+ branch in combination with other branches). Other times it may be the
+ package which eventually gets merged to the trunk (rather than the
+ development branches). e.g.
+ branches/pkg/frdm/vn6.1_TestImprovedDeepConvection</dd>
+
+ <dt>Shared packages</dt>
+
+ <dd><samp>branches/pkg/Share/<Branch_Name></samp> E.g.
+ branches/pkg/Share/vn6.1_NewConvectionScheme.</dd>
+
+ <dt>Configurations</dt>
+
+ <dd><samp>branches/pkg/Config/<Branch_Name></samp> These are major
+ packages which combine together a number of different packages and
+ development branches. e.g. branches/pkg/Config/vn6.1_HadGEM1a.</dd>
+
+ <dt>Releases</dt>
+
+ <dd><samp>branches/pkg/Rel/<Branch_Name></samp> These may be bug-fix
+ branches for system releases, if required. They can also be branches on
+ which stable releases are prepared if you don't do this on the trunk
+ (although you lose the ability to branch from stable releases if you work
+ this way). e.g. branches/pkg/Rel/vn6.1_BugFixes.</dd>
+ </dl>
+ <pre>
+(SHELL PROMPT)$ fcm bcreate -k 23 my_test_branch fcm:test
+Starting nedit to create commit message ...
+Change summary:
+------------------------------------------------------------------------
+A svn://fcm1/repos/OPS/branches/dev/frsn/r118_my_test_branch
+------------------------------------------------------------------------
+Commit message is as follows:
+------------------------------------------------------------------------
+Create an example branch to demonstrate branch creation for the user guide.
+#23: Created /OPS/branches/dev/frsn/r118_my_test_branch from /OPS/trunk at 118.
+------------------------------------------------------------------------
+Would you like to go ahead and create this branch?
+Enter "y" or "n" (or just press <return> for "n"): y
+Creating branch svn://fcm1/repos/OPS/branches/dev/frsn/r118_my_test_branch ...
+
+Committed revision 169.
+</pre>
+
+ <h4 id="svn_branching_list">Listing Branches Created by You or Other
+ Users</h4>
+
+ <p>The command <code>fcm branch-list</code> (or simply <code>fcm bls</code>)
+ can be used to list the branches you have created at the HEAD of a
+ repository. If you specify the <code>--user <userid></code> option, the
+ branches created by <userid> are listed instead. You can specify
+ multiple users with multiple <code>--user <userid></code> options, or
+ with a colon (:) separated list to a single <code>--user
+ <userid:list></code> option. Note that you can also list shared
+ branches by specifying <userid> as <code>Share</code>, configuration
+ branches by specifying <userid> as <code>Config</code> and release
+ branches by specifying <userid> as <code>Rel</code>. The command
+ returns 0 (success) if one or more branches is found for the specified users,
+ or 1 (failure) if no branch is found.</p>
+ <pre>
+(SHELL PROMPT)$ fcm branch-list fcm:gen
+1 branch found for frsn in svn://fcm1/GEN_svn/GEN
+fcm:GEN-br/dev/frsn/r1191_clean_up/
+(SHELL PROMPT)$ echo $?
+0
+(SHELL PROMPT)$ fcm branch-list --user frbj --user frsn fcm:gen
+2 branches found for frbj, frsn in svn://fcm1/GEN_svn/GEN
+fcm:GEN-br/dev/frbj/r1177_gen_ui_for_scs/
+fcm:GEN-br/dev/frsn/r1191_clean_up/
+(SHELL PROMPT)$ echo $?
+0
+(SHELL PROMPT)$ fcm branch-list --user frva fcm:gen
+0 branch found for frva in svn://fcm1/GEN_svn/GEN
+(SHELL PROMPT)$ echo $?
+1
+</pre>
+
+ <h4 id="svn_branching_info">Getting Information About Branches</h4>
+
+ <p>The command <code>fcm branch-info</code> (or simply <code>fcm
+ binfo</code>) can be used to get various information about a branch. In
+ particular, it summarises information about merges to and from the branch and
+ its parent.</p>
+ <pre>
+(SHELL PROMPT)$ fcm branch-info
+URL: svn://fcm1/FCM_svn/FCM/branches/dev/frsn/r1346_merge
+Repository Root: svn://fcm1/FCM_svn
+Revision: 1385
+Last Changed Author: frsn
+Last Changed Rev: 1385
+Last Changed Date: 2006-04-20 11:08:45 +0100 (Thu, 20 Apr 2006)
+--------------------------------------------------------------------------------
+Branch Create Author: frsn
+Branch Create Rev: 1354
+Branch Create Date: 2006-04-04 14:27:47 +0100 (Tue, 04 Apr 2006)
+Branch Parent: svn://fcm1/FCM_svn/FCM/trunk@1346
+Last Merge From Parent, Revision: 1444
+Last Merge From Parent, Delta: /FCM/trunk at 1439 cf. /FCM/trunk at 1395
+Merges Avail From Parent: 1445
+Merges Avail Into Parent: 1453 1452 1449 1446 1444 1443 1441 1434 1397 1396 ...
+</pre>
+
+ <p>If you need information on the current children of the branch, use the
+ <code>--show-children</code> option of the <code>fcm branch-info</code>
+ command. If you need information on recent merges to and from the branch and
+ its siblings, use the <code>--show-siblings</code> option of the <code>fcm
+ branch-info</code> command.</p>
+
+ <p>To find out what changes have been made on a branch relative to its parent
+ you can use the command <code>fcm branch-diff</code> (or simply <code>fcm
+ bdi</code>.</p>
+
+ <ul>
+ <li>You can combine this with the options:
+
+ <dl>
+ <dt><code>--graphical</code></dt>
+
+ <dd>to display the differences using a graphical <em>diff</em>
+ tool</dd>
+
+ <dt><code>--trac</code></dt>
+
+ <dd>to display the differences using Trac</dd>
+
+ <dt><code>--wiki</code></dt>
+
+ <dd>to print a wiki syntax suitable for inserting into Trac</dd>
+ </dl>
+ </li>
+
+ <li>The base of the difference is adjusted to account for any merges from
+ the branch to its parent or vice-versa.</li>
+ </ul>
+
+ <h4 id="svn_branching_switch">Switching your working copy to point to another
+ branch</h4>
+
+ <p>The command <code>fcm switch</code> can be used to switch your working
+ copy to point to another branch. For example, if you have a working copy at
+ <samp>$HOME/work</samp>, currently pointing to the trunk or a branch of a
+ project at <samp>svn://fcm1/FCM_svn/FCM/trunk</samp>, you can switch the
+ working copy to point to another branch of same project:</p>
+ <pre>
+(Shell prompt)$ cd $HOME/work
+(Shell prompt)$ fcm sw dev/frsn/r959_blockdata
+-> svn switch --revision HEAD svn://fcm1/FCM_svn/FCM/branches/dev/frsn/r959_blockdata
+U doc/user_guide/getting_started.html
+U doc/user_guide/code_management.html
+U doc/user_guide/command_ref.html
+U src/lib/FCM1/SrcFile.pm
+U src/lib/FCM1/Util.pm
+U src/lib/FCM1/Build.pm
+U src/lib/FCM1/Cm.pm
+U src/lib/FCM1/SrcPackage.pm
+U src/bin/fcm_internal
+U src/bin/fcm_gui
+Updated to revision 1009.
+</pre>
+
+ <p>Unlike <code>svn switch</code>, <code>fcm switch</code> does extra
+ checking to ensure that your whole working copy is switched to the new branch
+ at the correct level of sub-directory. In addition, you can specify only the
+ <em>branch</em> part of the URL, such as <samp>trunk</samp>,
+ <samp>branches/dev/fred/r1234_bob</samp> or even
+ <samp>dev/fred/r1234_bob</samp> and the command will work out the full URL
+ for you.</p>
+
+ <h4 id="svn_branching_delete">Deleting Branches</h4>
+
+ <p>The command <code>fcm branch-delete</code> (or simply <code>fcm
+ bdel</code>) can be used to delete branches which are no longer required.
+ Before being asked to confirm that you want to delete the branch, you will
+ first see the same output as from <code>fcm branch-info</code>. This allows
+ you to check, for example, whether your branch is being used anywhere else or
+ whether the latest changes on your branch have been merged to the trunk. You
+ will be prompted to edit your commit log message. If you need to add anything
+ to the commit log message, please do so <strong>above</strong> the line that
+ says <samp>--Add your commit message ABOVE - do not alter this line or those
+ below--</samp>.</p>
+
+ <h4 id="svn_branching_merge">Merging</h4>
+
+ <p>As mentioned earlier, <code>fcm</code> has its own merge tracking solution
+ which is optimised for our recommended working practice. The solution assumes
+ the following:</p>
+
+ <ul>
+ <li>That all merges are performed using FCM and are identified using a
+ standard template in the commit log message.</li>
+
+ <li>That you only ever merge all the changes available on the source branch
+ up to a chosen point (i.e. you can't only include a subset of the changes
+ made to the branch).</li>
+
+ <li>That the source and target are both branches (or the trunk) in the same
+ FCM project.</li>
+
+ <li>That the source and target are directly related, i.e. they must either
+ have a parent/child relationship or they are siblings from the same parent
+ branch.</li>
+ </ul>
+
+ <p>Note that the term <em>source branch</em> and <em>target branch</em>
+ referred to above can also mean the trunk.</p>
+
+ <p>To perform a merge, use the command <code>fcm merge <source></code>.
+ This includes a number of important features:</p>
+
+ <ul>
+ <li>If it finds any local modifications in your working copy then it checks
+ whether you wish to continue (in most cases you won't want to mix a merge
+ with other changes).</li>
+
+ <li>It determines the base revision and path of the <em>common
+ ancestor</em> to be used for the merge, taking into account any merges from
+ the <em>source</em> to the <em>target</em> or vice-versa.</li>
+
+ <li>Before doing the merge, (unless you specify the
+ <code>--non-interactive</code> option), it reports what changes will result
+ from performing the merge and checks that you wish to continue.</li>
+
+ <li>It adds details of the merge, using a standard template, into the
+ commit message file (<samp>#commit_message#</samp>). If you need to add any
+ extra comment, you should do so <strong>above</strong> the line that says
+ <samp>--Add your commit message ABOVE - do not alter this line or those
+ below--</samp>.
+
+ <ul>
+ <li>If you decide to revert the merge, you should remove the template
+ line manually from the commit message file, making sure that you do not
+ alter the standard template by accident.</li>
+
+ <li>If the <code>--auto-log</code> option is specified, it adds the log
+ messages of the merged revisions as well as the standard template. This
+ is particularly useful when a small change is prepared in a branch, and
+ often the same commit log messages have to be repeated when the change is
+ merged and committed to the trunk. The option does not work very well if
+ the branch contains merges from another branch.</li>
+ </ul>
+ </li>
+ </ul>
+ <pre>
+(SHELL PROMPT)$ fcm merge trunk # merge changes from the trunk into the branch
+Eligible merge(s) from FCM/trunk: 1383 1375
+Enter a revision (or just press <return> for "1383"):
+Merge: /FCM/trunk at 1383
+ c.f.: /FCM/trunk at 1371
+-------------------------------------------------------------------------dry-run
+A doc/fortran_standards/index.html
+U src/lib/FCM1/ReposBranch.pm
+-------------------------------------------------------------------------dry-run
+Would you like to go ahead with the merge?
+Enter "y" or "n" (or just press <return> for "n"): y
+Merge succeeded.
+</pre>
+
+ <h3 id="svn_gui">Using the GUI</h3>
+
+ <p>So far, all the tools described have been command line tools. Many people
+ will be happy with these but, for those who prefer it, there is also a simple
+ Graphical User Interface (GUI).</p>
+
+ <h4 id="svn_gui_start">Starting the GUI</h4>
+
+ <p>To run the GUI simply issue the command <code>fcm gui</code> from the
+ directory you want as your working directory.</p>
+
+ <p>The GUI consists of several sections:</p>
+
+ <ul>
+ <li>The top section contains a row of buttons to allow you to select which
+ command you want to run.</li>
+
+ <li>Beneath this is shown the current working directory and the top level
+ directory of your working copy (these may be the same).</li>
+
+ <li>Beneath this come various buttons and entry boxes to allow you to
+ configure the command you have selected. These vary according to the
+ command.</li>
+
+ <li>Beneath this comes a further row of buttons
+
+ <ul>
+ <li><em>Quit</em> - this exits the GUI.</li>
+
+ <li><em>Help</em> - this displays the help message for the selected
+ command.</li>
+
+ <li><em>Clear</em> - this empties the text window.</li>
+
+ <li><em>Run</em> - this allows you to run your command.</li>
+ </ul>
+ </li>
+
+ <li>Beneath this comes a scrolling text window where the output from the
+ commands is displayed.</li>
+
+ <li>The bottom section displays help information when you position the
+ cursor over various parts of the GUI.</li>
+ </ul>
+
+ <p class="image"><img src="gui1.png" alt=
+ "Example GUI screen with the Status commands selected" /><br />
+ Example GUI screen with the <kbd>Status</kbd> commands selected</p>
+
+ <p>If you run a more complicated command, like <code>fcm
+ branch-create</code>, which prompts for input then extra entry windows will
+ pop up.</p>
+
+ <p class="image"><img src="gui2.png" alt="Example GUI pop-up window" /><br />
+ Example GUI pop-up window</p>
+
+ <h4 id="svn_gui_commands">GUI Commands</h4>
+
+ <p>The commands available from the GUI should be self explanatory. A few
+ points to note:</p>
+
+ <ul>
+ <li>If the current directory is not a working copy, you will only be able
+ to Checkout a working copy or create a branch from the GUI.</li>
+
+ <li>The <kbd>Checkout</kbd> command is only available if you start the GUI
+ in a directory which is not already a working copy. After successfully
+ running a checkout the GUI automatically sets the working directory to the
+ top of this new working copy.</li>
+
+ <li>With some commands (Status, Diff, Add, Delete, Conflicts) you can
+ choose whether to run from the top level of your working copy or from your
+ working directory. With the remaining commands this would not make sense
+ and they can only be run from the top level.</li>
+
+ <li>You can only issue commands from the GUI if they do not need to prompt
+ you for authentication (i.e. the Subversion command can be run with the
+ <code>--non-interactive</code> option).
+
+ <ul>
+ <li>If authentication is required then the command issued by the GUI
+ will fail. For the <code>branch-create</code>,
+ <code>branch-delete</code> and <code>commit</code> commands, which
+ support the <code>--password</code> option, you should specify your
+ password in <kbd>Other options</kbd> and click <kbd>Run</kbd> again.
+ For other commands, you should run the command in interactive mode on
+ the command line. Use the command displayed in the GUI text window but
+ remove the <code>--non-interactive</code> option.</li>
+
+ <li>Most repositories will be configured so that you only need
+ authentication for writing (not reading). Therefore, the first command
+ requiring authentication will probably be creating a branch or
+ commiting to the trunk.</li>
+
+ <li>You should only need to do this the first time you ever issue such
+ a command on a each repository (unless the repository is moved to a new
+ location) since the Subversion client caches this information for
+ future comamnds .</li>
+ </ul>
+ </li>
+ </ul>
+
+ <h3 id="svn_problems">Known Problems with Subversion</h3>
+
+ <p>There is a limitation with Subversion which you should be aware of. The
+ <code>svn rename</code> command is not a true rename/move operation, but is
+ implemented as a copy and delete. As a result, if you rename an item in a
+ branch, and later attempt to merge it back to the trunk, the operation may
+ not be handled correctly by <code>svn merge</code> (see <a href=
+ "http://subversion.tigris.org/issues/show_bug.cgi?id=898">subversion issue
+ 898</a> for further details). Until such time as support for a <q title=
+ "http://subversion.tigris.org/issues/show_bug.cgi?id=898">true rename</q> is
+ implemented in Subversion, you should avoid renaming of files or directories
+ unless you can ensure that no-one is working in parallel on the affected
+ areas of the project.</p>
+
+ <h2 id="trac">Using Trac</h2>
+
+ <p><cite>Trac</cite> has a simple and intuitive web interface which is
+ relatively easy to pick up. It also includes a <a href=
+ "http://trac.edgewall.org/wiki/TracGuide">User and Administration Guide</a>
+ which is full of helpful information (and is referred to extensively in this
+ section).</p>
+
+ <p>Trac contains a menu bar at the top of each page (which we will refer to
+ as the <cite>Trac menu</cite>). This provides access to all the main
+ features.</p>
+
+ <h3 id="trac_login">Logging In</h3>
+
+ <p>Although different projects may choose their own rules, we expect that
+ most systems will have Trac configured so that all the information is
+ viewable by anyone. However, in order to make any changes you will need to
+ login. This ensures that any changes are identified with the appropriate
+ userid.</p>
+
+ <p>In the rest of this section it is assumed that you have logged in to Trac
+ and are therefore able to make changes.</p>
+
+ <p>If you haven't yet got a Trac userid (which should be the same as the
+ userid you use for committing changes to Subversion) then please contact your
+ system manager.</p>
+
+ <h3 id="trac_wiki">Using the Wiki Pages</h3>
+
+ <p>A wiki enables documents to be written in a simple markup language using a
+ web browser. See the Trac Guide for information on the <a href=
+ "http://trac.edgewall.org/wiki/TracWiki">Trac Wiki Engine</a>. Make sure that
+ you read the information provided on:</p>
+
+ <ul>
+ <li><a href="http://trac.edgewall.org/wiki/WikiFormatting">Wiki
+ Formatting</a> which explains how to format your wiki pages.</li>
+
+ <li><a href="http://trac.edgewall.org/wiki/WikiPageNames">Wiki Page
+ Names</a> which explains how <em>CamelCase</em> is used to create <a href=
+ "http://trac.edgewall.org/wiki/WikiNewPage">New Wiki Pages</a>.</li>
+
+ <li><a href="http://trac.edgewall.org/wiki/TracLinks">Trac Links</a> which
+ allow hyperlinking between Trac entities (tickets, reports, changesets,
+ Wiki pages, milestones and source files). This is a fundamental feature of
+ Trac which makes it easy, for example, to link a bug report (ticket) to the
+ changeset which fixed the bug (and vice-versa).</li>
+ </ul>
+
+ <p>Whenever you are viewing a wiki page in Trac you should see several
+ buttons at the bottom of the page:</p>
+
+ <ul>
+ <li><kbd>Edit This Page</kbd> - Clicking this will bring up a page where
+ you can edit the page contents. Before saving your changes you can preview
+ how the modified page will appear. You can also leave a comment explaining
+ what changes you made.</li>
+
+ <li><kbd>Attach File</kbd> - Allows you to attach files to a page, e.g. an
+ image.</li>
+
+ <li>If you have admin rights then you will also see
+
+ <ul>
+ <li><kbd>Delete This Version</kbd> - Delete the particular version of
+ the page you are viewing.</li>
+
+ <li><kbd>Delete Page</kbd> - Delete the page and all its history.</li>
+ </ul>Use with care - these operations are irreversible!
+ </li>
+ </ul>
+
+ <p>At the top of each wiki page at the right hand side you can select
+ <kbd>Page History</kbd>. This shows you the full history of each page with
+ details of when each change was made, who made the change and what the
+ changes were.</p>
+
+ <h3 id="trac_browser">Using the Repository Browser</h3>
+
+ <p>The <a href="http://trac.edgewall.org/wiki/TracBrowser">Trac Browser</a>
+ is used to view the contents of your repository. To get to it just select
+ <kbd>Browse Source</kbd> from the Trac menu. You can view directories and
+ files at any version, see their revision histories and view <a href=
+ "http://trac.edgewall.org/wiki/TracChangeset">changesets</a>. Any wiki
+ formatting in log messages is recognised and interpreted so you can easily
+ link a changeset to a Trac ticket by using <a href=
+ "http://trac.edgewall.org/wiki/TracLinks">Trac Links</a>.</p>
+
+ <h3 id="trac_tickets">Using the Issue Tracker</h3>
+
+ <p>The Trac issue database provides a way of tracking issues within a project
+ (e.g. bug reports, feature requests, software support issues, project tasks).
+ Within Trac an issue is often referred to as a <em>Ticket</em>.</p>
+
+ <p>Please refer to the Trac Guide for the following information:</p>
+
+ <ul>
+ <li>
+ <a href="http://trac.edgewall.org/wiki/TracTickets">The Trac Ticket
+ System</a> - Creating and modifying tickets.
+
+ <ul>
+ <li>Only Trac accounts with admin rights can modify ticket
+ descriptions.</li>
+ </ul>
+ </li>
+
+ <li><a href="http://trac.edgewall.org/wiki/TracQuery">Trac Ticket
+ Queries</a> - List tickets matching your chosen criterion.</li>
+ </ul>
+
+ <h3 id="trac_roadmap">Using the Roadmap</h3>
+
+ <p>Each ticket can be assigned to a milestone. The Trac Roadmap can then be
+ used to provide a view on the ticket system. This can useful to see what
+ changes went into a particular system release or what changes are outstanding
+ before a milestone can be reached.</p>
+
+ <p>Please refer to the Trac Guide for further information on the <a href=
+ "http://trac.edgewall.org/wiki/TracRoadmap">Trac Roadmap</a>.</p>
+
+ <ul>
+ <li>Only Trac accounts with admin rights can add, modify and remove
+ milestones using the web interface.</li>
+ </ul>
+
+ <h3 id="trac_timeline">Using the Timeline</h3>
+
+ <p>The <a href="http://trac.edgewall.org/wiki/TracTimeline">Trac Timeline</a>
+ allows you to list all the activity on a project over any given period. It
+ can list:</p>
+
+ <ul>
+ <li>Creation and changes to wiki pages.</li>
+
+ <li>Creation, closure and changes to tickets.</li>
+
+ <li>Commits to the Subversion repository.</li>
+
+ <li>Milestones reached.</li>
+ </ul>
+
+ </div>
+ </div>
+ </div>
+
+ <hr/>
+ <div class="container-fluid text-center">
+ <div class="row-fluid"><div class="span12">
+ <address><small>
+ © British Crown Copyright 2006-14
+ <a href="http://www.metoffice.gov.uk">Met Office</a>.
+ See <a href="../etc/fcm-terms-of-use.html">Terms of Use</a>.<br />
+ This document is released under the British <a href=
+ "http://www.nationalarchives.gov.uk/doc/open-government-licence/" rel=
+ "license">Open Government Licence</a>.<br />
+ </small></address>
+ </div></div>
+ </div>
+
+ <script type="text/javascript" src="../etc/jquery.min.js"></script>
+ <script type="text/javascript" src="../etc/bootstrap/js/bootstrap.min.js"></script>
+ <script type="text/javascript" src="../etc/fcm.js"></script>
+ <script type="text/javascript" src="../etc/fcm-version.js"></script>
+</body>
+</html>
diff --git a/doc/user_guide/command_ref.html b/doc/user_guide/command_ref.html
new file mode 100644
index 0000000..efc3426
--- /dev/null
+++ b/doc/user_guide/command_ref.html
@@ -0,0 +1,2005 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>FCM: User Guide: FCM Command Reference</title>
+ <meta name="author" content="FCM team" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link rel="icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link rel="shortcut icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link href="../etc/bootstrap/css/bootstrap.min.css" rel="stylesheet" media="screen" />
+ <link href="../etc/fcm.css" rel="stylesheet" media="screen" />
+</head>
+<body>
+ <div class="navbar navbar-inverse">
+ <div class="navbar-inner">
+ <a class="brand" href=".."><span class="fcm-version">FCM</span></a>
+ <ul class="nav">
+ <li><a href="../installation/">Installation</a></li>
+
+ <li class="active"><a href=".">User Guide</a></li>
+ </ul>
+ </div>
+ </div>
+
+ <div class="page-header">
+ <div class="fcm-page-content pull-right well well-small"></div>
+ <h1>FCM: User Guide: FCM Command Reference</h1>
+ </div>
+
+ <div class="container">
+ <div class="row">
+ <div class="span12">
+
+ <h2 id="introduction">Introduction</h2>
+
+ <p>This chapter describes all commands supported by <code>fcm</code>.
+ <code>fcm</code> has its own set of functionalities, but it also wraps all
+ <code>svn</code> commands.</p>
+
+ <p>In most wrappers to <code>svn</code>, <code>fcm</code> simply passes the
+ command directly on to <code>svn</code> (after expanding any keywords). These
+ commands are listed in the <a href="#svn">Other Subversion Commands</a>
+ section.</p>
+
+ <p>Where <code>fcm</code> adds more functionality to an <code>svn</code>
+ command, the command is discussed individually.</p>
+
+ <p>All command abbreviations supported by <code>svn</code> work with
+ <code>fcm</code>.</p>
+
+ <p>Subversion may prompt you for authentication if it is the first time you
+ write to a repository. The command fails if the authentication fails. A
+ command may support the <code>--non-interactive</code> or
+ <code>--svn-non-interactive</code> option. If such an option is specified,
+ Subversion will not prompt you for authentication, and the command will
+ simply fail if authentication is required. Please note that the option is
+ normally specified if you are running a command from the FCM GUI. If
+ authentication is required, you should run the command in interactive mode on
+ a command line, or by using the <code>--password=PASSWORD</code> option in
+ the <kbd>Other options</kbd>.</p>
+
+ <h2 id="env">Environment Variables</h2>
+
+ <p>The following environment variables are used by the <code>fcm</code>
+ command.</p>
+
+ <dl>
+ <dt id="env.FCM_CONF_PATH">FCM_CONF_PATH='/path/to/conf1
+ /path/to/conf2'</dt>
+
+ <dd>This variable is mainly used to test FCM. If specified, override the
+ paths for site and user configuration files. The value should be a space
+ delimited list of paths where FCM site and user configuration files can be
+ found. The value can also be set to a null string to allow FCM to run with
+ no site or user configuration. If not defined, the default to look for site
+ and user configuration files from <code>$FCM_HOME/etc/fcm/</code> and
+ <code>~/.metomi/fcm/</code> (where <var>$FCM_HOME/bin/fcm</var> is where the
+ <code>fcm</code> command is invoked.</dd>
+
+ <dt id="env.FCM_DEBUG">FCM_DEBUG=true</dt>
+
+ <dd>If specified, raises the verbosity to the <dfn>debug</dfn> level. This
+ is useful in debugging especially if a command does not accept a
+ <code>-v</code> option.</dd>
+
+ <dt id="env.FCM_GRAPHIC_DIFF">FCM_GRAPHIC_DIFF=<kbd>command</kbd></dt>
+
+ <dd>(Deprecated) Specifies an alternate command for doing graphical diff
+ tool. The <a href="annex_cfg.html#external">external</a> configuration file
+ should be used instead of this environment variable.</dd>
+
+ <dt id="env.FCM_GRAPHIC_MERGE">FCM_GRAPHIC_MERGE=<kbd>command</kbd></dt>
+
+ <dd>(Deprecated) Specifies an alternate command for doing graphical merge
+ tool. The <a href="annex_cfg.html#external">external</a> configuration file
+ should be used instead of this environment variable.</dd>
+
+ <dt id="env.FCM_VERBOSE">FCM_VERBOSE=<kbd>N</kbd></dt>
+
+ <dd>(Deprecated) An alternate way to specify the verbosity for FCM 1 extract
+ and build systems.</dd>
+ </dl>
+
+ <h2 id="fcm-add">fcm add</h2>
+
+ <dl>
+ <dt>Usage</dt>
+
+ <dd><code>fcm add --check (-c) [PATH]</code><br />
+ <code>fcm add <any valid <em>svn add</em> options></code></dd>
+
+ <dt>Description</dt>
+
+ <dd>
+ <p>In the 1st form (i.e. <code>fcm add --check</code>), the system checks
+ for any files which are not currently under version control (i.e. those
+ marked with a <samp>?</samp> by <code>svn status</code>) and prompts the
+ user to make a decision on whether to schedule them for addition at the
+ next commit (using <code>svn add</code>).</p>
+
+ <p>In the 2nd form (i.e. without the <code>--check</code> option),
+ <code>fcm add</code> simply pass control to <code>svn add</code>. (For
+ detail of usage, please refer to the <a href=
+ "http://svnbook.red-bean.com/en/1.8/svn.ref.svn.c.add.html">Subversion
+ book</a>.)</p>
+
+ <p>For further details refer to the section <a href=
+ "code_management.html#svn_basic_check">Adding and Removing Files</a>.</p>
+ </dd>
+ </dl>
+
+ <h2 id="fcm-branch">fcm branch</h2>
+
+ <dl>
+ <dt>Description</dt>
+
+ <dd>
+ <p>Deprecated. The 4 usages of this command have been replaced by the
+ following commands:</p>
+
+ <dl>
+ <dt><code>fcm branch --create --name NAME</code></dt>
+
+ <dd><a href="#fcm-branch-create">fcm branch-create</a></dd>
+
+ <dt><code>fcm branch --delete</code></dt>
+
+ <dd><a href="#fcm-branch-delete">fcm branch-delete</a></dd>
+
+ <dt><code>fcm branch [--info]</code></dt>
+
+ <dd><a href="#fcm-branch-info">fcm branch-info</a></dd>
+
+ <dt><code>fcm branch --list</code></dt>
+
+ <dd><a href="#fcm-branch-list">fcm branch-list</a></dd>
+ </dl>
+ </dd>
+
+ <dt>Alternate Names</dt>
+
+ <dd>br</dd>
+ </dl>
+
+ <h2 id="fcm-branch-create">fcm branch-create</h2>
+
+ <dl>
+ <dt>Usage</dt>
+
+ <dd><code>fcm branch-create [OPTIONS] NAME [SOURCE]</code></dd>
+
+ <dt>Description</dt>
+
+ <dd>
+ <p>Creates a new branch.</p>
+
+ <p>The 1st argument <var>NAME</var> must be the short name for your
+ branch. The name of the branch must contain only characters in the set
+ <code>[A-Za-z0-9_-.]</code>. If the <code>--ticket=N</code> option is not
+ specified and <var>NAME</var> contains only a list of positive integers
+ separated by <code>[_-]</code> (an underscore or a hyphen), the command
+ will assume that <var>NAME</var> also specifies the related ticket
+ numbers.</p>
+
+ <p>If the 2nd argument <var>SOURCE</var> is specified, it must either be
+ a URL or a path to a working copy of a standard FCM project. Otherwise,
+ the current working directory must be a working copy of a standard FCM
+ project.</p>
+
+ <p>This command performs the following actions:</p>
+
+ <ul>
+ <li>It determines the last changed revision of the trunk/source branch
+ at the HEAD (or the specified) revision.</li>
+
+ <li>It constructs the branch name from the option you have specified
+ and reports it.</li>
+
+ <li>It checks that the chosen branch name does not currently exist. If
+ so, the command aborts with an error.</li>
+
+ <li>If you do not specify the <code>--non-interactive</code> option, it
+ starts an editor (using a similar convention as <a href=
+ "#fcm-commit">commit</a>) to allow you to add further comment to the
+ commit log message. A standard commit log template and change summary
+ is provided for you below the line that says <samp>--Add your commit
+ message ABOVE - do not alter this line or those below--</samp>. If you
+ need to add any extra message to the log, please do so
+ <strong>above</strong> this line. When you exit the editor, the command
+ will report the commit log before prompting for confirmation that you
+ wish to proceed (it aborts if not).</li>
+
+ <li>It uses <code>svn copy</code> to create the branch.</li>
+ </ul>
+
+ <p>For further details refer to the section <a href=
+ "code_management.html#svn_branching_create">Creating Branches</a>.</p>
+ </dd>
+
+ <dt>Options</dt>
+
+ <dd>
+ <dl>
+ <dt><code>--branch-of-branch</code></dt>
+
+ <dd>If the source URL is a valid URL of a branch in a standard FCM
+ project, this option tells the system to create a branch of the source
+ branch. Otherwise, it will normally create a branch from the
+ trunk.</dd>
+
+ <dt><code>--non-interactive</code></dt>
+
+ <dd>Tells the system not to prompt for anything. (The
+ <code>--svn-non-interactive</code> option is set automatically when you
+ specify <code>--non-interactive</code>.)</dd>
+
+ <dt><code>--password=PASSWORD</code></dt>
+
+ <dd>Specifies the password for authentication.</dd>
+
+ <dt><code>--rev-flag=NONE|NORMAL|NUMBER</code></dt>
+
+ <dd>Alters the branch name prefix behaviour. Your branch name will
+ normally be prefixed by the revision number from which it is branched.
+ (E.g. if the branch name is <samp>my_branch</samp> and you are
+ branching from revision 123 of the trunk, the final name will be
+ <samp>r123_my_branch</samp>.) If this revision number is associated
+ with a revision keyword, the keyword will be used in place of the
+ revision number. (E.g. if revision 123 is associated with the keyword
+ vn6.1, <samp>r123_my_branch</samp> will become
+ <samp>vn6.1_my_branch</samp>.) If <code>NORMAL</code> is specified, it
+ uses the default behaviour. If <code>NUMBER</code> is specified, it
+ will always use the revision number as the prefix, regardless of
+ whether the revision number is defined as a keyword or not. If
+ <code>NONE</code> is specified, it will not add a prefix to your branch
+ name.</dd>
+
+ <dt><code>--svn-non-interactive</code></dt>
+
+ <dd>Tells the system to run <code>svn</code> in non-interactive
+ mode.</dd>
+
+ <dt><code>--switch</code>, <code>-s</code></dt>
+
+ <dd><code><a href="#fcm-switch">fcm switch</a></code> the current
+ working directory (if it contains a relevant working copy) to point to
+ the newly created branch after the branch is created.</dd>
+
+ <dt><code>--ticket=N</code>, <code>-k N</code></dt>
+
+ <dd>Specifies one or more Trac ticket numbers, which the branch relates
+ to. Multiple ticket numbers can be set by specifying this option
+ multiple times, or by using a comma-separated list of ticket numbers as
+ the argument to the option. If set, the ticket numbers will be included
+ in the commit log message.</dd>
+
+ <dt><code>--type=TYPE</code>, <code>-t TYPE</code></dt>
+
+ <dd>
+ Specifies the type of branch to create. The argument to the option
+ must be one of the following:
+
+ <dl>
+ <dt><code>DEV::USER</code>, <code>DEV</code>, <code>USER</code>
+ (default)</dt>
+
+ <dd>A development branch for the current user (e.g.
+ <samp>branches/dev/<user_id>/<branch_name></samp>)</dd>
+
+ <dt><code>DEV::SHARE</code>, <code>SHARE</code></dt>
+
+ <dd>A shared development branch (e.g.
+ <samp>branches/dev/Share/<branch_name></samp>)</dd>
+
+ <dt><code>TEST::USER</code>, <code>TEST</code></dt>
+
+ <dd>A test branch for the current user (e.g.
+ <samp>branches/test/<user_id>/<branch_name></samp>)</dd>
+
+ <dt><code>TEST::SHARE</code></dt>
+
+ <dd>A shared test branch (e.g.
+ <samp>branches/test/Share/<branch_name></samp>)</dd>
+
+ <dt><code>PKG::USER</code>, <code>PKG</code></dt>
+
+ <dd>A package branch for the current user (e.g.
+ <samp>branches/pkg/<user_id>/<branch_name></samp>)</dd>
+
+ <dt><code>PKG::SHARE</code></dt>
+
+ <dd>A shared package branch (e.g.
+ <samp>branches/pkg/Share/<branch_name></samp>)</dd>
+
+ <dt><code>PKG::CONFIG</code>, <code>CONFIG</code></dt>
+
+ <dd>A configuration branch (e.g.
+ <samp>branches/pkg/Config/<branch_name></samp>)</dd>
+
+ <dt><code>PKG::REL</code>, <code>REL</code></dt>
+
+ <dd>A release branch (e.g.
+ <samp>branches/pkg/Rel/<branch_name></samp>)</dd>
+ </dl>
+ </dd>
+ </dl>
+ </dd>
+
+ <dt>Alternate Names</dt>
+
+ <dd>bcreate, bc</dd>
+ </dl>
+
+ <h2 id="fcm-branch-delete">fcm branch-delete</h2>
+
+ <dl>
+ <dt>Usage</dt>
+
+ <dd><code>fcm branch-delete [OPTIONS] [TARGET]</code></dd>
+
+ <dt>Description</dt>
+
+ <dd>
+ <p>Deletes a branch.</p>
+
+ <p>If <var>TARGET</var> is specified, it must either be a URL or a path
+ to a local working copy of a valid branch of a standard FCM project.
+ Otherwise, the current working directory must be a working copy of a
+ valid branch of a standard FCM project.</p>
+
+ <p>This command performs the following actions:</p>
+
+ <ul>
+ <li>Firstly, it provides exactly the same output as <a href=
+ "#fcm-branch-info">fcm branch-info</a>.</li>
+
+ <li>If you do not specify the <code>--non-interactive</code> option, it
+ starts an editor (using a similar convention as <a href=
+ "#fcm-commit">commit</a>) to allow you to add further comment to the
+ commit log message. A standard commit log template and change summary
+ is provided for you below the line that says <samp>--Add your commit
+ message ABOVE - do not alter this line or those below--</samp>. If you
+ need to add any extra message to the log, please do so
+ <strong>above</strong> this line. When you exit the editor, the command
+ will report the commit log before prompting for confirmation that you
+ wish to proceed with deleting the branch (it aborts if not).</li>
+ </ul>
+
+ <p>For further details refer to the section <a href=
+ "code_management.html#svn_branching_delete">Deleting Branches</a>.</p>
+ </dd>
+
+ <dt>Options</dt>
+
+ <dd>
+ <p>The command supports all options of <a href="#fcm-branch-info">fcm
+ branch-info</a> as well as the following:</p>
+
+ <dl>
+ <dt><code>--non-interactive</code></dt>
+
+ <dd>Tells the system not to prompt for anything. (The
+ <code>--svn-non-interactive</code> option is set automatically when you
+ specify <code>--non-interactive</code>.)</dd>
+
+ <dt><code>--password=PASSWORD</code></dt>
+
+ <dd>Specifies the password for authentication.</dd>
+
+ <dt><code>--svn-non-interactive</code></dt>
+
+ <dd>Tells the system to run <code>svn</code> in non-interactive
+ mode.</dd>
+
+ <dt><code>--switch</code>, <code>-s</code></dt>
+
+ <dd>If SOURCE not specified in the argument list,
+ <code><a href="#fcm-switch">fcm switch</a></code> the current working
+ copy to point to the <em>trunk</em> after the branch deletion.</dd>
+ </dl>
+ </dd>
+
+ <dt>Alternate Names</dt>
+
+ <dd>bdelete, bdel, brm</dd>
+ </dl>
+
+ <h2 id="fcm-branch-diff">fcm branch-diff</h2>
+
+ <dl>
+ <dt>Usage</dt>
+
+ <dd><code>fcm branch-diff [OPTIONS] [TARGET]</code></dd>
+
+ <dt>Description</dt>
+
+ <dd>
+ <p>The command displays the differences between the target branch and its
+ parent. This should show you the differences which you would get if you
+ tried to merge the changes in the branch into its parent.</p>
+
+ <p>If an argument <var>TARGET</var> is specified, it must either be a URL
+ or a path to a local working copy. Otherwise, the current working
+ directory must be a working copy. The specified URL or that of the
+ working copy must be a valid branch in a standard FCM project.</p>
+
+ <p>The command determines the base of the branch relative to its parent.
+ This is adjusted to account for any merges from the branch to its parent
+ or vice-versa. It then reports what path and revision it is comparing
+ against using <code>svn diff</code> or otherwise.</p>
+
+ <p>For further details refer to the section <a href=
+ "code_management.html#svn_basic_diff">Examining Changes</a>.</p>
+ </dd>
+
+ <dt>Options</dt>
+
+ <dd>
+ <dl>
+ <dt><code>--diff-cmd=COMMAND</code></dt>
+
+ <dd>Option passed to <code>svn diff</code>.</dd>
+
+ <dt><code>--extensions=EXT</code>, <code>-x EXT</code></dt>
+
+ <dd>Option passed to <code>svn diff</code>.</dd>
+
+ <dt><code>--graphical</code>, <code>-g</code></dt>
+
+ <dd>Tells the <code>svn diff</code> to use a graphical tool to display
+ the differences. (The default graphical diff tool is
+ <code>xxdiff</code>, but you can alter the behaviour by following the
+ instruction discussed in the sub-section on <a href=
+ "code_management.html#svn_basic_diff">Examining Changes</a>.) This
+ switch should not be used with <code>--diff-cmd</code>,
+ <code>--extensions</code>, <code>--trac</code> and
+ <code>--wiki</code>.</dd>
+
+ <dt><code>--summarize</code>, <code>--summarise</code></dt>
+
+ <dd>Reports using <code>svn diff --summarize</code>.</dd>
+
+ <dt><code>--xml</code></dt>
+
+ <dd>Used with --summarise to change output format to XML.</dd>
+
+ <dt><code>--trac</code>, <code>-t</code></dt>
+
+ <dd>Launches Trac with your default web browser to report the diff.
+ Note: if <var>TARGET</var> is a working copy, local changes in it will
+ not be displayed.</dd>
+
+ <dt><code>--wiki</code>, <code>-w</code></dt>
+
+ <dd>Prints a Trac wiki syntax to represent the diff.</dd>
+ </dl>
+ </dd>
+
+ <dt>Alternate Names</dt>
+
+ <dd>bdiff, bdi</dd>
+ </dl>
+
+ <h2 id="fcm-branch-info">fcm branch-info</h2>
+
+ <dl>
+ <dt>Usage</dt>
+
+ <dd><code>fcm branch-info [OPTIONS] [TARGET]</code></dd>
+
+ <dt>Description</dt>
+
+ <dd>
+ <p>Displays information about a branch.</p>
+
+ <p>If the argument <var>TARGET</var> is specified, it must either be a
+ URL or a path to a local working copy of a valid branch of a standard FCM
+ project. Otherwise, the current working directory must be a working copy
+ of a valid branch of a standard FCM project.</p>
+
+ <p>It performs the following actions:</p>
+
+ <ul>
+ <li>It reports the basic information of the branch URL, as returned by
+ <code>svn info</code>.</li>
+
+ <li>If <code>--verbose</code> is set, it also prints the log message of
+ the last change revision.</li>
+
+ <li>If the URL is not the trunk:
+
+ <ul>
+ <li>It reports the branch creation information, including the
+ revision, author and date. It also reports the parent URL at REV of
+ the branch. If <code>--verbose</code> is set, it prints the log
+ message of the branch creation revision.</li>
+
+ <li>If the branch does not exist at the HEAD, it reports the
+ revision at which it is deleted.</li>
+
+ <li>It reports the last merges into and from the parent branch. If
+ <code>--verbose</code> is set, it also prints the log message of
+ these merges.</li>
+
+ <li>It reports the revisions available for merging into and from
+ the parent branch. If <code>--verbose</code> is set, it also prints
+ the log message of these revisions.</li>
+ </ul>
+ </li>
+
+ <li>It reports relationship with other branches, depending on
+ options.</li>
+ </ul>
+
+ <p>For further details refer to the section <a href=
+ "code_management.html#svn_branching_info">Getting Information About
+ Branches</a>.</p>
+ </dd>
+
+ <dt>Options</dt>
+
+ <dd>
+ <dl>
+ <dt><code>--show-all</code>, <code>-a</code></dt>
+
+ <dd>Turns on <code>--show-children</code>, <code>--show-other</code>
+ and <code>--show-siblings</code>.</dd>
+
+ <dt><code>--show-children</code></dt>
+
+ <dd>Lists the current children of the branch and their create
+ revisions. Where appropriate, it reports the revision of each child,
+ which is last merged from/into the current branch. It also reports the
+ available merges from/into each child into the current branch.</dd>
+
+ <dt><code>--show-other</code></dt>
+
+ <dd>Reports all custom and reverse merges into the current branch.</dd>
+
+ <dt><code>--show-siblings</code></dt>
+
+ <dd>Reports recent merges from/into sibling branches. It also reports
+ the available merges from/into sibling branches where recent merges are
+ detected. If <code>--verbose</code> is set, it also prints the log
+ message of these merges.</dd>
+
+ <dt><code>--verbose</code>, <code>-v</code></dt>
+
+ <dd>Increases the verbosity.</dd>
+ </dl>
+ </dd>
+
+ <dt>Alternate Names</dt>
+
+ <dd>binfo</dd>
+ </dl>
+
+ <h2 id="fcm-branch-list">fcm branch-list</h2>
+
+ <dl>
+ <dt>Usage</dt>
+
+ <dd><code>fcm branch-list [OPTIONS] [TARGET ...]</code></dd>
+
+ <dt>Description</dt>
+
+ <dd>
+ <p>Searches and lists branches in projects. By default, it lists only
+ branches created by the current user.</p>
+
+ <p>If no <var>TARGET</var> is specified, the current working directory is
+ assumed to be the target. Each target must either be a
+ <var>URL[@REV]</var> or a <var>PATH[@REV]</var> to a working copy of a
+ standard FCM project.</p>
+ </dd>
+
+ <dt>Options</dt>
+
+ <dd>
+ <dl>
+ <dt><code>--only=DEPTH:PATTERN</code></dt>
+
+ <dd>Specify a regular expression to match at various depth. E.g. with
+ the normal FCM branch naming convention, <samp>--only=1:dev
+ --only=2:fred</samp> will display only the development branches owned by
+ user ID <samp>fred</samp>. (This option is cumalative, and overrides the
+ <code>--show-all</code> and <code>--user=PATTERN</code> options.)</dd>
+
+ <dt><code>--quiet</code>, <code>-q</code></dt>
+
+ <dd>Decreases verbosity. Only prints branches matching the search
+ criteria.</dd>
+
+ <dt><code>--show-all</code>, <code>-a</code></dt>
+
+ <dd>Prints branches of all users. (This option overrides the
+ <code>--user=USER</code> option.)</dd>
+
+ <dt><code>--url</code></dt>
+
+ <dd>Displays Subversion URL instead of FCM location keywords.</dd>
+
+ <dt><code>--user=PATTERN</code>, <code>-u PATTERN</code></dt>
+
+ <dd>Equivalent to <code>--only=2:^PATTERN$</code> for projects with the
+ normal FCM branch naming convention. Lists branches created by the
+ specified list of users instead of the current user. With the normal FCM
+ branch naming convention, you can also list shared branches by
+ specifying the user as <code>Share</code>, configuration branches by
+ specifying the user as <code>Config</code> and release branches by
+ specifying the user as <code>Rel</code>. (This option is
+ cumalative.)</dd>
+ </dl>
+ </dd>
+
+ <dt>Alternate Names</dt>
+
+ <dd>blist, bls</dd>
+ </dl>
+
+ <h2 id="fcm-browse">fcm browse</h2>
+
+ <dl>
+ <dt>Usage</dt>
+
+ <dd><code>fcm browse [OPTIONS] [TARGET ...]</code></dd>
+
+ <dt>Description</dt>
+
+ <dd>
+ <p><code>fcm browse</code> invokes the web-browser to launch the
+ corresponding URL of the web-based repository browser (currently Trac
+ browser) to view the Subversion repository specified by
+ <var>TARGET</var>.</p>
+
+ <p>If <var>TARGET</var> is specified, it must be a path to a local
+ working copying, a Subversion URL or an FCM URL keyword. Otherwise, it is
+ set to <samp>.</samp>, the current working directory. If
+ <var>TARGET</var> is a directory in the local file system, the command
+ will determine whether it is a working copy. If so, its associated
+ Subversion URL will be used. The command fails if the directory is not a
+ working copy. The Subversion URL must be associated with an FCM location
+ keyword, so that the system knows how to map the Subversion URL to the
+ web browser URL.</p>
+ </dd>
+
+ <dt>Options</dt>
+
+ <dd>
+ <dl>
+ <dt><code>--browser=COMMAND</code>, <code>-b COMMAND</code></dt>
+
+ <dd>
+ If this option is specified, its argument <var>COMMAND</var> must be
+ a valid command to a web browser. If this option is not specified,
+ the default is to use <code>firefox</code>, or the <var>browser</var>
+ setting in the external configuration files (i.e.
+ <samp>$FCM/etc/fcm/external.cfg</samp> and
+ <samp>$HOME/.metomi/fcm/external.cfg</samp>). For example:
+ <pre>
+browser = konqueror
+</pre>
+ </dd>
+ </dl>
+ </dd>
+
+ <dt>Alternate Names</dt>
+
+ <dd>trac, www</dd>
+ </dl>
+
+ <h2 id="fcm-build">fcm build</h2>
+
+ <dl>
+ <dt>Usage</dt>
+
+ <dd><code>fcm build [OPTIONS...] [CFGFILE]</code></dd>
+
+ <dt>Description</dt>
+
+ <dd>
+ <p><code>fcm build</code> invokes the deprecated FCM 1 build system.</p>
+
+ <p>The path to a valid build configuration file <var>CFGFILE</var> may be
+ provided as either a URL or a pathname. Otherwise, the build system
+ searches the default locations for a build configuration file.</p>
+
+ <p>For further details, please refer to the chapter on <a href=
+ "build.html">The FCM 1 Build System</a>.</p>
+ </dd>
+
+ <dt>Option</dt>
+
+ <dd>
+ <p>If no option is specified, the system uses the <code>-s 5 -t all -j 1
+ -v 1</code> by default.</p>
+
+ <dl>
+ <dt><code>--archive</code>, <code>-a</code></dt>
+
+ <dd>This option can be specified to switch on the archive mode. In
+ archive mode, sub-directories produced by the build will be archived in
+ <code>tar</code> format at the end of a successful build. This option
+ should not be used if the current build is intended to be re-used as a
+ pre-compiled build.</dd>
+
+ <dt><code>--clean</code></dt>
+
+ <dd>If this option is specified, the build system will parse the
+ configuration file, remove contents generated by the build system in
+ the destination and exit.</dd>
+
+ <dt><code>--full</code>, <code>-f</code></dt>
+
+ <dd>If this option is specified, the build system will attempt to
+ perform a full/clean build by removing any previous build files.
+ Otherwise, the build system will attempt to perform an incremental
+ build where appropriate.</dd>
+
+ <dt><code>--ignore-lock</code></dt>
+
+ <dd>When the build system is invoked, it sets a lock file in the build
+ root directory to prevent other extracts/builds taking place in the
+ same location. The lock file is normally removed when the build system
+ exits. (However, a lock file may be left behind if the user interrupts
+ the command, e.g. by typing <kbd>Ctrl-C</kbd>.) You can bypass the
+ check for lock files by using this option.</dd>
+
+ <dt><code>--jobs=N</code>, <code>-j N</code></dt>
+
+ <dd>This option can be used to specify the number of parallel jobs that
+ can be handled by the <code>make</code> command. The argument
+ <var>N</var> must be a natural integer to represent the number of jobs.
+ If not specified, the default is to perform serial <code>make</code>
+ (i.e. 1 job).</dd>
+
+ <dt><code>--stage=STAGE</code>, <code>-s STAGE</code></dt>
+
+ <dd>
+ This option can be used to limit the actions performed by the build
+ system, up to a named stage determined by the argument
+ <var>STAGE</var>. If not specified, the default is 5. The stages are:
+
+ <ul>
+ <li><dfn>1, s or setup</dfn>: Stage 1, read configuration and set
+ up the build</li>
+
+ <li><dfn>2, pp or pre_process</dfn>: Stage 2, perform
+ pre-processing for source files that require pre-processing</li>
+
+ <li><dfn>3, gd or generate_dependency</dfn>: Stage 3, scan source
+ files for dependency information and generate <code>make</code>
+ rules for them</li>
+
+ <li><dfn>4, gi or generate_interface</dfn>: Stage 4, generate
+ interface files for Fortran 9X source files</li>
+
+ <li><dfn>5, m or make</dfn>: Stage 5, invoke the <code>make</code>
+ command to build the project</li>
+ </ul>
+ </dd>
+
+ <dt><code>--targets=TARGETS</code>, <code>-t TARGETS</code></dt>
+
+ <dd>This option can be used to specify the targets to be built. The
+ argument <var>TARGETS</var> must be a colon-separated list of valid
+ targets. If not specified, the default to be built is the
+ <samp>all</samp> target.</dd>
+
+ <dt><code>--verbose=N</code>, <code>-v N</code></dt>
+
+ <dd>This option can be specified to alter the level of diagnostic
+ output. The argument <var>N</var> to this option must be an integer
+ greater than or equal to 0. The verbose level increases with this
+ number. If not specified, the default verbose level is 1.</dd>
+ </dl>
+ </dd>
+
+ <dt>Alternate Names</dt>
+
+ <dd>bld</dd>
+ </dl>
+
+ <h2 id="fcm-cfg-print">fcm cfg-print</h2>
+
+ <dl>
+ <dt>Usage</dt>
+
+ <dd><code>fcm cfg-print [OPTIONS] [TARGET ...]</code></dd>
+
+ <dt>Description</dt>
+
+ <dd>
+ <p>Parses each FCM configuration file specified in the argument list, and
+ prints the result to STDOUT.</p>
+ </dd>
+
+ <dt>Options</dt>
+
+ <dd>
+ <dl>
+ <dt><code>--fcm1</code>, <code>-f</code></dt>
+
+ <dd>If specified, targets should be in FCM 1 format. Otherwise, they
+ should be in FCM 2 format.</dd>
+ </dl>
+ </dd>
+
+ <dt>Alternate Names</dt>
+
+ <dd>cfg</dd>
+ </dl>
+
+ <h2 id="fcm-cmp-ext-cfg">fcm cmp-ext-cfg</h2>
+
+ <dl>
+ <dt>Usage</dt>
+
+ <dd><code>fcm cmp-ext-cfg [OPTIONS] CFG1 CFG2</code></dd>
+
+ <dt>Description</dt>
+
+ <dd>
+ <p><code>fcm cmp-ext-cfg</code> compares the deprecated FCM 1 extract
+ configurations of two similar extract configuration files <var>CFG1</var>
+ and <var>CFG2</var>. It reports repository branches and source
+ directories that are declared in one file but not another. If a source
+ directory is declared in both files, it compares their versions. If they
+ differ, it uses <code>svn log</code> to obtain a list of revision numbers
+ at which changes are made to the source directory. It then reports, for
+ each declared repository branch, the revisions at which changes occur in
+ their declared source directories.</p>
+
+ <p>The list of revisions for each declared repository branch is normally
+ printed out as a simple list in plain text.</p>
+ </dd>
+
+ <dt>Options</dt>
+
+ <dd>
+ <dl>
+ <dt><code>--verbose=N</code>, <code>-v N</code></dt>
+
+ <dd>You can use this option to print the log of each revision, by
+ setting <var>N</var> to 2.</dd>
+
+ <dt><code>--wiki-format=TARGET</code>, <code>--wiki=TARGET</code>,
+ <code>-w TARGET</code></dt>
+
+ <dd>Alternatively, you can use this option to change that into an
+ tabular output suitable for inserting into a Trac wiki page. This
+ option must be specified with an argument, which must be the Subversion
+ URL or FCM URL keyword of an FCM project associated with the intended
+ Trac system. The URL allows the command to work out the correct wiki
+ syntax to use.</dd>
+ </dl>
+ </dd>
+ </dl>
+
+ <h2 id="fcm-commit">fcm commit</h2>
+
+ <dl>
+ <dt>Usage</dt>
+
+ <dd><code>fcm commit [OPTIONS] [PATH]</code></dd>
+
+ <dt>Description</dt>
+
+ <dd>
+ <p><code>fcm commit</code> sends changes from your working copy in the
+ current working directory (or from <var>PATH</var> if it is specified) to
+ the repository.</p>
+
+ <p>This command performs the following actions:</p>
+
+ <ul>
+ <li>It checks that the current working directory (or <var>PATH</var> if
+ it is specified) is a working copy. (If not, it aborts with an
+ error).</li>
+
+ <li>It always commits from the top level of the working copy.</li>
+
+ <li>It checks that there are no files in conflict, missing or out of
+ date (it aborts if there are).</li>
+
+ <li>It checks that any files which have been added have the
+ <var>svn:executable</var> property set correctly (in case a script was
+ added before the execute bit was set correctly).</li>
+
+ <li>It reads in any existing commit message.
+
+ <ul>
+ <li>The commit message is stored in the file
+ <samp>#commit_message#</samp> in the top level of your working
+ copy.</li>
+ </ul>
+ </li>
+
+ <li>It adds the following line to the commit log message: <samp>--Add
+ your commit message ABOVE - do not alter this line or those
+ below--</samp>. This line, and anything below it, is automatically
+ ignored by <code>svn commit</code>. If you need to add any extra
+ message to the log, please do so <strong>above</strong> this line.</li>
+
+ <li>If you have run the <a href="#fcm-merge">merge</a> command before
+ the commit, you will get a standard commit log template below a line
+ that says <samp>--FCM message (will be inserted
+ automatically)--</samp>. Please do not try to alter this message (your
+ changes will be ignored if you do).</li>
+
+ <li>It adds current status information to the commit message showing
+ the list of modifications below a line that says <samp>--Change summary
+ (not part of commit message)--</samp>.</li>
+
+ <li>It starts an editor to allow you to edit the commit message.
+
+ <ul>
+ <li>If defined, the environment variable <var>SVN_EDITOR</var>
+ specifies the editor.</li>
+
+ <li>Otherwise the environment variable <var>VISUAL</var> specifies
+ the editor.</li>
+
+ <li>Otherwise the environment variable <var>EDITOR</var> specifies
+ the editor.</li>
+
+ <li>Otherwise the editor <code>nedit</code> is used.</li>
+ </ul>
+ </li>
+
+ <li>It reports the commit message that will be sent to Subversion and
+ then asks if you want to proceed (it aborts if not).</li>
+
+ <li>It calls <code>svn commit</code> to send the changes to the
+ repository.</li>
+
+ <li>It calls <code>svn update</code> to bring your working copy up to
+ the new revision.</li>
+ </ul>
+
+ <p>For further details refer to the section <a href=
+ "code_management.html#svn_basic_commit">Committing Changes</a>.</p>
+ </dd>
+
+ <dt>Options</dt>
+
+ <dd>
+ <dl>
+ <dt><code>--dry-run</code></dt>
+
+ <dd>Prevents the command from committing any changes. This can be used
+ to allow you to add notes to your commit message whilst you are still
+ preparing your change.</dd>
+
+ <dt><code>--password=PASSWORD</code></dt>
+
+ <dd>Specifies the password for authentication.</dd>
+
+ <dt><code>--svn-non-interactive</code></dt>
+
+ <dd>Tells the system to run <code>svn</code> in non-interactive
+ mode.</dd>
+ </dl>
+ </dd>
+
+ <dt>Alternate Names</dt>
+
+ <dd>ci</dd>
+ </dl>
+
+ <h2 id="fcm-conflicts">fcm conflicts</h2>
+
+ <dl>
+ <dt>Usage</dt>
+
+ <dd><code>fcm conflicts [PATH]</code></dd>
+
+ <dt>Description</dt>
+
+ <dd>
+ <p><code>fcm conflicts</code> helps you to resolve any text files in your
+ working copy which have conflicts by using the graphical merge tool
+ <code>xxdiff</code>. If <var>PATH</var> is set, it must be a working
+ copy, and the command will operate in it. If <var>PATH</var> is not set,
+ the command will operate in your current working directory.</p>
+
+ <p>This command performs the following actions:</p>
+
+ <ul>
+ <li>For each text file reported as being in conflict (i.e. marked with
+ a <samp>C</samp> by <code>svn status</code>) it calls
+ <code>xxdiff</code>.</li>
+
+ <li>If <code>xxdiff</code> reports all conflicts resolved then if asks
+ if you wish to run <code>svn resolved</code> on that file.</li>
+ </ul>
+
+ <p>For further details refer to the section <a href=
+ "code_management.html#svn_basic_conflicts">Resolving Conflicts</a>.</p>
+ </dd>
+
+ <dt>Alternate Names</dt>
+
+ <dd>cf</dd>
+ </dl>
+
+ <h2 id="fcm-delete">fcm delete</h2>
+
+ <dl>
+ <dt>Usage</dt>
+
+ <dd><code>fcm delete --check (-c)</code><br />
+ <code>fcm delete <any valid <em>svn delete</em> options></code></dd>
+
+ <dt>Description</dt>
+
+ <dd>
+ <p>In the 1st form (i.e. <code>fcm delete --check</code>), the system
+ checks for any files which are missing (i.e. those marked with a
+ <samp>!</samp> by <code>svn status</code>) and prompts the user to make a
+ decision on whether to schedule them for deletion at the next commit
+ (using <code>svn delete</code>).</p>
+
+ <p>In the 2nd form (i.e. without the <code>--check</code> option),
+ <code>fcm delete</code> simply pass control to <code>svn delete</code>.
+ (For detail of usage, please refer to the <a href=
+ "http://svnbook.red-bean.com/en/1.8/svn.ref.svn.c.delete.html">Subversion
+ book</a>.)</p>
+
+ <p>For further details refer to the section <a href=
+ "code_management.html#svn_basic_check">Adding and Removing Files</a>.</p>
+ </dd>
+ </dl>
+
+ <h2 id="fcm-diff">fcm diff</h2>
+
+ <dl>
+ <dt>Usage</dt>
+
+ <dd><code>fcm diff [OPTIONS] [TARGET ...]</code></dd>
+
+ <dt>Description</dt>
+
+ <dd>
+ <p>Display the differences between two revisions or paths. <code>fcm
+ diff</code> supports all of the arguments and alternate names supported
+ by <code>svn diff</code> (refer to the <a href=
+ "http://svnbook.red-bean.com/en/1.8/svn.ref.svn.c.diff.html">Subversion
+ book</a> for details).</p>
+
+ <p>For further details refer to the section <a href=
+ "code_management.html#svn_basic_diff">Examining Changes</a>.</p>
+ </dd>
+
+ <dt>Options</dt>
+
+ <dd>
+ <p><code>fcm diff</code> supports the following options in addition to
+ the options of <code>svn diff</code> (refer to the <a href=
+ "http://svnbook.red-bean.com/en/1.8/svn.ref.svn.c.diff.html">Subversion
+ book</a> for details):</p>
+
+ <dl>
+ <dt><code>--graphical</code>, <code>-g</code></dt>
+
+ <dd>If this option is specified, the command uses a graphical tool to
+ display the differences. (The default graphical diff tool is
+ <code>xxdiff</code>, but you can alter the behaviour by following the
+ instruction discussed in the sub-section on <a href=
+ "code_management.html#svn_basic_diff">Examining Changes</a>.) This
+ option can be used in combination with all other valid options except
+ <code>--diff-cmd</code> and <code>--extensions</code>.</dd>
+
+ <dt><code>--summarise</code></dt>
+
+ <dd>This option is implemented in FCM as a wrapper to the Subversion
+ <code>--summarize</code> option. It prints only a summary of the
+ results.</dd>
+
+ <dt><code>--branch</code>, <code>-b</code></dt>
+
+ <dd>This usage is deprecated. It is replaced by the <a href=
+ "#fcm-branch-diff">fcm branch-diff</a> command.</dd>
+ </dl>
+ </dd>
+ </dl>
+
+ <h2 id="fcm-export-items">fcm export-items</h2>
+
+ <dl>
+ <dt>Usage</dt>
+
+ <dd><code>fcm export-items [OPTIONS...] SOURCE</code></dd>
+
+ <dt>Description</dt>
+
+ <dd>
+ <p><code>fcm export-items</code> exports directories in SOURCE as a list
+ of versioned items. The SOURCE should be the URL of a branch in a
+ Subversion repository with the standard FCM layout.</p>
+
+ <p>This command is used to support a legacy working practice, in which
+ directories in a source tree are regarded as individual versioned
+ items.</p>
+
+ <p>The configuration file should be in the deprecated FCM 1 configuration
+ format. The label in each entry should be a path relative to the source
+ URL. If the path ends in <samp>*</samp> then the path is expanded
+ recursively and any sub-directories containing regular files are added to
+ the list of relative paths to export. The value may be empty, or it may
+ be a list of space separated <em>conditions</em>. Each condition is a
+ conditional operator (<code>></code>, <code>>=</code>,
+ <code><</code>, <code><=</code>, <code>==</code> or
+ <code>!=</code>) followed by a revision number. The command uses the
+ revision log to determine the revisions at which the relative path has
+ been updated in the source URL. If these revisions also satisfy the
+ conditions set by the user, they will be considered in the export.</p>
+
+ <p>For further details, please refer to <a href=
+ "system_admin.html#alternate_versions">System Administration >
+ Maintaining alternate versions of namelists and data files</a>.</p>
+ </dd>
+
+ <dt>Options</dt>
+
+ <dd>
+ <dl>
+ <dt><code>--config-file=PATH</code>, <code>--file=PATH</code>, <code>-f
+ PATH</code></dt>
+
+ <dd>Specifies the path to the configuration file.
+ (default=<samp>$PWD/fcm-export-items.cfg</samp>)</dd>
+
+ <dt><code>--directory=PATH</code>, <code>-C PATH</code></dt>
+
+ <dd>Specifies the path to the destination.
+ (default=<samp>$PWD</samp>)</dd>
+
+ <dt><code>--new</code></dt>
+
+ <dd>Specifies the new mode. In this mode, everything is re-exported.
+ Otherwise, the system runs in incremental mode, in which the version
+ directories are only updated if they do not already exist.</dd>
+ </dl>
+ </dd>
+ </dl>
+
+ <h2 id="fcm-extract">fcm extract</h2>
+
+ <dl>
+ <dt>Usage</dt>
+
+ <dd><code>fcm extract [OPTIONS...] [CFGFILE]</code></dd>
+
+ <dt>Description</dt>
+
+ <dd>
+ <p><code>fcm extract</code> invokes the deprecated FCM 1 extract
+ system.</p>
+
+ <p>The path to a valid extract configuration file <var>CFGFILE</var> may
+ be provided as either a URL or a pathname. Otherwise, the extract system
+ searches the default locations for an extract configuration file.</p>
+
+ <p>For further details, please refer to the chapter on <a href=
+ "extract.html">The FCM 1 Extract System</a>.</p>
+ </dd>
+
+ <dt>Options</dt>
+
+ <dd>
+ <dl>
+ <dt><code>--clean</code></dt>
+
+ <dd>If this option is specified, the extract system will parse the
+ configuration file, remove contents generated by previous extract in
+ the destination and exit.</dd>
+
+ <dt><code>--full</code>, <code>-f</code></dt>
+
+ <dd>If this option is specified, the extract system will attempt to
+ perform a full extract by removing any previous extracted files.
+ Otherwise, the extract system will attempt to perform an incremental
+ extract where appropriate.</dd>
+
+ <dt><code>--ignore-lock</code></dt>
+
+ <dd>When the extract system is invoked, it sets a lock file in the
+ extract destination root directory to prevent other extracts/builds
+ taking place in the same location. The lock file is normally removed
+ when the extract system exits. (However, a lock file may be left behind
+ if the user interrupts the command, e.g. by typing <kbd>Ctrl-C</kbd>.)
+ You can bypass the check for lock files by using this option.</dd>
+
+ <dt><code>--verbose=N</code>, <code>-v N</code></dt>
+
+ <dd>This option can be specified to alter the level of diagnostic
+ output. The argument <var>N</var> to this option must be an integer
+ greater than or equal to 0. The verbose level increases with this
+ number. If not specified, the default verbose level is 1.</dd>
+ </dl>
+ </dd>
+
+ <dt>Alternate Names</dt>
+
+ <dd>ext</dd>
+ </dl>
+
+ <h2 id="fcm-gui">fcm gui</h2>
+
+ <dl>
+ <dt>Usage</dt>
+
+ <dd><code>fcm gui [DIR]</code></dd>
+
+ <dt>Description</dt>
+
+ <dd>
+ <p><code>fcm gui</code> starts up the FCM GUI. If <var>DIR</var> is
+ specified then this is used as the working directory.</p>
+
+ <p>For further details, please refer to the section <a href=
+ "code_management.html#svn_gui">Using the GUI</a>.</p>
+ </dd>
+ </dl>
+
+ <h2 id="fcm-keyword-print">fcm keyword-print</h2>
+
+ <dl>
+ <dt>Usage</dt>
+
+ <dd><code>fcm keyword-print [OPTIONS] [TARGET ...]</code></dd>
+
+ <dt>Description</dt>
+
+ <dd>
+ <p>If no argument is specified, <code>fcm keyword-print</code> prints all
+ the registered FCM location keywords. Otherwise, it prints the location
+ and revision keywords according to the argument <var>TARGET</var>, which
+ must be an FCM URL keyword, a Subversion URL or a path to a Subversion
+ working copy.</p>
+ </dd>
+
+ <dt>Options</dt>
+
+ <dd>
+ <dl>
+ <dt><code>--verbose</code>, <code>-v</code></dt>
+
+ <dd>Prints implied location keywords as well.</dd>
+ </dl>
+ </dd>
+
+ <dt>Alternate Names</dt>
+
+ <dd>kp</dd>
+ </dl>
+
+ <h2 id="fcm-loc-layout">fcm loc-layout</h2>
+
+ <dl>
+ <dt>Usage</dt>
+
+ <dd><code>fcm loc-layout [OPTIONS] [TARGET ...]</code></dd>
+
+ <dt>Description</dt>
+
+ <dd>
+ <p>Parse the URL of a FCM/Subversion <var>TARGET</var>, and print its FCM
+ layout information.</p>
+
+ <p>If no argument is specified, <var>TARGET</var> is the current working
+ directory.</p>
+
+ <p>See also <a href="system_admin.html#svn_layout">System Administration
+ > Subversion > Repository Layout</a>.</p>
+ </dd>
+
+ <dt>Options</dt>
+
+ <dd>
+ <dl>
+ <dt><code>--verbose</code>, <code>-v</code></dt>
+
+ <dd>Increase verbosity.</dd>
+ </dl>
+ </dd>
+ </dl>
+
+ <h2 id="fcm-make">fcm make</h2>
+
+ <dl>
+ <dt>Usage</dt>
+
+ <dd><code>fcm make [OPTIONS] [DECLARATION ...]</code></dd>
+
+ <dt>Description</dt>
+
+ <dd>
+ <p><code>fcm make</code> invokes the FCM make system, which is used to
+ run the extract and build systems and other utilities.</p>
+
+ <p>For further details, please refer to the chapter on <a href=
+ "make.html">FCM Make</a>.</p>
+ </dd>
+
+ <dt>Arguments</dt>
+
+ <dd>
+ <p>Each argument is considered to be a declaration line to append to the
+ configuration file.</p>
+ </dd>
+
+ <dt>Options</dt>
+
+ <dd>
+ <dl>
+ <dt><code>--config-file-path=PATH, -F PATH</code></dt>
+
+ <dd>Specifies paths for searching configuration files specified in
+ relative paths.</dd>
+
+ <dt><code>--config-file=PATH, --file=PATH, -f PATH</code></dt>
+
+ <dd>Specifies paths to the configuration files either as a URL or a
+ pathname. (default=<samp>fcm-make.cfg</samp> in the current working
+ directory)</dd>
+
+ <dt><code>--directory=PATH, -C PATH</code></dt>
+
+ <dd>Specifies the path to the destination. (default=<samp>$PWD</samp>
+ or whatever is specified in the <code>dest</code> setting in the
+ configuration file)</dd>
+
+ <dt><code>--ignore-lock</code></dt>
+
+ <dd>Ignores lock file. When the system is invoked, it sets up a lock
+ file in the destination. The lock is normally removed when the system
+ completes the make. While the lock file is in place, another make invoked
+ in the same destination will fail. This option can be used to bypass
+ this check.</dd>
+
+ <dt><code>--jobs=N, -j N</code></dt>
+
+ <dd>Specifies the number of (child) processes that can be run
+ simultaneously.</dd>
+
+ <dt><code>--new</code></dt>
+
+ <dd>Removes items in the destination created by the previous make, and
+ starts a new make.</dd>
+
+ <dt><code>--quiet, -q</code></dt>
+
+ <dd>Decreases the verbosity level.</dd>
+
+ <dt><code>--verbose, -v</code></dt>
+
+ <dd>Increases the verbosity level.</dd>
+ </dl>
+ </dd>
+ </dl>
+
+ <h2 id="fcm-merge">fcm merge</h2>
+
+ <dl>
+ <dt>Usage</dt>
+
+ <dd><code>fcm merge [OPTIONS] SOURCE</code><br />
+ <code>fcm merge --custom --revision N[:M] [OPTIONS] SOURCE</code><br />
+ <code>fcm merge --custom [OPTIONS] URL1[@REV1] URL2[@REV2]</code><br />
+ <code>fcm merge --reverse --revision [M:]N [OPTIONS]</code></dd>
+
+ <dt>Description</dt>
+
+ <dd>
+ <p><code>fcm merge</code> allows you to merge changes from a source into
+ your working copy.</p>
+
+ <p>Before it begins, the command does the following:</p>
+
+ <ul>
+ <li>If a <var>SOURCE</var> or <var>URL</var> is specified, it can be a
+ full URL or a partial URL starting at the branches, trunk or tags
+ level.
+
+ <ul>
+ <li>If a partial URL is given, and the path name does not begin
+ with <samp>trunk</samp>, <samp>tags</samp> or <samp>branches</samp>
+ then <samp>branches/</samp> is automatically added to the beginning
+ of your path.</li>
+ </ul>
+ </li>
+
+ <li>It determines the <var>TARGET</var> URL by examining your working
+ copy.</li>
+
+ <li>If the current directory is not the top of your working copy, it
+ changes the current directory to the top of your working copy.</li>
+
+ <li>If your working copy is not pointing to a branch of a project
+ managed by FCM, the command aborts with an error.</li>
+
+ <li>If you do not specify the <code>--non-interactive</code> option, it
+ checks for any local modifications in your working copy. If it finds
+ any it reports them and asks you to confirm that you wish to continue
+ (it aborts if not).</li>
+ </ul>
+
+ <dl>
+ <dt>Automatic mode (i.e. neither <code>--custom</code> nor
+ <code>--reverse</code> is specified)</dt>
+
+ <dd>
+ <p>Automatic merges are used to merge changes between two directly
+ related branches, (i.e. the branches must either be created from the
+ same parent or have a parent/child relationship). These merges are
+ tracked by FCM and can be used by subsequent FCM commands. The merge
+ delta is calculated by doing the following:</p>
+
+ <ul>
+ <li>It checks that the <var>SOURCE</var> and <var>TARGET</var> are
+ directly related.</li>
+
+ <li>It determines the base revision and path of the <em>common
+ ancestor</em> of the <var>SOURCE</var> and <var>TARGET</var>.</li>
+
+ <li>The base revision and path are adjusted to account for any
+ merges from the <var>SOURCE</var> to the <var>TARGET</var> or
+ vice-versa.</li>
+
+ <li>It reports the revisions from <var>SOURCE</var> available for
+ merging into <var>TARGET</var>. If the <code>--verbose</code>
+ option is set, it prints the log for these revisions. It aborts if
+ no revision is available for merging.</li>
+
+ <li>If there are 2 or more revisions available for merging and you
+ do not specify the <code>--non-interactive</code> target, it asks
+ you which revision of the <var>SOURCE</var> you wish to merge from.
+ The default is the last changed revision of the <var>SOURCE</var>.
+ The merge delta is between the base and the specified revision of
+ the <var>SOURCE</var>.</li>
+
+ <li>If your working copy is a sub-tree of the <var>TARGET</var>, it
+ ensures that the <var>SOURCE</var> contains only changes in the
+ same sub-tree. Otherwise, the merge is unsafe, and the command will
+ abort with an error.
+
+ <p>N.B.: The command looks for changes in the <var>SOURCE</var>
+ by going through the list of changed files since the
+ <var>SOURCE</var> was last merged into the <var>TARGET</var>. (If
+ there is no previous merge from SOURCE to <var>TARGET</var>, the
+ common ancestor is used.) It is worth noting that there are
+ situations when the command will regard your merge as
+ <em>unsafe</em> (and so will fail incorrectly) even if the
+ changes in the <var>SOURCE</var> outside of the current sub-tree
+ will result in a null merge. This can happen if the changes are
+ the results of a previous merge from the <var>TARGET</var> to the
+ <var>SOURCE</var> or if these changes have been reversed. In such
+ case, you will have to perform your merge in a working copy of a
+ full tree.</p>
+ </li>
+ </ul>
+ </dd>
+
+ <dt>Custom mode (i.e. <code>--custom</code> is specified)</dt>
+
+ <dd>
+ <p>The custom mode is useful if you need to merge changes selectively
+ from another branch. The custom mode can be used in two forms:</p>
+
+ <ul>
+ <li>In the first form, you must specify a <var>SOURCE</var> as well
+ as a revision (range) using the <code>--revision</code> option. If
+ you specify a single revision <var>N</var>, the merge delta is
+ between revision <var>N - 1</var> and revision <var>N</var> of the
+ SOURCE. Otherwise, the merge delta is between revision <var>N</var>
+ and revision <var>M</var>, where <var>N</var> <
+ <var>M</var>.</li>
+
+ <li>In the second form, you must specify two URLs. The merge delta
+ is simply between the two URLs. (For each URL, if you do not
+ specify a peg revision, the command will peg the URL with its last
+ changed revision.)</li>
+ </ul>
+
+ <p>N.B. Unlike automatic merges, custom merges are not tracked or
+ used by subsequent FCM <code>diff</code> or <code>merge</code>
+ commands, (although <code>branch-info</code> can be set to report
+ them). Custom merges are always allowed, even if your working copy is
+ pointing to a sub-tree of a branch. However, there is no checking
+ mechanism to ensure the safety of your sub-tree custom merge so you
+ should only do this if you are confident it is what you want.
+ Therefore, it is recommended that you use automatic merges where
+ possible, and use custom merges only if you know what you are
+ doing.</p>
+ </dd>
+
+ <dt>Reverse mode (i.e. <code>--reverse</code> is specified)</dt>
+
+ <dd>
+ <p>The reverse mode is useful if you need to reverse a changeset (or
+ a range of changesets) in the current source of the working copy. In
+ this mode, you must specify a revision (range) using the
+ <code>--revision</code> option. If you specify a single revision
+ <var>N</var>, the merge delta is between revision <var>N</var> and
+ revision <var>N - 1</var> of the current branch. Otherwise, the merge
+ delta is between revision <var>M</var> and revision <var>N</var>,
+ where <var>M</var> > <var>N</var>.</p>
+
+ <p>N.B. Like custom merges, reverse merges are not tracked or used by
+ subsequent FCM <code>diff</code> or <code>merge</code> commands,
+ (although <code>branch-info</code> can be set to report them).
+ Likewise, reverse merges in sub-trees are always allowed, although
+ there is no checking mechanism to ensure the safety of your sub-tree
+ reverse merge.</p>
+ </dd>
+ </dl>
+
+ <p>Once the merge delta is determined, the command performs the
+ following:</p>
+
+ <ul>
+ <li>If you set the <code>--dry-run</code> option or if you are running
+ in the interactive mode, it reports what changes will result from
+ performing this merge by calling <code>svn merge --dry-run</code>.
+
+ <ul>
+ <li>It prints the actual <code>svn merge --dry-run</code> command
+ if the <code>--verbose</code> option is specified.</li>
+
+ <li>If you specify the <code>--dry-run</code> option, it exits
+ after reporting what changes will result from performing the
+ merge.</li>
+ </ul>
+ </li>
+
+ <li>If you are running in the interactive mode, it asks if you want to
+ go ahead with the merge (it aborts if not).</li>
+
+ <li>It performs the merge by calling <code>svn merge</code> to apply
+ the delta between the base and the <var>SOURCE</var> on your working
+ copy.
+
+ <ul>
+ <li>It prints the actual <code>svn merge</code> command if the
+ <code>--verbose</code> option is specified.</li>
+ </ul>
+ </li>
+
+ <li>It adds a standard template into the commit message to provide
+ details of the merge. The template is written below the line that says
+ <samp>--FCM message (will be inserted automatically)--</samp>. The
+ <a href="fcm-commit">fcm commit</a> command will detect the existence
+ of the template, so that you will not be able to alter it by accident.
+
+ <ul>
+ <li>The commit message is stored in the file
+ <samp>#commit_message#</samp> in the top level of your working
+ copy. It is created by the merge command if it does not already
+ exist.</li>
+
+ <li>If the <code>--auto-log</code> option is specified in the
+ automatic mode, it adds the log messages of the merged revisions as
+ well as the standard template.</li>
+ </ul>
+ </li>
+ </ul>
+
+ <p>For further details refer to the section <a href=
+ "code_management.html#svn_branching_merge">Merging</a>.</p>
+ </dd>
+
+ <dt>Options</dt>
+
+ <dd>
+ <dl>
+ <dt><code>--auto-log</code></dt>
+
+ <dd>In automatic mode, adds the log messages of the merged revisions in
+ the commit log. Has no effect in other merge modes.</dd>
+
+ <dt><code>--dry-run</code></dt>
+
+ <dd>Tries operation but make no changes.</dd>
+
+ <dt><code>--non-interactive</code></dt>
+
+ <dd>Tells the system not to prompt for anything.</dd>
+
+ <dt><code>--revision=REV</code>, <code>-r REV</code></dt>
+
+ <dd>Specifies a revision or a revision range.</dd>
+
+ <dt><code>--verbose</code>, <code>-v</code></dt>
+
+ <dd>Prints extra information.</dd>
+ </dl>
+ </dd>
+ </dl>
+
+ <h2 id="fcm-mkpatch">fcm mkpatch</h2>
+
+ <dl>
+ <dt>Usage</dt>
+
+ <dd><code>fcm mkpatch [OPTIONS] URL [OUTDIR]</code></dd>
+
+ <dt>Description</dt>
+
+ <dd>
+ <p><code>fcm mkpatch</code> creates patches from the specified revisions
+ of the specified <var>URL</var>, which must be a branch URL of a valid
+ FCM project. If the <var>URL</var> is a sub-directory of a branch, it
+ will use the root of the branch.</p>
+
+ <p>If <var>OUTDIR</var> is specified, the output is sent to
+ <var>OUTDIR</var>. Otherwise, the output will be sent to a default
+ location in the current directory (<samp>$PWD/fcm-mkpatch-out/</samp>).
+ The output directory will contain the patch for each revision as well as
+ a script for importing the patch.</p>
+
+ <p>Within the output directory are the <em>patches</em> and the log
+ message file for each revision. It also contains a generated script
+ <code>fcm-import-patch</code> for importing the patches. The user of the
+ script can invoke the script with either a URL or a working copy
+ argument, and the script will attempt to import the patches into the
+ given URL or working copy.</p>
+
+ <p>It is worth noting that changes in Subversion properties, including
+ changes in executable permissions, are not handled by the import
+ script.</p>
+ </dd>
+
+ <dt>Options</dt>
+
+ <dd>
+ <dl>
+ <dt><code>--exclude=PATH</code></dt>
+
+ <dd>Excludes a path in the URL. The specified path must be a relative
+ path of the URL. Glob patterns such as <code>*</code> and
+ <code>?</code> are acceptable. Changes in an excluded path will not be
+ considered in the patch. A changeset containing changes only in the
+ excluded path will not be considered at all. Multiple paths can be
+ specified by using a colon-separated list of paths, or by specifying
+ this option multiple times.</dd>
+
+ <dt><code>--organisation=NAME</code></dt>
+
+ <dd>Specifies the name of your organisation. The command will attempt
+ to parse the commit log message for each revision in the patch. It will
+ remove all merge templates, replace Trac links with a modified string,
+ and add information about the original changeset. If you specify the
+ name of your organisation, it will replace Trac links such as
+ <samp>ticket:123</samp> with <samp>$organisation_ticket:123</samp>, and
+ report the orginal changeset with a message such as
+ <samp>$organisation_changeset:1000</samp>. If the organisation name is
+ not specified then it defaults to <samp>original</samp>.</dd>
+
+ <dt><code>--revision=REV</code>, <code>-r REV</code></dt>
+
+ <dd>Specifies a revision or a revision range, at which the patch
+ will be based on. If a revision is not specified, it will attempt to
+ create a patch based on the changes at the HEAD revision. If a revision
+ range is specified, it will attempt to create a patch for each revision
+ in that range (including the change in the lower range) where changes
+ have taken place in the URL. No output will be written if there is no
+ change in the given revision (range).</dd>
+ </dl>
+ </dd>
+ </dl>
+
+ <h2 id="fcm-project-create">fcm project-create</h2>
+
+ <dl>
+ <dt>Usage</dt>
+
+ <dd><code>fcm project-create [OPTIONS] PROJECT-NAME REPOS-ROOT-URL</code></dd>
+
+ <dt>Description</dt>
+
+ <dd>
+ <p>Create a new project and its trunk directory in a repository.</p>
+
+ <p>If you do not specify the <code>--non-interactive</code> option, it
+ starts an editor (using a similar convention as <a href=
+ "#fcm-commit">commit</a>) to allow you to add further comment to the
+ commit log message. A standard commit log template and change summary
+ is provided for you below the line that says <samp>--Add your commit
+ message ABOVE - do not alter this line or those below--</samp>. If you
+ need to add any extra message to the log, please do so
+ <strong>above</strong> this line. When you exit the editor, the command
+ will report the commit log before prompting for confirmation that you
+ wish to proceed (it aborts if not).</p>
+ </dd>
+
+ <dt>Options</dt>
+ <dd>
+ <dl>
+ <dt><code>--non-interactive</code></dt>
+
+ <dd>Tells the system not to prompt for anything. (The
+ <code>--svn-non-interactive</code> option is set automatically when you
+ specify <code>--non-interactive</code>.)</dd>
+
+ <dt><code>--password=PASSWORD</code></dt>
+
+ <dd>Specifies the password for authentication.</dd>
+
+ <dt><code>--svn-non-interactive</code></dt>
+
+ <dd>Tells the system to run <code>svn</code> in non-interactive
+ mode.</dd>
+ </dl>
+ </dd>
+ </dl>
+
+ <h2 id="fcm-switch">fcm switch</h2>
+
+ <dl>
+ <dt>Usage</dt>
+
+ <dd><code>fcm switch [OPTIONS] URL[@REV1] [PATH]</code><br />
+ <code>fcm switch --relocate [OPTIONS] FROM TO [PATH]</code></dd>
+
+ <dt>Description</dt>
+
+ <dd>
+ <p><code>fcm switch</code> supports the arguments and alternate names
+ supported by <code>svn switch</code>. If <code>--relocate</code> is
+ specified, it supports all options supported by <code>svn switch</code>.
+ Otherwise, it supports <code>--non-interactive</code>,
+ <code>--revision=REV</code> (<code>-r REV</code>) and
+ <code>--quiet</code> (<code>-q</code> only. (Please refer to the <a href=
+ "http://svnbook.red-bean.com/en/1.8/svn.branchmerge.switchwc.html">Subversion
+ book</a> for details).</p>
+
+ <p>If <code>--relocate</code> is specified, FCM will pass the options and
+ arguments directly to the corresponding Subversion command. Otherwise,
+ FCM will ensure that your working copy switches safely through the
+ following actions:</p>
+
+ <ul>
+ <li>If <var>PATH</var> (or the current working directory if
+ <var>PATH</var> is not specified) is not at the top of a working copy,
+ the command will automatically search for the top of the working copy,
+ and the switch command will always apply recursively from that
+ level.</li>
+
+ <li>You can specify only the <em>branch</em> part of the URL, such as
+ <samp>trunk</samp>, <samp>branches/dev/fred/r1234_bob</samp> or even
+ <samp>dev/fred/r1234_bob</samp> and the command will work out the full
+ URL for you.</li>
+
+ <li>If you do not specify the <code>--non-interactive</code> option, it
+ checks for any local modifications in your working copy. If it finds
+ any it reports them and asks you to confirm that you wish to continue
+ (it aborts if not).</li>
+
+ <li>If you have some template messages in the
+ <samp>#commit_message#</samp> file in the top level of your working
+ copy, (e.g. after you have performed a merge), the command will report
+ an error. You should remove the template message manually from the
+ <samp>#commit_message#</samp> file before re-running
+ <code>switch</code>.</li>
+
+ <li>The command will analyse the current working copy URL and the
+ specified URL to ensure that they are in the same project. If your
+ working copy is a sub-tree of a project, the command will assume that
+ you want the same sub-tree in the new URL.</li>
+ </ul>
+
+ <p>For further details refer to the section <a href=
+ "code_management.html#svn_branching_switch">Switching your working copy
+ to point to another branch</a>.</p>
+ </dd>
+ </dl>
+
+ <h2 id="fcm-update">fcm update</h2>
+
+ <dl>
+ <dt>Usage</dt>
+
+ <dd><code>fcm update [OPTIONS] [PATH ...]</code></dd>
+
+ <dt>Description</dt>
+
+ <dd>
+ <p><code>fcm update</code> supports the arguments and alternate names
+ supported by <code>svn update</code>. It supports the options
+ <code>--non-interactive</code>, <code>--revision=REV</code> (<code>-r
+ REV</code>) and <code>--quiet</code> (<code>-q</code>) only. (Please
+ refer to the <a href=
+ "http://svnbook.red-bean.com/en/1.8/svn.ref.svn.c.update.html">Subversion
+ book</a> for details).</p>
+
+ <p>FCM will ensure that your working copies updates safely through the
+ following actions:</p>
+
+ <ul>
+ <li>If <var>PATH</var> (or the current working directory if
+ <var>PATH</var> is not specified) is not at the top of a working copy,
+ the command will automatically search for the top of the working copy,
+ and the update command will always apply recursively from that
+ level.</li>
+
+ <li>If you do not specify the <code>--non-interactive</code> option, it
+ uses <code>svn status --show-updates</code> to display what will be
+ updated in your working copies and to check for local modifications (if
+ you specify <code>--revision=REV</code> (<code>-r REV</code> then it
+ just uses <code>svn status</code>). If it finds any it reports them and
+ asks you to confirm that you wish to continue (it aborts if not).</li>
+ </ul>
+ </dd>
+ </dl>
+
+ <h2 id="fcm-version">fcm version</h2>
+
+ <dl>
+ <dt>Usage</dt>
+
+ <dd><code>fcm version</code></dd>
+
+ <dt>Description</dt>
+
+ <dd>
+ <p>Print FCM version string.</p>
+ </dd>
+
+ <dt>Alternate Names</dt>
+
+ <dd>--version, -V</dd>
+ </dl>
+
+ <h2 id="svn">Other Subversion Commands</h2>
+
+ <p>Other <code>svn</code> commands are supported by <code>fcm</code> with the
+ following minor enhancements:</p>
+
+ <ul>
+ <li>Where appropriate, FCM performs repository and revision keywords
+ expansion.</li>
+
+ <li>The <code>fcm checkout</code> command fails if you attempt to checkout
+ into an existing working copy.</li>
+ </ul>
+
+ <p>The following is a list of the commands:</p>
+
+ <ul>
+ <li><a href=
+ "http://svnbook.red-bean.com/en/1.8/svn.ref.svn.c.blame.html">svn
+ blame</a></li>
+
+ <li><a href="http://svnbook.red-bean.com/en/1.8/svn.ref.svn.c.cat.html">svn
+ cat</a></li>
+
+ <li><a href=
+ "http://svnbook.red-bean.com/en/1.8/svn.ref.svn.c.checkout.html">svn
+ checkout</a></li>
+
+ <li><a href=
+ "http://svnbook.red-bean.com/en/1.8/svn.ref.svn.c.cleanup.html">svn
+ cleanup</a></li>
+
+ <li><a href=
+ "http://svnbook.red-bean.com/en/1.8/svn.ref.svn.c.copy.html">svn
+ copy</a></li>
+
+ <li><a href=
+ "http://svnbook.red-bean.com/en/1.8/svn.ref.svn.c.export.html">svn
+ export</a></li>
+
+ <li><a href=
+ "http://svnbook.red-bean.com/en/1.8/svn.ref.svn.c.import.html">svn
+ import</a></li>
+
+ <li><a href=
+ "http://svnbook.red-bean.com/en/1.8/svn.ref.svn.c.info.html">svn
+ info</a></li>
+
+ <li><a href=
+ "http://svnbook.red-bean.com/en/1.8/svn.ref.svn.c.list.html">svn
+ list</a></li>
+
+ <li><a href=
+ "http://svnbook.red-bean.com/en/1.8/svn.ref.svn.c.lock.html">svn
+ lock</a></li>
+
+ <li><a href="http://svnbook.red-bean.com/en/1.8/svn.ref.svn.c.log.html">svn
+ log</a></li>
+
+ <li><a href=
+ "http://svnbook.red-bean.com/en/1.8/svn.ref.svn.c.mergeinfo.html">svn
+ mergeinfo</a></li>
+
+ <li><a href=
+ "http://svnbook.red-bean.com/en/1.8/svn.ref.svn.c.mkdir.html">svn
+ mkdir</a></li>
+
+ <li><a href=
+ "http://svnbook.red-bean.com/en/1.8/svn.ref.svn.c.move.html">svn
+ move</a></li>
+
+ <li><a href=
+ "http://svnbook.red-bean.com/en/1.8/svn.ref.svn.c.patch.html">svn
+ patch</a></li>
+
+ <li><a href=
+ "http://svnbook.red-bean.com/en/1.8/svn.ref.svn.c.propdel.html">svn
+ propdel</a></li>
+
+ <li><a href=
+ "http://svnbook.red-bean.com/en/1.8/svn.ref.svn.c.propedit.html">svn
+ propedit</a></li>
+
+ <li><a href=
+ "http://svnbook.red-bean.com/en/1.8/svn.ref.svn.c.propget.html">svn
+ propget</a></li>
+
+ <li><a href=
+ "http://svnbook.red-bean.com/en/1.8/svn.ref.svn.c.proplist.html">svn
+ proplist</a></li>
+
+ <li><a href=
+ "http://svnbook.red-bean.com/en/1.8/svn.ref.svn.c.propset.html">svn
+ propset</a></li>
+
+ <li><a href=
+ "http://svnbook.red-bean.com/en/1.8/svn.ref.svn.c.relocate.html">svn
+ relocate</a></li>
+
+ <li><a href=
+ "http://svnbook.red-bean.com/en/1.8/svn.ref.svn.c.resolve.html">svn
+ resolve</a></li>
+
+ <li><a href=
+ "http://svnbook.red-bean.com/en/1.8/svn.ref.svn.c.resolved.html">svn
+ resolved</a></li>
+
+ <li><a href=
+ "http://svnbook.red-bean.com/en/1.8/svn.ref.svn.c.revert.html">svn
+ revert</a></li>
+
+ <li><a href=
+ "http://svnbook.red-bean.com/en/1.8/svn.ref.svn.c.status.html">svn
+ status</a></li>
+
+ <li><a href=
+ "http://svnbook.red-bean.com/en/1.8/svn.ref.svn.c.unlock.html">svn
+ unlock</a></li>
+
+ <li><a href=
+ "http://svnbook.red-bean.com/en/1.8/svn.ref.svn.c.upgrade.html">svn
+ upgrade</a></li>
+ </ul>
+
+ <p>Please refer to the <a href=
+ "http://svnbook.red-bean.com/en/1.8/svn.ref.html">Subversion Complete
+ Reference</a> in the Subversion book for details of these commands.</p>
+
+ </div>
+ </div>
+ </div>
+
+ <hr/>
+ <div class="container-fluid text-center">
+ <div class="row-fluid"><div class="span12">
+ <address><small>
+ © British Crown Copyright 2006-14
+ <a href="http://www.metoffice.gov.uk">Met Office</a>.
+ See <a href="../etc/fcm-terms-of-use.html">Terms of Use</a>.<br />
+ This document is released under the British <a href=
+ "http://www.nationalarchives.gov.uk/doc/open-government-licence/" rel=
+ "license">Open Government Licence</a>.<br />
+ </small></address>
+ </div></div>
+ </div>
+
+ <script type="text/javascript" src="../etc/jquery.min.js"></script>
+ <script type="text/javascript" src="../etc/bootstrap/js/bootstrap.min.js"></script>
+ <script type="text/javascript" src="../etc/fcm.js"></script>
+ <script type="text/javascript" src="../etc/fcm-version.js"></script>
+</body>
+</html>
diff --git a/doc/user_guide/create_branch.png b/doc/user_guide/create_branch.png
new file mode 100644
index 0000000..8de76cd
Binary files /dev/null and b/doc/user_guide/create_branch.png differ
diff --git a/doc/user_guide/extract.html b/doc/user_guide/extract.html
new file mode 100644
index 0000000..ca04f0e
--- /dev/null
+++ b/doc/user_guide/extract.html
@@ -0,0 +1,1171 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>FCM: User Guide: Annex: The FCM 1 Extract System</title>
+ <meta name="author" content="FCM team" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link rel="icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link rel="shortcut icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link href="../etc/bootstrap/css/bootstrap.min.css" rel="stylesheet" media="screen" />
+ <link href="../etc/fcm.css" rel="stylesheet" media="screen" />
+</head>
+<body>
+ <div class="navbar navbar-inverse">
+ <div class="navbar-inner">
+ <a class="brand" href=".."><span class="fcm-version">FCM</span></a>
+ <ul class="nav">
+ <li><a href="../installation/">Installation</a></li>
+
+ <li class="active"><a href=".">User Guide</a></li>
+ </ul>
+ </div>
+ </div>
+
+ <div class="page-header">
+ <div class="fcm-page-content pull-right well well-small"></div>
+ <h1>FCM: User Guide: Annex: The FCM 1 Extract System</h1>
+ </div>
+
+ <div class="container">
+ <div class="row">
+ <div class="span12">
+
+ <h2 id="introduction">Introduction</h2>
+
+ <p><em>The FCM 1 extract system is deprecated. The documentation for the
+ current extract system can be found at <a href="make.html">FCM
+ Make</a>.</em></p>
+
+ <p>The extract system provides an interface between the revision control
+ system (currently Subversion) and the build system. Where appropriate, it
+ extracts code from the repository and other user-defined locations to a
+ directory tree suitable for feeding into the build system. In this chapter,
+ we shall use many examples to explain how to use the extract system. At the
+ end of this chapter, you will be able to extract code from the local file
+ system as well as from different branches of different repository URLs. You
+ will also learn how to mirror code to an alternate destination. Finally, you
+ will be given an introduction on how to specify configurations for the build
+ system via the extract configuration file. (For further information on the
+ build system, please see the next chapter <a href="build.html">The Build
+ System</a>.) The last section of the chapter tells you what you can do in the
+ case when Subversion is not available.</p>
+
+ <h2 id="command">The Extract Command</h2>
+
+ <p>To invoke the extract system, simply issue the command:</p>
+ <pre>
+fcm extract
+</pre>
+
+ <p>By default, the extract system searches for an extract configuration file
+ <samp>ext.cfg</samp> in <samp>$PWD</samp> and then <samp>$PWD/cfg</samp>. If
+ an extract configuration file is not found in these directories, the command
+ fails with an error. If an extract configuration file is found, the system
+ will use the configuration specified in the file to perform the current
+ extract.</p>
+
+ <p>If the destination of the extract does not exist, the system performs a
+ new full extract to the destination. If a previous extract already exists at
+ the destination, the system performs an incremental extract, updating any
+ modifications if necessary. If a full (fresh) extract is required for
+ whatever reason, you can invoke the extract system using the <code>-f</code>
+ option, (i.e. the command becomes <code>fcm extract -f</code>). If you simply
+ want to remove all the items generated by a previous extract in the
+ destination, you can invoke the extract system using the <code>--clean</code>
+ option.</p>
+
+ <p>For further information on the extract command, please see <a href=
+ "command_ref.html#fcm-extract">FCM Command Reference > fcm extract</a>.</p>
+
+ <h2 id="simple">Simple Usage</h2>
+
+ <p>The extract configuration file is the main user interface of the extract
+ system. It is a line based text file. For a complete set of extract
+ configuration file declarations, please refer to the <a href=
+ "annex_ext_cfg.html">Annex: Declarations in FCM extract configuration
+ file</a>.</p>
+
+ <h3 id="simple_local">Extract from a local path</h3>
+
+ <p>A simple example of a basic extract configuration file is given below:</p>
+ <pre id="example_1">
+# Example 1
+# ----------------------------------------------------------------------
+cfg::type ext # line 1
+cfg::version 1.0 # line 2
+ # line 3
+dest $PWD # line 4
+ # line 5
+repos::var::user $HOME/var # line 6
+ # line 7
+expsrc::var::user code # line 8
+</pre>
+
+ <p>The above demonstrates how to use the extract system to extract code from
+ a local user directory. Here is an explanation of what each line does:</p>
+
+ <ul>
+ <li><dfn>line 1</dfn>: the label <code>CFG::TYPE</code> declares the type
+ of the configuration file. The value <samp>ext</samp> tells the system that
+ it is an extract configuration file.</li>
+
+ <li><dfn>line 2</dfn>: the label <code>CFG::VERSION</code> declares the
+ version of the extract configuration file. The current default is
+ <samp>1.0</samp>. Although it is not currently used, if we have to change
+ the format of the configuration file at a later stage, we shall be able to
+ use this number to determine whether we are reading a file with an older
+ format or one with a newer format.</li>
+
+ <li><dfn>line 3</dfn>: a blank line or a line beginning with a
+ <code>#</code> is a comment, and is ignored by the interpreter.</li>
+
+ <li><dfn>line 4</dfn>: the label <code>DEST</code> declares the destination
+ root directory of this extract. The value <samp>$PWD</samp> expands to the
+ current working directory.</li>
+
+ <li><dfn>line 5</dfn>: comment line, ignored.</li>
+
+ <li><dfn>line 6</dfn>: the label
+ <code>REPOS::<pck>::<branch></code> declares the top level URL
+ or path of a repository. The package name of the repository is given by
+ <pck>. In our example, we choose <samp>var</samp> as the name of the
+ package. (You can choose any name you like, however, it is usually sensible
+ to use a package name that matches the name of the project or system you
+ are working with.) The branch name in the repository is given by
+ <branch>. (Again, you can choose any name you like, however, it is
+ usually sensible to use a name such as <samp>base</samp>, <samp>user</samp>
+ or something that matches your branch name.) In our example, the word
+ <samp>user</samp> is normally used to denote a local user directory. Hence
+ the statement declares that the repository path for the <samp>var</samp>
+ package in the <samp>user</samp> branch can be found at
+ <samp>$HOME/var</samp>.</li>
+
+ <li><dfn>line 7</dfn>: comment line, ignored.</li>
+
+ <li><dfn>line 8</dfn>: the label
+ <code>EXPSRC::<pck>::<branch></code> declares an
+ <em>expandable</em> source directory for the package <pck> in the
+ branch <branch>. In our example, the package name is
+ <samp>var</samp>, and the branch name is <samp>user</samp>. These match the
+ package and the branch names of the repository declaration in line 6. It
+ means that the source directory declaration is associated with the path
+ <samp>$HOME/var</samp>. The value of the declaration <samp>code</samp> is
+ therefore a sub-directory under <samp>$HOME/var</samp>. By declaring a
+ source directory using an <code>EXPSRC</code> label, the system
+ automatically searches for all sub-directories (recursively) under the
+ declared source directory.</li>
+ </ul>
+
+ <p>Invoking the extract system using the above configuration file will
+ extract all sub-directories under <samp>$HOME/var/code</samp> to
+ <samp>$PWD/src/var/code</samp>. Note: the extract system ignores symbolic
+ links and hidden files, (i.e. file names beginning with a <samp>.</samp>). It
+ will write a build configuration file to <samp>$PWD/cfg/bld.cfg</samp>. The
+ configuration used for this extract will be written to the configuration file
+ at <samp>$PWD/cfg/ext.cfg</samp>.</p>
+
+ <dl>
+ <dt>Note - incremental extract</dt>
+
+ <dd>Suppose you have already performed an extract using the above
+ configuration file. At a later time, you have made some changes to some of
+ the files in the source directory. Re-running the extract system on the
+ same configuration will trigger an incremental extract. In an incremental
+ extract, the system will update only those files that are modified. If the
+ last modified time (or last commit revision) of a source file in the
+ current extract differs from that in the previous extract, the system will
+ attempt a content comparison. The system updates the destination only if
+ the content and/or file access permission of the source differs from that
+ of the destination.</dd>
+ </dl>
+
+ <h3 id="simple_url">Extract from a Subversion URL</h3>
+
+ <p>The next example demonstrates how to extract from a Subversion repository
+ URL:</p>
+ <pre id="example_2">
+# Example 2
+# ----------------------------------------------------------------------
+cfg::type ext # line 1
+cfg::version 1.0 # line 2
+ # line 3
+dest $PWD # line 4
+ # line 5
+repos::var::base svn://server/var/trunk # line 6
+revision::var::base 1234 # line 7
+ # line 8
+expsrc::var::base code # line 9
+</pre>
+
+ <ul>
+ <li><dfn>line 1-5</dfn>: same as <a href="#example_1">example 1</a>.</li>
+
+ <li><dfn>line 6</dfn>: the line declares the repository location of the
+ <samp>base</samp> branch of the <samp>var</samp> package to be the
+ Subversion URL <samp>svn://server/var/trunk</samp>.</li>
+
+ <li><dfn>line 7</dfn>: the label
+ <code>REVISION::<pck>::<branch></code> declares the revision of
+ the repository associated with the package <pck> in the branch
+ <branch>. The current line tells the extract system to use revision
+ 1234 of <samp>svn://server/var/trunk</samp>. It is worth noting that the
+ declared revision must be a revision when the declared branch exists. The
+ actual revision used is the last changed revision of the declared one. If
+ the revision is not declared, the default is to use the last changed
+ revision at the HEAD of the branch.</li>
+
+ <li><dfn>line 8</dfn>: comment line, ignored.</li>
+
+ <li><dfn>line 9</dfn>: the line declares an expandable source directory in
+ the repository <samp>svn://server/var/trunk</samp>.</li>
+ </ul>
+
+ <p>Invoking the extract system using the above configuration file will
+ extract all sub-directories under <samp>svn://server/var/trunk/code</samp> to
+ <samp>$PWD/src/var/code</samp>. It will write a build configuration file to
+ <samp>$PWD/cfg/bld.cfg</samp>. The configuration used for this extract will
+ be written to the configuration file at <samp>$PWD/cfg/ext.cfg</samp>.</p>
+
+ <dl>
+ <dt>EXPSRC or SRC?</dt>
+
+ <dd>
+ <p>So far, we have only declared source directories using the
+ <code>EXPSRC</code> statement, which stands for <em>expandable source
+ directory</em>. A source directory declared using this statement will
+ trigger the system to search recursively for any sub-directories under
+ the declared one. Any sub-directories containing regular source files
+ will be included in the extract. Symbolic links, hidden files and empty
+ directories (or those containing only symbolic links and/or hidden files)
+ are ignored.</p>
+
+ <p>If you do not want the system to search for sub-directories underneath
+ your declared source directory, you can declare your source directory
+ using the <code>SRC</code> statement. The <code>SRC</code> statement is
+ essentially the same as <code>EXPSRC</code> except that it does not
+ trigger the automatic recursive search for source directories. In fact,
+ the system implements the <code>EXPSRC</code> statement by expanding it
+ into a list of <code>SRC</code> statements.</p>
+ </dd>
+
+ <dt>Package and sub-package</dt>
+
+ <dd>
+ <p>The second field of a repository, revision or source directory
+ declaration label is the name of the container package. It is a name
+ selected by the user to identify the system or project he/she is working
+ on. (Therefore, it is often sensible to choose an identifier that matches
+ the name of the project or system.) The package name provides a unique
+ namespace for a file container. Source directories are automatically
+ arranged into sub-packages, using the names of the sub-directories as the
+ names of the sub-packages. For example, the declaration at line 9 in
+ <a href="#example_2">example 2</a> will put the source directory in the
+ <samp>var/code</samp> sub-package automatically.</p>
+
+ <p>Note that, in additional to slash <code>/</code>, double colon
+ <code>::</code> and double underscore <code>__</code> (internal only)
+ also act as delimiters for package names. Please avoid using them for
+ naming your files and directories.</p>
+
+ <p>You can declare a sub-package name explicitly in your source directory
+ statement. For example, the following two lines are equivalent:</p>
+ <pre>
+src::var::base code/VarMod_Surface
+src::var/code/VarMod_Surface::base code/VarMod_Surface
+</pre>
+
+ <p>Explicit sub-package declaration should not be used normally, as it
+ requires a lot more typing (although there are some situations where it
+ can be useful, e.g. if you need to re-define the package name).</p>
+
+ <p>Currently, the extract system only supports non-space characters in
+ the package name, as the space character is used as a delimiter between
+ the declaration label and its value. If there are spaces in the path name
+ to a file or directory, you should explicity re-define the package name
+ of that path to a package name with no space using the above method.
+ However, we recommend that only non-space characters are used for naming
+ directories and files to make life simpler.</p>
+ </dd>
+ </dl>
+
+ <dl>
+ <dt>The expanded extract configuration file</dt>
+
+ <dd>
+ <p>At the end of a successful extract, the configuration used by the
+ current extract is written in <samp>cfg/ext.cfg</samp> under the extract
+ destination root. This file is an <em>expanded</em> version of the
+ original, with changes in the following declarations:</p>
+
+ <ul>
+ <li>All revision keywords are converted into revision numbers.</li>
+
+ <li>If a revision is not defined for a repository, it is set to the
+ corresponding revision number of the HEAD revision.</li>
+
+ <li>All URL keywords are converted into the full URLs.</li>
+
+ <li>All <code>EXPSRC</code> declarations are expanded into
+ <code>SRC</code> declarations.</li>
+
+ <li>All other variables are expanded.</li>
+ </ul>
+
+ <p>With this file, it should be possible for a later extract to re-create
+ the current configuration even if the contents of the repository have
+ changed. (This applies only to code stored in the repository.)</p>
+ </dd>
+ </dl>
+
+ <h3 id="simple_mirror">Mirror code to an alternate location</h3>
+
+ <p>The next example demonstrates how to extract from a repository and mirror
+ the code to an alternate location. It is essentially the same as <a href=
+ "#example_2">example 2</a>, except that it has three new lines to describe
+ how the system can mirror the extracted code to an alternate location.</p>
+ <pre id="example_3">
+# Example 3
+# ----------------------------------------------------------------------
+cfg::type ext
+cfg::version 1.0
+
+dest $PWD
+
+rdest::machine tx01 # line 6
+rdest::logname frva # line 7
+rdest /scratch/frva/extract/example3 # line 8
+
+repos::var::base svn://server/var/trunk
+revision::var::base 1234
+
+expsrc::var::base code
+</pre>
+
+ <p>Here is an explanation of what each line does:</p>
+
+ <ul>
+ <li><dfn>line 6</dfn>: <code>RDEST::MACHINE</code> declares the target
+ machine to which the code will be mirrored. The example mirrors the code to
+ the machine named <samp>tx01</samp>.</li>
+
+ <li><dfn>line 7</dfn>: <code>RDEST::LOGNAME</code> declares the user name
+ of the target machine, to which the user has login access. If this is not
+ declared, the system uses the login name of the current user on the local
+ machine.</li>
+
+ <li><dfn>line 8</dfn>: <code>RDEST</code> declares the root directory of
+ the alternate destination, where the mirror version of the extract will be
+ sent.</li>
+ </ul>
+
+ <p>Invoking the extract system on the above configuration will trigger an
+ extract similar to that given in <a href="#example_2">example 2</a>, but it
+ will also attempt to mirror the contents at <samp>$PWD/src/var/code</samp> to
+ <samp>/scratch/frva/extract/example3/src</samp> on the alternate destination.
+ It will also mirror the expanded extract configuration file
+ <samp>$PWD/cfg/ext.cfg</samp> to
+ <samp>/scratch/frva/extract/example3/cfg/ext.cfg</samp> and
+ <samp>$PWD/cfg/bld.cfg</samp> to
+ <samp>/scratch/frva/extract/example3/cfg/bld.cfg</samp>. It is also worth
+ noting that the content of the build configuration file will be slightly
+ different, since it will include directory names appropriate for the
+ alternate destination.</p>
+
+ <dl>
+ <dt>Note - mirroring command</dt>
+
+ <dd>
+ <p>The extract system currently supports <code>rdist</code> and
+ <code>rsync</code> as its mirroring tool. The default is
+ <code>rsync</code>. To use <code>rdist</code> instead of
+ <code>rsync</code>, add the following line to your extract configuration
+ file:</p>
+ <pre>
+rdest::mirror_cmd rdist
+</pre>
+
+ <p>If <code>rsync</code> is used to mirror an extract, the system needs to
+ issue a separate remote shell command to create the container directory of
+ the mirror destination. The default is to issue a shell command in the
+ form <samp>ssh -n -oBatchMode=yes LOGNAME at MACHINE mkdir -p DEST</samp>.
+ The following declarations can be used to modify the command:</p>
+ <pre>
+# Examples using the default settings:
+rdest::rsh_mkdir_rsh ssh
+rdest::rsh_mkdir_rshflags -n -oBatchMode=yes
+rdest::rsh_mkdir_mkdir mkdir
+rdest::rsh_mkdir_mkdirflags -p
+</pre>
+
+ <p>In addition, the default <code>rsync</code> shell command is
+ <samp>rsync -a --exclude='.*' --delete-excluded --timeout=900 --rsh='ssh
+ -oBatchMode=yes' SOURCE DEST</samp>. The following declarations can be
+ used to modify the command:</p>
+ <pre>
+# Examples using the default settings:
+rdest::rsync rsync
+rdest::rsyncflags -a --exclude='.*' --delete-excluded --timeout=900 \
+ --rsh='ssh -oBatchMode=yes'
+</pre>
+ </dd>
+ </dl>
+
+ <h2 id="advanced">Advanced Usage</h2>
+
+ <h3 id="advanced_multi">Extract from multiple repositories</h3>
+
+ <p>So far, we have only extracted from a single location. The extract system
+ is not much use if that is the only thing it can do. In fact, the extract
+ system supports extract of multiple source directories from multiple branches
+ in multiple repositories. The following configuration file is an example of
+ how to extract from multiple repositories:</p>
+ <pre id="example_4">
+# Example 4
+# ----------------------------------------------------------------------
+cfg::type ext
+cfg::version 1.0
+
+dest $PWD
+
+repos::var::base fcm:var_tr # line 6
+repos::ops::base fcm:ops_tr # line 7
+repos::gen::base fcm:gen_tr # line 8
+
+revision::gen::base 2468 # line 10
+
+expsrc::var::base src/code # line 12
+expsrc::var::base src/scripts # line 13
+expsrc::ops::base src/code # line 14
+src::gen::base src/code/GenMod_Constants # line 15
+src::gen::base src/code/GenMod_Control # line 16
+src::gen::base src/code/GenMod_FortranIO # line 17
+src::gen::base src/code/GenMod_GetEnv # line 18
+src::gen::base src/code/GenMod_ModelIO # line 19
+src::gen::base src/code/GenMod_ObsInfo # line 20
+src::gen::base src/code/GenMod_Platform # line 21
+src::gen::base src/code/GenMod_Reporting # line 22
+src::gen::base src/code/GenMod_Trace # line 23
+src::gen::base src/code/GenMod_UMConstants # line 24
+src::gen::base src/code/GenMod_Utilities # line 25
+</pre>
+
+ <p>Here is an explanation of what each line does:</p>
+
+ <ul>
+ <li><dfn>line 6-8</dfn>: these lines declare the repositories for the
+ <samp>base</samp> branches of the <samp>var</samp>, <samp>ops</samp> and
+ <samp>gen</samp> packages respectively. It is worth noting that the values
+ of the declarations are no longer Subversion URLs but are FCM URL keywords.
+ These keywords are normally declared in the central configuration file of
+ the FCM system, and will be expanded into the corresponding Subversion URLs
+ by the FCM system. For further information on URL keywords, please see
+ <a href="code_management.html#svn_basic_keywords">Code Management System
+ > Using Subversion > Basic Command Line Usage > Repository &
+ Revision Keywords</a>.</li>
+
+ <li><dfn>line 10</dfn>: this line declares the revision number for the
+ <samp>base</samp> branch of the <samp>gen</samp> package, i.e. for the
+ <samp>fcm:gen_tr</samp> repository. It is worth noting that the revision
+ numbers for the <samp>var</samp> and <samp>ops</samp> packages have not
+ been declared. By default, their revision numbers will be set to the last
+ changed revision at the HEAD.</li>
+
+ <li><dfn>line 12-14</dfn>: these line declares the source directories for
+ the <samp>base</samp> branches of the <samp>var</samp> and <samp>ops</samp>
+ packages. For the <samp>var</samp> package, we are extracting everything
+ from the <samp>code</samp> and the <samp>scripts</samp> sub-directory. For
+ the <samp>ops</samp> package, we are extracting everything from the
+ <samp>code</samp> directory.</li>
+
+ <li><dfn>line 15-25</dfn>: these line declares the source directories for
+ the <samp>base</samp> branch of the <samp>gen</samp> package. The source
+ directories declared will not be searched for sub-directories underneath
+ the declared directories.</li>
+ </ul>
+
+ <p>We shall end up with a directory tree such as:</p>
+ <pre>
+$PWD
+ |
+ |--- cfg
+ | |
+ | |--- bld.cfg
+ | |--- ext.cfg
+ |
+ |--- src
+ |
+ |--- gen
+ | |
+ | |--- code
+ | |
+ | |--- GenMod_Constants
+ | |--- GenMod_Control
+ | |--- GenMod_FortranIO
+ | |--- GenMod_GetEnv
+ | |--- GenMod_ModelIO
+ | |--- GenMod_ObsInfo
+ | |--- GenMod_Platform
+ | |--- GenMod_Reporting
+ | |--- GenMod_Trace
+ | |--- GenMod_UMConstants
+ | |--- GenMod_Utilities
+ |
+ |--- ops
+ | |
+ | |--- code
+ | |
+ | |--- ...
+ |
+ |--- var
+ |
+ |--- code
+ | |
+ | |--- ...
+ |
+ |--- scripts
+ |
+ |--- ...
+</pre>
+
+ <dl>
+ <dt>Note - revision number</dt>
+
+ <dd>
+ <p>As seen in the above example, if a revision number is not specified
+ for a repository URL, it defaults to the last changed revision at the
+ HEAD of the branch. The revision number can also be declared in other
+ ways:</p>
+
+ <ul>
+ <li>Any revision arguments acceptable by Subversion are allowed. You
+ can use a valid revision number, a date between a pair of curly
+ brackets (e.g. <samp>{2005-05-01T12:00}</samp>) or the keyword HEAD.
+ However, please do not use the keywords BASE, COMMITTED or PREV as
+ these are reserved for working copy only.</li>
+
+ <li>FCM revision keywords are allowed. These must be defined for the
+ corresponding repository URLs in either the central or the user FCM
+ configuration file. For further information on revision keywords,
+ please see <a href="code_management.html#svn_basic_keywords">Code
+ Management > Using Subversion > Basic Command Line Usage >
+ Repository & Revision Keywords</a>.</li>
+
+ <li>Do not use the keyword USER, as it is used internally by the
+ extract system.</li>
+ </ul>
+
+ <p>If a revision number is specified for a branch, the actual revision
+ used by the extract system is the last changed revision of the branch,
+ which may differ from the declared revision. While this behaviour is
+ useful in most situations, some users may find it confusing to work with.
+ It is possible to alter this behaviour so that extract will fail if the
+ declared revision does not correspond to a changeset of the declared
+ branch. Make the following declaration to switch on this checking:</p>
+ <pre>
+revmatch true
+</pre>
+ </dd>
+ </dl>
+
+ <h3 id="advanced_branches">Extract from multiple branches</h3>
+
+ <p>We have so far dealt with a single branch in any package. The extract
+ system can be used to <em>combine</em> changes from different branches of a
+ package. An example is given below:</p>
+ <pre id="example_5">
+# Example 5
+# ----------------------------------------------------------------------
+cfg::type ext
+cfg::version 1.0
+
+dest $PWD
+
+repos::var::base fcm:var_tr
+repos::ops::base fcm:ops_tr
+repos::gen::base fcm:gen_tr
+
+revision::gen::base 2468
+
+expsrc::var::base src/code
+expsrc::var::base src/scripts
+expsrc::ops::base src/code
+src::gen::base src/code/GenMod_Constants
+src::gen::base src/code/GenMod_Control
+src::gen::base src/code/GenMod_FortranIO
+src::gen::base src/code/GenMod_GetEnv
+src::gen::base src/code/GenMod_ModelIO
+src::gen::base src/code/GenMod_ObsInfo
+src::gen::base src/code/GenMod_Platform
+src::gen::base src/code/GenMod_Reporting
+src::gen::base src/code/GenMod_Trace
+src::gen::base src/code/GenMod_UMConstants
+src::gen::base src/code/GenMod_Utilities
+
+repos::var::branch1 fcm:var_br/frva/r1234_new_stuff # line 27
+repos::var::branch2 fcm:var_br/frva/r1516_bug_fix # line 28
+repos::ops::branch1 fcm:ops_br/opsrc/r3188_good_stuff # line 29
+</pre>
+
+ <p>The configuration file in <a href="#example_5">example 5</a> is similar to
+ that of <a href="#example_4">example 4</a> except for the last three lines.
+ Here is an explanation of what they do:</p>
+
+ <ul>
+ <li>
+ <dfn>line 27</dfn>: this line declares a repository URL for the
+ <samp>branch1</samp> branch of the <samp>var</samp> package. From the URL
+ of the branch, we know that the branch was created by the user
+ <samp>frva</samp> based on the trunk at revision at 1234. The description
+ of the branch is <samp>branch1</samp>. The following points are worth
+ noting:
+
+ <ul>
+ <li>By declaring a new branch with the same package name to a
+ previously declared branch, it is assumed that both branches reside in
+ the same Subversion repository.</li>
+
+ <li>No revision is declared for this URL, so the default is used which
+ is the last changed revision at the HEAD of the branch.</li>
+
+ <li>No source directory is declared for this URL. By default, if no
+ source directory is declared for a branch repository, it will attempt
+ to use the same set of source directories as the first declared branch
+ of the package. In this case, the source directories declared for the
+ <samp>base</samp> branch of the <samp>var</samp> package will be
+ used.</li>
+ </ul>
+ </li>
+
+ <li><dfn>line 28</dfn>: this line declares another branch called
+ <samp>branch2</samp> for the <samp>var</samp> package. No source directory
+ is declared for this URL either, so it will use the same set of source
+ directories declared for the <samp>base</samp> branch.</li>
+
+ <li><dfn>line 29</dfn>: this line declares a branch called
+ <samp>branch1</samp> for the <samp>ops</samp> package. It will use the same
+ set of source directories declared for the <samp>ops</samp> package
+ <samp>base</samp> branch.</li>
+ </ul>
+
+ <p>When we invoke the extract system, it will attempt to extract from the
+ first declared branch of a package, if the last changed revision of the
+ source directory is the same in all the branches. However, if the last
+ changed revision of the source directory differs for different branches, the
+ system will attempt to obtain an extract priority list for each source
+ directory, using the following logic:</p>
+
+ <ol>
+ <li>The system looks for source directory packages from the first declared
+ branch to the last declared branch.</li>
+
+ <li>The branch in which a source directory package is first declared is the
+ <samp>base</samp> branch of the source directory package.</li>
+
+ <li>The last changed revision of a source directory package in a
+ subsequently declared repository branch is compared with that of the base
+ branch. If the last changed revision is the same as that of the base
+ branch, the source directory of this branch is discarded. Otherwise, it is
+ placed at the end of the extract priority list.</li>
+ </ol>
+
+ <p>For the <samp>var</samp> package in the above example, let us assume that
+ we have three source directory packages X, Y and Z under <samp>code</samp>,
+ and their last changed revisions under <samp>base</samp> are 100. Let's say
+ we have committed some changes to X and Z in the <samp>branch1</samp> branch
+ at revision 102, and other changes to Y and Z in the <samp>branch2</samp>
+ branch at revision 104, the extract priority lists for X, Y and Z will look
+ like:</p>
+
+ <ul>
+ <li>X: base (100, base), branch1 (102), branch2 (100, discarded)</li>
+
+ <li>Y: base (100, base), branch1 (100, discarded), branch2 (104)</li>
+
+ <li>Z: base (100, base), branch1 (102), branch2 (104)</li>
+ </ul>
+
+ <p>Once we have an extract priority list for a source directory, we can begin
+ extracting source files in the source directory. The source directory of the
+ base branch is extracted first, followed by that in the subsequent branches.
+ If a source file in a subsequent branch has the same content as the that in
+ the base branch, it is discarded. Otherwise, the following logic determines
+ the branch to use:</p>
+
+ <ol>
+ <li>If a source file is modified in only one subsequent branch, the source
+ file in that branch is extracted.</li>
+
+ <li>If a source file is modified in two or more subsequent branches, but
+ their modifications are the same, then the source file in the first
+ modification is used.</li>
+
+ <li>If a source file is modified in two or more subsequent branches and
+ their modifications differ, then the behaviour depends on the "conflict
+ mode" setting, which can be <code>fail</code>, <code>merge</code> (default)
+ and <code>override</code>. If the conflict mode is <code>fail</code>, the
+ extract fails. If the conflict mode is <code>merge</code>, the system will
+ attempt to merge the changes using a tool such as <code>diff3</code>. The
+ result of the merge will be used to update the destination. The extract
+ fails only if there are unresolved conflicts in the merge. (In which case,
+ the conflict should be resolved using the version control system before
+ re-running the extract system.) If the conflict mode is
+ <code>override</code>, the change in the latest declared branch takes
+ precedence, and the changes in all other branches will be ignored. The
+ conflict mode can be changed using the <code>CONFLICT</code> declaration in
+ the extract configuration file. E.g:
+ <pre>
+conflict fail
+</pre>
+ </li>
+ </ol>
+
+ <p>Once the system has established which source files to use, it determines
+ whether the destination file is out of date or not. The destination file is
+ out of date if it does not exist or if its content differs from the version
+ of the source file we are using. The system only updates the destination if
+ it is considered to be out of date.</p>
+
+ <p>The extract system can also combine changes from branches in the Subversion
+ repository and the local file system. This is demonstrated in the next
+ example.</p>
+ <pre id="example_6">
+# Example 6
+# ----------------------------------------------------------------------
+cfg::type ext
+cfg::version 1.0
+
+dest $PWD
+
+repos::var::base fcm:var_tr
+repos::ops::base fcm:ops_tr
+repos::gen::base fcm:gen_tr
+
+revision::gen::base 2468
+
+expsrc::var::base src/code
+expsrc::var::base src/scripts
+expsrc::ops::base src/code
+src::gen::base src/code/GenMod_Constants
+src::gen::base src/code/GenMod_Control
+src::gen::base src/code/GenMod_FortranIO
+src::gen::base src/code/GenMod_GetEnv
+src::gen::base src/code/GenMod_ModelIO
+src::gen::base src/code/GenMod_ObsInfo
+src::gen::base src/code/GenMod_Platform
+src::gen::base src/code/GenMod_Reporting
+src::gen::base src/code/GenMod_Trace
+src::gen::base src/code/GenMod_UMConstants
+src::gen::base src/code/GenMod_Utilities
+
+repos::var::branch1 fcm:var_br/frva/r1234_new_stuff
+repos::var::branch2 fcm:var_br/frva/r1516_bug_fix
+repos::ops::branch1 fcm:ops_br/opsrc/r3188_good_stuff
+
+repos::var::user $HOME/var # line 31
+repos::gen::user $HOME/gen # line 32
+</pre>
+
+ <p><a href="#example_6">Example 6</a> is similar to <a href=
+ "#example_5">example 5</a> except that it is also extracting from local
+ directories. Here is an explanation of the lines:</p>
+
+ <ul>
+ <li><dfn>line 31-32</dfn>: these line declare the repositories for the
+ <samp>user</samp> branches of the <samp>var</samp> and <samp>gen</samp>
+ packages respectively. Both are local paths at the local file system. There
+ are no declarations for source directories for the <samp>user</samp>
+ branches, so they use the same set of source directories of the first
+ declared branches, the <samp>base</samp> branches in both cases.</li>
+ </ul>
+
+ <dl>
+ <dt>Note - the INC declaration</dt>
+
+ <dd>
+ You have probably realised that the above examples have many repeated
+ lines. To avoid having repeated lines in multiple extract configuration
+ files, you can use <code>INC</code> declarations to include other extract
+ configuration files. For example, if the configuration file of example 5
+ is stored in the file <samp>$HOME/example5/ext.cfg</samp>, line 1 to 29
+ of <a href="#example_6">example 6</a> can be replaced with an
+ <code>INC</code> declaration. <a href="#example_6">Example 6</a> can then
+ be written as:
+ <pre>
+inc $HOME/example5/ext.cfg
+
+repos::var::user $HOME/var
+repos::gen::user $HOME/gen
+</pre>
+
+ <p>Note: the <code>INC</code> declaration supports the special
+ environment variable <var>$HERE</var>. If this variable is already set in
+ the environment, it acts as a normal environment variable. However, if it
+ is not set, it will be expanded into the container directory of the
+ current extract configuration file. This feature is particularly useful
+ if you are including a hierarchy of extract configurations from files in
+ the same container directory in a repository.</p>
+ </dd>
+ </dl>
+
+ <h3 id="advanced_inherit">Inherit from a previous extract</h3>
+
+ <p>All the examples above dealt with standalone extract, that is, the current
+ extract is independent of any other extract. If a previous extract exists in
+ another location, the extract system can inherit from this previous extract
+ in your current extract. This works like a normal incremental extract, except
+ that your extract will only contain the changes you have specified (compared
+ with the inherited extract) instead of the full source directory tree. This
+ type of incremental extract is useful in several ways. For instance:</p>
+
+ <ul>
+ <li>It is fast, because you only have to extract and mirror files that you
+ have changed.</li>
+
+ <li>The subsequent build will also be fast, since it will use incremental
+ build.</li>
+
+ <li>You do not need write access to the original extract. A system
+ administrator can set up a stable version in a central account, which
+ developers can then inherit from.</li>
+
+ <li>You want an incremental extract, but you need to leave the original
+ extract unmodified.</li>
+ </ul>
+
+ <p>The following example is based on <a href="#example_4">example 4</a> and
+ <a href="#example_6">example 6</a>. The assumption is that an extract has
+ already been performed at the directory <samp>~frva/var/vn22.0</samp> based
+ on the configuration file in <a href="#example_4">example 4</a>.</p>
+ <pre id="example_7">
+# Example 7
+# ----------------------------------------------------------------------
+cfg::type ext
+cfg::version 1.0
+
+dest $PWD
+
+use ~frva/var/vn22.0 # line 6
+
+repos::var::branch1 fcm:var_br/frva/r1234_new_stuff # line 8
+repos::var::branch2 fcm:var_br/frva/r1516_bug_fix # line 9
+repos::ops::branch1 fcm:ops_br/opsrc/r3188_good_stuff # line 10
+
+repos::var::user $HOME/var # line 12
+repos::gen::user $HOME/gen # line 13
+</pre>
+
+ <ul>
+ <li><dfn>line 6</dfn>: this line replaces line 1 to 25 of <a href=
+ "#example_6">example 6</a>. It declares that the current extract should
+ inherit from the previous extract located at
+ <samp>~frva/var/vn22.0</samp>.</li>
+ </ul>
+
+ <p>Running the extract system using the above configuration will trigger an
+ incremental extract, as if you are running an incremental extract having
+ modified the configuration file in <a href="#example_4">example 4</a> to that
+ of <a href="#example_6">example 6</a>. The only difference is that the
+ original extract using the <a href="#example_4">example 4</a> configuration
+ will be left untouched at <samp>~frva/var/vn22.0</samp>, and the new extract
+ will contain only the changes in the branches declared from line 8 to 13.</p>
+
+ <p>Note: extract inheritance allows you to add more branches to a package,
+ but you should not redefine the <code>REPOS</code>, <code>REVISION</code>,
+ <code>EXPSRC</code> or <code>SRC</code> declarations of a branch that is
+ already declared (and already extracted) in the inherited extract. Although
+ the system will not stop you from doing so, you may end up with an extract
+ that does not quite do what it is supposed to do. For example, if the
+ <samp>base</samp> branch in the <samp>foo</samp> package
+ (<tt>repos::foo::base</tt>) is already defined and extracted in an extract
+ you are inheriting from, you should not redefine any of the
+ <tt>*::foo::base</tt> declarations in your current extract. However, you are
+ free to add more branches for the same package with new labels (e.g.
+ <tt>repos::foo::b1</tt>), and indeed new packages that are not already
+ defined in the inherited extract (e.g. <tt>repos::bar::base</tt>).</p>
+
+ <p>If you are setting up an extract to be inherited, you do not have to
+ perform a build. If you don't you will still gain the benefit of incremental
+ file extract, but you will be performing a full build of the code.</p>
+
+ <dl>
+ <dt>Note - inherit and mirror</dt>
+
+ <dd>
+ <p>It is worth bearing in mind that <tt>rdest::*</tt> settings are not
+ inherited. If mirroring is required in the inheriting extract, it will
+ require its own set of <tt>rdest::*</tt> declarations.</p>
+
+ <p>The system will, however, assume that a mirrored version of the
+ inherited extract is available for inheritance from the mirrored
+ destination of the current extract.</p>
+
+ <p>E.g.: Consider an extract at <samp>/path/to/inherited/</samp> and an
+ inheriting extract at <samp>/path/to/current/</samp>. If the former does
+ not have a mirror, the latter should not have one either. If the former
+ mirrors to <samp>machine@/path/to/inherited/mirror/</samp> and the latter
+ mirrors to <samp>machine@/path/to/current/mirror/</samp>, the system will
+ assume that the subsequent build at
+ <samp>machine@/path/to/current/mirror/</samp> can inherit from the build
+ at <samp>machine@/path/to/inherited/mirror/</samp>. This is illustrated
+ below:</p>
+
+ <pre>
+/path/to/current/ => at machine: /path/to/current/mirror/
+use /path/to/inherited/ => at machine: use /path/to/inherited/mirror/
+</pre>
+ </dd>
+ </dl>
+
+ <h3 id="advanced_build">Extract - Build Configuration</h3>
+
+ <p>Configuration settings for feeding into the build system can be declared
+ through the extract configuration file using the <code>BLD::</code> prefix.
+ Any line in an extract configuration containing a label with such a prefix
+ will be considered a build system variable. At the end of a successful
+ extract, the system strips out the <code>BLD::</code> prefix before writing
+ these variables to the build configuration file. Some example entries are
+ given between line 17 and 22 in the following configuration file:</p>
+ <pre id="example_8">
+# Example 8
+# ----------------------------------------------------------------------
+cfg::type ext
+cfg::version 1.0
+
+dest $PWD
+
+repos::var::base fcm:var_tr
+repos::ops::base fcm:ops_tr
+repos::gen::base fcm:gen_tr
+
+revision::gen::base 2468
+
+expsrc::var::base src/code
+expsrc::var::base src/scripts
+expsrc::ops::base src/code
+src::gen::base src/code/GenMod_Constants
+src::gen::base src/code/GenMod_Control
+src::gen::base src/code/GenMod_FortranIO
+src::gen::base src/code/GenMod_GetEnv
+src::gen::base src/code/GenMod_ModelIO
+src::gen::base src/code/GenMod_ObsInfo
+src::gen::base src/code/GenMod_Platform
+src::gen::base src/code/GenMod_Reporting
+src::gen::base src/code/GenMod_Trace
+src::gen::base src/code/GenMod_UMConstants
+src::gen::base src/code/GenMod_Utilities
+
+bld::target VarProg_AnalysePF.exe # line 27
+
+bld::tool::fc sxmpif90 # line 29
+bld::tool::cc sxmpic++ # line 30
+bld::tool::ld sxmpif90 # line 31
+</pre>
+
+ <p>The above example is essentially the same as <a href="#example_4">example
+ 4</a>, apart from the additional build configuration. The following is a
+ simple explanation of what the lines represent: (For detail of the build
+ system, please see the next chapter on <a href="build.html">The Build
+ System</a>.)</p>
+
+ <ul>
+ <li><dfn>line 27</dfn>: the line declares a default target of the
+ build.</li>
+
+ <li><dfn>line 29-31</dfn>: the lines declare the Fortran compiler, the C
+ compiler and the linker respectively.</li>
+ </ul>
+
+ <dl>
+ <dt>Note - use of variables</dt>
+
+ <dd>
+ <p>When you start using the extract system to define compiler flags for
+ the build system, you may end up having to make a lot of long and
+ repetitive declarations. In this case, you may want to define variables
+ to replace the repetitive parts of the declarations.</p>
+
+ <p>Environment variables whose names contain only upper case latin
+ alphabets, numbers and underscores can be referenced in a declaration
+ value via the syntax <code>$NAME</code> or <code>${NAME}</code>. For
+ example:</p>
+ <pre>
+repos::um::base ${HOME}/svn-wc/um
+bld::tool::fflags $MY_FFLAGS
+</pre>
+
+ <p>You can define a user variable by making a declaration with a label
+ that begins with a percent sign <code>%</code>. The value of a user
+ variable remains in memory until the end of the current file is reached.
+ You can reference a user variable in a declaration value via the syntax
+ <code>%NAME</code> or <code>%{NAME}</code>. For example:</p>
+ <pre>
+# Declare a variable %fred
+%fred -Cdebug -eC -Wf,-init heap=nan stack=nan
+
+bld::tool::fflags %fred
+# bld::tool::fflags -Cdebug -eC -Wf,-init heap=nan stack=nan
+
+bld::tool::fflags::foo %fred -f0
+# bld::tool::fflags::foo -Cdebug -eC -Wf,-init heap=nan stack=nan -f0
+
+bld::tool::fflags::bar -w %fred
+# bld::tool::fflags::bar -w -Cdebug -eC -Wf,-init heap=nan stack=nan
+</pre>
+
+ <p>Further to this, each declaration results in an internal variable of
+ the same name and you can also refer to any of these internal variables in
+ the same way. So, the example given above could also be written as
+ follows:</p>
+ <pre>
+bld::tool::fflags -Cdebug -eC -Wf,-init heap=nan stack=nan
+bld::tool::fflags::foo %bld::tool::fflags -f0
+bld::tool::fflags::bar -w %bld::tool::fflags
+</pre>
+ </dd>
+
+ <dt>Note - as-parsed configuration</dt>
+
+ <dd>
+ <p>If you use a hierarchy of <code>INC</code> declarations or variables,
+ you may end up with a configuration file that is difficult to understand.
+ To help you with this, the extract system generates an as-parsed
+ configuration file at <samp>cfg/parsed_ext.cfg</samp> of the destination.
+ The content of the as-parsed configuration file is what the extract
+ system actually reads. It should contain everything in your original
+ extract configuration file, except that all <code>INC</code>
+ declarations, environment variables and user/internal variables are
+ expanded.</p>
+ </dd>
+ </dl>
+
+ <h2 id="verbose">Diagnostic verbose level</h2>
+
+ <p>The amount of diagnostic messages generated by the extract system is
+ normally set to a level suitable for normal everyday operation. This is the
+ default diagnostic verbose level 1. If you want a minimum amount of
+ diagnostic messages, you should set the verbose level to 0. If you want more
+ diagnostic messages, you can set the verbose level to 2 or 3. You can modify
+ the verbose level in two ways. The first way is to set the environment
+ variable <var>FCM_VERBOSE</var> to the desired verbose level. The second way
+ is to invoke the extract system with the <code>-v <level></code>
+ option. (If set, the command line option overrides the environment
+ variable.)</p>
+
+ <p>The following is a list of diagnostic output at each verbose level:</p>
+
+ <dl>
+ <dt>Level 0</dt>
+
+ <dd>
+ <ul>
+ <li>Report the time taken to extract the code.</li>
+
+ <li>Report the time taken to mirror the code.</li>
+
+ <li>If <code>rdist</code> is used to mirror the code, run the command
+ with the <code>-q</code> option.</li>
+ </ul>
+ </dd>
+
+ <dt>Level 1</dt>
+
+ <dd>
+ <ul>
+ <li>Everything at verbose level 0.</li>
+
+ <li>Report the name of the extract configuration file.</li>
+
+ <li>Report the location of the extract destination.</li>
+
+ <li>Report date/time at the beginning of the extract step.</li>
+
+ <li>If the revision specified for a repository branch is not its last
+ changed revision, print an information statement to inform the user of
+ the last changed revision of the branch.</li>
+
+ <li>Summarises the destination status and the source status.</li>
+
+ <li>Report date/time at the beginning of the mirror step.</li>
+
+ <li>Report the location of the alternate destination.</li>
+
+ <li>Report total time.</li>
+ </ul>
+ </dd>
+
+ <dt>Level 2</dt>
+
+ <dd>
+ <ul>
+ <li>Everything at verbose level 1.</li>
+
+ <li>If the revision specified for a repository branch is not current
+ (i.e. the specified revision number is less than the revision number of
+ the last commit revision), print an information statement to inform the
+ user of the last commit revision of the branch.</li>
+
+ <li>Report the detail of each change in the destination.</li>
+
+ <li>If <code>rdist</code> is used to mirror the code, run the command
+ without the <code>-q</code> option.</li>
+ </ul>
+ </dd>
+
+ <dt>Level 3</dt>
+
+ <dd>
+ <ul>
+ <li>Everything at verbose level 2.</li>
+
+ <li>Report all shell commands invoked by the extract system with
+ timestamp.</li>
+
+ <li>If <code>rdist</code> is used to mirror the code, print the
+ <samp>distfile</samp> supplied to the command.</li>
+
+ <li>If <code>rsync</code> is used to mirror the code, invoke the
+ command with the <code>-v</code> option.</li>
+ </ul>
+ </dd>
+ </dl>
+
+ <h2 id="nosvn">When Subversion Is Not Available</h2>
+
+ <p>The extract system can still be used if Subversion is not available.
+ Clearly, you can only use local repositories. However, you can still do
+ incremental extract, mirror an extract to an alternate location, or combine
+ code from multiple local repositories.</p>
+
+ <p>If you are using Subversion but your server is down then clearly there is
+ little you can do. However, if you already have an extract then you can
+ re-run <code>fcm extract</code> as long as the extract configuration file
+ only refers to fixed revisions. If this is not the case then you can always
+ use the expanded extract configuration file which can be found in
+ <samp>cfg/ext.cfg</samp> under the extract destination root. This means that
+ you can continue to makes changes to local code and do incremental extracts
+ even whilst your Subversion server is down.</p>
+
+ </div>
+ </div>
+ </div>
+
+ <hr/>
+ <div class="container-fluid text-center">
+ <div class="row-fluid"><div class="span12">
+ <address><small>
+ © British Crown Copyright 2006-14
+ <a href="http://www.metoffice.gov.uk">Met Office</a>.
+ See <a href="../etc/fcm-terms-of-use.html">Terms of Use</a>.<br />
+ This document is released under the British <a href=
+ "http://www.nationalarchives.gov.uk/doc/open-government-licence/" rel=
+ "license">Open Government Licence</a>.<br />
+ </small></address>
+ </div></div>
+ </div>
+
+ <script type="text/javascript" src="../etc/jquery.min.js"></script>
+ <script type="text/javascript" src="../etc/bootstrap/js/bootstrap.min.js"></script>
+ <script type="text/javascript" src="../etc/fcm.js"></script>
+ <script type="text/javascript" src="../etc/fcm-version.js"></script>
+</body>
+</html>
diff --git a/doc/user_guide/fcm_overview.png b/doc/user_guide/fcm_overview.png
new file mode 100644
index 0000000..acf3e7d
Binary files /dev/null and b/doc/user_guide/fcm_overview.png differ
diff --git a/doc/user_guide/getting_started.html b/doc/user_guide/getting_started.html
new file mode 100644
index 0000000..26d02f4
--- /dev/null
+++ b/doc/user_guide/getting_started.html
@@ -0,0 +1,1490 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>FCM: User Guide: Getting Started</title>
+ <meta name="author" content="FCM team" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link rel="icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link rel="shortcut icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link href="../etc/bootstrap/css/bootstrap.min.css" rel="stylesheet" media="screen" />
+ <link href="../etc/fcm.css" rel="stylesheet" media="screen" />
+</head>
+<body>
+ <div class="navbar navbar-inverse">
+ <div class="navbar-inner">
+ <a class="brand" href=".."><span class="fcm-version">FCM</span></a>
+ <ul class="nav">
+ <li><a href="../installation/">Installation</a></li>
+
+ <li class="active"><a href=".">User Guide</a></li>
+ </ul>
+ </div>
+ </div>
+
+ <div class="page-header">
+ <div class="fcm-page-content pull-right well well-small"></div>
+ <h1>FCM: User Guide: Getting Started</h1>
+ </div>
+
+ <div class="container">
+ <div class="row">
+ <div class="span12">
+
+ <h2 id="introduction">Introduction</h2>
+
+ <p>This chapter takes a <em>hands-on</em> approach to help you set up your
+ FCM session, and familiarise yourself with some of the system's basic
+ concepts and working practices. It is designed to complement other sections
+ of the User Guide.</p>
+
+ <p>You may also find it useful to refer to the <a href=
+ "annex_quick_ref.html">Annex: Quick reference</a>.</p>
+
+ <h2 id="setup">How to set yourself up to run FCM</h2>
+
+ <p>It is easy to set yourself up to run FCM. Simply follow the steps
+ below:</p>
+
+ <dl>
+ <dt>Installation</dt>
+
+ <dd>If FCM is not yet installed at your site, please refer to the <a href=
+ "../installation/">FCM: Installation</a> for detail.</dd>
+
+ <dt>Configure your editor for Subversion</dt>
+
+ <dd>
+ <p>When you attempt to create a branch or commit changes to the
+ repository, you will normally be prompted to edit your commit log message
+ using a text editor. The order of priority for determining the editor
+ command (where lower-numbered locations take precedence over
+ higher-numbered locations) is:</p>
+
+ <ol>
+ <li>environment variable <var>SVN_EDITOR</var></li>
+
+ <li>the <var>editor-cmd</var> option in the <var>[helpers]</var>
+ section of the user's <samp>$HOME/.subversion/config</samp> file.</li>
+
+ <li>environment variable <var>VISUAL</var></li>
+
+ <li>environment variable <var>EDITOR</var></li>
+
+ <li>default to <code>vi</code> (or <code>gedit</code> when running the
+ FCM GUI)</li>
+ </ol>
+
+ <p>It is worth bearing in mind that an editor must be able to run in the
+ foreground. For example, you can add one of the followings in your
+ <samp>$HOME/.kshrc</samp> (ksh) or <samp>$HOME/.bashrc</samp> (bash):</p>
+ <pre>
+# GVim
+export SVN_EDITOR='gvim -f'
+
+# Emacs
+export SVN_EDITOR=emacs
+
+# gedit
+export SVN_EDITOR=gedit
+
+# Kate
+export SVN_EDITOR=kate
+</pre>
+ </dd>
+
+ <dt>Configure your e-mail address in Trac</dt>
+
+ <dd>
+ <p>Trac can be configured to send automatic e-mail notifications to
+ authors of any ticket whenever there are changes to that ticket (and we
+ would expect most systems to be configured in this way). You should check
+ that the settings for your name and e-mail address are correct. To do
+ this you need to go to the Settings page once you are logged into Trac.
+ (Click on <kbd>Settings</kbd> just above the menu bar). Check that your
+ settings are entered correctly.</p>
+ </dd>
+
+ <dt>Configure your web browser</dt>
+
+ <dd>
+ <p>FCM assumes that <code>firefox</code> is the command to invoke your
+ default web browser. If you use another web browser, you should configure
+ it in your <samp>$HOME/.metomi/fcm/external.cfg</samp> file. See the
+ section on <a href="command_ref.html#fcm-browse">fcm browse</a> for
+ further information.</p>
+ </dd>
+ </dl>
+
+ <h2 id="tutorial">Tutorial</h2>
+
+ <h3 id="tutorial_intro">Introduction</h3>
+
+ <p>This tutorial leads you through the basics of using FCM to make changes to
+ your source code, and demonstrates the recommended practices for working with
+ it. A tutorial Subversion repository, with its own Trac system, is available
+ for you to practice for working with the FCM system. You will work through
+ the following activities:</p>
+
+ <ul>
+ <li><a href="#tutorial_create-ticket">Create a new ticket</a></li>
+
+ <li><a href="#tutorial_create-branch">Create a branch</a></li>
+
+ <li><a href="#tutorial_checkout">Checkout a working copy</a></li>
+
+ <li><a href="#tutorial_change">Make changes to files in your working
+ copy</a></li>
+
+ <li><a href="#tutorial_commit">Commit your changes to the
+ repository</a></li>
+
+ <li><a href="#tutorial_extract">Test your changes</a></li>
+
+ <li><a href="#tutorial_merge">Merge changes from the trunk and resolve
+ conflicts</a></li>
+
+ <li><a href="#tutorial_review-ticket">Review changes</a></li>
+
+ <li><a href="#tutorial_merge-back">Commit to the trunk</a></li>
+
+ <li><a href="#tutorial_extra-extract">Extra activities on the extract and
+ build systems</a></li>
+
+ <li><a href="#tutorial_delete-branch">Delete your branch</a></li>
+
+ <li><a href="#tutorial_delete-final-comments">Final comments</a></li>
+ </ul>
+
+ <p>We recommend that you create a work area in your filespace, for example,
+ <samp>$HOME/tutorial/work</samp> for your working copy, and
+ <samp>$HOME/tutorial/test</samp> for your build test.</p>
+
+ <p>If you have not already done so, you should set up your desktop
+ environment as described above in the <a href="#setup">How to set yourself up
+ to run FCM</a> section.</p>
+
+ <p>It is also worth knowing that the <a href=
+ "http://svnbook.red-bean.com/en/1.8/">Subversion Book</a> is a great source
+ of reference of Subversion features. In particular, the <a href=
+ "http://svnbook.red-bean.com/en/1.8/svn.basic.html">Fundamental Concepts</a>
+ and <a href="http://svnbook.red-bean.com/en/1.8/svn.tour.html">Basic
+ Usage</a> chapters are well worth reading.</p>
+
+ <h3 id="tutorial_create-ticket">Create a new ticket</h3>
+
+ <p><em>Trac is an integrated web-based issue tracker and wiki system. You
+ will use it to manage and keep track of changes in your project. The issue
+ tracker is called the ticket system. When you want to report a problem or
+ submit a change request, you will create a new ticket. In a typical
+ situation, you and/or your colleagues will make changes to your system in
+ order to resolve the problem or change request, and you will monitor these
+ changes via the ticket.</em></p>
+
+ <p>After completing this sub-section, you will learn how to:</p>
+
+ <ul>
+ <li>launch a Trac system,</li>
+
+ <li>create a new Trac ticket,</li>
+
+ <li>search for a Trac ticket, and</li>
+
+ <li>accept a Trac ticket.</li>
+ </ul>
+
+ <p>Further reading:</p>
+
+ <ul>
+ <li><a href="overview.html">System Overview</a></li>
+
+ <li>Code Management System > <a href=
+ "code_management.html#svn_basic">Basic Command Line Usage</a></li>
+
+ <li>Code Management System > <a href="code_management.html#trac">Using
+ Trac</a></li>
+
+ <li>FCM Command Reference > <a href="command_ref.html#fcm-browse">fcm
+ browse</a></li>
+ </ul>
+
+ <h4>Launch a Trac system</h4>
+
+ <p>To launch the Trac system for the tutorial: type and <kbd>Enter</kbd> the
+ following command:</p>
+ <pre>
+(SHELL PROMPT)$ fcm browse fcm:tutorial
+</pre>
+
+ <p>This is probably the first time you have used the <code>fcm</code>
+ command. The command has the general syntax:</p>
+ <pre>
+fcm <sub-command> [<options...>] <arguments>
+</pre>
+
+ <p>For example, if you type <code>fcm help</code>, it will display a listing
+ of what sub-commands are available, and if you type <code>fcm help
+ <sub-command></code>, it will display help for that particular
+ sub-command.</p>
+
+ <p>The <code>trac</code> sub-command launches the corresponding Trac system
+ browser for a Subversion URL specified in your argument. In this case, we are
+ asking it to display the Trac system browser for the tutorial. The argument
+ <samp>fcm:tutorial</samp> is a FCM URL keyword and will be expanded by FCM
+ into a real Subversion URL (e.g.
+ <samp>svn://fcm1/tutorial_svn/tutorial</samp>). You are encouraged to use FCM
+ URL keywords throughout the tutorial, as it will save you a lot of
+ typing.</p>
+
+ <p>Note: Although we use the Trac system as a browser for a Subversion
+ repository, they do not interact in any other ways. Having access to a Trac
+ system does not guarantee the same privilege to a Subversion repository. In
+ particular, you should note the differences between the URLs of a Subversion
+ repository path and its equivalence in a Trac browser.</p>
+
+ <p>There are other ways to launch the Trac system for a project. If you know
+ its URL, you can launch the Trac system by entering it in the address box of
+ your favourite browser. If you often access a Trac system for a particular
+ project, you should bookmark it in your favourite browser.</p>
+
+ <h4>Create a new Trac ticket</h4>
+
+ <p>Click on <kbd>Login</kbd> just above the menu bar, enter your Unix/Linux
+ user ID as your user name and leave the password empty. Then click on
+ <kbd>OK</kbd> to proceed.</p>
+
+ <p>Once you have logged in, the <kbd>New Ticket</kbd> link will become
+ available on the menu bar. Click on it to display a new ticket form, where
+ you can enter details about your problem or change request. In the tutorial,
+ it does not matter what you enter, but you should feel free to play around
+ with wiki formatting when entering the <em>Full description</em>. (Click on
+ <kbd>WikiFormatting</kbd> to see how you can use it.) For example:</p>
+
+ <ul>
+ <li>Short summary:
+ <pre>
+Tutorial to change repository files and resolve conflicts with the trunk
+</pre>
+ </li>
+
+ <li>Full description:
+ <pre>
+In this tutorial, I shall:
+ 1. use FCM commands
+ 2. play with WikiFormatting in Trac tickets
+ 3. create a branch and checkout a working copy
+ 4. make changes to files in it
+ 5. commit my changes and assign the ticket for review
+ 6. record the review and assign the ticket back to the author
+ 7. merge in the trunk, and resolve any conflicts
+ 8. merge my changes back to the trunk
+ 9. close the ticket
+ 10. delete my branch
+</pre>
+ </li>
+
+ <li>Feel free to select an option you desire for each of the other ticket
+ properties: Type, Component, Priority, Version and Milestone.</li>
+ </ul>
+
+ <p>At the bottom of the page, click the <kbd>Preview</kbd> button to see what
+ the description would look like. When you are happy, click the <kbd>Submit
+ changes</kbd> button. Trac will create the new ticket and return it in a
+ state where you can append to it.</p>
+
+ <p>When the ticket is created, you should get an automatic e-mail notication
+ from the Trac system. In real life, depending on the setting, the owner of
+ your Trac system may also get a similar e-mail notification. It is worth
+ noting that each time the ticket is modified, the Trac system will send out
+ an e-mail notification to you (the reporter) and anyone who modified the
+ ticket subsequently.</p>
+
+ <h4>Search for a Trac ticket</h4>
+
+ <p>You should remember the number of your new ticket, as you will have to
+ revisit it later.</p>
+
+ <p>In real work, it is often not practical to have to remember the numbers of
+ all the tickets you have created. Trac provides a powerful custom query for
+ searching a ticket. You can search for the ticket you have just created by
+ clicking the <kbd>View Tickets</kbd> link. Feel free to play with the custom
+ query tool. Add or remove filters and try grouping your results by different
+ categories.</p>
+
+ <p>In addition, you can search your ticket using the keyword
+ <kbd>Search</kbd> utility at the top right hand corner of each Trac page. (If
+ you enter <kbd>#<number></kbd> in the search box, it will take you
+ directly to that ticket.) In the tutorial, however, it may be easiest if you
+ simply leave the tutorial Trac system open, so that you do not have to login
+ again when you come back to your ticket.</p>
+
+ <h4>Start work on your Trac ticket</h4>
+
+ <p>The status of the ticket is <em>new</em>. When you start working on a
+ problem reported in a ticket it is good practice to change the status to
+ <em>in_progress</em> to indicate that you are working on it. For the purpose
+ of the tutorial, however, this is entirely optional since you know you will
+ be doing all the work any way.</p>
+
+ <p>To start work on a ticket, click on <kbd>start work</kbd> in the
+ <em>Action</em> box at the bottom of the page, and then click on <kbd>Submit
+ changes</kbd>.</p>
+
+ <h3 id="tutorial_create-branch">Create a branch</h3>
+
+ <p><em>You create a branch by making a copy of your project at a particular
+ revision. Most often, this will be a particular revision of the trunk, i.e.
+ the main branch/development line in your project. A branch resides in the
+ repository. It allows you to work in parallel with your colleagues without
+ affecting one another, while keeping your changes under version
+ control.</em></p>
+
+ <p>After completing this sub-section, you will learn how to:</p>
+
+ <ul>
+ <li>create a branch in a Subversion repository, and</li>
+
+ <li>update a ticket with a link to a branch.</li>
+ </ul>
+
+ <p>Further reading:</p>
+
+ <ul>
+ <li>Code Management System > Branching & Merging > <a href=
+ "code_management.html#svn_branching_create">Creating Branches</a></li>
+
+ <li>Code Management Working Practices > <a href=
+ "working_practices.html#branching">Branching & Merging</a></li>
+
+ <li>FCM Command Reference > <a href=
+ "command_ref.html#fcm-branch-create">fcm branch-create</a></li>
+ </ul>
+
+ <h4>Create a branch in a Subversion repository</h4>
+
+ <p><strong>Important note: please ensure that your branch is created from
+ revision 1 of the trunk here, or the tutorial on merge will fail to work
+ later.</strong></p>
+
+ <p><dfn>Command line</dfn>: issue the <code>fcm branch-create [-rREV] TICKET
+ URL-PROJECT</code> command. (Note: you can write <code>fcm
+ branch-create</code> as <code>fcm bcreate</code> or even <code>fcm
+ bc</code>.) E.g. if your ticket number is <samp>#133</samp>:</p>
+ <pre>
+(SHELL PROMPT)$ fcm bc 133 fcm:tutorial at 1
+</pre>
+
+ <p>You will be prompted to edit the message log file. A standard template is
+ automatically supplied for the commit. However, if you want to add extra
+ comment for the branch, please do so <strong>above</strong> the line that
+ says <samp>--Add your commit message ABOVE - do not alter this line or those
+ below--</samp>. When you are ready, save your change and exit the editor.
+ Answer <kbd>Yes</kbd> when you are prompted to go ahead and create the
+ branch.</p>
+
+ <p>Note: Subversion will prompt you for a password the first time you access
+ a repository. The password will normally be cached by the client, and you
+ will not have to specify a password on subsequent access.</p>
+
+ <p>When creating branches for the first time, you will notice that FCM will
+ create and commit any missing sub-directories it needs to set up your branch
+ inside the repository, before creating your branch and commiting it.</p>
+
+ <p>Take a note of the revision number the branch was created at, and its
+ branch name. (The revision number is the number following the last output
+ that says "Committed revision". In the example above, the branch created at
+ <samp>r811</samp> is called <samp>branches/dev/matt/r1_133</samp>, which is a
+ branch of the <samp>tutorial</samp> project in the
+ <samp>svn://fcm1/tutorial_svn</samp> repository.)</p>
+
+ <h4>Update your ticket with a link to your branch</h4>
+
+ <p>If you wish, you can update your ticket with details of the branch. Note
+ that this step is entirely optional. It is useful for developments which will
+ take a long time to complete. For short lived branches, this step is probably
+ unnecessary.</p>
+
+ <p>In the ticket you have created, refer to the revision number in the
+ <kbd>Add/Change</kbd> box, for example:</p>
+ <pre>
+r811: created source:tutorial/branches/dev/matt/r1_133 at 811.
+</pre>
+
+ <p>Note:</p>
+
+ <ul>
+ <li><samp>source:tutorial/branches/dev/matt/r1_133 at 811</samp> is a Trac
+ wiki link. In this syntax, you do not have to put in the root URL, (e.g.
+ <samp>svn://fcm1/tutorial_svn/</samp>), but you should specify your branch
+ using the project name (<samp>tutorial</samp>), the branch name
+ (<samp>branches/dev/matt/r1_133</samp>), and a revision number. Trac will
+ translate this into a link to that branch.</li>
+
+ <li>Trac will translate the syntax <code>r<number></code> or
+ <code>[<number>]</code> into a link to the numbered changeset.</li>
+ </ul>
+
+ <p>Click on <kbd>Preview</kbd> and check that the links work correctly, and
+ on <kbd>Submit changes</kbd> when you are ready.</p>
+
+ <h3 id="tutorial_checkout">Checkout a working copy</h3>
+
+ <p><em>A Subversion working copy is an ordinary directory tree on your local
+ system, containing a collection of files. It is your private working area in
+ which you can make changes before publishing them back to the repository. You
+ create a working copy by using the checkout command on some subtree of the
+ repository.</em></p>
+
+ <p>After completing this sub-section, you will learn how to:</p>
+
+ <ul>
+ <li>checkout a Subversion working copy.</li>
+ </ul>
+
+ <p>Further reading:</p>
+
+ <ul>
+ <li>Code Management System > <a href=
+ "code_management.html#svn_concepts">Basic Concepts</a></li>
+
+ <li>FCM Command Reference > <a href="command_ref.html#svn">Other
+ Subversion Commands</a></li>
+ </ul>
+
+ <h4>Checkout a Subversion working copy</h4>
+
+ <p><dfn>Command line</dfn>: issue the <code>fcm checkout</code> (or simply
+ <code>fcm co</code>) command. E.g.:</p>
+ <pre>
+(SHELL PROMPT)$ fcm checkout fcm:tutorial_br/dev/matt/r1_133 tutorial
+</pre>
+
+ <ul>
+ <li>In the example, we have replaced the leading part of the Subversion URL
+ <samp>svn://fcm1/tutorial_svn/tutorial/branches</samp> with the FCM URL
+ keyword <samp>fcm:tutorial_br</samp>. This is mainly to save you from
+ having to type in the full URL. However, you may find it easier to
+ copy-and-paste the full Subversion URL from the output generated when you
+ created the branch.</li>
+
+ <li>In the above command, we have asked the system to create a working copy
+ in <samp>$PWD/tutorial</samp>. If you do not specify a local directory
+ <var>PATH</var> in the <code>checkout</code> command, it will create a
+ working copy in the current working directory, using the basename of the
+ URL. For example, when you checkout the branch you have just created, the
+ command would create the working copy in <samp>$PWD/r1_133</samp>, which is
+ often undesirable. Make a note of the location of your working copy, in
+ case you forget where you have put it.</li>
+
+ <li>If you do not specify a revision to checkout, it will checkout the
+ HEAD, i.e. the latest, revision.</li>
+ </ul>
+
+ <p>Example:</p>
+ <pre>
+=> fcm checkout fcm:tutorial_br/dev/matt/r1_133 tutorial
+A tutorial/doc
+A tutorial/doc/hello.html
+A tutorial/src
+A tutorial/src/subroutine
+A tutorial/src/subroutine/hello_c.c
+A tutorial/src/subroutine/hello_sub.f90
+A tutorial/src/module
+A tutorial/src/module/hello_num.f90
+A tutorial/src/module/hello_constants.f90
+A tutorial/src/program
+A tutorial/src/program/hello.f90
+A tutorial/fcm-make.cfg
+Checked out revision 811.
+</pre>
+
+ <h3 id="tutorial_change">Make changes to files in your working copy</h3>
+
+ <p><em>Subversion provides various useful commands to help you monitor your
+ working copy. The most useful ones are "diff", "revert" and "status". You
+ will also find "add", "copy", "delete" and "move" useful when you are
+ rearranging your files and directories.</em></p>
+
+ <p>After completing this sub-section, you will learn how to:</p>
+
+ <ul>
+ <li>make and revert changes,</li>
+
+ <li>add and remove files,</li>
+
+ <li>inspect the status of a working copy, and</li>
+
+ <li>display changes in a working copy.</li>
+ </ul>
+
+ <p>Further reading:</p>
+
+ <ul>
+ <li>Code Management System > <a href=
+ "code_management.html#svn_basic">Basic Command Line Usage</a></li>
+
+ <li>FCM Command Reference > <a href="command_ref.html#fcm-add">fcm
+ add</a>, <a href="command_ref.html#fcm-diff">fcm diff</a>, <a href=
+ "command_ref.html#fcm-delete">fcm delete</a>, <a href=
+ "command_ref.html#svn">Other Subversion Commands</a></li>
+ </ul>
+
+ <h4>Make and revert changes</h4>
+
+ <p>For the later part of the tutorial to work, you must make the following
+ modifications:</p>
+
+ <ul>
+ <li>Change to the <samp>src/module/</samp> sub-directory in your working
+ copy.</li>
+
+ <li>Edit <samp>hello_constants.f90</samp>, using your favourite editor, and
+ change: <samp>Hello World!</samp> to <kbd>Hello Earthlings!</kbd>. Save
+ your change and exit the editor.</li>
+
+ <li>Edit <samp>hello_num.f90</samp> and either add a comment, or make a
+ minor code change - for example, use one of :
+ <pre>
+!This will print a really really big integer.
+WRITE(*, *) "Sadly, there's no encoding for Martian base-60"
+</pre>
+ </li>
+ </ul>
+
+ <p>Try the following so that you know how to restore a changed file:</p>
+
+ <ul>
+ <li>Change to the <samp>src/subroutine/</samp> directory of your working
+ copy.</li>
+
+ <li>Make a change in file <b>hello_c.c</b>, using your favourite
+ editor.</li>
+
+ <li>To see that you have <strong>M</strong>odified this file: issue the
+ <code>fcm status</code> command</li>
+
+ <li>Run the <code>revert</code> command to get the file back unmodified:
+ <pre>
+(SHELL PROMPT)$ fcm revert hello_c.c
+</pre>
+ </li>
+ </ul>
+
+ <h4>Add and remove files</h4>
+
+ <p>You may also want to try the following FCM commands in your
+ <samp>doc/</samp> sub-directory. You can safely make changes here since they
+ will not interfere with your code changes.</p>
+
+ <ul>
+ <li>change to the <samp>doc/</samp> directory of your working copy.</li>
+
+ <li>Echo some text into a new file and then run the <code>add</code>
+ command, which lets the repository know you're adding a new file at the
+ next commit. For example:
+ <pre>
+(SHELL PROMPT)$ echo 'Some text' >new_file.txt
+(SHELL PROMPT)$ fcm add new_file.txt
+</pre>
+ </li>
+
+ <li>Make a copy of <samp>hello.html</samp> and remove the original, using
+ the <code>copy</code> and <code>delete</code> commands. For example:
+ <pre>
+(SHELL PROMPT)$ fcm copy hello.html add.html
+(SHELL PROMPT)$ fcm delete hello.html
+</pre>
+ </li>
+
+ <li>You can use a simple <code>move</code> sub-command for the above
+ <code>copy</code> and <code>delete</code>.</li>
+ </ul>
+
+ <h4>Inspect the status of a working copy</h4>
+
+ <p>Change to the root directory of your working copy.</p>
+
+ <p><dfn>Command line</dfn>: issue the <code>fcm status</code> (or simply
+ <code>fcm st</code>) command.</p>
+
+ <p>Example:</p>
+ <pre>
+=> fcm status
+D doc/hello.html
+A doc/new_file.txt
+A + doc/add.html
+M src/module/hello_num.f90
+M src/module/hello_constants.f90
+</pre>
+
+ <p>This confirms the actions you have taken. You have
+ <strong>D</strong>eleted a file, <strong>A</strong>dded a new file,
+ <strong>A</strong>dded a file with history (<strong>+</strong>) and
+ <strong>M</strong>odified two others. It also confirms the action of the
+ <code>revert</code> command.</p>
+
+ <h4>Display changes in a working copy</h4>
+
+ <p>You can view the changes you have made to your working copy.</p>
+
+ <p><dfn>Command line</dfn>: issue the <code>fcm diff --graphical</code> (or
+ simply <code>fcm di -g</code>) command.</p>
+
+ <p>A listing of the files you have changed will be displayed, and a graphical
+ diff tool will open up for each modified file.</p>
+
+ <h3 id="tutorial_commit">Commit your changes to the repository</h3>
+
+ <p><em>The change in your working copy remains local until you commit it to
+ the repository where it becomes permanent. If you are planning to make a
+ large number of changes, you are encouraged to commit regularly to your
+ branch at appropriate intervals.</em></p>
+
+ <p>After completing this sub-section, you will learn how to:</p>
+
+ <ul>
+ <li>commit your changes, and</li>
+
+ <li>inspect your changes using Trac.</li>
+ </ul>
+
+ <p>Further reading:</p>
+
+ <ul>
+ <li>Code Management System > <a href=
+ "code_management.html#svn_basic">Basic Command Line Usage</a></li>
+
+ <li>FCM Command Reference > <a href="command_ref.html#fcm-commit">fcm
+ commit</a></li>
+ </ul>
+
+ <h4>Commit changes</h4>
+
+ <p><dfn>Command line</dfn>: issue the <code>fcm commit</code> (or simply
+ <code>fcm ci</code>) command.</p>
+
+ <p>A text editor will appear to allow you to edit the commit message. You
+ must add a commit message to describe your change <strong>above</strong> the
+ line that says <samp>--Add your commit message ABOVE - do not alter this line
+ or those below--</samp>. (A suggestion is given as the highlighted text in
+ the example below.) Your commit will fail if you do not enter a commit
+ message.</p>
+
+ <p>Save your change and exit the editor. Answer <kbd>Yes</kbd> when you are
+ prompted to confirm the commit. For example:</p>
+ <pre>
+[info] gvim -f: starting commit message editor...
+Change summary:
+--------------------------------------------------------------------------------
+[Project: tutorial]
+[Branch : branches/dev/matt/r1_133]
+[Sub-dir: <top>]
+
+D doc/hello.html
+A doc/new_file.txt
+A + doc/add.html
+M src/module/hello_num.f90
+M src/module/hello_constants.f90
+--------------------------------------------------------------------------------
+Commit message is as follows:
+--------------------------------------------------------------------------------
+#133: tutorial is fun.
+--------------------------------------------------------------------------------
+Would you like to commit this change?
+Enter "y" or "n" (or just press <return> for "n"): y
+Adding doc/add.html
+Deleting doc/hello.html
+Adding doc/new_file.txt
+Sending src/module/hello_constants.f90
+Sending src/module/hello_num.f90
+Transmitting file data ...
+Committed revision 812.
+=> svn update
+At revision 812.
+</pre>
+
+ <h4>Inspect changes using Trac</h4>
+
+ <p>Click on <kbd>Timeline</kbd> in Trac. Drill down to your changeset and see
+ how it appears. (Alternatively, if you enter <samp>r<number></samp>
+ into the search box at the top right, it will take you directly to the
+ numbered changeset.)</p>
+
+ <p>Note:</p>
+
+ <ul>
+ <li>Wiki Formatting, used in the commit message, has customised the
+ changeset message.</li>
+
+ <li>All your changes are listed.</li>
+ </ul>
+
+ <h3 id="tutorial_extract">Test your changes</h3>
+
+ <p><em>You should test the changes in your branch before asking a colleague
+ to review them. FCM features a build system that allows you to build your
+ code easily. As your changes may be located in a repository branch and/or a
+ working copy, you should work with the extract system to extract the correct
+ code to build. The extract system allows you to extract code from the
+ repository, combining changes in different branches and your working copy,
+ before generating a configuration file and a suitable source tree for feeding
+ into the build system.</em></p>
+
+ <p><em>In this sub-section of the tutorial, you will be shown how to extract
+ and build the code from your branch. (There are some <a href=
+ "#tutorial_extra-extract">extra activities on the extract and build
+ systems</a> in a later sub-section of the tutorial should you want to explore
+ the extract and build systems in more depth.) In the example here, the
+ extract and build systems will be shown to you in their simplest form. In
+ real life, the managers of the systems you are developing code for will
+ provide you with more information on how to extract and build their
+ systems.</em></p>
+
+ <p>After completing this sub-section, you will learn how to:</p>
+
+ <ul>
+ <li>set up a simple FCM make configuration file for extract and build,
+ and</li>
+
+ <li>use the <a href="make.html">FCM Make</a> system to perform simple
+ extracts and builds.</li>
+ </ul>
+
+ <p>Further reading:</p>
+
+ <ul>
+ <li><a href="make.html">FCM Make</a></li>
+ </ul>
+
+ <p>You should extract and build your code in a different directory to your
+ working copy. For example, you may want to create a sub-directory
+ <samp>$HOME/tutorial/test/</samp> and change to it:</p>
+ <pre>
+(SHELL PROMPT)$ mkdir -p $HOME/tutorial/test
+(SHELL PROMPT)$ cd $HOME/tutorial/test
+</pre>
+
+ <h4>Set up a FCM make configuration file</h4>
+
+ <p>To set up a FCM make configuration file from scratch, launch your
+ favourite editor and add the following lines:</p>
+ <pre>
+steps = extract build
+extract.ns = tutorial
+extract.location[tutorial] = branches/dev/$LOGNAME/r1_133
+extract.path-root[tutorial] = src
+build.target{task} = link
+</pre>
+
+ <p>Note:</p>
+
+ <ul>
+ <li>The <code><a href=
+ "annex_cfg.html#make.extract.location">extract.location</a></code>
+ declaration is set to <samp>branches/dev/$LOGNAME/r1_133</samp>. If you
+ have named your branch differently, you should modify the right hand side
+ of the declaration.</li>
+
+ <li>The build system uses <code>gfortran</code> and <code>gcc</code> as the
+ default Fortran and C (respectively) compilers. If you do not have these
+ compilers installed on your system, you can configure your Fortran and C
+ compilers using the <code><a href=
+ "annex_cfg.html#make.build.prop.fc">build.prop{fc}</a></code> and
+ <code><a href="annex_cfg.html#make.build.prop.cc">build.prop{cc}</a></code>
+ declarations.</li>
+ </ul>
+
+ <p>Save the file as <samp>fcm-make.cfg</samp> and exit your editor.</p>
+
+ <h4>Perform an extract and a build</h4>
+
+ <p>Issue the command <code><a href="command_ref.html#fcm-make">fcm
+ make</a></code> and you should get an output similar to the following:</p>
+ <pre>
+(SHELL PROMPT)$ fcm make
+[init] make # 2011-11-03 10:31:10Z
+[init] make config-parse # 2011-11-03 10:31:10Z
+[info] config-file=/home/matt/tutorial/test/fcm-make.cfg
+[done] make config-parse # 0.0s
+[init] make dest-init # 2011-11-03 10:31:10Z
+[info] dest=frsn at eld081:/home/matt/tutorial/test
+[info] mode=new
+[done] make dest-init # 0.0s
+[init] make extract # 2011-11-03 10:31:10Z
+[info] location tutorial: 0: svn://fcm1/tutorial_svn/tutorial/branches/dev/matt/r1_133@811
+[info] dest: 5 [A added]
+[info] source: 5 [U from base]
+[done] make extract # 0.1s
+[init] make build # 2011-11-03 10:31:10Z
+[info] sources: total=5, analysed=5, elapsed-time=0.0s, total-time=0.0s
+[info] compile targets: modified=5, unchanged=0, total-time=0.3s
+[info] compile+ targets: modified=2, unchanged=0, total-time=0.0s
+[info] ext-iface targets: modified=1, unchanged=0, total-time=0.0s
+[info] link targets: modified=1, unchanged=0, total-time=0.0s
+[info] TOTAL targets: modified=9, unchanged=0, elapsed-time=0.4s
+[done] make build # 0.4s
+[done] make # 0.5s
+</pre>
+
+ <p>If nothing goes wrong, you should end up with the sub-direcories
+ <samp>extract/</samp> and <samp>build/</samp> in your working directory. The
+ <samp>extract/</samp> sub-directory contains the result of the extract and
+ the <samp>build/</samp> sub-directory contains the result of the build.</p>
+
+ <p>N.B. You should also find a <samp>.fcm-make/</samp> sub-directory. It is
+ used by <code>fcm make</code> as a working area for your extract and build.
+ It also contains a diagnostic <samp>log</samp> file generated by the latest
+ <code>fcm make</code> command. The log file contains the diagnostic output in
+ high verbosity. If anything goes wrong, it is worth checking the content of
+ the log file for clues.</p>
+
+ <p>The executable you have built is <samp>hello.exe</samp>, which is located
+ in the <samp>build/bin/</samp> sub-directory. You can test your executable by
+ running it. You should get an output similar to the following:</p>
+ <pre>
+(SHELL PROMPT)$ PATH=$PWD/build/bin:$PATH hello.exe
+hello: Hello Earthlings!
+hello_sub: Hello Earthlings!
+hello_huge_number: maximum integer: 2147483647
+hello_c: Hello World!
+</pre>
+
+ <h3 id="tutorial_merge">Merge changes from the trunk and resolve
+ conflicts</h3>
+
+ <p><em>Your branch is normally isolated from other development lines in your
+ project. However, at some point during your development, you may need to
+ merge your changes with those of your colleagues. In some cases, it is
+ desirable to merge changes regularly from the trunk to keep your branch up to
+ date with the latest development. The automatic merge provided by FCM allows
+ you to do this easily.</em></p>
+
+ <p><em>A merge results in a conflict if changes being applied to a file
+ overlap. FCM uses a graphical merge tool to help you resolve overlaps in file
+ text changes (</em>text conflicts<em>). If some of the changes include a
+ deletion, renaming, or addition of the file, a filesystem conflict (</em>tree
+ conflict<em>) may occur, which needs to be dealt with manually.</em></p>
+
+ <p>After completing this sub-section, you will learn how to:</p>
+
+ <ul>
+ <li>merge changes from the trunk into your working copy, and</li>
+
+ <li>resolve text and tree conflicts in your working copy.</li>
+ </ul>
+
+ <p>Further reading:</p>
+
+ <ul>
+ <li>Code Management System > Basic Command Line Usage > <a href=
+ "code_management.html#svn_basic_conflicts">Resolving Conflicts</a></li>
+
+ <li>Code Management System > <a href=
+ "code_management.html#svn_branching">Branching & Merging</a></li>
+
+ <li>Code Management Working Practices > <a href=
+ "working_practices.html#branching">Branching & Merging</a></li>
+
+ <li>FCM Command Reference > <a href="command_ref.html#fcm-conflicts">fcm
+ conflicts</a></li>
+
+ <li>FCM Command Reference > <a href="command_ref.html#fcm-merge">fcm
+ merge</a></li>
+ </ul>
+
+ <h4>Merge changes from the trunk into a working copy</h4>
+
+ <p>Perform the merge in your working copy.</p>
+
+ <p><dfn>Command line</dfn>: issue the <code>fcm merge</code> command.
+ E.g.</p>
+ <pre>
+(SHELL PROMPT)$ fcm merge trunk
+</pre>
+
+ <p>If there is more than one revision of the source that you can merge with,
+ you will be prompted for the revision number you wish to merge from. You will
+ not be prompted in this case, because there is only one revision of the
+ source that you can merge with.</p>
+
+ <p>Example:</p>
+ <pre>
+Eligible merge(s) from /tutorial/trunk: 2
+Merge: /tutorial/trunk at 2
+ c.f.: /tutorial/trunk at 1
+-------------------------------------------------------------------------dry-run
+--- Merging r2 into '.':
+U src/subroutine/hello_c.c
+A src/module/hello_number.f90
+C src/module/hello_constants.f90
+ C src/module/hello_num.f90
+Summary of conflicts:
+ Text conflicts: 1
+ Tree conflicts: 1
+-------------------------------------------------------------------------dry-run
+Would you like to go ahead with the merge?
+Enter "y" or "n" (or just press <return> for "n"): y
+Merge succeeded.
+</pre>
+
+ <h4>Status of the working copy after a merge</h4>
+
+ <p>In the output of the merge, the <samp>C</samp> status at the beginning of
+ a line indicates that the first file you changed,
+ <samp>src/module/hello_constants.f90</samp>, is now in <em>text
+ conflict</em>. The <samp>C</samp> status in the 4th column of a line
+ indicates that the second file you changed,
+ <samp>src/subroutines/hello_num.f90</samp>, is now in <em>tree conflict</em>.
+ If you run <code>fcm status</code>, you will see extra information about the
+ merge, which may help you to resolve the conflicts:</p>
+ <pre>
+=> fcm status
+ M .
+? #commit_message#
+M src/subroutine/hello_c.c
+? src/module/hello_constants.f90.merge-left.r1
+? src/module/hello_constants.f90.merge-right.r2
+? src/module/hello_constants.f90.working
+ C src/module/hello_num.f90
+ > local edit, incoming delete upon merge
+A + src/module/hello_number.f90
+C src/module/hello_constants.f90
+</pre>
+
+ <p>In the case of the file <samp>hello_constants.f90</samp>, the extra files
+ created (ending with <samp>working</samp>, <samp>merge-left.r1</samp>,
+ <samp>merge-right.r2</samp>) will be used to resolve the text conflict using
+ the 3-way difference tool <code>xxdiff</code>.</p>
+
+ <p>In the case of the file <samp>hello_num.f90</samp>, the extra line
+ underneath (<em>local edit, incoming delete upon merge</em>) displays the
+ conflict or dilemma that you must resolve - you have made a change to the
+ file in your branch (<em>local edit</em>) but someone has deleted the file on
+ the trunk (<em>incoming delete upon merge</em>). If you inspect the log of
+ the trunk, by typing e.g. <code>fcm log -v -rHEAD:1
+ fcm:tutorial/trunk</code>, you will find that someone has renamed
+ <samp>src/module/hello_num.f90</samp> to
+ <samp>src/module/hello_number.f90</samp>.</p>
+
+ <p>The line: <samp>M .</samp> just refers to Subversion's merge tracking,
+ which is not relevant here.</p>
+
+ <p>You will now have to resolve the conflicts.</p>
+
+ <h4>Resolve the conflicts</h4>
+
+ <p>Issue the <code>fcm conflicts</code> (or simply <code>fcm cf</code>)
+ command.</p>
+
+ <p>The <code>xxdiff</code> program comes into play:</p>
+
+ <p class="image"><img src="xxdiff_tutorial.png" alt="3-way diff" /></p>
+
+ <p>See the sub-section on <a href=
+ "code_management.html#svn_basic_conflicts">resolving conflicts</a>, or the
+ <cite>xxdiff User's Manual</cite> (click on <em>Help</em>) to guide you
+ through this process. (If you do not want to learn how to use
+ <code>xxdiff</code> now, you can just click on the highlighted line in the
+ left hand column, and select <kbd>Exit with MERGED</kbd> from the
+ <em>File</em> menu. This saves the file you are merging in as the result of
+ the merge, i.e. you have <em>merged</em> the changes).</p>
+
+ <p>On resolving this conflict, you will be asked to run <code>svn
+ resolved</code>. Answer <kbd>Yes</kbd>.</p>
+
+ <p>You are now prompted to try to solve the <em>tree conflict</em>.</p>
+ <pre>
+[info] src/module/hello_num.f90: in tree conflict.
+Locally: modified
+Externally: renamed to src/module/hello_number.f90
+Answer (y) to keep the old name.
+Answer (n) to accept the rename.
+You can then merge in changes.
+Keep the local version?
+Enter "y" or "n" (or just press <return> for "n")
+</pre>
+
+ <p>Entering <samp>y</samp> will keep the file as it is, and entering
+ <samp>n</samp> will accept the external changes. Your problem is that the
+ edit you made to <samp>hello_num.f90</samp> is no longer valid on the trunk,
+ because there the file has been renamed to <samp>hello_number.f90</samp>. To
+ Subversion, it looks like <samp>hello_num.f90</samp> disappeared. Your
+ choices would be either to delete the new file by answering <samp>y</samp>,
+ or incorporate your changes into the new file (<samp>hello_number.f90</samp>)
+ by answering <samp>n</samp>. As the new filename comes from the trunk, we
+ would normally accept it and incorporate changes into it, rather than delete
+ it.</p>
+
+ <p>Answer <samp>n</samp> to accept the renaming of the file and merge in
+ changes. This will occur using <code>xxdiff</code>, as above.</p>
+
+ <p>Exit the <code>xxdiff</code> window as before, with <kbd>Exit with
+ MERGED</kbd>.</p>
+
+ <p>If you now run <code>status</code>, you will notice that these extra
+ conflict files have disappeared, and there are no more <samp>C</samp>
+ filename statuses.</p>
+
+ <p>Example:</p>
+ <pre>
+=> fcm status
+ M .
+? #commit_message#
+M src/subroutine/hello_c.c
+D src/module/hello_num.f90
+A + src/module/hello_number.f90
+</pre>
+
+ <p>You have now resolved all the conflicts.</p>
+
+ <h4>Commit after the merge</h4>
+
+ <p>It is important to remember that the <code>fcm merge</code> command only
+ applies changes to your working copy. Therefore, you must now commit the
+ change in order for it to become permanent in the repository. Similar to
+ other changes, it is a good practice to use <code>fcm diff</code> to inspect
+ the changes before committing.</p>
+
+ <p>When you run <code>fcm commit</code>, you will be prompted to edit the
+ commit log as usual. However, you may notice that a standard template is
+ already provided for you by the <code>fcm merge</code> command. In most
+ cases, the standard message should be sufficient. However, if you want to add
+ extra comment to the commit, please do so <strong>above</strong> the line
+ that says <samp>--Add your commit message ABOVE - do not alter this line or
+ those below--</samp>. This is useful, for example, if there were significant
+ issues addressed in the merge.</p>
+
+ <h3 id="tutorial_review-ticket">Review changes</h3>
+
+ <p><em>For the purpose of this tutorial, we assume that your changes are
+ complete, have been tested and committed to the repository, and are now ready
+ for review. You should assign the ticket to the reviewer and inform him/her
+ where to find the changes you wish him/her to review. The reviewer will
+ record any issues in the ticket, perhaps linking to other documents as
+ required. Once completed, he/she will record the outcome in the ticket and
+ assign it back to the you.</em></p>
+
+ <p>After completing this sub-section, you will learn how to:</p>
+
+ <ul>
+ <li>display changes in a branch, and</li>
+
+ <li>re-assign a ticket.</li>
+ </ul>
+
+ <p>Further reading:</p>
+
+ <ul>
+ <li>Code Management System > Branching & Merging > <a href=
+ "code_management.html#svn_branching_info">Getting Information About
+ Branches</a></li>
+
+ <li>Code Management System > <a href="code_management.html#trac">Using
+ Trac</a></li>
+
+ <li>Code Management Working Practices > <a href=
+ "working_practices.html#tickets">Using Tickets</a></li>
+
+ <li>FCM Command Reference > <a href="command_ref.html#fcm-diff">fcm
+ diff</a></li>
+ </ul>
+
+ <h4>Display changes in a branch</h4>
+
+ <p>Before you ask someone to review your code, it is often a good idea to
+ have a look at the changes one more time. To view the changes in a branch,
+ you can look at all the changes relative to its base.</p>
+
+ <p><dfn>Command line</dfn>: issue the <code>fcm branch-diff
+ --graphical</code> (or simply <code>fcm bdi -g</code>) command.</p>
+
+ <p>You should be presented with the differences between the branch and the
+ trunk (since the last merge).</p>
+
+ <p>Note: you can also use the <code>--trac</code> (<code>-t</code>) option
+ instead of <code>--graphical</code> (<code>-g</code>) to view the changes in
+ a branch using Trac rather than using a graphical diff tool.</p>
+
+ <p><dfn>Command line</dfn>: issue the <code>fcm branch-diff --trac</code> (or
+ simply <code>fcm bdi -t</code>) command.</p>
+
+ <p>Take note of the Trac URL for displaying the differences. The part that
+ begins with <code>diff:</code> is of particular interest to you, as it is a
+ Trac link that can be inserted into a Trac wiki/ticket. In the above example,
+ the Trac link would look like:
+ <samp>diff:/tutorial/trunk at 2///tutorial/branches/dev/matt/r1_133 at 813</samp>.</p>
+
+ <h4>Re-assign a ticket to a reviewer</h4>
+
+ <p>Back in your ticket, add an appropriate comment showing where to find your
+ changes, in the <em>Add/Change</em> box. Include a link to your branch and a
+ diff link (see above) in the comment. For example:</p>
+ <pre>
+The [log:tutorial/branches/dev/matt/r1_133 at 811:813] branch proposes
+changes to the greeting in hello_constants.f90. It also contains some new
+documents. See
+[diff:/tutorial/trunk at 2///tutorial/branches/dev/matt/r1_133 at 813] for the
+changes.
+
+Fred, could you review the change, please?
+</pre>
+
+ <p>Note: the syntax
+ <samp>[log:tutorial/branches/dev/matt/r1_133 at 811:813]</samp> will be
+ translated by Trac into a link to the revision log browser to display the log
+ between revision 811 and 813 of the <samp>branches/dev/matt/r1_133</samp>
+ branch in the <samp>tutorial</samp> project; and the syntax
+ <samp>[diff:/tutorial/trunk at 2///tutorial/branches/dev/matt/r1_133 at 813]</samp>
+ will be translated into a link to display the differences between the trunk
+ at revision 2 and the branch at revision 813. Click on <kbd>Preview</kbd> and
+ check that the links work correctly.</p>
+
+ <p>To re-assign a ticket to your reviewer, click on the <kbd>reassign
+ to</kbd> button in the <em>Action</em> box section and enter the reviewer's
+ User ID.</p>
+
+ <p>When you are ready, click on <kbd>Submit changes</kbd>.</p>
+
+ <h4>Reassign the ticket back to the author</h4>
+
+ <p>For the purpose of this tutorial, you will act as the reviewer of the
+ changes you have made. Following the review, you should record its outcome
+ and re-assign the ticket back to the author. Enter the comment <kbd>No issues
+ were found during the review</kbd>. Click on the <kbd>reassign to</kbd>
+ button in the <em>Action</em> box section, and enter your guest account name.
+ Click on <kbd>Submit changes</kbd> when you are ready.</p>
+
+ <h3 id="tutorial_merge-back">Commit to the trunk</h3>
+
+ <p><em>Your changes in the branch have been tested and reviewed. It is now
+ time to merge and commit it to the trunk. Once you have committed your
+ change, you will close your ticket to complete the work cycle.</em></p>
+
+ <p>After completing this sub-section, you will learn how to:</p>
+
+ <ul>
+ <li>switch a working copy,</li>
+
+ <li>merge and commit your changes into the trunk, and</li>
+
+ <li>close a ticket</li>
+ </ul>
+
+ <p>Further reading:</p>
+
+ <ul>
+ <li>Code Management System > <a href=
+ "code_management.html#svn_branching">Branching & Merging</a></li>
+
+ <li>Code Management Working Practices > <a href=
+ "working_practices.html#branching">Branching & Merging</a></li>
+
+ <li>FCM Command Reference > <a href="command_ref.html#fcm-switch">fcm
+ switch</a></li>
+ </ul>
+
+ <h4>Switch a working copy to point to the trunk</h4>
+
+ <p><dfn>Command line</dfn>: issue the <code>fcm switch</code> (or simply
+ <code>fcm sw</code>) command. E.g.:</p>
+ <pre>
+(SHELL PROMPT)$ fcm sw trunk
+</pre>
+
+ <p><dfn>Command line</dfn>: To check that your working copy is pointing to
+ the trunk, issue the <code>fcm info</code> command.</p>
+
+ <h4>Merge and commit your changes into the trunk</h4>
+
+ <p><dfn>Command line</dfn>: issue the <code>fcm merge</code> command.
+ E.g.</p>
+ <pre>
+(SHELL PROMPT)$ fcm merge branches/dev/matt/r1_133
+</pre>
+
+ <p>Example:</p>
+ <pre>
+Eligible merge(s) from /tutorial/branches/dev/matt/r1_133: 813 812
+Enter a revision (or just press <return> for "813"):
+Merge: /tutorial/branches/dev/matt/r1_133 at 813
+ c.f.: /tutorial/trunk at 2
+-------------------------------------------------------------------------dry-run
+--- Merging differences between repository URLs into '.':
+A doc/new_file.txt
+A doc/add.html
+D doc/hello.html
+U src/module/hello_number.f90
+U src/module/hello_constants.f90
+-------------------------------------------------------------------------dry-run
+Would you like to go ahead with the merge?
+Enter "y" or "n" (or just press <return> for "n"): y
+Merge succeeded.
+</pre>
+
+ <p>Since there is more than one revision available for merging, you will be
+ prompted for the revision number you wish to merge from. The default is the
+ last changed revision of your branch. which is the revision you want to merge
+ with, so you should just proceed with the default.</p>
+
+ <p>Since we merged in the latest changes from the trunk into the branch,
+ there should be no conflicts from this merge.</p>
+
+ <p>Once again, please remember that the merge command only changes your
+ working copy. You need to commit the change before it becomes permanent in
+ the repository. Before you commit to the trunk, however, it is often sensible
+ to have a last look at what you are going to change using the
+ <code>diff</code> command.</p>
+
+ <p>Note: We have set up the repository to prevent any commits to the trunk to
+ preserve the tutorial for other users, so your commit to the trunk will fail.
+ However, you should try doing it any way to complete the exercise.</p>
+
+ <p><dfn>Command line</dfn>: issue the <code>fcm commit</code> (or simply
+ <code>fcm ci</code>) command.</p>
+
+ <p>A text editor will appear to allow you to edit the commit message. You
+ must add a commit message to describe your change <strong>above</strong> the
+ line that says <samp>--Add your commit message ABOVE - do not alter this line
+ or those below--</samp>. Since you are going to commit changes to the trunk,
+ you should provide a useful message, including a link to your ticket. For
+ example:</p>
+ <pre>
+#133: tutorial completed.
+</pre>
+
+ <p>When you are ready, save your change and exit the editor.</p>
+
+ <p>As we have said before, the command will fail when you try to proceed with
+ the commit.</p>
+
+ <h4>Close your ticket</h4>
+
+ <p>As you have completed your work, you should now update and close your
+ ticket. In real life, you will typically include a closing comment with an
+ appropriate Trac wiki link to the changeset in the trunk that fixes the
+ ticket.</p>
+
+ <p>Since you cannot commit to the trunk in the tutorial, you can include a
+ Trac link to the latest changeset in your branch. For example, you can put
+ <samp>r813: fixed.</samp> in the comment. To mark the ticket as
+ <em>fixed</em>, move down to the <em>Action</em> box section, click on
+ <kbd>resolve as</kbd> and choose <kbd>fixed</kbd>. Use <kbd>Preview</kbd> to
+ ensure that your links work correctly. When you are happy, click on
+ <kbd>Submit changes</kbd>.</p>
+
+ <h3 id="tutorial_extra-extract">Extra activities on the extract and build
+ systems</h3>
+
+ <p><em>The extract and build systems are very flexible. If you have time, you
+ may want to explore their uses in more depth.</em></p>
+
+ <p>After completing this sub-section, you will learn how to:</p>
+
+ <ul>
+ <li>extract from a working copy,</li>
+
+ <li>change a compiler flag, and</li>
+
+ <li>extract from a particular branch and/or revision from the
+ repository.</li>
+ </ul>
+
+ <p>Further reading:</p>
+
+ <ul>
+ <li><a href="make.html">FCM Make</a></li>
+ </ul>
+
+ <h4>Extract from a working copy</h4>
+
+ <p>Modify the source files in your working copy and commit the changes back
+ to your branch in the repository. Re-run <code><a href=
+ "command_ref.html#fcm-make">fcm make</a></code> and see the results of the
+ changes.</p>
+
+ <p>In fact, you can test changes in your working copy directly using a
+ similar extract and build mechanism. In such case, you need to modify the
+ <code><a href=
+ "annex_cfg.html#make.extract.location">extract.location</a></code>
+ declaration. For example:</p>
+ <pre>
+extract.location[tutorial] = $HOME/work/tutorial
+</pre>
+
+ <h4>Change a compiler flag</h4>
+
+ <p>Modify the compiler flags, and re-run <code><a href=
+ "command_ref.html#fcm-make">fcm make</a></code> and see the results of the
+ changes. To modify the compiler flags, edit your FCM make configuration file,
+ and add the declarations for changing compiler flags. For example:</p>
+ <pre>
+# Declare extra options for Fortran compiler
+build.prop{fc.flags} = -i8 -O3
+</pre>
+
+ <p>For further information on how to set your compiler flags, please refer to
+ the <a href="make.html">FCM Make</a> > <a href=
+ "make.html#build">Build</a>.</p>
+
+ <h4>Extract from a particular branch and/or revision</h4>
+
+ <p>Try extracting from an earlier revision of your branch. Suppose the HEAD
+ of your branch is revision 813, and the branch was created at an earlier
+ revision. You can extract your branch at, say, revision 811 by adding the
+ following to the <code><a href=
+ "annex_cfg.html#make.extract.location">extract.location</a></code>
+ declaration in your FCM make configuration file:</p>
+ <pre>
+extract.location[tutorial] = branches/dev/$LOGNAME/r1_133 at 811
+</pre>
+
+ <p>You can also try extracting from the trunk. To extract from the
+ <samp>trunk at HEAD</samp>, simply comment out or remove the <code><a href=
+ "annex_cfg.html#make.extract.location">extract.location</a></code>
+ declaration. To extract from a given revision of the trunk, you will need to
+ modify the <code><a href=
+ "annex_cfg.html#make.extract.location">extract.location</a></code>
+ declaration in your FCM make configuration file. For example:</p>
+ <pre>
+extract.location[tutorial] = trunk at 1
+</pre>
+
+ <h3 id="tutorial_delete-branch">Delete your branch</h3>
+
+ <p><em>You should remove your branch when it is no longer required. When you
+ remove it, it becomes invisible from the HEAD revision, but will continue to
+ exist in the repository, should you want to refer to it in the
+ future.</em></p>
+
+ <p>After completing this sub-section, you will learn how to:</p>
+
+ <ul>
+ <li>list branches owned by you, and</li>
+
+ <li>delete a branch.</li>
+ </ul>
+
+ <p>Further reading:</p>
+
+ <ul>
+ <li>Code Management System > Branching & Merging > <a href=
+ "code_management.html#svn_branching_list">Listing Branches Created by You
+ or Other Users</a></li>
+
+ <li>Code Management System > Branching & Merging > <a href=
+ "code_management.html#svn_branching_delete">Deleting Branches</a></li>
+ </ul>
+
+ <h4>List branches owned by you</h4>
+
+ <p>If you forget what your branch is called and/or what other branches you
+ have created, you can get a listing of all the branches you have created in a
+ project.</p>
+
+ <p><dfn>Command line</dfn>: issue the <code>fcm branch-list</code> (or simply
+ <code>fcm bls</code>) command</p>
+
+ <h4>Delete a branch</h4>
+
+ <p>Switch your working copy to point back to your branch. Before you do so,
+ revert any changes you have made in the working copy by issuing the <code>fcm
+ revert -R .</code> command. If a <samp>#commit_message#</samp> file exists,
+ remove it by issuing the <code>rm '#commit_message#'</code> command.</p>
+
+ <p><dfn>Command line</dfn>: issue the <code>fcm switch <URL></code> (or
+ simply <code>fcm sw <URL></code>) command.</p>
+
+ <p>You can continue your work in the branch if you wish, but once you have
+ finished all the work, you should delete it.</p>
+
+ <p><dfn>Command line</dfn>: issue the <code>fcm branch-delete</code> (or
+ simply <code>fcm bdel</code>) command.</p>
+
+ <p>Example:</p>
+ <pre>
+URL: svn://fcm1/tutorial_svn/tutorial/branches/dev/matt/r1_133
+Repository Root: svn://fcm1/tutorial_svn
+Repository UUID: cb858ce8-0f05-0410-9e64-efa98b760b62
+Revision: 813
+Node Kind: directory
+Last Changed Author:
+Last Changed Rev: 813
+Last Changed Date: 2005-11-09 09:11:57 +0000 (Wed, 09 Nov 2005)
+--------------------------------------------------------------------------------
+Branch Create Rev: 811
+Branch Create Date: 2005-11-09 08:34:22 +0000 (Wed, 09 Nov 2005)
+Branch Parent: svn://fcm1/tutorial_svn/tutorial/trunk@1
+--------------------------------------------------------------------------------
+Last Merge From Trunk: /tutorial/branches/dev/matt/r1_133 at 813 /tutorial/trunk at 2
+Avail Merges Into Trunk: 813 812
+[info] gvim -f: starting commit message editor...
+Change summary:
+------------------------------------------------------------------------
+D svn://fcm1/tutorial_svn/tutorial/branches/dev/matt/r1_133
+------------------------------------------------------------------------
+Commit message is as follows:
+------------------------------------------------------------------------
+Deleted tutorial/branches/dev/matt/r1_133.
+------------------------------------------------------------------------
+Would you like to go ahead and delete this branch?
+Enter "y" or "n" (or just press <return> for "n"): y
+Deleting branch svn://fcm1/tutorial_svn/tutorial/branches/dev/matt/r1_133 ...
+
+Committed revision 813.
+</pre>
+
+ <p>You will be prompted to edit the commit message file. A standard template
+ is automatically supplied for the commit. However, if you want to add extra
+ comment for the branch, please do so <strong>above</strong> the line that
+ says <samp>--Add your commit message ABOVE - do not alter this line or those
+ below--</samp>. Save your change and exit the editor.</p>
+
+ <p>Your working copy is now pointing to a branch that no longer exists at the
+ HEAD revision of the repository. If you want to try the tutorial again, you
+ may want to create another branch, and switch your working copy to point to
+ the new branch. Otherwise, you can remove your working copy by issuing a
+ careful <code>rm -rf</code> command.</p>
+
+ <h3 id="tutorial_delete-final-comments">Final comments</h3>
+
+ <p>We have guided you through the basics of the complete change process,
+ using recommended ways of working. Most of the basic and important commands
+ have been covered by the tutorial. (The exceptions are <code>fcm log</code>
+ and <code>fcm update</code>, which you may have to use regularly. For
+ information on these commands, please refer to the section on <a href=
+ "http://svnbook.red-bean.com/en/1.8/svn.tour.history.html#svn.tour.history.log">
+ svn log</a> and <a href=
+ "http://svnbook.red-bean.com/en/1.8/svn.tour.cycle.html#svn.tour.cycle.update">
+ Update Your Working Copy</a> in the <a href=
+ "http://svnbook.red-bean.com/en/1.8/">Subversion book</a>.) You should now be
+ in a position to continue with your development work with FCM. However, if at
+ any time you are unsure about any aspect of using FCM, please consult the
+ relevant section of the <a href="index.html">FCM User Guide</a>.</p>
+
+ <p>Feel free to use the tutorial, at any time, for testing out any aspect of
+ the system. You may wish to do this rather than use your own repository and
+ ticket system, to avoid cluttering them with unwanted junk.</p>
+
+ </div>
+ </div>
+ </div>
+
+ <hr/>
+ <div class="container-fluid text-center">
+ <div class="row-fluid"><div class="span12">
+ <address><small>
+ © British Crown Copyright 2006-14
+ <a href="http://www.metoffice.gov.uk">Met Office</a>.
+ See <a href="../etc/fcm-terms-of-use.html">Terms of Use</a>.<br />
+ This document is released under the British <a href=
+ "http://www.nationalarchives.gov.uk/doc/open-government-licence/" rel=
+ "license">Open Government Licence</a>.<br />
+ </small></address>
+ </div></div>
+ </div>
+
+ <script type="text/javascript" src="../etc/jquery.min.js"></script>
+ <script type="text/javascript" src="../etc/bootstrap/js/bootstrap.min.js"></script>
+ <script type="text/javascript" src="../etc/fcm.js"></script>
+ <script type="text/javascript" src="../etc/fcm-version.js"></script>
+</body>
+</html>
diff --git a/doc/user_guide/gui1.png b/doc/user_guide/gui1.png
new file mode 100644
index 0000000..b22f6af
Binary files /dev/null and b/doc/user_guide/gui1.png differ
diff --git a/doc/user_guide/gui2.png b/doc/user_guide/gui2.png
new file mode 100644
index 0000000..2248f33
Binary files /dev/null and b/doc/user_guide/gui2.png differ
diff --git a/doc/user_guide/index.html b/doc/user_guide/index.html
new file mode 100644
index 0000000..593f06e
--- /dev/null
+++ b/doc/user_guide/index.html
@@ -0,0 +1,102 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>FCM: User Guide</title>
+ <meta name="author" content="FCM team" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link rel="icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link rel="shortcut icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link href="../etc/bootstrap/css/bootstrap.min.css" rel="stylesheet" media="screen" />
+ <link href="../etc/fcm.css" rel="stylesheet" media="screen" />
+</head>
+<body>
+ <div class="navbar navbar-inverse">
+ <div class="navbar-inner">
+ <a class="brand" href=".."><span class="fcm-version">FCM</span></a>
+ <ul class="nav">
+ <li><a href="../installation/">Installation</a></li>
+
+ <li class="active"><a href=".">User Guide</a></li>
+ </ul>
+ </div>
+ </div>
+
+ <div class="page-header">
+ <h1>FCM: User Guide</h1>
+ </div>
+
+ <div class="container">
+ <div class="row">
+ <div class="span12">
+
+ <h2>Contents</h2>
+
+ <ul class="fcm-top-content unstyled">
+ <li><a href="introduction.html">Introduction</a></li>
+
+ <li><a href="overview.html">System Overview</a></li>
+
+ <li><a href="getting_started.html">Getting Started</a></li>
+
+ <li><a href="code_management.html">Code Management System</a></li>
+
+ <li><a href="working_practices.html">Code Management Working
+ Practices</a></li>
+
+ <li><a href="make.html">FCM Make</a></li>
+
+ <li><a href="system_admin.html">System Administration</a></li>
+
+ <li><a href="command_ref.html">FCM Command Reference</a></li>
+
+ <li><a href="api.html">A Brief Introduction to the FCM Perl API</a></li>
+ </ul>
+
+ <p>Annex:</p>
+
+ <ul class="fcm-top-content unstyled">
+ <li><a href="annex_quick_ref.html">Quick reference</a></li>
+
+ <li><a href="annex_quick_ref_tree_conflicts.html">Quick reference: Tree
+ Conflict Resolution</a></li>
+
+ <li><a href="annex_cfg.html">FCM Configuration File</a></li>
+
+ <li><a href="annex_fcm_cfg.html">Declarations in FCM 1 central/user
+ configuration file</a></li>
+
+ <li><a href="extract.html">The FCM 1 Extract System</a></li>
+
+ <li><a href="annex_ext_cfg.html">Declarations in FCM 1 extract
+ configuration file</a></li>
+
+ <li><a href="build.html">The FCM 1 Build System</a></li>
+
+ <li><a href="annex_bld_cfg.html">Declarations in FCM 1 build
+ configuration file</a></li>
+ </ul>
+
+ </div>
+ </div>
+ </div>
+
+ <hr/>
+ <div class="container-fluid text-center">
+ <div class="row-fluid"><div class="span12">
+ <address><small>
+ © British Crown Copyright 2006-14
+ <a href="http://www.metoffice.gov.uk">Met Office</a>.
+ See <a href="../etc/fcm-terms-of-use.html">Terms of Use</a>.<br />
+ This document is released under the British <a href=
+ "http://www.nationalarchives.gov.uk/doc/open-government-licence/" rel=
+ "license">Open Government Licence</a>.<br />
+ </small></address>
+ </div></div>
+ </div>
+
+ <script type="text/javascript" src="../etc/jquery.min.js"></script>
+ <script type="text/javascript" src="../etc/bootstrap/js/bootstrap.min.js"></script>
+ <script type="text/javascript" src="../etc/fcm.js"></script>
+ <script type="text/javascript" src="../etc/fcm-version.js"></script>
+</body>
+</html>
diff --git a/doc/user_guide/introduction.html b/doc/user_guide/introduction.html
new file mode 100644
index 0000000..d5fad9f
--- /dev/null
+++ b/doc/user_guide/introduction.html
@@ -0,0 +1,115 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>FCM: User Guide: Introduction</title>
+ <meta name="author" content="FCM team" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link rel="icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link rel="shortcut icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link href="../etc/bootstrap/css/bootstrap.min.css" rel="stylesheet" media="screen" />
+ <link href="../etc/fcm.css" rel="stylesheet" media="screen" />
+</head>
+<body>
+ <div class="navbar navbar-inverse">
+ <div class="navbar-inner">
+ <a class="brand" href=".."><span class="fcm-version">FCM</span></a>
+ <ul class="nav">
+ <li><a href="../installation/">Installation</a></li>
+
+ <li class="active"><a href=".">User Guide</a></li>
+ </ul>
+ </div>
+ </div>
+
+ <div class="page-header">
+ <div class="fcm-page-content pull-right well well-small"></div>
+ <h1>FCM: User Guide: Introduction</h1>
+ </div>
+
+ <div class="container">
+ <div class="row">
+ <div class="span12">
+
+ <p>This is the User Guide for the <em>Flexible Configuration Management</em>
+ system which is known as <em>FCM</em>. It is designed to tell you everything
+ you need to know if you want to develop code which has been configured within
+ FCM. In addition it also provides the extra information you will need if you
+ are system manager of a project within FCM.</p>
+
+ <p>This guide consists of the following sections:</p>
+
+ <dl>
+ <dt><a href="overview.html">System Overview</a></dt>
+
+ <dd>A brief description of the main features of FCM.</dd>
+
+ <dt><a href="getting_started.html">Getting Started</a></dt>
+
+ <dd>How to start using FCM. It includes a tutorial for you to work through
+ and familiarise yourself with some FCM activities.</dd>
+
+ <dt><a href="code_management.html">Code Management System</a></dt>
+
+ <dd>How to use the code management system to manage code changes.</dd>
+
+ <dt><a href="working_practices.html">Code Management Working
+ Practices</a></dt>
+
+ <dd>Recommended ways of working with the code management system.</dd>
+
+ <dt><a href="make.html">FCM Make</a></dt>
+
+ <dd>How to use the make command to invoke the extract and build systems.</dd>
+
+ <dt><a href="system_admin.html">System Administration</a></dt>
+
+ <dd>How to configure and maintain a new system within FCM.</dd>
+
+ <dt><a href="command_ref.html">FCM Command Reference</a></dt>
+
+ <dd>Detailed information about each of the <code>fcm</code> commands.</dd>
+
+ <dt>Annex:</dt>
+
+ <dd>
+ <dl>
+ <dt><a href="annex_quick_ref.html">Quick reference</a></dt>
+
+ <dd>A quick reference to many useful FCM code management system
+ commands.</dd>
+
+ <dt><a href="annex_cfg.html">FCM Configuration File</a></dt>
+
+ <dd>Detailed definitions of what declarations are allowed in
+ different FCM configuration files.</dd>
+ </dl>
+
+ <p>The annex also contains further sections relating to some deprecated
+ features.</p>
+ </dd>
+ </dl>
+
+ </div>
+ </div>
+ </div>
+
+ <hr/>
+ <div class="container-fluid text-center">
+ <div class="row-fluid"><div class="span12">
+ <address><small>
+ © British Crown Copyright 2006-14
+ <a href="http://www.metoffice.gov.uk">Met Office</a>.
+ See <a href="../etc/fcm-terms-of-use.html">Terms of Use</a>.<br />
+ This document is released under the British <a href=
+ "http://www.nationalarchives.gov.uk/doc/open-government-licence/" rel=
+ "license">Open Government Licence</a>.<br />
+ </small></address>
+ </div></div>
+ </div>
+
+ <script type="text/javascript" src="../etc/jquery.min.js"></script>
+ <script type="text/javascript" src="../etc/bootstrap/js/bootstrap.min.js"></script>
+ <script type="text/javascript" src="../etc/fcm.js"></script>
+ <script type="text/javascript" src="../etc/fcm-version.js"></script>
+</body>
+</html>
diff --git a/doc/user_guide/make.html b/doc/user_guide/make.html
new file mode 100644
index 0000000..f4d3fee
--- /dev/null
+++ b/doc/user_guide/make.html
@@ -0,0 +1,2302 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>FCM: User Guide: FCM Make</title>
+ <meta name="author" content="FCM team" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link rel="icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link rel="shortcut icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link href="../etc/bootstrap/css/bootstrap.min.css" rel="stylesheet" media="screen" />
+ <link href="../etc/fcm.css" rel="stylesheet" media="screen" />
+</head>
+<body>
+ <div class="navbar navbar-inverse">
+ <div class="navbar-inner">
+ <a class="brand" href=".."><span class="fcm-version">FCM</span></a>
+ <ul class="nav">
+ <li><a href="../installation/">Installation</a></li>
+
+ <li class="active"><a href=".">User Guide</a></li>
+ </ul>
+ </div>
+ </div>
+
+ <div class="page-header">
+ <div class="fcm-page-content pull-right well well-small"></div>
+ <h1>FCM: User Guide: FCM Make</h1>
+ </div>
+
+ <div class="container">
+ <div class="row">
+ <div class="span12">
+
+ <h2 id="introduction">Introduction</h2>
+
+ <p>The FCM make system provides a common environment for running the extract
+ system, build system, and other utilities. Simply put, it controls a chain of
+ steps. It is NOT to be confused with the standard Unix utility
+ <code>make</code>.</p>
+
+ <p>See <a href="annex_cfg.html#make">Annex: FCM Configuration File > FCM
+ Make Configuration</a> for a full list of declarations in an FCM Make
+ configuration file.</p>
+
+ <h2 id="concept">FCM Make: Concept</h2>
+
+ <p>The FCM make system can be invoked using the command line interface:</p>
+ <pre>
+fcm make [OPTIONS] [DECLARAION ...]
+</pre>
+
+ <p>The FCM make system relies on a configuration file to tell it what to do.
+ It looks for configurations with the following logic:</p>
+
+ <ol>
+ <li>It reads each configuration file specified using the
+ <code>--config-file=PATH</code> option in the order they are
+ specified.</li>
+
+ <li>If the <code>--config-file=PATH</code> option is not specified, it will
+ attempt to read <code>fcm-make.cfg</code> if it exists.</li>
+
+ <li>It looks at the current working directory, or the directory specified
+ in the <code>--directory=PATH</code> option for configuration files
+ specified as relative paths.</li>
+
+ <li>If one or more <code>--config-file-path=PATH</code> option is
+ specified, it also searches for configuration files under the specified
+ values, in the order the options are specified.</li>
+
+ <li>Finally, each <var>KEY=VALUE</var> command line argument is considered
+ a configuration declaration line.</li>
+ </ol>
+
+ <p>For details of the other command line options please see <a href=
+ "command_ref.html#fcm-make">FCM Command Reference > fcm make</a>.</p>
+
+ <p>The FCM make configuration file is expected to be an FCM configuration
+ file. (See <a href="annex_cfg.html">Annex: FCM Configuration File</a>.) A
+ typical FCM make configuration file may look like:</p>
+ <pre>
+steps = extract build # 1
+
+extract.ns = egg ham bacon # 2
+# ... more extract configuration
+
+build.target = egg.exe ham.exe bacon # 3
+# ... more build configuration
+</pre>
+
+ <p>At point 1, the <code><a href="annex_cfg.html#make.steps">steps</a></code>
+ declaration tells the system to invoke two steps. Their IDs are
+ <samp>extract</samp> and <samp>build</samp>, both are IDs for built-in
+ systems. (The other 2 being <samp>mirror</samp> and <samp>preprocess</samp>.)
+ Each step has its own declarations, which are prefixed with the step ID and a
+ full stop. The declarations at point 2 are used by the logic for running the
+ <samp>extract</samp> step and the declarations at point 3 are used by the
+ logic for running the <samp>build</samp> step.</p>
+
+ <p>There are times when you may need to invoke the same system (e.g. the
+ build system) in slightly different configurations. To do this you need to
+ use the <code><a href="annex_cfg.html#make.step.class">step.class</a></code>
+ declaration to define custom step IDs for each build. E.g.:</p>
+ <pre>
+step.class[build-this build-that] = build # 1
+steps = extract build-this build-that # 2
+
+extract.ns = egg ham bacon # 3
+# ... more extract configuration
+
+build-this.prop{fc.defs} = DEFS TO BUILD THIS # 4
+build-this.target = this.exe
+# ... more build configuration for "build-this"
+
+build-that.prop{fc.defs} = DEFS TO BUILD THAT # 5
+build-that.target = that.exe
+# ... more build configuration for "build-that"
+</pre>
+
+ <p>At point 1, we define the IDs <samp>build-this</samp> and
+ <samp>build-that</samp> to be an instance of the <samp>build</samp> class. At
+ point 2, we tell the system to run <samp>extract</samp>, then
+ <samp>build-this</samp>, then <samp>build-that</samp>. At point 4 and 5, we
+ define the configurations for <samp>build-this</samp> and
+ <samp>build-that</samp> respectively.</p>
+
+ <dl>
+ <dt>Directory Structure</dt>
+
+ <dd>
+ <p>When you run <code>fcm make</code>, it will create a directory
+ structure that may look like:</p>
+ <pre>
+.fcm-make/...
+extract/...
+build/...
+...
+</pre>
+
+ <p>Each normal sub-directory, such as <samp>extract/</samp> and
+ <samp>build/</samp>, contains the result of each step. The hidden
+ sub-directory <samp>.fcm-make/</samp> is used by <code>fcm make</code> as
+ a working area. It may contain the following items:</p>
+
+ <p><samp>.fcm-make/cache/</samp> An area for caching non-local items.
+ E.g. the extract system exports source trees from the version control
+ system into this cache.</p>
+
+ <p><samp>fcm-make-as-parsed.cfg ->
+ .fcm-make/config-as-parsed.cfg</samp> The configuration file as
+ parsed by the latest <code>fcm make</code> command at this
+ destination.</p>
+
+ <p><samp>fcm-make-on-success.cfg ->
+ .fcm-make/config-on-success.cfg</samp> The configuration file
+ that can be used to repeat the latest successful <code>fcm make</code>
+ command at this destination.</p>
+
+ <p><samp>.fcm-make/ctx.gz</samp> A serialised data structure that
+ represents the context of the latest <code>fcm make</code> command at
+ this destination. It is a <code>gzip</code> file containing data in the
+ <a href="http://perldoc.perl.org/Storable.html">Perl Storable</a>
+ format.</p>
+
+ <p><samp>fcm-make.log -> .fcm-make/log</samp>
+ A diagnostic log generated by the latest
+ <code>fcm make</code> command at this destination. The content should be
+ equivalent to the diagnostic output in STDOUT and STDERR at
+ <code>-vv</code> verbosity.</p>
+ </dd>
+ </dl>
+
+ <h2 id="extract">Extract</h2>
+
+ <p>The extract system provides an interface between the version control
+ system (currently Subversion) and the build system. Where appropriate, it
+ extracts code and combines changes from the repositories and other
+ user-defined locations to a directory tree suitable for feeding into the build
+ system.</p>
+
+ <p>The extract system supports the <code>--jobs=N</code> option of the
+ <code>fcm make</code> command. It uses <var>N</var> child processes to get
+ source trees information and to export source trees from their repositories in
+ parallel.</p>
+
+ <h3 id="extract.basic">Extract: Basic</h3>
+
+ <p>The following is an example of how to extract the source trees from a file
+ system path and the trunks of 2 known projects (version controlled in
+ Subversion repositories with a standard FCM layout):</p>
+ <pre>
+steps = extract # line 1
+extract.ns = ops var um # line 2
+extract.location[ops] = $HOME/ops # line 3
+</pre>
+
+ <p>Here is an explanation of what each line does:</p>
+
+ <ul>
+ <li><dfn>line 1</dfn>: the <code><a href=
+ "annex_cfg.html#make.steps">steps</a></code> declaration tells the make
+ system to invoke the extract system.</li>
+
+ <li><dfn>line 2</dfn>: the <code><a href=
+ "annex_cfg.html#make.extract.ns">extract.ns</a></code> declaration is used
+ to specify a space delimited list of names of the projects to extract.</li>
+
+ <li><dfn>line 3</dfn>: the <code><a href=
+ "annex_cfg.html#make.extract.location">extract.location</a></code>
+ declaration is used to specify the base source tree of a project
+ (<samp>ops</samp> in this case) to extract. The value of the declaration
+ can be a path in the file system or a location in a version control system.
+ In this case, the declaration specifies the base source tree of the
+ <samp>ops</samp> project to be a path in the file system at
+ <samp>$HOME/ops</samp>. If the base source tree of a specified project
+ name-space is not declared, the system will make an assumption. For a
+ project hosted in a Subversion repository, the system will assume
+ <samp>trunk at HEAD</samp> if the project URL is registered in the keyword
+ configuration file or with the <code><a href=
+ "annex_cfg.html#make.extract.location.primary">extract.location{primary}</a></code>
+ declaration (see below).</li>
+ </ul>
+
+ <p>If you save this file as <samp>fcm-make.cfg</samp> and invoke the
+ <code>fcm make</code> command you should end up with a source tree in the
+ current working directory that looks like (hidden path not shown):</p>
+ <pre>
+extract/ops/...
+extract/um/...
+extract/var/...
+</pre>
+
+ <p>The result of the extract can be found in the <samp>extract/</samp>
+ sub-directory. Note: The generated source tree will not contain any symbolic
+ links or hidden files (e.g. file names beginning with a <samp>.</samp>),
+ because the extract system ignores them.</p>
+
+ <div class="well">
+ <p><strong><i class="icon-pencil"></i> Note - project name-space and
+ location</strong></p>
+
+ <p>Whoever installed FCM at your site would have defined the name-spaces
+ and the locations of the common projects at your site using the keyword
+ configuration file at <samp>$FCM/etc/fcm-keyword.cfg</samp> (where
+ <samp>$FCM/bin</samp> is the path at which FCM is installed).
+ Alternatively, users can define their own set of project name-spaces and
+ their locations using <samp>$HOME/.metomi/fcm/keyword.cfg</samp>. For
+ information on keyword configuration files, please refer to <a href=
+ "system_admin.html#fcm-keywords">System Administration > FCM
+ keywords</a>. If a project name-space is not defined in a keyword
+ configuration, it can be defined in the FCM make configuration using the
+ <code><a href=
+ "annex_cfg.html#make.extract.location.primary">extract.location{primary}</a></code>
+ declaration. E.g. to define the location of the <samp>foo</samp> project,
+ you can do: <samp>extract.location{primary}[foo] =
+ svn://server/repos/foo</samp>.</p>
+ </div>
+
+ <div class="well">
+ <p><strong><i class="icon-pencil"></i> Note - incremental mode</strong></p>
+
+ <p>Suppose you have already invoked <code>fcm make</code> using the above
+ configuration file. At a later time, you have made some changes to some of
+ the files in the source trees. Re-running <code>fcm make</code> on the same
+ configuration will trigger the incremental mode. In the incremental mode,
+ the extract system will update only those files that are modified. If the
+ last modified time (or last commit revision) of a source file in the
+ current extract differs from that in the previous extract, the system will
+ attempt a content comparison. The system updates the destination only if
+ the content and/or file access permission of the source differs from that
+ of the destination. To avoid the incremental mode and start afresh, invoke
+ <code>fcm make</code> with the <code>--new</code> option.</p>
+ </div>
+
+ <h3 id="extract.location-types">Extract: Location Types</h3>
+
+ <p>The extract system currently supports the following location types:</p>
+
+ <dl class="dl-horizontal">
+ <dt>fs</dt>
+
+ <dd>A readable location in the local file system. E.g.
+ <code>~/my-project</code>, <code>/home/lily/my-project</code>, etc.</dd>
+
+ <dt>ssh</dt>
+
+ <dd>A readable location in the file system of a remote host accessible via
+ <code>ssh</code> and <code>rsync</code>, specified in the form
+ <var>[USER@]HOST:PATH</var>. The <code>ssh</code> access must be
+ without a password, and you must be able to run the <a href=
+ "http://www.gnu.org/software/coreutils/">GNU coreutils</a> version of the
+ <code>find</code> and <code>stat</code> on the remote host. E.g.
+ <code>mylinuxbox:my-project</code>,
+ <code>holly at hpc:/data/holly/my-project</code>.</dd>
+
+ <dt>svn</dt>
+
+ <dd>A Subversion location, which may be a working copy or a valid Subversion
+ URL. Supported URL schemes are: <code>file</code>, <code>http</code>,
+ <code>https</code>, <code>svn</code> and <code>svn+ssh</code>. E.g.
+ <code>svn://mysvnhost/my-project/trunk@40778</code>,
+ <code>~/my-project at 32734</code>.</dd>
+ </dl>
+
+ <p>See also <a href=
+ "annex_cfg.html#make.extract.location">extract.location</a>.</p>
+
+ <h3 id="extract.path-root">Extract: Redefine the Root of the Source Tree of a
+ Project</h3>
+
+ <p>Consider a project called <samp>foo</samp> with a source tree that looks
+ like:</p>
+ <pre>
+doc/...
+src/bar/...
+src/baz/...
+</pre>
+
+ <p>Suppose you are only interested in the contents of the <samp>src/</samp>
+ sub-tree. You can specify the root of the extract using the <code><a href=
+ "annex_cfg.html#make.extract.path-root">extract.path-root</a></code>
+ declaration. E.g.:</p>
+ <pre>
+steps = extract # line 1
+extract.ns = foo # line 2
+extract.path-root[foo] = src # line 3
+</pre>
+
+ <p>Running <code>fcm make</code> with this configuration should give a source
+ tree that looks like (hidden path not shown):</p>
+ <pre>
+extract/foo/bar/...
+extract/foo/baz/...
+</pre>
+
+ <h3 id="extract.path-filter">Extract: Filter the Paths in the Source Tree of
+ a Project</h3>
+
+ <p>Going back to the above source tree in the <samp>foo</samp> project,
+ imagine the <samp>src/bar/</samp> sub-directory contains:</p>
+ <pre>
+src/bar/wine/red.c
+src/bar/wine/rose.c
+src/bar/wine/white.c
+src/bar/...
+</pre>
+
+ <p>If you do not want <samp>src/bar/wine/rose.c</samp> in the extract, you
+ can ask for it to be excluded using the <code><a href=
+ "annex_cfg.html#make.extract.path-excl">extract.path-excl</a></code>
+ declaration. E.g.:</p>
+ <pre>
+steps = extract # line 1
+extract.ns = foo
+extract.path-root[foo] = src # line 3
+extract.path-excl[foo] = bar/wine/rose.c # line 4
+</pre>
+
+ <p>Note: Because the root is redefined in line 3, the path in the
+ <code><a href=
+ "annex_cfg.html#make.extract.path-excl">extract.path-excl</a></code>
+ declaration in line 4 is declared from the new root level.</p>
+
+ <p>Running <code>fcm make</code> with this configuration should give a source
+ tree that looks like (hidden path not shown):</p>
+ <pre>
+extract/foo/bar/wine/red.c
+extract/foo/bar/wine/white.c
+extract/foo/bar/...
+extract/foo/baz/...
+</pre>
+
+ <p>On the other hand, if you only want <samp>src/bar/wine/rose.c</samp> in
+ the extract, you can ask the system to exclude everything in
+ <samp>src/bar/wine/</samp> but include <samp>src/bar/wine/rose.c</samp> using
+ the <code><a href=
+ "annex_cfg.html#make.extract.path-incl">extract.path-incl</a></code>
+ declaration. E.g.:</p>
+ <pre>
+steps = extract # line 1
+extract.ns = foo # line 2
+extract.path-root[foo] = src # line 3
+extract.path-excl[foo] = bar/wine # line 4
+extract.path-incl[foo] = bar/wine/rose.c # line 5
+</pre>
+
+ <p>Running <code>fcm make</code> with this configuration should give a source
+ tree that looks like (hidden path not shown):</p>
+ <pre>
+extract/foo/bar/wine/rose.c
+extract/foo/bar/...
+extract/foo/baz/...
+</pre>
+
+ <h3 id="extract.location-diff">Extract: Combining Changes from Multiple
+ Branches of a Project</h3>
+
+ <p>We have so far only dealt with extracts from a single base source tree in
+ each project. The extract system can also be used to <em>combine</em> changes
+ from different source trees (against a base source tree) of a project.</p>
+
+ <p>E.g. consider a project called <samp>food</samp>. In the latest release
+ (<samp>trunk at 3739</samp>), the source tree looks like this:</p>
+ <pre>
+doc/...
+src/egg/boiled.y
+src/egg/microwave.x
+src/egg/omelette.c
+src/egg/poached.f90
+src/...
+</pre>
+
+ <p>Jamie, Gordon and Rick are all developing changes against the latest
+ release of the project.</p>
+
+ <p>Jamie has made the following changes (displayed using the notation of
+ <code>svn status</code>) in his branch at
+ <samp>branches/dev/jamie/r3739_t381 at 3984</samp>:</p>
+ <pre>
+D src/egg/boiled.y
+A src/egg/fried.pl
+M src/egg/omelette.c
+</pre>
+
+ <p>Gordon has made the following changes in his branch at
+ <samp>branches/dev/gordon/r3739_t376 at 3993</samp>:</p>
+ <pre>
+M src/egg/omelette.c
+M src/egg/poached.f90
+</pre>
+
+ <p>Rick has made the following changes in his working copy at
+ <samp>~rick/food</samp>, but has yet to commit his changes back:</p>
+ <pre>
+M src/egg/boiled.y
+A src/egg/scrambled.bash
+</pre>
+
+ <p>To combine their changes in an extract, the FCM make configuration file
+ should look like:</p>
+ <pre id="example.extract">
+steps = extract
+extract.ns = food
+extract.location[food] = trunk at 3739
+extract.location{diff}[food] = \
+ branches/dev/jamie/r3739_t381 at 3984 \
+ branches/dev/gordon/r3739_t376 at 3993 \
+ ~rick/food
+</pre>
+
+ <p>Here we have an <code><a href=
+ "annex_cfg.html#make.extract.location">extract.location</a></code>
+ declaration like before. This time it is pointing to the latest release of
+ the <samp>food</samp> project. The changes against the base source tree are
+ declared using the <code><a href=
+ "annex_cfg.html#make.extract.location.diff">extract.location{diff}</a></code>
+ declaration.</p>
+
+ <p>Invoking <code>fcm make</code> with this configuration file will result in
+ a source tree that looks like (hidden file not shown):</p>
+ <pre>
+extract/doc/...
+extract/src/egg/fried.pl
+extract/src/egg/microwave.x
+extract/src/egg/omelette.c
+extract/src/egg/poached.f90
+extract/src/egg/scrambled.bash
+extract/src/...
+</pre>
+
+ <p>Note:</p>
+
+ <ul>
+ <li>There is no <samp>extract/src/egg/boiled.y</samp> file in the extract
+ tree because it is deleted by Jamie's branch. Even though this file is
+ modified in Rick's working copy, the extract will ignore this and assume
+ that the deletion takes precedence.</li>
+
+ <li>The <samp>extract/src/egg/fried.pl</samp> file comes from Jamie's
+ branch.</li>
+
+ <li>The <samp>extract/src/egg/microwave.x</samp> file comes from the latest
+ release (base).</li>
+
+ <li>The <samp>extract/src/egg/omelette.c</samp> file is the result of a
+ merge of the changes in Jamie's and Gordon's branches. This example assumes
+ that there is no conflict in the merge. If the merge results in a conflict,
+ the extract will fail.</li>
+
+ <li>The <samp>extract/src/egg/poached.f90</samp> file comes from Gordon's
+ branch.</li>
+
+ <li>The <samp>extract/src/egg/scrambled.bash</samp> file comes from Rick's
+ working copy.</li>
+ </ul>
+
+ <h3 id="extract.inherit">Extract Inheritance</h3>
+
+ <p>If a previous extract with a similar configuration exists in another
+ location, it can be more efficient to inherit from this previous extract in
+ your current extract. This works like a normal incremental extract, except
+ that your extract will only contain the changes you have specified (compared
+ with the inherited extract) instead of the full directory tree in the
+ destination. This type of incremental extract is useful in several ways. For
+ instance:</p>
+
+ <ul>
+ <li>It is fast, because you only have to extract files that you have
+ changed.</li>
+
+ <li>The subsequent build will also be fast, since it will act like an
+ incremental build.</li>
+
+ <li>You do not need write access to the original extract. A system
+ administrator can set up a stable version in a central account, which
+ developers can then inherit from.</li>
+
+ <li>You want an incremental extract, but you need to leave the original
+ extract unmodified.</li>
+ </ul>
+
+ <p>Consider the <a href="#example.extract">previous example</a>. Imagine an
+ extract already exists for the latest release for the <samp>food</samp>
+ project at <samp>/home/food/latest/</samp>, and now you want to test the
+ changes introduced by Jamie, Gordon and Rick. You can <code><a href=
+ "annex_cfg.html#make.use">use</a></code> the original extract with the
+ changes:</p>
+ <pre>
+use = /home/food/latest
+extract.location{diff}[food] = \
+ branches/dev/jamie/r3739_t381 at 3984 \
+ branches/dev/gordon/r3739_t376 at 3993 \
+ ~rick/food
+</pre>
+
+ <p>Invoking <code>fcm make</code> with this configuration file will result in
+ a source tree that looks like (hidden file not shown):</p>
+ <pre>
+extract/src/egg/fried.pl
+extract/src/egg/omelette.c
+extract/src/egg/poached.f90
+extract/src/egg/scrambled.bash
+</pre>
+
+ <p>The extract will work in an incremental-like mode. The only difference is
+ that the original extract at <samp>/home/food/latest/</samp> will be left
+ untouched, and the new extract will contain only the changes introduced by
+ the diff locations. Note that, although
+ <samp>extract/src/egg/boiled.y</samp> remains in the original extract, it
+ will not be used in any subsequent build step.</p>
+
+ <dl>
+ <dt>Extract inheritance limitation</dt>
+
+ <dd>
+ <p>Extract inheritance allows you to add more diff locations to a project,
+ but you should not include any other declarations relating to the extract.
+ Doing so is not safe and should trigger an exception.</p>
+
+ <p>In some situations this implies that it will not be possible to use
+ inherited extracts. You should use a new extract if, for instance, a new
+ diff location contains a change which requires the use of source files in
+ a previously excluded name-space.</p>
+ </dd>
+ </dl>
+
+ <h3 id="extract.diagnostic">Extract Diagnostic</h3>
+
+ <p>The amount of diagnostic messages generated by the extract system is
+ dependent on the diagnostic verbosity level that can be modified by the
+ <code>-v</code> and <code>-q</code> options to the <code>fcm make</code>
+ command.</p>
+
+ <p>The following is a list of diagnostic output at each verbosity level:</p>
+
+ <dl>
+ <dt>-q</dt>
+
+ <dd>
+ <ul>
+ <li>Exceptions.</li>
+ </ul>
+ </dd>
+
+ <dt>default</dt>
+
+ <dd>
+ <ul>
+ <li>Everything at the -q level.</li>
+
+ <li>Start time of the extract.</li>
+
+ <li>The number and location of each source tree in each project. The
+ base source tree is number 0. E.g.:
+ <pre>
+[info] location um: 0: svn://fcm2/UM_svn/UM/trunk@11732
+[info] location um: 1: svn://fcm2/UM_svn/UM/branches/dev/Share/VN7.3_hg3_dust_443@11858
+[info] location um: 2: svn://fcm2/UM_svn/UM/branches/dev/Share/VN7.3_hg3_ccw_precip@11857
+[info] location um: 3: svn://fcm2/UM_svn/UM/branches/dev/hadco/VN7.3_HG3_porting_lsp_fixes@12029
+...
+</pre>
+ </li>
+
+ <li>The number of targets by their destination status and their source
+ status. E.g.:
+ <pre>
+[info] dest: 3 [A added]
+[info] dest: 134 [a added, overriding inherited]
+[info] dest: 6 [d deleted, overriding inherited]
+[info] dest: 2818 [U unchanged]
+[info] source: 3 [A added by a diff source tree]
+[info] source: 6 [D deleted by a diff source tree]
+[info] source: 16 [G merged from 2+ diff source trees]
+[info] source: 118 [M modified by a diff source tree]
+[info] source: 2818 [U from base]
+</pre>
+ </li>
+
+ <li>Total time.</li>
+ </ul>
+ </dd>
+
+ <dt>-v</dt>
+
+ <dd>
+ <ul>
+ <li>Everything at the default level.</li>
+
+ <li>The destination and source status for each modified target. E.g.:
+ <pre>
+...
+[info] aM um:0,13 atmosphere/short_wave_radiation/r2_lwrad3c.F90
+[info] aG um:0,6,13 control/top_level/scm_main.F90
+[info] AA um:-,8 include/constant/cnv_parc_lim.h
+...
+</pre>
+
+ <p>The 2 letters following <samp>[info]</samp> is the destination
+ status and the source status of the target. This is followed by the
+ name-space of the project, a colon and the source tree number(s)
+ providing the source for the target (in a comma separated list). This
+ is then followed by the name-space of the target path. The source
+ tree number <samp>0</samp> denotes the base source tree, a dash
+ <samp>-</samp> in place of a <samp>0</samp> means that the source
+ only exists in a diff source tree.</p>
+ </li>
+ </ul>
+ </dd>
+
+ <dt>-vv</dt>
+
+ <dd>
+ <ul>
+ <li>Everything at the -v level.</li>
+
+ <li>Each shell command invoked with elapsed time and return code.</li>
+ </ul>
+ </dd>
+ </dl>
+
+ <p>Here is an explanation of each target destination status:</p>
+
+ <dl>
+ <dt><samp>[A added]</samp></dt>
+
+ <dd>Target newly added to the destination.</dd>
+
+ <dt><samp>[a added, overriding inherited]</samp></dt>
+
+ <dd>Target added to the current extract destination, i.e. modified compared
+ with the target with the same name-space in an inherited extract
+ destination.</dd>
+
+ <dt><samp>[D deleted]</samp></dt>
+
+ <dd>Target deleted from the extract destination during an incremental
+ extract.</dd>
+
+ <dt><samp>[d deleted, overriding inherited]</samp></dt>
+
+ <dd>Target deleted from the current extract destination, i.e. a target with
+ the same name-space exists in an inherited extract destination.</dd>
+
+ <dt><samp>[M modified]</samp></dt>
+
+ <dd>Target modified in the extract destination during an incremental
+ extract.</dd>
+
+ <dt><samp>[U unchanged]</samp></dt>
+
+ <dd>Target unchanged compared with the previous (or any inherited)
+ extract.</dd>
+
+ <dt><samp>[? unknown]</samp></dt>
+
+ <dd>Target does not have a destination. This destination status is normally
+ associated with the <samp>[D deleted]</samp> source status.</dd>
+ </dl>
+
+ <p>Here is an explanation of each target source status:</p>
+
+ <dl>
+ <dt><samp>[A added by a diff source tree]</samp></dt>
+
+ <dd>The source of the target comes from a diff source tree, and the base
+ source tree does not have a source in the same name-space.</dd>
+
+ <dt><samp>[D deleted by a diff source tree]</samp></dt>
+
+ <dd>Target is deleted by a diff source tree, (i.e. exists in base source
+ tree, but missing from a diff source tree).</dd>
+
+ <dt><samp>[G merged from 2+ diff source trees]</samp></dt>
+
+ <dd>The source of the target comes from 2 or more diff source trees, (i.e.
+ the actual source is the result of a merge between all the changes).</dd>
+
+ <dt><samp>[M modified by a diff source tree]</samp></dt>
+
+ <dd>The source of the target comes from a diff source tree.</dd>
+
+ <dt><samp>[U from base]</samp></dt>
+
+ <dd>The source of the target comes from the base source tree, (i.e. the
+ source is unchanged by any diff source tree).</dd>
+
+ <dt><samp>[? unknown]</samp></dt>
+
+ <dd>The target has no source. This source status is normally displayed in an
+ incremental extract, where a target in a previous extract is not a target in
+ the current extract, and is normally associated with the <samp>[D
+ deleted]</samp> destination status.</dd>
+ </dl>
+
+ <h2 id="mirror">Mirror</h2>
+
+ <p>The mirror system provides a way to mirror the results of the make steps
+ to another location, where the FCM make may need to continue. It is typically
+ used after an extract to set up the build on an alternate platform.</p>
+
+ <h3 id="mirror.basic">Mirror: Basic</h3>
+
+ <p>Consider the following example:</p>
+ <pre>
+steps = extract mirror # 1
+# ... some extract declarations
+mirror.target = user at somewhere:/path/in/somewhere # 2
+mirror.prop{config-file.steps} = preprocess build # 3
+# ... some preprocess declarations
+# ... some build declarations
+</pre>
+
+ <p>When the system runs with this configuration, the system will mirror the
+ result of the extract to the <a href=
+ "annex_cfg.html#make.mirror.target">mirror.target</a>, i.e.
+ <samp>user at somewhere:/path/in/somewhere</samp> using <code>ssh</code> and
+ <code>rsync</code>. With the <a href=
+ "annex_cfg.html#make.mirror.prop.config-file.steps">config-file.steps</a>
+ property set at point 3, it will write a <samp>fcm-make.cfg</samp> in the
+ mirror target so that the <samp>preprocess</samp> and <samp>build</samp>
+ steps can continue there.</p>
+
+ <h3 id="mirror.diagnostic">Mirror Diagnostic</h3>
+
+ <p>The amount of diagnostic messages generated by the mirror system is
+ dependent on the diagnostic verbosity level that can be modified by the
+ <code>-v</code> and <code>-q</code> options to the <code>fcm make</code>
+ command.</p>
+
+ <p>The following is a list of diagnostic output at each verbosity level:</p>
+
+ <dl>
+ <dt>-q</dt>
+
+ <dd>
+ <ul>
+ <li>Exceptions.</li>
+ </ul>
+ </dd>
+
+ <dt>default</dt>
+
+ <dd>
+ <ul>
+ <li>Everything at the -q level.</li>
+
+ <li>Start time of the mirror.</li>
+
+ <li>The updated destination and its source.</li>
+
+ <li>Total time.</li>
+ </ul>
+ </dd>
+
+ <dt>-v</dt>
+
+ <dd>
+ <ul>
+ <li>Everything at the default level.</li>
+ </ul>
+ </dd>
+
+ <dt>-vv</dt>
+
+ <dd>
+ <ul>
+ <li>Everything at the -v level.</li>
+
+ <li>Each shell command invoked with elapsed time and return code.</li>
+ </ul>
+ </dd>
+ </dl>
+
+ <h2 id="build">Build</h2>
+
+ <p>The build system performs actions on a set of source files using its
+ predefined logic and the properties specified in the configuration. For
+ example, it will attempt to create a binary executable for a source file
+ containing a Fortran program.</p>
+
+ <p>The build system supports the <code>--jobs=N</code> option of the <code>fcm
+ make</code> command. It uses <var>N</var> child processes to analyse the
+ source files and to update the targets in parallel.</p>
+
+ <h3 id="build.basic">Build: Basic</h3>
+
+ <p>Consider a source tree at <samp>$HOME/my-source-tree/</samp> containing
+ some Fortran source files including at least one with a main program (and
+ maybe other supported types of source files), you can set up an FCM make
+ configuration file to build an executable. E.g.:</p>
+ <pre>
+steps = build
+build.target{task} = link
+build.source = $HOME/my-source-tree
+</pre>
+
+ <p>In this simple 3 line configuration, the <code><a href=
+ "annex_cfg.html#make.steps">steps</a></code> declaration tells the make
+ system to invoke the build system, the <code><a href=
+ "annex_cfg.html#make.build.target">build.target</a></code> declaration tells
+ the system to build any targets which require linking (i.e. any main
+ programs), and the <code><a href=
+ "annex_cfg.html#make.build.source">build.source</a></code> declaration
+ specifies the location of the source tree.</p>
+
+ <p>If you save this file as <samp>fcm-make.cfg</samp> and invoke the
+ <code>fcm make</code> command it should attempt to build the source tree in
+ the current working directory, using the default properties. If the default
+ Fortran compiler <samp>gfortran</samp> is installed, and nothing goes wrong,
+ you will end up with a directory tree that looks like (hidden path not
+ shown):</p>
+ <pre>
+build/bin/...
+build/include/...
+build/o/...
+</pre>
+
+ <p>The result of the build can be found in the sub-directories of the
+ <samp>build/</samp> sub-directory. Each <samp>build/*/</samp> sub-directory
+ contains a category of targets:</p>
+
+ <dl>
+ <dt id="build.category.bin"><samp>bin</samp></dt>
+
+ <dd>e.g. executable binary and script.</dd>
+
+ <dt id="build.category.etc"><samp>etc</samp></dt>
+
+ <dd>e.g. data files.</dd>
+
+ <dt id="build.category.include"><samp>include</samp></dt>
+
+ <dd>e.g. include files and Fortran module definition files.</dd>
+
+ <dt id="build.category.lib"><samp>lib</samp></dt>
+
+ <dd>e.g. object archives.</dd>
+
+ <dt id="build.category.o"><samp>o</samp></dt>
+
+ <dd>e.g. object files</dd>
+ </dl>
+
+ <p>Sub-directories are only created as necessary, so you may not find all of
+ the above in your destination tree.</p>
+
+ <p>To use a different compiler and/or compiler options for Fortran/C/C++, you
+ use the <a href="annex_cfg.html#make.build.prop">build.prop</a> declaration to
+ redefine the build properties. E.g.:</p>
+ <pre>
+steps = build
+build.target{task} = link
+build.source = $HOME/my-source-tree
+
+# Set Fortran compiler/linker
+build.prop{fc} = ifort
+# Set Fortran compiler options
+build.prop{fc.flags} = -i8 -r8 -O3
+# Add include paths to Fortran compiler
+build.prop{fc.include-paths} = /a/path/to/include /more/path/to/include
+# Set link libraries for Fortran executables
+build.prop{fc.lib-paths} = /path/to/my-lib
+build.prop{fc.libs} = mine
+# Set C compiler/linker
+build.prop{cc} = icc
+# Set C compiler options
+build.prop{cc.flags} = -O3
+# Set C++ compiler options
+build.prop{cxx.flags} = -O2
+# Set link libraries for C executables
+build.prop{cc.lib-paths} = /path/to/my-lib /path/to/your-lib
+build.prop{cc.libs} = mine yours
+# Set linker, if compiler cannot be used as linker
+#build.prop{ld} = ld
+</pre>
+
+ <h3 id="build.source-locations">Build Source Locations</h3>
+
+ <p>The build system locates its source files from various places,
+ including:</p>
+
+ <ul>
+ <li>Inherited locations. (See <a href="#build.inherit">Build
+ Inheritance</a>.)</li>
+
+ <li>Usable target locations of previous steps in the make. E.g. targets of
+ an extract step, and <samp><a href=
+ "#preprocess.category.src">src</a></samp> category targets of a preprocess
+ step can both be source files of the build system. The <code><a href=
+ "annex_cfg.html#make.build.prop.no-step-source">build.prop{no-step-source}</a></code>
+ declaration can be used to switch off this behaviour.</li>
+
+ <li>Locations specified by the <code><a href=
+ "annex_cfg.html#make.build.source">build.source</a></code> declaration.
+ Note: If you assign a relative path to the <code><a href=
+ "annex_cfg.html#make.build.source">build.source</a></code> declaration, the
+ system will assume the path to be relative to the make destination (not the
+ current working directory, unless they happen to be the same).</li>
+ </ul>
+
+ <p>There are situations when it may not be desirable to consider every source
+ file in a build. You can apply filters by source file name-spaces using the
+ <code><a href="annex_cfg.html#make.build.ns-excl">build.ns-excl</a></code>
+ and <code><a href=
+ "annex_cfg.html#make.build.ns-incl">build.ns-incl</a></code> declarations.
+ E.g.:</p>
+ <pre>
+# To include items in "foo" and "bar/baz" only
+build.ns-excl = / # exclude everything ...
+build.ns-incl = foo bar/baz # but include items from these name-spaces
+</pre>
+
+ <h3 id="build.source-ns">Build Source Name-spaces and Properties</h3>
+
+ <p>Each source file is assigned a name-space (which is used to fine tune the
+ build properties, such as the compiler flags). If the source file is a target
+ of an extract, the name-space will be the same as the extract target (the
+ relative path of the <samp>extract/</samp> sub-directory in the make
+ destination). If the source file is a file in a source tree specified by
+ <code><a href="annex_cfg.html#make.build.source">build.source</a></code>, the
+ name-space is the relative path to the specified value. The <code><a href=
+ "annex_cfg.html#make.build.source">build.source</a></code> declaration also
+ accepts an optional name-space, in which case the name-space of each source
+ file in the tree will be prefixed with the specified name-space. Suppose you
+ have a source tree in <samp>$HOME/food</samp>:</p>
+ <pre>
+$HOME/food/egg.c
+$HOME/food/ham.f90
+</pre>
+
+ <p>If you specify the source tree with <samp>build.source =
+ $HOME/food</samp>, then <samp>$HOME/food/egg.c</samp> will be given the
+ name-space <samp>egg.c</samp> and <samp>$HOME/food/ham.f90</samp> will be
+ given the name-space <samp>ham.f90</samp>.</p>
+
+ <p>On the other hand, if you specify the source tree with
+ <samp>build.source[food] = $HOME</samp>, then
+ <samp>$HOME/food/egg.c</samp> will be given the name-space
+ <samp>food/egg.c</samp> and <samp>$HOME/food/ham.f90</samp> will be given the
+ name-space <samp>food/ham.f90</samp>.</p>
+
+ <p>The name-space is organised in a simple hierarchy. For instance, the
+ <samp>foo/bar/egg</samp> name-space belongs to <samp>foo/bar</samp>, which
+ belongs to <samp>foo</samp>, which belongs to the root name-space. (The root
+ name-space is either an empty string or a <samp>/</samp>.)</p>
+
+ <p>For instance, you can set the flags of the Fortran compiler using the
+ <code><a href=
+ "annex_cfg.html#make.build.prop.fc.flags">build.prop{fc.flags}</a></code>
+ declaration at different name-space levels:</p>
+ <pre>
+# The global Fortran compiler flags
+build.prop{fc.flags} = -O3
+
+# The Fortran compiler flags for the "food" name-space
+build.prop{fc.flags}[food] = -O2 -i8 -r8
+
+# The Fortran compiler flags for a source file
+build.prop{fc.flags}[food/bacon.f90] = -O0 -g -C
+</pre>
+
+ <h3 id="build.source-types">Build Source Types</h3>
+
+ <p>Before the build system can do anything with its source files, it needs to
+ know what they are. It determines the type of each source file by looking at
+ its file name extension, then the file name itself, and then the
+ <code>#!</code> line for a text file. Source files without a type are treated
+ as data files.</p>
+
+ <p>The following types are associated with file extensions:</p>
+
+ <dl>
+ <dt><a href="annex_cfg.html#make.build.prop.file-ext.c">c</a> (C source
+ file)</dt>
+
+ <dd>.c .i .m .mi</dd>
+
+ <dt><a href="annex_cfg.html#make.build.prop.file-ext.cxx">cxx</a> (C++
+ source file)</dt>
+
+ <dd>.cc .cp .cxx .cpp .CPP .c++ .C .mm .M .mii</dd>
+
+ <dt><a href="annex_cfg.html#make.build.prop.file-ext.fortran">fortran</a>
+ (Fortran source file)</dt>
+
+ <dd>.F .FOR .FTN .F90 .F95 .f .for .ftn .f90 .f95 .inc</dd>
+
+ <dt><a href="annex_cfg.html#make.build.prop.file-ext.h">h</a> (Preprocessor
+ header file)</dt>
+
+ <dd>.h</dd>
+
+ <dt><a href="annex_cfg.html#make.build.prop.file-ext.script">script</a>
+ (script in various languages)</dt>
+
+ <dd>(empty)</dd>
+ </dl>
+
+ <p>The <code>prop{file-ext.type} = extensions</code> declaration can be used
+ to modify the extensions associated with a type. E.g. if you need to add
+ <samp>.fort</samp> as a file extension for a Fortran source file, you can
+ do:</p>
+ <pre>
+build.prop{file-ext.fortran} = .F .FOR .FORT .FTN .F90 .F95 \
+ .f .for .fort .ftn .f90 .f95 .inc
+</pre>
+
+ <p>You can associate file names to some file types using a
+ <code>prop{file-pat.type} = regular-expression</code> declaration. E.g. if
+ you have executable scripts in the source tree with no <code>#!</code> lines
+ but are recognised by a <samp>*Scr_*</samp> pattern of their file names, you
+ can specify a regular expression to match their file names using the <a href=
+ "annex_cfg.html#make.build.prop.file-pat.script">file-pat.script</a>
+ property:</p>
+ <pre>
+build.prop{file-pat.script} = (?msx-i:\w+Scr_\w+)
+</pre>
+
+ <p>All other text files with a <code>#!</code> line are recognised as scripts
+ by the build system.</p>
+
+ <h3 id="build.source-analysis">Build Source Analysis</h3>
+
+ <p>Each source file with a known type (that is not ignored) is analysed by
+ the build system for dependencies and other information. Here is a list of
+ what the system looks for in each type of file:</p>
+
+ <dl>
+ <dt>c and cxx</dt>
+
+ <dd>
+ <p><dfn>main program</dfn>: e.g. <samp>int main()</samp>.</p>
+
+ <p><dfn>dependency on include</dfn>: e.g. <samp>#include
+ "name.h"</samp>.</p>
+
+ <p><dfn>dependency on object</dfn>: e.g. <samp>/* depends on: name.o
+ */</samp> (for <a href="#build.source-analysis.legacy">legacy
+ support</a>).</p>
+ </dd>
+
+ <dt>fortran</dt>
+
+ <dd>
+ <p><dfn>main program</dfn>: e.g. <samp>program name</samp>.</p>
+
+ <p><dfn>list of symbols</dfn>: i.e. names of top level program units
+ including blockdata, function, module, program and subroutine.</p>
+
+ <p><dfn>dependency on include</dfn>: e.g. <samp>#include "name.h"</samp>
+ and <samp>include 'name.f90'</samp>.</p>
+
+ <p><dfn>dependency on module</dfn>: e.g. <samp>use name</samp>.</p>
+
+ <p><dfn>dependency on object</dfn>: e.g. <samp>! depends on:
+ name.o</samp> (for <a href="#build.source-analysis.legacy">legacy
+ support</a>).</p>
+ </dd>
+
+ <dt>h</dt>
+
+ <dd>
+ <p><dfn>dependency on include</dfn>: e.g. <samp>#include
+ "file-name"</samp> (and <samp>include 'file-name'</samp> for <a href=
+ "#build.source-analysis.legacy">legacy support</a>).</p>
+ </dd>
+
+ <dt>script</dt>
+
+ <dd>
+ <p><dfn>dependency on executable</dfn>: e.g. <samp># calls: name</samp>
+ (for <a href="#build.source-analysis.legacy">legacy support</a>).</p>
+ </dd>
+ </dl>
+
+ <div class="well">
+ <p><strong><i class="icon-pencil"></i> Note: The following features are for
+ legacy support.</strong></p>
+
+ <dl id="build.source-analysis.legacy">
+ <dt><code>DEPENDS ON: x</code> directives in C/Fortran source files</dt>
+
+ <dd>The <code>DEPENDS ON: x</code> directive can be used to identify
+ dependencies on other compiled objects. However, it is much better to
+ specify this kind of dependency information in the configuration for the
+ build where necessary. In any case, in modern Fortran code almost all
+ dependencies should be identified automatically via the use of modules
+ and/or interface files.</dd>
+
+ <dt><code>calls: x</code> directives in scripts</dt>
+
+ <dd>The <code>calls: x</code> directive can be used to identify a
+ dependency on another executable. However, it is much better to specify
+ this kind of dependency information in the configuration for the build, and
+ leave the source code to concentrate on the run time logic.</dd>
+
+ <dt><samp>*.h</samp> files as Fortran include files</dt>
+
+ <dd><samp>*.h</samp> files are normally identified as C header files.
+ However, they are also being used by some old Fortran programs as include
+ files. Therefore, when the system analyses a <samp>*.h</samp> file, it has
+ to detect the Fortran include syntax, i.e. <samp>include 'file-name'</samp>
+ as well as the regular C preprocessor include syntax.</dd>
+ </dl>
+ </div>
+
+ <div class="well">
+ <p><strong><i class="icon-pencil"></i> Note: Dependency Analysis and Fortran
+ OpenMP Sentinels.</strong></p>
+
+ <p>The build system recognises statements with Fortran OpenMP sentinels that
+ affect build dependencies. E.g.:</p>
+
+ <pre>
+!$ USE my_omp_mod, ONLY: my_omp_sub
+! ...
+!$ INCLUDE 'my_omp_logic'
+</pre>
+
+ <p>These dependencies are normally ignored. However, if a relevant
+ <code>build.prop{fc.flag-omp}</code> property is specified, the build system
+ will treat these statements as normal dependency statements.</p>
+ </div>
+
+ <p>There are some situations when it is not possible for the system to
+ identify a dependency. E.g. a Fortran source file may depend on external
+ objects that are not detected by the automatic analysis. Therefore, the
+ system allows you to specify manual dependencies in the configuration file
+ using the <code>build.prop{dep.type}</code> and
+ <code>build.prop{ns-dep.type}</code> declarations. E.g.:</p>
+ <pre>
+# Tell the system that (the object of) food/egg.c depends on chicken.o
+build.prop{dep.o}[food/egg.c] = chicken.o
+
+# Tell the system that (the object of) meal/big.c depends on all objects in the
+# "food" and "drink" name-spaces
+build.prop{ns-dep.o}[meal/big.c] = food drink
+</pre>
+
+ <p>Like all declarations that accept name-spaces, if you specify a name-space
+ in this declaration, the property will apply to all source files in the
+ name-space. If you do not specify a name-space, it applies to the root
+ name-space (i.e. globally to all relevant source files).</p>
+
+ <p>The following manual dependency declarations are recognised:</p>
+
+ <dl>
+ <dt><a href=
+ "annex_cfg.html#make.build.prop.dep.bin">build.prop{dep.bin}</a></dt>
+
+ <dd>Specifies a list of dependencies on a script or a binary
+ executable.</dd>
+
+ <dt><a href=
+ "annex_cfg.html#make.build.prop.dep.f.module">build.prop{dep.f.module}</a></dt>
+
+ <dd>Specifies a list of Fortran module import dependencies. Note: a
+ dependency on a Fortran module called <samp>module_1</samp> becomes an
+ <dfn>include</dfn> dependency on <samp>module_1.mod</samp> when the system
+ turns the source file into its targets.</dd>
+
+ <dt><a href=
+ "annex_cfg.html#make.build.prop.dep.include">build.prop{dep.include}</a></dt>
+
+ <dd>Specifies a list of include file dependencies.</dd>
+
+ <dt><a href=
+ "annex_cfg.html#make.build.prop.dep.o">build.prop{dep.o}</a></dt>
+
+ <dd>Specifies a list of link-time object dependencies.</dd>
+
+ <dt><a href=
+ "annex_cfg.html#make.build.prop.dep.o.special">build.prop{dep.o.special}</a></dt>
+
+ <dd>Specifies a list of special type of link-time object dependencies.
+ Normally, an object file can be put in an object archive before being
+ linked with the main object. There are special cases when an object file
+ must be specified on the command line of the linker. (E.g. an object file
+ containing a Fortran blockdata program unit.) This special behaviour must
+ be declared using this declaration.</dd>
+
+ <dt><a href=
+ "annex_cfg.html#make.build.prop.ns-dep.o">build.prop{ns-dep.o}</a></dt>
+
+ <dd>Specifies a list of link-time object dependencies on all objects in the
+ specified name-space.</dd>
+ </dl>
+
+ <p>There are times when you know that your source tree does not contain a
+ particular type of dependency, in which case you can switch off the automatic
+ analysis by using the <code>build.prop{no-dep.type}</code> declaration. E.g.
+ if you know that all include files in the <samp>food</samp> name-space are
+ provided outside of the source tree, you can do:</p>
+ <pre>
+# Do not check for "include" dependencies
+build.prop{no-dep.include}[food] = *
+</pre>
+
+ <p>All the types supported by the <code>build.prop{dep.type}</code>
+ declarations are supported by the <code>build.prop{no-dep.type}</code>,
+ except that there is no <code>build.prop{no-dep.o.special}</code>
+ (because this type of dependency is never automatic).</p>
+
+ <h3 id="build.target-source">Build Targets from Source Files</h3>
+
+ <p>The system derives the build targets from the source files. E.g. a C
+ source file <samp>egg.c</samp> is turned into a compile target to generate
+ <samp>egg.o</samp>.</p>
+
+ <p>The following is a list of what targets are available for each type of
+ file. The title of each item in the list is in the format <dfn>source type
+ -> target key</dfn>. The <dfn>description</dfn> of each target describes
+ what the target is, and where appropriate, explains how the target keys are
+ named. The <dfn>task</dfn> is the action the target needs to perform to get
+ up to date. The <dfn>category and destination</dfn> is the sub-directory and
+ destination of the target. The <dfn>properties</dfn> are the list of
+ properties that may be used by the <dfn>task</dfn> to update the target. The
+ <dfn>dependencies</dfn> list the types of dependencies the target may have.
+ The <dfn>update if</dfn> is the condition when the target is considered out
+ of date. The <dfn>pass on</dfn> information is a list of dependeny types
+ which a target can pass on the status, (see <a href=
+ "#build.target-update">Build Targets Update in Incremental Mode</a> for an
+ explanation of what this means.)</p>
+
+ <dl>
+ <dt>c/cxx -> name</dt>
+
+ <dd>
+ <p><dfn>description</dfn>: source file as an include file.</p>
+
+ <p><dfn>task</dfn>: <a href="#build.task.install">install</a>.</p>
+
+ <p><dfn>category and destination</dfn>: <a href=
+ "#build.category.include">include</a> and include/name</p>
+
+ <p><dfn>dependencies</dfn>: include and o (object).</p>
+
+ <p><dfn>update if</dfn>: source file is modified.</p>
+
+ <p><dfn>pass on</dfn>: include, o and o.special.</p>
+ </dd>
+
+ <dt>c/cxx -> name.o</dt>
+
+ <dd>
+ <p><dfn>description</dfn>: object file. The file is named by mapping the
+ base name of the original source file in lower case characters, with the
+ file extension replaced by the first value of the <code><a href=
+ "annex_cfg.html#make.build.prop.file-ext.o">file-ext.o</a></code>
+ property.</p>
+
+ <p><dfn>task</dfn>: <a href="#build.task.compile">compile</a> (cc).</p>
+
+ <p><dfn>category and destination</dfn>: <a href="#build.category.o">o</a>
+ and o/name.o</p>
+
+ <p><dfn>properties</dfn>: <a href=
+ "annex_cfg.html#make.build.prop.cc">cc</a>, <a href=
+ "annex_cfg.html#make.build.prop.cc.flags">cc.flags</a>, <a href=
+ "annex_cfg.html#make.build.prop.cc.defs">cc.defs</a>, <a href=
+ "annex_cfg.html#make.build.prop.cc.flag-compile">cc.flag-compile</a>,
+ <a href=
+ "annex_cfg.html#make.build.prop.cc.flag-define">cc.flag-define</a>,
+ <a href=
+ "annex_cfg.html#make.build.prop.cc.flag-include">cc.flag-include</a>,
+ <a href=
+ "annex_cfg.html#make.build.prop.cc.include-paths">cc.include-paths</a>,
+ <a href=
+ "annex_cfg.html#make.build.prop.cc.flag-omp">cc.flag-omp</a>,
+ <a href=
+ "annex_cfg.html#make.build.prop.cc.flag-output">cc.flag-output</a></p>
+
+ <p><dfn>dependencies</dfn>: include and o (object).</p>
+
+ <p><dfn>update if</dfn>: source file or any of the required properties
+ are modified, or if any include dependencies are updated.</p>
+
+ <p><dfn>pass on</dfn>: o and o.special.</p>
+ </dd>
+
+ <dt>c/cxx (with main function) -> name.exe</dt>
+
+ <dd>
+ <p><dfn>description</dfn>: binary executable. The file is named after the
+ base name of the original source file, with the file extension replaced by
+ the first value of the <code><a href=
+ "annex_cfg.html#make.build.prop.file-ext.bin">file-ext.bin</a></code>
+ property.</p>
+
+ <p><dfn>task</dfn>: <a href="#build.task.link">link</a> (cc).</p>
+
+ <p><dfn>category and destination</dfn>: <a href=
+ "#build.category.bin">bin</a> and bin/name.exe</p>
+
+ <p><dfn>properties</dfn>:
+ <a href="annex_cfg.html#make.build.prop.ar">ar</a>,
+ <a href="annex_cfg.html#make.build.prop.ar.flags">ar.flags</a>,
+ <a href="annex_cfg.html#make.build.prop.file-ext.a">file-ext.a</a>,
+ <a href="annex_cfg.html#make.build.prop.cc">cc</a>,
+ <a href="annex_cfg.html#make.build.prop.cc.flags-ld">cc.flags-ld</a>,
+ <a href="annex_cfg.html#make.build.prop.cc.flag-lib">cc.flag-lib</a>,
+ <a href=
+ "annex_cfg.html#make.build.prop.cc.flag-lib-path">cc.flag-lib-path</a>,
+ <a href="annex_cfg.html#make.build.prop.cc.libs">cc.libs</a>,
+ <a href="annex_cfg.html#make.build.prop.cc.lib-paths">cc.lib-paths</a>,
+ <a href=
+ "annex_cfg.html#make.build.prop.cc.flag-omp">cc.flag-omp</a>,
+ <a href=
+ "annex_cfg.html#make.build.prop.cc.flag-output">cc.flag-output</a></p>
+
+ <p><dfn>dependencies</dfn>: name.o and other objects (o and
+ o.special).</p>
+
+ <p><dfn>update if</dfn>: source file or any of the required properties
+ are modified, or if any dependencies are updated.</p>
+ </dd>
+
+ <dt>fortran -> name</dt>
+
+ <dd>
+ <p><dfn>description</dfn>: source file as an include file.</p>
+
+ <p><dfn>task</dfn>: <a href="#build.task.install">install</a>.</p>
+
+ <p><dfn>category and destination</dfn>: <a href=
+ "#build.category.include">include</a> and include/name</p>
+
+ <p><dfn>dependencies</dfn>: include and o (object). A source's f.module
+ dependency on a module called <samp>xyz</samp> is turned into an include
+ dependency on the <samp>xyz.mod</samp>.</p>
+
+ <p><dfn>update if</dfn>: source file is modified.</p>
+
+ <p><dfn>pass on</dfn>: include, o and o.special.</p>
+ </dd>
+
+ <dt>fortran (with a valid Fortran program unit) -> unit.o</dt>
+
+ <dd>
+ <p><dfn>description</dfn>: object file. The file is named by concatenating
+ the lower case characters of the name of the first program unit in the
+ source file and the first value of the <code><a href=
+ "annex_cfg.html#make.build.prop.file-ext.o">file-ext.o</a></code>
+ property.</p>
+
+ <p><dfn>task</dfn>: <a href="#build.task.compile">compile</a> (fc).</p>
+
+ <p><dfn>category and destination</dfn>: <a href="#build.category.o">o</a>
+ and o/unit.o</p>
+
+ <p><dfn>properties</dfn>: <a href=
+ "annex_cfg.html#make.build.prop.fc">fc</a>, <a href=
+ "annex_cfg.html#make.build.prop.fc.flags">fc.flags</a>, <a href=
+ "annex_cfg.html#make.build.prop.fc.defs">fc.defs</a>, <a href=
+ "annex_cfg.html#make.build.prop.fc.flag-compile">fc.flag-compile</a>,
+ <a href=
+ "annex_cfg.html#make.build.prop.fc.flag-define">fc.flag-define</a>,
+ <a href=
+ "annex_cfg.html#make.build.prop.fc.flag-include">fc.flag-include</a>,
+ <a href=
+ "annex_cfg.html#make.build.prop.fc.include-paths">fc.include-paths</a>,
+ <a href=
+ "annex_cfg.html#make.build.prop.fc.flag-module">fc.flag-module</a>,
+ <a href=
+ "annex_cfg.html#make.build.prop.fc.flag-omp">fc.flag-omp</a>,
+ <a href=
+ "annex_cfg.html#make.build.prop.fc.flag-output">fc.flag-output</a></p>
+
+ <p><dfn>dependencies</dfn>: include and o (object). A source's f.module
+ dependency on a module called <samp>xyz</samp> is turned into an include
+ dependency on the <samp>xyz.mod</samp>.</p>
+
+ <p><dfn>update if</dfn>: source file or any of the required properties
+ are modified, or if any include dependencies are updated.</p>
+
+ <p><dfn>pass on</dfn>: o and o.special.</p>
+
+ <p><dfn>remark</dfn>: trigger <samp>unit.mod</samp> targets if source
+ file contains a Fortran module.</p>
+ </dd>
+
+ <dt>fortran (with function or subroutine) -> name.interface</dt>
+
+ <dd>
+ <p><dfn>description</dfn>: Fortran interface file. The file is named by
+ concatenating the base name of the source file with the file extension
+ replaced by the <code><a href=
+ "annex_cfg.html#make.build.prop.file-ext.f90-interface">file-ext.f90-interface</a></code>
+ property.</p>
+
+ <p><dfn>task</dfn>: <a href="#build.task.ext-iface">ext-iface</a></p>
+
+ <p><dfn>category and destination</dfn>: <a href=
+ "#build.category.include">include</a> and include/name.interface</p>
+
+ <p><dfn>dependencies</dfn>: unit.o</p>
+
+ <p><dfn>update if</dfn>: source file or unit.o is modified.</p>
+
+ <p><dfn>pass on</dfn>: include, o and o.special.</p>
+ </dd>
+
+ <dt>fortran (each module in source) -> unit.mod</dt>
+
+ <dd>
+ <p><dfn>description</dfn>: Fortran module definition file. The file is
+ named by concatenating the lower case characters of the name of the
+ module and the first value of the <code><a href=
+ "annex_cfg.html#make.build.prop.file-ext.f90-mod">file-ext.f90-mod</a></code>
+ property.</p>
+
+ <p><dfn>task</dfn>: <a href="#build.task.compile-plus">compile+</a>.</p>
+
+ <p><dfn>category and destination</dfn>: <a href=
+ "#build.category.include">include</a> and include/unit.mod</p>
+
+ <p><dfn>dependencies</dfn>: unit.o</p>
+
+ <p><dfn>update if</dfn>: source file or unit.o is modified.</p>
+
+ <p><dfn>pass on</dfn>: o.</p>
+ </dd>
+
+ <dt>fortran (with program) -> name.exe</dt>
+
+ <dd>
+ <p><dfn>description</dfn>: binary executable. The object file is named
+ after the base name of the original source file, with the file extension
+ replaced by the first value of the <code><a href=
+ "annex_cfg.html#make.build.prop.file-ext.bin">file-ext.bin</a></code>
+ property.</p>
+
+ <p><dfn>task</dfn>: <a href="#build.task.link">link</a> (fc).</p>
+
+ <p><dfn>category and destination</dfn>: <a href=
+ "#build.category.bin">bin</a> and bin/name.exe</p>
+
+ <p><dfn>properties</dfn>:
+ <a href="annex_cfg.html#make.build.prop.ar">ar</a>,
+ <a href="annex_cfg.html#make.build.prop.ar.flags">ar.flags</a>,
+ <a href="annex_cfg.html#make.build.prop.fc">fc</a>,
+ <a href="annex_cfg.html#make.build.prop.fc.flags-ld">fc.flags-ld</a>,
+ <a href="annex_cfg.html#make.build.prop.fc.flag-lib">fc.flag-lib</a>,
+ <a href=
+ "annex_cfg.html#make.build.prop.fc.flag-lib-path">fc.flag-lib-path</a>,
+ <a href="annex_cfg.html#make.build.prop.fc.libs">fc.libs</a>,
+ <a href="annex_cfg.html#make.build.prop.fc.lib-paths">fc.lib-paths</a>,
+ <a href=
+ "annex_cfg.html#make.build.prop.fc.flag-omp">fc.flag-omp</a>,
+ <a href=
+ "annex_cfg.html#make.build.prop.fc.flag-output">fc.flag-output</a></p>
+
+ <p><dfn>dependencies</dfn>: unit.o and other objects (o and
+ o.special).</p>
+
+ <p><dfn>update if</dfn>: source file or any of the required properties
+ are modified, or if any dependencies are updated.</p>
+ </dd>
+
+ <dt>h -> name</dt>
+
+ <dd>
+ <p><dfn>description</dfn>: a header (include) file.</p>
+
+ <p><dfn>task</dfn>: <a href="#build.task.install">install</a>.</p>
+
+ <p><dfn>category and destination</dfn>: <a href=
+ "#build.category.include">include</a> and include/name</p>
+
+ <p><dfn>dependencies</dfn>: include and o (object).</p>
+
+ <p><dfn>update if</dfn>: source file is modified.</p>
+
+ <p><dfn>pass on</dfn>: include, o and o.special.</p>
+ </dd>
+
+ <dt>script -> name</dt>
+
+ <dd>
+ <p><dfn>description</dfn>: an executable script.</p>
+
+ <p><dfn>task</dfn>: <a href="#build.task.install">install</a>.</p>
+
+ <p><dfn>category and destination</dfn>: <a href=
+ "#build.category.bin">bin</a> and bin/name</p>
+
+ <p><dfn>dependencies</dfn>: bin (executable).</p>
+
+ <p><dfn>update if</dfn>: source file is modified.</p>
+
+ <p><dfn>pass on</dfn>: bin.</p>
+ </dd>
+
+ <dt>data -> name-space</dt>
+
+ <dd>
+ <p><dfn>description</dfn>: a data file.</p>
+
+ <p><dfn>task</dfn>: <a href="#build.task.install">install</a>.</p>
+
+ <p><dfn>category and destination</dfn>: <a href=
+ "#build.category.etc">etc</a> and etc/name-space</p>
+
+ <p><dfn>update if</dfn>: source file is modified.</p>
+ </dd>
+ </dl>
+
+ <p>Here is an explanation of what each build system <dfn>task</dfn> does:</p>
+
+ <dl>
+ <dt id="build.task.archive">archive</dt>
+
+ <dd>Creates an object archive by invoking an archiver command. (See
+ <a href="#build.target-ns">Build Targets from Name-space</a>.)</dd>
+
+ <dt id="build.task.compile">compile</dt>
+
+ <dd>Creates an object file by invoking the C/C++/Fortran compiler on the
+ source file.</dd>
+
+ <dt id="build.task.compile-plus">compile+</dt>
+
+ <dd>Copies the Fortran module definition file created by a compile task to
+ the include sub-directory.</dd>
+
+ <dt id="build.task.ext-iface">ext-iface</dt>
+
+ <dd>Extracts the calling interfaces of all functions and subroutines in a
+ Fortran source file (free format only) and writes the results in an
+ interface block that can be included by other Fortran source files with an
+ <samp>INCLUDE 'name.interface'</samp> statement. In an incremental build,
+ if you have modified a Fortran source file, its interface file will only be
+ re-generated if the content of the interface has changed. This can make
+ incremental build very efficient, as non-interface changes in a function or
+ subroutine will only trigger a re-link of the executable.</dd>
+
+ <dt id="build.task.install">install</dt>
+
+ <dd>Copies the source file to the destination.</dd>
+
+ <dt id="build.task.link">link</dt>
+
+ <dd>Creates an executable by invoking the archiver to load all required
+ objects into an archive, and then the C/C++/Fortran compiler on the object
+ file previously compiled using a source file containing a main program, with
+ the temporary archive.</dd>
+ </dl>
+
+ <h3 id="build.target-prop">Build Targets and Properties</h3>
+
+ <p>If you need to specify a property for a specific target, you can either use
+ their source file namespace or the target key. E.g. If the
+ <samp>sausage.o</samp> target is generated from the source file in the
+ <samp>src/food/sausage.f90</samp> namespace, you can specify its Fortran
+ compiler flags <code><a href=
+ "annex_cfg.html#make.build.prop.fc.flags">build.prop{fc.flags}</a></code> by
+ doing either:</p>
+
+ <pre>
+build.prop{fc.flags}[sausage.o] = -O4
+# would be the same as:
+build.prop{fc.flags}[src/food/sausage.f90] = -O4
+</pre>
+
+ <h3 id="build.target-source-fortran">Build Targets from Source Files: Fortran
+ Specifics</h3>
+
+ <p>To ensure that a Fortran application is built automatically, its source
+ code should be designed with the following considerations:</p>
+
+ <p><dfn>The name of each compilable program unit should be unique</dfn> in
+ the source tree, bearing in mind that Fortran is NOT case sensitive.</p>
+
+ <p><dfn>Always supply an interface for functions and subroutines</dfn>,
+ i.e.:</p>
+
+ <ul>
+ <li>Place functions and subroutines in a module, and give them the
+ <code>PUBLIC</code> attribute. Import them with the <code>USE
+ <module></code> statement. We recommend adding the <code>ONLY</code>
+ clause in a <code>USE <module></code> when importing symbols from a
+ module. This makes it easier to locate the source of each symbol, and avoids
+ unintentional access to other <code>PUBLIC</code> symbols within the
+ <code>MODULE</code>. If you are importing from an intrinsic module, you
+ should add the <code>INTRINSIC</code> clause to the <code>USE
+ <module></code> statement to tell the build system not to look for the
+ module from your source tree.</li>
+
+ <li>Place functions and subroutines in the <code>CONTAINS</code> section of
+ a standalone program unit. There are two advantages for this approach.
+ Firstly, the sub-programs will get an automatic interface when the container
+ program unit is compiled. Secondly, it should be easier for the compiler to
+ provide optimisation when the sub-programs are internal to the caller. The
+ disadvantage of this approach is that the sub-programs are local to the
+ caller, and so they cannot be called by other program units. Therefore, this
+ approach is only suitable for small sub-programs local to a particular
+ program unit.</li>
+
+ <li>Use the build system's automatic interface file feature. See below.</li>
+ </ul>
+
+ <p>For each free format Fortran source file, e.g. <samp>name.f90</samp>, with
+ 1 or more top level function and/or subroutine, the system creates a target
+ with the <a href="#build.task.ext-iface">ext-iface</a> task in the
+ <a href="#build.category.include">include</a> category, e.g.
+ <samp>name.interface</samp>, to extract the calling interfaces of all
+ functions and subroutines into an interface block. Another Fortran source
+ file, e.g. <samp>caller.f90</samp> that relies on the functions and/or
+ subroutines in <samp>name.f90</samp> can have an <samp>INCLUDE
+ 'name.interface'</samp> statement in its specification section, which serves
+ 2 purposes:</p>
+
+ <ul>
+ <li>It allows <samp>caller.f90</samp> to call the functions and/or
+ subroutines in <samp>name.f90</samp> with explicit interfaces.</li>
+
+ <li>It introduces an <a href=
+ "annex_cfg.html#make.build.prop.dep.include">include</a> dependency for
+ <samp>caller.o</samp> on <samp>name.interface</samp>.</li>
+ </ul>
+
+ <p>In an incremental build, if you modify <samp>name.f90</samp>, the system
+ will only regenerate <samp>name.interface</samp> if only the calling
+ interfaces of the functions and/or subroutines in <samp>name.f90</samp> have
+ changed. Consequently, non-interface changes in <samp>name.f90</samp> will
+ not trigger the re-compile of <samp>caller.o</samp>, but will only trigger a
+ re-link of the executable.</p>
+
+ <h3 id="build.target-selection">Build Targets Selection and Rename</h3>
+
+ <p>You need to tell the build system what targets to build or it will do
+ nothing. The <code><a href=
+ "annex_cfg.html#make.build.target">build.target</a></code> declaration allows
+ you to select targets according to their categories, source name-spaces,
+ tasks and keys. The logic is demonstrated by the following example:</p>
+ <pre>
+# Selects targets matching these keys
+build.target = egg.bin ham.o bacon.sh
+# OR (
+# those doing these tasks
+build.target{task} = install link
+# AND in these categories
+build.target{category} = bin
+# AND in these name-spaces
+build.target{ns} = foo bar/baz
+# )
+</pre>
+
+ <p>There are times when an automatic target name is not what you want. In
+ which case, you can rename a target using the <code><a href=
+ "annex_cfg.html#make.build.target-rename">build.target-rename</a></code>
+ declaration to specify an alternate name. E.g. if the target
+ <samp>bacon.sh</samp> should be called <samp>streaky</samp>, you can do:</p>
+ <pre>
+build.target-rename = bacon.sh:streaky
+</pre>
+
+ <p>In order for a target to build, all its dependencies must be satisfied. If
+ a target has a dependency that is not available in the list of targets, the
+ build will fail. Normally, you can avoid this by using one of the
+ <code>build.prop{no-dep.*}</code> declarations to switch off a non-existent
+ dependency, as described in the <a href="#build.source-analysis">Build Source
+ Analysis</a> section. However, there may be times when this is inefficient or
+ insufficient, in which case you can use the property <code><a href=
+ "annex_cfg.html#make.build.prop.ignore-missing-dep-ns">ignore-missing-dep-ns</a></code>
+ to specify a list of source name-spaces, in which targets can ignore missing
+ dependencies. E.g.:</p>
+ <pre>
+# Allows targets in the "foo" and "bar/baz"
+# name-spaces to ignore missing dependencies.
+build.prop{ignore-missing-dep-ns} = foo bar/baz
+</pre>
+
+ <h3 id="build.target-file-ext">Build Targets File Extensions</h3>
+
+ <p>You can rename the file name extension of the targets using
+ <code>build.prop{file-ext.type}</code> declaration (provided that the file
+ name extension is supported by your compiler, etc). E.g. if you want your
+ binary executables to have <samp>.bin</samp> extension rather than the
+ default <samp>.exe</samp>, you can do:</p>
+ <pre>
+build.prop{file-ext.bin} = .bin
+</pre>
+
+ <p>The following file extensions are currently used by the system:</p>
+
+ <dl>
+ <dt><a href="annex_cfg.html#make.build.prop.file-ext.a">a</a> (object
+ archive)</dt>
+
+ <dd>.a</dd>
+
+ <dt><a href="annex_cfg.html#make.build.prop.file-ext.bin">bin</a> (binary
+ executable)</dt>
+
+ <dd>.exe</dd>
+
+ <dt><a href=
+ "annex_cfg.html#make.build.prop.file-ext.f90-interface">f90-interface</a>
+ (Fortran free format interface file)</dt>
+
+ <dd>.interface</dd>
+
+ <dt><a href="annex_cfg.html#make.build.prop.file-ext.f90-mod">f90-mod</a>
+ (Fortran compiler module definition file)</dt>
+
+ <dd>.mod</dd>
+
+ <dt><a href="annex_cfg.html#make.build.prop.file-ext.o">o</a> (object
+ file)</dt>
+
+ <dd>.o</dd>
+ </dl>
+
+ <h3 id="build.target-ns">Build Targets from Name-space</h3>
+
+ <p>Apart from source file targets, the build system also generates targets
+ for each (directory-level) name-space. One target is for creating an object
+ archive to contain all object files in the name-space. The other target is a
+ convenient shorthand to allow all data files in the name-space to be
+ installed. The following is the full description:</p>
+
+ <dl>
+ <dt>name-space > name-space/libo.a</dt>
+
+ <dd>
+ <p><dfn>description</dfn>: object archive.</p>
+
+ <p><dfn>task</dfn>: <a href="#build.task.archive">archive</a>.</p>
+
+ <p><dfn>category and destination</dfn>: <a href=
+ "#build.category.lib">lib</a> and lib/name-space/libo.a</p>
+
+ <p><dfn>dependencies</dfn>: all o (object) targets in the name-space.</p>
+
+ <p><dfn>properties</dfn>: <a href=
+ "annex_cfg.html#make.build.prop.ar">ar</a>, <a href=
+ "annex_cfg.html#make.build.prop.ar.flags">ar.flags</a></p>
+
+ <p><dfn>update if</dfn>: any dependencies or properties are modified.</p>
+ </dd>
+
+ <dt>name-space > name-space/.etc</dt>
+
+ <dd>
+ <p><dfn>description</dfn>: dummy file.</p>
+
+ <p><dfn>task</dfn>: <a href="#build.task.install">install</a>.</p>
+
+ <p><dfn>category and destination</dfn>: <a href=
+ "#build.category.etc">etc</a> and etc/name-space/.etc</p>
+
+ <p><dfn>dependencies</dfn>: all data files in the name-space.</p>
+
+ <p><dfn>update if</dfn>: any dependencies are modified.</p>
+ </dd>
+ </dl>
+
+ <h3 id="build.target-update">Build Targets Update in Incremental Mode</h3>
+
+ <p>In incremental mode, a target is only updated if it is marked out of date.
+ A target is considered out of date if:</p>
+
+ <ul>
+ <li>the source file's MD5 checksum is changed.</li>
+
+ <li>a required property is modified.</li>
+
+ <li>a dependency is marked as modified, and the dependency is a type that
+ the target cannot pass on. E.g. If <samp>object_1.o</samp> depends on
+ <samp>object_2.o</samp>, and <samp>object_2.o</samp> is marked as modified,
+ the system does not need to re-compile <samp>object_1.o</samp> (as long as
+ its source file and properties remain unchanged). However,
+ <samp>object_1.o</samp> will have to pass the information up the dependency
+ tree, so that a target with a <samp><a href=
+ "#build.task.link">link</a></samp> task to build an executable (or an
+ <samp><a href="#build.task.archive">archive</a></samp> task to build a
+ library) will know that it needs to be updated.</li>
+
+ <li>a dependency is passing on a <q>modified</q> status for a dependency
+ type, which cannot be passed on by the target.</li>
+
+ <li>the target does not exist or its MD5 checksum is changed.</li>
+ </ul>
+
+ <p>If, after an update, the target's MD5 checksum is the same as before, the
+ target will be considered unchanged and up to date. In an incremental build,
+ the use of checksum ensures that any targets manually modified by the user
+ after the previous build is rebuilt accordingly. It also prevents unnecessary
+ updates of targets in incremental and inherited builds.</p>
+
+ <p>E.g. Consider an incremental build where the only change is the content of
+ a Fortran module <samp>my_mod.f90</samp>. The content change should trigger
+ an update of the <samp>my_mod.o</samp> and <samp>my_mod.mod</samp> targets,
+ and everything depending on them. However, if the source content is modified
+ in such a way that it does not affect the module's public interface, most
+ compilers will generate an identical <samp>my_mod.mod</samp>. The system can
+ detect this by comparing the checksums. If <samp>my_mod.mod</samp> is
+ unchanged, the build system will not need to trigger the re-compile of all
+ targets depending on <samp>my_mod.mod</samp>, and it will only need to
+ re-link the executable. This allows incremental builds to be more
+ efficient.</p>
+
+ <h3 id="build.inherit">Build Inheritance</h3>
+
+ <p>If a previous build with a similar configuration exists in another
+ location, it can be more efficient to inherit from this previous build in
+ your current build. This works like a normal incremental build, except that
+ your build will only contain the changes you have specified (compared with
+ the inherited build) instead of the full set of targets.</p>
+
+ <p>The current build inherits all properties and target settings, as well as
+ sources and targets from the inherited build. While properties and target
+ settings can be overridden with a corresponding declaration, source
+ inheritance can only be prevented by using a <code><a href=
+ "annex_cfg.html#make.build.prop.no-inherit-source">build.prop{no-inherit-source}</a></code>
+ declaration. E.g.:</p>
+ <pre>
+# Prevents inheritance from some name-spaces:
+build.prop{no-inherit-source} = food/mint drink/soft/cola.c
+</pre>
+
+ <p>For multiple inheritance, the last one takes precedence, and any search
+ for source files or targets are recursive and depth first. For instance, if
+ we have the following declarations in the current FCM make configuration:</p>
+ <pre>
+use = /path/to/a /path/to/b /path/to/c
+</pre>
+
+ <p>and the following in the FCM make configuration of
+ <samp>/path/to/b</samp>:</p>
+ <pre>
+use /path/to/d
+</pre>
+
+ <p>The relationship looks like:</p>
+ <pre>
+/path/to/current
+ /path/to/c
+ /path/to/b
+ /path/to/d
+ /path/to/a
+</pre>
+
+ <p>Therefore, we would expect the search path to follow the order:</p>
+ <pre>
+/path/to/current
+/path/to/c
+/path/to/b
+/path/to/d
+/path/to/a
+</pre>
+
+ <p>In its normal setting, the system does not inherit targets in the
+ <samp>bin</samp>, <samp>etc</samp> and <samp>lib</samp> categories. A target
+ in one of these categories is rebuilt in the current destination, whether the
+ inherited target is up to date or not. This allows someone to use the
+ executables of the build by setting the <var>PATH</var> environment variable
+ to point only to <samp>$DEST/build/bin/</samp> (where <var>$DEST</var> is the
+ destination of the current make). If this behaviour is undesirable for
+ whatever reason, it can be altered using the <code><a href=
+ "annex_cfg.html#make.build.prop.no-inherit-target-category">build.prop{no-inherit-target-category}</a></code>
+ declaration.</p>
+
+ <dl>
+ <dt>Build inheritance limitation: handling of include files</dt>
+
+ <dd>
+ <p>The build system uses the compiler's <code>-I</code> option to specify
+ the search path for include files. E.g. it uses this option to specify
+ the <samp>inc/</samp> sub-directories of the current build and its
+ inherited build.</p>
+
+ <p>However, some compilers (e.g. <code>cpp</code>) search for include
+ files from the container directory of the source file before searching
+ for the paths specified by the <code>-I</code> options. This behaviour
+ may cause the build to behave incorrectly.</p>
+
+ <p>Consider a source file <samp>egg/hen.c</samp> that includes
+ <samp>fried.h</samp>. If the directory structure looks like:</p>
+ <pre>
+# Sources in inherited build:
+egg/hen.c
+egg/fried.h
+
+# Sources in current build:
+egg/fried.h
+</pre>
+
+ <p>The system will correctly identify that <samp>fried.h</samp> is out of
+ date, and trigger a re-compilation of <samp>egg/hen.c</samp>. However, if
+ the compiler searches for the include files from the container directory
+ of the source file first, it will wrongly use the include file in the
+ inherited build instead of the current one.</p>
+
+ <p>If your directory structure does not have any include files in the same
+ directory as the source files that include them then you do not need to
+ worry. If it does then you need to check whether you are affected by this
+ problem before using an inherited build. The situation will vary
+ according to whether the affected code uses Fortran or preprocessor
+ include statements and also whether you are using the
+ <a href="#preprocess">preprocess system</a>. Some compilers (e.g.
+ <code>gfortran</code>) work fine for Fortran includes but not preprocessor
+ includes. Others (e.g. <code>ifort</code>) have options which can be used
+ (e.g. <code>-assume nosource_include</code>) to get the desired behaviour.
+ The FCM distribution includes some simple test code to help you test how
+ your chosen compilers behave. If you cannot ensure the correct behaviour
+ then it is safer not to use inherited builds.</p>
+ </dd>
+ </dl>
+
+ <h3 id="build.diagnostic">Build Diagnostic</h3>
+
+ <p>The amount of diagnostic messages generated by the build system is
+ dependent on the diagnostic verbosity level that can be modified by the
+ <code>-v</code> and <code>-q</code> options to the <code>fcm make</code>
+ command.</p>
+
+ <p>The following is a list of diagnostic output at each verbosity level:</p>
+
+ <dl>
+ <dt>-q</dt>
+
+ <dd>
+ <ul>
+ <li>Exceptions.</li>
+ </ul>
+ </dd>
+
+ <dt>default</dt>
+
+ <dd>
+ <ul>
+ <li>Everything at the -q level.</li>
+
+ <li>Start time of the build.</li>
+
+ <li>The summary of source analysis.</li>
+
+ <li>The summary of targets. Each row except the last reports the number
+ of modified and unchanged targets with a given type of task, and the
+ total time spent to perform the tasks. The last row reports the total
+ number of modified and unchanged targets, and the actual elapsed time.
+ It is worth noting that the elapsed time in a multi-process build
+ should be significant shorter than the sum of the total time for each
+ type of task. E.g.:
+ <pre>
+[info] compile targets: modified=5, unchanged=0, total-time=0.5s
+[info] compile+ targets: modified=1, unchanged=0, total-time=0.0s
+[info] ext-iface targets: modified=2, unchanged=0, total-time=0.0s
+[info] install targets: modified=1, unchanged=0, total-time=0.0s
+[info] link targets: modified=1, unchanged=0, total-time=0.1s
+[info] TOTAL targets: modified=10, unchanged=0, elapsed-time=0.7s
+</pre>
+ </li>
+
+ <li>Total time.</li>
+ </ul>
+ </dd>
+
+ <dt>-v</dt>
+
+ <dd>
+ <ul>
+ <li>Everything at the default level.</li>
+
+ <li>Elapsed time and name-space for each analysed source.</li>
+
+ <li>Task name, elapsed time, target status (<samp>M</samp> for modified
+ or <samp>U</samp> for unchanged), target key, and source name-space for
+ each modified target. E.g.:
+ <pre>
+[info] compile 0.0 M hello_func.o <- lib/function/hello_func.f90
+[info] ext-iface 0.0 M hello_func.interface <- lib/function/hello_func.f90
+[info] install 0.0 M hello_inc.f90 <- include/hello_inc.f90
+[info] compile 0.0 M hello_1.o <- bin/hello_1.f90
+[info] link 0.1 M hello_1.exe <- bin/hello_1.f90
+</pre>
+ </li>
+ </ul>
+ </dd>
+
+ <dt>-vv</dt>
+
+ <dd>
+ <ul>
+ <li>Everything at the -v level.</li>
+
+ <li>A list of dependencies (type and name) of each analysed source.
+ E.g.
+ <pre>
+[info] analyse 0.0 bin/hello_1.f90
+[info] -> ( include) hello_inc.f90
+[info] -> ( o) hello_void.o
+[info] -> ( f.module) hello_mod
+</pre>
+ </li>
+
+ <li>A list of the available targets from the sources. Each row contains
+ the source namespace, the target task, the target category and the key
+ of the target. E.g.:
+ <pre>
+[info] source->target / -> (archive) lib/ libo.a
+[info] source->target hello.f90 -> (link) bin/ hello.exe
+[info] source->target hello.f90 -> (install) include/ hello.f90
+[info] source->target hello.f90 -> (compile) o/ hello.o
+[info] source->target world.f90 -> (install) include/ world.f90
+[info] source->target world.f90 -> (compile+) include/ world.mod
+[info] source->target world.f90 -> (compile) o/ world.o
+</pre>
+ </li>
+
+ <li>A list of the required targets for this build. Each row contains
+ the task, the category and the key of the target. E.g.:
+ <pre>
+[info] required-target: link bin hello_1.exe
+</pre>
+ </li>
+
+ <li>The dependency tree of all the required targets. (N.B. A
+ <samp>(n-deps=N)</samp> at the end of a line means that the target has
+ already appeared earlier and that it has <samp>N</samp> direct
+ dependencies, which will not be reported again.) E.g.:
+ <pre>
+[info] target hello_1.exe
+[info] target - hello_void.o
+[info] target - hello_1.o
+[info] target - - hello_mod.mod
+[info] target - - - hello_mod.o
+[info] target - - hello_void.o
+[info] target - - hello_inc.f90
+[info] target - - - hello_sub.interface
+[info] target - - - - hello_sub.o
+[info] target - - - - - hello_func.interface
+[info] target - - - - - - hello_func.o
+[info] target - hello_2.o
+[info] target - - hello_mod.mod (n-deps=1)
+</pre>
+ </li>
+
+ <li>Each shell command invoked with elapsed time and return code.</li>
+
+ <li>STDOUT and STDERR from shell commands invoked by the build
+ tasks, e.g. diagnostic output from compilers and linkers.</li>
+ </ul>
+ </dd>
+ </dl>
+
+ <h2 id="preprocess">Preprocess</h2>
+
+ <p>As most modern compilers can handle preprocessing, you should normally
+ leave preprocessing to the compiler. However, it is recognised that some code
+ is written with preprocessor directives that can alter the calling interface
+ of the procedure and/or their dependencies. If a source file requires
+ preprocessing in such a way, we have to preprocess it before feeding it to
+ the build system. The preprocess system can be used to do this. It is
+ typically run as a step before build.</p>
+
+ <p>However, using a separate preprocess step is not the best way of working,
+ as it adds an overhead to the build process. If your code requires
+ preprocessing, you should try to design it to avoid changes in the above.</p>
+
+ <p>In practice, the only reasonable use of a preprocessor with Fortran is for
+ code selection. For example, preprocessing is useful for isolating machine
+ specific libraries or instructions, where it may be appropriate to use inline
+ alternatives for small sections of code. Another example is when multiple
+ versions of the same procedure exist in the source tree and you need to use
+ the preprocessor to select the correct version for your build.</p>
+
+ <p>Avoid using the a preprocessor for code inclusion, as you should be able
+ to do the same via the Fortran <code>INCLUDE</code> statement. You should
+ also avoid embedding preprocessor macros within the continuations of a
+ Fortran statement, as it can make your code very confusing.</p>
+
+ <p>The preprocess system works using the same logic as the build system, but
+ is configured primarily to preprocess C/C++ and Fortran source files. We
+ shall document only the main differences to the build system in the remainder
+ of this section.</p>
+
+ <h3 id="preprocess.basic">Preprocess: Basic</h3>
+
+ <p>A typical usage of the preprocess system may look like:</p>
+ <pre>
+steps = extract preprocess build
+
+# ... some extract configuration
+
+# Only preprocess source files in these name-spaces
+preprocess.target{ns} = foo/bar egg/fried.F90
+# Specifies the macro definitions for the Fortran preprocessor
+preprocess.prop{fpp.defs} = THING=stuff HIGH=tall
+# Specifies the macro definitions for the C preprocessor
+preprocess.prop{cpp.defs} = LOWER=lower UNDER LINUX
+
+# ... some build configuration
+</pre>
+
+ <p>The result of the preprocess can be found in the sub-directories of the
+ <samp>preprocess/</samp> sub-directory. There are only 2 target
+ categories:</p>
+
+ <dl>
+ <dt id="preprocess.category.include"><samp>include</samp></dt>
+
+ <dd>e.g. include files.</dd>
+
+ <dt id="preprocess.category.src"><samp>src</samp></dt>
+
+ <dd>e.g. preprocessed source files</dd>
+ </dl>
+
+ <h3 id="preprocess.source-type">Preprocess Source Types</h3>
+
+ <p>Only files in the following types (with the given file extensions) are
+ recognised by the preprocess system:</p>
+
+ <dl>
+ <dt><a href="annex_cfg.html#make.preprocess.prop.file-ext.cpp">cpp</a>
+ (C/C++ source file)</dt>
+
+ <dd>.c .m .cc .cp .cxx .cpp .CPP .c++ .C .mm .M</dd>
+
+ <dt><a href="annex_cfg.html#make.preprocess.prop.file-ext.fpp">fpp</a>
+ (Fortran source file requiring preprocessing)</dt>
+
+ <dd>.F .FOR .FTN .F90 .F95</dd>
+
+ <dt><a href="annex_cfg.html#make.preprocess.prop.file-ext.h">h</a>
+ (Preprocessor header file)</dt>
+
+ <dd>.h</dd>
+ </dl>
+
+ <h3 id="preprocess.source-analysis">Preprocess Source Analysis</h3>
+
+ <p>The preprocess system only looks for include dependencies using the
+ pattern <samp>#include "name.h"</samp>. Macros using the angle brackets
+ syntax (e.g. <samp>#include <name.h></samp>) are ignored.</p>
+
+ <h3 id="preprocess.target-source">Preprocess Targets from Source Files</h3>
+
+ <p>The preprocess system only generates targets from source files, (i.e.
+ targets are not generated by name-spaces). Targets of the preprocess system
+ perform one of the following <dfn>tasks</dfn>:</p>
+
+ <dl>
+ <dt id="preprocess.task.install">install</dt>
+
+ <dd>Copies the source file to the destination.</dd>
+
+ <dt id="preprocess.task.process">process</dt>
+
+ <dd>Creates a new source file by invoking the C/Fortran preprocessor on the
+ original source file.</dd>
+ </dl>
+
+ <p>By default, it attempts to build all targets with a <samp><a href=
+ "#preprocess.task.process">process</a></samp> task, i.e.:</p>
+ <pre>
+preprocess.target =
+preprocess.target{task} = process
+preprocess.target{category} =
+preprocess.target{ns} =
+</pre>
+
+ <p>Here is a list of what targets are available for each type of file:</p>
+
+ <dl>
+ <dt>cpp -> name-space</dt>
+
+ <dd>
+ <p><dfn>description</dfn>: the preprocessed version of the original
+ file.</p>
+
+ <p><dfn>task</dfn>: <a href="#preprocess.task.process">process</a>
+ (cpp).</p>
+
+ <p><dfn>category and destination</dfn>: <a href=
+ "#preprocess.category.src">src</a> and src/name-space</p>
+
+ <p><dfn>properties</dfn>: <a href=
+ "annex_cfg.html#make.preprocess.prop.cpp">cpp</a>, <a href=
+ "annex_cfg.html#make.preprocess.prop.cpp.flags">cpp.flags</a>, <a href=
+ "annex_cfg.html#make.preprocess.prop.cpp.defs">cpp.defs</a>, <a href=
+ "annex_cfg.html#make.preprocess.prop.cpp.flag-define">cpp.flag-define</a>,
+ <a href=
+ "annex_cfg.html#make.preprocess.prop.cpp.flag-include">cpp.flag-include</a>,
+ <a href=
+ "annex_cfg.html#make.preprocess.prop.cpp.include-paths">cpp.include-paths</a></p>
+ <p><dfn>dependencies</dfn>: include.</p>
+
+ <p><dfn>update if</dfn>: source file or any of the required properties
+ are modified, or if any include dependencies are updated.</p>
+ </dd>
+
+ <dt>fpp -> name-space</dt>
+
+ <dd>
+ <p><dfn>description</dfn>: the preprocessed version of the original
+ file.</p>
+
+ <p><dfn>task</dfn>: <a href="#preprocess.task.process">process</a>
+ (fpp).</p>
+
+ <p><dfn>category and destination</dfn>: <a href=
+ "#preprocess.category.src">src</a> and src/name-space</p>
+
+ <p><dfn>properties</dfn>: <a href=
+ "annex_cfg.html#make.preprocess.prop.fpp">fpp</a>, <a href=
+ "annex_cfg.html#make.preprocess.prop.fpp.flags">fpp.flags</a>, <a href=
+ "annex_cfg.html#make.preprocess.prop.fpp.defs">fpp.defs</a>, <a href=
+ "annex_cfg.html#make.preprocess.prop.fpp.flag-define">fpp.flag-define</a>,
+ <a href=
+ "annex_cfg.html#make.preprocess.prop.fpp.flag-include">fpp.flag-include</a>,
+ <a href=
+ "annex_cfg.html#make.preprocess.prop.fpp.include-paths">fpp.include-paths</a></p>
+
+ <p><dfn>dependencies</dfn>: include.</p>
+
+ <p><dfn>update if</dfn>: source file or any of the required properties
+ are modified, or if any include dependencies are updated.</p>
+ </dd>
+
+ <dt>h -> name</dt>
+
+ <dd>
+ <p><dfn>description</dfn>: a header (include) file.</p>
+
+ <p><dfn>task</dfn>: <a href="#preprocess.task.install">install</a>.</p>
+
+ <p><dfn>category and destination</dfn>: <a href=
+ "#preprocess.category.include">include</a> and include/name</p>
+
+ <p><dfn>dependencies</dfn>: include.</p>
+
+ <p><dfn>update if</dfn>: source file is modified.</p>
+ </dd>
+ </dl>
+
+ </div>
+ </div>
+ </div>
+
+ <hr/>
+ <div class="container-fluid text-center">
+ <div class="row-fluid"><div class="span12">
+ <address><small>
+ © British Crown Copyright 2006-14
+ <a href="http://www.metoffice.gov.uk">Met Office</a>.
+ See <a href="../etc/fcm-terms-of-use.html">Terms of Use</a>.<br />
+ This document is released under the British <a href=
+ "http://www.nationalarchives.gov.uk/doc/open-government-licence/" rel=
+ "license">Open Government Licence</a>.<br />
+ </small></address>
+ </div></div>
+ </div>
+
+ <script type="text/javascript" src="../etc/jquery.min.js"></script>
+ <script type="text/javascript" src="../etc/bootstrap/js/bootstrap.min.js"></script>
+ <script type="text/javascript" src="../etc/fcm.js"></script>
+ <script type="text/javascript" src="../etc/fcm-version.js"></script>
+</body>
+</html>
diff --git a/doc/user_guide/overview.html b/doc/user_guide/overview.html
new file mode 100644
index 0000000..8f20123
--- /dev/null
+++ b/doc/user_guide/overview.html
@@ -0,0 +1,155 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>FCM: User Guide: System Overview</title>
+ <meta name="author" content="FCM team" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link rel="icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link rel="shortcut icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link href="../etc/bootstrap/css/bootstrap.min.css" rel="stylesheet" media="screen" />
+ <link href="../etc/fcm.css" rel="stylesheet" media="screen" />
+</head>
+<body>
+ <div class="navbar navbar-inverse">
+ <div class="navbar-inner">
+ <a class="brand" href=".."><span class="fcm-version">FCM</span></a>
+ <ul class="nav">
+ <li><a href="../installation/">Installation</a></li>
+
+ <li class="active"><a href=".">User Guide</a></li>
+ </ul>
+ </div>
+ </div>
+
+ <div class="page-header">
+ <div class="fcm-page-content pull-right well well-small"></div>
+ <h1>FCM: User Guide: System Overview</h1>
+ </div>
+
+ <div class="container">
+ <div class="row">
+ <div class="span12">
+
+ <p>The FCM system is designed to simplify the task of managing and building
+ source code. It consists of the following components:</p>
+
+ <h2 id="code-management">Code Management</h2>
+
+ <p>FCM uses <a href="http://subversion.apache.org/">Subversion</a> for
+ version control. For a summary of its main features please refer to the
+ <a href="http://svnbook.red-bean.com/">Version Control with
+ Subversion</a> book. Subversion is a generalised tool which can be used
+ in lots of different ways. This makes some day-to-day tasks more complex
+ than they need be. FCM defines a simplified process and appropriate
+ naming conventions. It then adds a layer on top of Subversion to provide
+ a natural interface which is specifically tailored to this process. Where
+ appropriate it simply makes use of the command line tools provided by
+ Subversion. However, in other cases it provides significant additional
+ functionality, e.g.:</p>
+
+ <ul>
+ <li>By making some assumptions about the repository layout (i.e. by
+ imposing a standard working practice) FCM simplifies the task of
+ creating branches and enforces a standard branch naming
+ convention.</li>
+
+ <li>Having defined working practices and standard log messages allows
+ FCM to greatly simplify the process of merging changes between
+ branches.</li>
+
+ <li>FCM makes use of <a href="http://furius.ca/xxdiff/">xxdiff</a> (a
+ graphical diff and merge tool) to allow users to easily examine changes
+ they have made and to simplify the process of resolving any conflicts
+ which result from a merge.</li>
+ </ul>
+
+ <p>FCM uses <a href="http://trac.edgewall.org/">Trac</a>, a powerful web
+ based tool, to manage software projects. <a href=
+ "http://trac.edgewall.org/">Trac</a> has the following features:</p>
+
+ <ul>
+ <li>A flexible issue tracker which can be used to keep track of bugs,
+ feature requests, etc. Each issue (known as a <q title=
+ "http://trac.edgewall.org/wiki/TracTickets">ticket</q> within Trac) can
+ be given a priority and assigned to a particular person. Changes made
+ to your Subversion repository can easily be traced to the relevant
+ ticket. Where appropriate, tickets can be used to record information
+ about who has reviewed each change.</li>
+
+ <li>A <q title="http://trac.edgewall.org/wiki/TracRoadmap">roadmap</q>
+ feature which helps you to plan and manage project releases. Each
+ ticket can be associated with a particular milestone. Trac can then
+ easily show you what features or fixes went into a particular release
+ or what work remains before a particular milestone is reached.</li>
+
+ <li>A <q title="http://trac.edgewall.org/wiki/TracWiki">wiki</q> which
+ can be used for project documentation.</li>
+
+ <li>A browser for viewing your Subversion repository which allows you
+ to browse the project tree / files and examine revision logs and
+ changesets.</li>
+
+ <li>A timeline view which summarises all the activity on a project
+ (changes to the tickets, wiki pages or the Subversion repository).</li>
+ </ul>
+
+ <h2 id="build-and-extract">Build and Extract</h2>
+
+ <p>FCM features a powerful build system, mainly aimed at building modern
+ Fortran software applications. It has the following features:</p>
+
+ <ul>
+ <li>Parallel build.</li>
+
+ <li>Efficient incremental build. Changes to the MD5 checksums of source
+ files and/or the build configuration (e.g. changes to the compiler
+ flags) trigger the appropriate re-compilation.</li>
+
+ <li>Inheritance of items from an existing build.</li>
+
+ <li>Build dependency analysis.</li>
+
+ <li>Automatic generation of include files to contain the calling
+ interfaces of standalone functions and subroutinues in Fortran source
+ files.</li>
+
+ <li>Extract of source files from multiple repositories and working
+ copies.</li>
+
+ <li>Extract and merge of source files from different branches of
+ development.</li>
+
+ <li>Minimal configuration.</li>
+ </ul>
+
+ <h2 id="illustration">Illustration</h2>
+
+ <p>The diagram below illustrates how these components fit together.</p>
+
+ <p><img class="img-polaroid" src="fcm_overview.png"
+ alt="FCM system overview" /></p>
+
+ </div>
+ </div>
+ </div>
+
+ <hr/>
+ <div class="container-fluid text-center">
+ <div class="row-fluid"><div class="span12">
+ <address><small>
+ © British Crown Copyright 2006-14
+ <a href="http://www.metoffice.gov.uk">Met Office</a>.
+ See <a href="../etc/fcm-terms-of-use.html">Terms of Use</a>.<br />
+ This document is released under the British <a href=
+ "http://www.nationalarchives.gov.uk/doc/open-government-licence/" rel=
+ "license">Open Government Licence</a>.<br />
+ </small></address>
+ </div></div>
+ </div>
+
+ <script type="text/javascript" src="../etc/jquery.min.js"></script>
+ <script type="text/javascript" src="../etc/bootstrap/js/bootstrap.min.js"></script>
+ <script type="text/javascript" src="../etc/fcm.js"></script>
+ <script type="text/javascript" src="../etc/fcm-version.js"></script>
+</body>
+</html>
diff --git a/doc/user_guide/system_admin.html b/doc/user_guide/system_admin.html
new file mode 100644
index 0000000..86b2c99
--- /dev/null
+++ b/doc/user_guide/system_admin.html
@@ -0,0 +1,612 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>FCM: User Guide: System Administration</title>
+ <meta name="author" content="FCM team" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link rel="icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link rel="shortcut icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link href="../etc/bootstrap/css/bootstrap.min.css" rel="stylesheet" media="screen" />
+ <link href="../etc/fcm.css" rel="stylesheet" media="screen" />
+</head>
+<body>
+ <div class="navbar navbar-inverse">
+ <div class="navbar-inner">
+ <a class="brand" href=".."><span class="fcm-version">FCM</span></a>
+ <ul class="nav">
+ <li><a href="../installation/">Installation</a></li>
+
+ <li class="active"><a href=".">User Guide</a></li>
+ </ul>
+ </div>
+ </div>
+
+ <div class="page-header">
+ <div class="fcm-page-content pull-right well well-small"></div>
+ <h1>FCM: User Guide: System Administration</h1>
+ </div>
+
+ <div class="container">
+ <div class="row">
+ <div class="span12">
+
+ <h2 id="introduction">Introduction</h2>
+
+ <p>This chapter provides an administration guide for managers of projects or
+ systems which are using FCM.</p>
+
+ <p>Note that, where this section refers to the <em>FCM team</em> this applies
+ only to Met Office users. Users at other sites will either need to refer to the
+ equivalent team within their organisation or will need to perfom these tasks
+ themselves.</p>
+
+ <h2 id="svn">Subversion</h2>
+
+ <h3 id="svn_layout">Repository Layout</h3>
+
+ <p>In theory you can set up your repository to have any random layouts.
+ However, many <code>fcm</code> commands have to make assumptions on a set of
+ working practices in order to function. The easiest way is to go with the
+ default:</p>
+ <pre>
+<root>
+ |
+ |-- <project 1>
+ | |
+ | |-- trunk
+ | |-- branches
+ | |-- tags
+ |
+ |-- <project 2>
+ | |
+ | |-- trunk
+ | |-- branches
+ | |-- tags
+ |
+ |-- ...
+</pre>
+
+ <p>In the default layout, each project is a sub-directory under the repository
+ root. Each project has a <code>trunk</code> sub-directory, and optionally a
+ <code>branches</code> sub-directory and a <code>tags</code> sub-directory. The
+ main line of development of the project lives directly under the
+ <code>trunk</code> sub-directory. A branch lives 3 levels under the
+ <code>branches</code> sub-directory. A tag lives 1 level under the
+ <code>tags</code> sub-directory.</p>
+
+ <p>FCM allows you to customise the layout for each repository by adding an
+ <code>fcm:layout</code> property at the HEAD of root of the repository.
+ E.g.:</p>
+
+ <pre>
+(shell prompt)$ fcm co -q -N svn://host/repos repos-root
+(shell prompt)$ cd repos-root
+(shell prompt)$ fcm pe fcm:layout .
+</pre>
+
+ <p>The default settings are given in the following. In the editor started by
+ <code>fcm pe</code>, add these settings and modify the VALUE of each KEY=VALUE
+ pair.</p>
+
+ <pre>
+depth-project =
+depth-branch = 3
+depth-tag = 1
+dir-trunk = trunk
+dir-branch = branches
+dir-tag = tags
+level-owner-branch = 2
+level-owner-tag =
+template-branch = {category}/{owner}/{name_prefix}{name}
+template-tag =
+</pre>
+
+ <p>The settings will become effective when you <code>fcm commit</code> them.
+ An empty VALUE denotes an undefined value. The meanings of the settings are
+ described below:</p>
+
+ <dl>
+ <dt><code>depth-project</code></dt>
+
+ <dd>Number of sub-directories expected to be used by the name of a project.
+ An undefined value means that a project can live under an
+ arbitrary number of sub-directories (or directly) below the repository
+ root.</dd>
+
+ <dt><code>depth-branch</code></dt>
+
+ <dd>Number of sub-directories (under the sub-directory defined by
+ <code>dir-branch</code>) expected to be used by the name of a branch. This
+ setting must be defined.</dd>
+
+ <dt><code>depth-tag</code></dt>
+
+ <dd>Number of sub-directories (under the sub-directory defined by
+ <code>dir-tag</code>) expected to be used by the name of a tag. This
+ setting must be defined.</dd>
+
+ <dt><code>dir-trunk</code></dt>
+
+ <dd>The sub-directory (under the project) where the trunk source tree lives.
+ This setting must be defined.</dd>
+
+ <dt><code>dir-branch</code></dt>
+
+ <dd>The sub-directory (under the project) where (the sub-directories
+ containing) all the branch source trees live.</dd>
+
+ <dt><code>dir-tag</code></dt>
+
+ <dd>The sub-directory (under the project) where (the sub-directories
+ containing) all the tag source trees live.</dd>
+
+ <dt><code>level-owner-branch</code></dt>
+
+ <dd>The sub-directory level in the name of a branch containing its owner.</dd>
+
+ <dt><code>level-owner-tag</code></dt>
+
+ <dd>The sub-directory level in the name of a tag containing its owner.</dd>
+
+ <dt><code>template-branch</code></dt>
+
+ <dd>The template string to construct a branch name.</dd>
+
+ <dt><code>template-tag</code></dt>
+
+ <dd>The template string to construct a tag name.</dd>
+ </dl>
+
+ <p>You will need to decide whether to use a single project tree for your
+ system or whether to use multiple projects.</p>
+
+ <p>Advantages of a single project tree:</p>
+
+ <ul>
+ <li>Changes to any part of the system can always be committed as a single
+ logical changeset. If you split your system into multiple projects then you
+ may have occasions when a logical change involves more than one project and
+ hence requires multiple commits (and branches).</li>
+ </ul>
+
+ <p>Disadvantages of a single project tree:</p>
+
+ <ul>
+ <li>If you have a large system then your working copies may become very
+ large and unwieldy. Basic commands such as <code>checkout</code> and
+ <code>status</code> can become frustratingly slow if your working copy is
+ too large.</li>
+
+ <li>Depending on how you work, you may end up doing lots more merges of
+ files that are unrelated to your work.</li>
+ </ul>
+
+ <p>One common approach is to split the admin type files (e.g. site
+ configurations that are unrelated to the main release) into a separate project
+ from the core system files. If you include any large data files under version
+ control you may also want to use a separate project for them to avoid making
+ your working copies very large when editing code.</p>
+
+ <p>Note that there is often no obvious right or wrong answer so you just have
+ to make a decision and see how it works out. You can always re-arrange your
+ repository in the future (although be aware that this will break any changes
+ being prepared on branches at the time).</p>
+
+ <p>You also need to decide whether your system requires its own repository
+ (or multiple repositories) or whether it can share with another system.</p>
+
+ <ul>
+ <li>The main disadvantage of having separate repositories for each system
+ is the maintenance overhead (although this is almost all automated by the
+ FCM team so is not a big deal).</li>
+
+ <li>We normally configure a single Trac environment per repository. If the
+ repository contains multiple systems then it makes it difficult to use the
+ Trac milestones to handle system releases. However, Trac now supports
+ restricting itself to a sub-directory within a repository so, again, this
+ is not a big deal.</li>
+
+ <li>If you share a repository with other systems then your revision numbers
+ can increase even when there are no changes to your system. This doesn't
+ matter but some people don't like it.</li>
+ </ul>
+
+ <p>For simplicity, in most cases you will probably want your own repository
+ for your system.</p>
+
+ <p>You will not normally want to have multiple repositories for a system. One
+ exception may be if you are storing large data files where you might not want
+ to keep all the old versions for ever. Removing old versions can't be done
+ without changing all the revision numbers which would mess up all your code
+ history and Trac tickets. Storing the large data files in a separate
+ repository reduces the impact if you do decide to remove old versions in the
+ future. One disadvantage of this approach is that, for the moment at least,
+ Trac only handles one repository so you will need a separate Trac environment for
+ the data files.</p>
+
+ <p>For further details please see the section <a href=
+ "http://svnbook.red-bean.com/en/1.8/svn.reposadmin.planning.html#svn.reposadmin.projects.chooselayout">
+ Planning Your Repository Organization</a> from the Subversion book.</p>
+
+ <h3 id="svn_create">Creating a Repository</h3>
+
+ <p>Normally the FCM team will help you to set up your initial repository.
+ However, it is quite simple if you need to do it yourself. First you need to
+ issue the command <code>svnadmin create /path/to/repos</code>. This creates an
+ empty repository which is now ready to accept an initial import. To do so, you
+ should create a directory tree in a suitable location, and issue the <code>fcm
+ project-create</code> command. At the root of the repository should be the
+ project directories. Each project should then contain the <samp>trunk</samp>
+ sub-directory. The sub-directories <samp>branches</samp> and
+ <samp>tags</samp> are optional. You can import your source files to the
+ <samp>trunk</samp> after the project is created. For example, if your
+ directory tree is located at <samp>$HOME/foo</samp>, you will do the following
+ to import it to a new repository:</p>
+ <pre>
+(SHELL PROMPT)$ svnadmin create FOO_svn
+(SHELL PROMPT)$ fcm project-create FOO file://$PWD/FOO_svn
+(SHELL PROMPT)$ fcm checkout file://$PWD/FOO_svn/FOO $HOME/svn-wc/foo
+(SHELL PROMPT)$ cd $HOME/svn-wc/foo
+(SHELL PROMPT)$ cp -r $HOME/foo/* .
+(SHELL PROMPT)$ fcm add *
+(SHELL PROMPT)$ fcm status
+(SHELL PROMPT)$ fcm commit
+</pre>
+
+ <p>Note that the <code>svnadmin</code> command takes a <var>PATH</var> as an
+ argument, as opposed to a URL for the <code>svn</code> command.</p>
+
+ <p>For further details please see the section <a href=
+ "http://svnbook.red-bean.com/en/1.8/svn.reposadmin.planning.html#svn.reposadmin.projects.chooselayout">
+ Planning Your Repository Organization</a> from the Subversion book.</p>
+
+ <h3 id="svn_access">Access Control</h3>
+
+ <p>Restrictions such as preventing anonymous read access or restricting write
+ access to the trunk to a limited set of users can be arranged if
+ necessary.</p>
+
+ <h3 id="svn_hosting">Repository Hosting</h3>
+
+ <p>The FCM team will organise the hosting of your repository. A number of
+ facilities will be set up for you as standard.</p>
+
+ <p>Your repository will be set up on a central FCM server and access will be
+ provided via <code>svnserve</code> (which we use in preference to
+ <cite>Apache</cite> for performance reasons). The FCM team will advise you of
+ the URL, and put in place standard hook scripts and backup procedures.</p>
+
+ <p>Note that if you want to use a Subversion repository for your own
+ individual use there is no need to get the FCM team to host it. You can
+ simply create your repository and then use a <code>file://</code> URL to
+ access it.</p>
+
+ <h2 id="trac">Trac</h2>
+
+ <h3 id="trac_config">Trac Configuration</h3>
+
+ <p>Normally the FCM team will set up your Trac environment for you. This
+ section describes some things you may wish to be configured. This can be done
+ when the Trac environment is set up or later if you are unsure what you will
+ require at first.</p>
+
+ <h4 id="trac_access">Access Control</h4>
+
+ <p>You will not normally want to allow anonymous users to make changes to
+ your Trac environment since this means that changes may not get identified with a
+ userid. The FCM team will normally set up your Trac environment such that any
+ authenticated users can make changes. Further restrictions such as
+ restricting write access to named accounts or preventing anonymous read
+ access can be arranged if necessary.</p>
+
+ <p>The system manager will normally be given <var>TRAC_ADMIN</var>
+ privileges. This allows them to do additional things which normal users
+ cannot do such as:</p>
+
+ <ul>
+ <li>Delete wiki pages (the latest version or the entire page).</li>
+
+ <li>Add or modify milestones, components and versions.</li>
+
+ <li>Modify ticket descriptions and delete ticket attachments.</li>
+
+ <li>Make wiki pages read-only.</li>
+
+ <li>Alter the permissions.</li>
+ </ul>
+
+ <p>For further details please see the section <a href=
+ "http://trac.edgewall.org/wiki/TracPermissions">Trac Permissions</a> from the
+ Trac documentation.</p>
+
+ <h4 id="trac_email">Email Notification</h4>
+
+ <p>By default, each Trac environment is configured such that the owner and
+ reporter and anyone on the <var>CC</var> list are notified whenever a change
+ is made to a ticket. If system mangers wish to be notified of all ticket
+ changes then this can also be configured. Alternatively, email notifications
+ can be disabled if they are not wanted.</p>
+
+ <h4 id="trac_misc">Other Configurable Items</h4>
+
+ <p>There are lots of other things that can be configured in your Trac
+ environment
+ such as:</p>
+
+ <ul>
+ <li>Custom fields</li>
+
+ <li>System icon</li>
+
+ <li>Stylesheets</li>
+ </ul>
+
+ <p>For further details please see the sections <a href=
+ "http://trac.edgewall.org/wiki/TracIni">The Trac Configuration File</a> and
+ <a href="http://trac.edgewall.org/wiki/TracTickets">The Trac Ticket
+ System</a> from the Trac documentation.</p>
+
+ <h3 id="trac_hosting">Trac Hosting</h3>
+
+ <p>The FCM team will organise the hosting of your Trac environment. It will be set
+ up on the same server that hosts your Subversion repository and access will
+ be provided via a web server. The FCM team will advise you of the URL, and put
+ in place the backup procedures.</p>
+
+ <h2 id="fcm-keywords">FCM Keywords</h2>
+
+ <p>When you set up a repository for a new project, you will normally want the
+ FCM team to set up a URL keyword for it in the FCM central configuration
+ file. The name of the project should be a short string containing only word
+ characters.</p>
+
+ <p>Individual projects can store revision keywords using the Subversion
+ property <code>fcm:revision</code> at registered URLs. Using the UM as an
+ example: if <samp>UM</samp> is a registered URL keyword, you can add the
+ <code>fcm:revision</code> property at the head of the UM project by doing a
+ non-recursive checkout. E.g.:</p>
+ <pre>
+(prompt)$ fcm co -q -N fcm:um um
+(prompt)$ fcm pe fcm:revision um
+</pre>
+
+ <p>In the editor, add the following and <code>fcm commit</code>:</p>
+ <pre>
+vn6.3 = 402
+vn6.4 = 1396
+vn6.5 = 2599
+vn6.6 = 4913
+vn7.0 = 6163
+</pre>
+
+ <p>In a subsequent invocation of <code>fcm</code>, if a revision keyword is
+ specified for a URL in the UM namespace, the command will attempt to load it
+ from the <code>fcm:revision</code> property at the head of the UM project.
+ Revision keywords can also be defined in the FCM central configuration file
+ if you prefer.</p>
+
+ <p>If the project has an associated Trac browser, you can also declare
+ browser URL mapping in the central configuration file. This allows FCM to
+ associate the Subversion URL with a Trac browser URL. There is an automatic
+ default for mapping URLs hosted by the FCM team at the Met Office. External
+ users of FCM may want to adjust this default for their site.</p>
+
+ <p>To change the default browser URL mapping, you need to make some
+ <code>browser.*[namespace] = value</code> declarations in your site's
+ <samp>$FCM/etc/fcm/keyword.cfg</samp> file. There are 3 components to this
+ declaration: <var>browser.comp-pat</var>, <var>browser.loc-tmpl</var> and
+ <var>browser.rev-tmpl</var>. The <var>browser.comp-pat</var> is a regular
+ expression, which is used to separate the scheme-specific part of a version
+ control system URL into a number of components by capturing its substrings.
+ These components are then used to fill in the numbered fields in the
+ <var>browser.loc-tmpl</var>. The template should have one more field than the
+ number of components captured by <var>browser.comp-pat</var>. The last field
+ is used to place the revision, which is generated via the
+ <var>browser.rev-tmpl</var>. This template should have a single numbered
+ field for filling in the revision number. This is best demonstrated by an
+ example. Consider the declarations:</p>
+ <pre>
+browser.comp-pat = (?msx-i:\A//([^/]+)/(.*)\z)
+browser.loc-tmpl = http://{1}/intertrac/source:{2}{3}
+browser.rev-tmpl = @{1}
+</pre>
+
+ <p>If we have a Subversion URL <samp>svn://repos/path/to/a/file</samp>, the
+ <var>browser.comp-pat</var> will capture the components [<samp>repos</samp>,
+ <samp>path/to/a/file</samp>]. When this is applied to the
+ <code>browser.loc-tmpl</code>, <var>{1}</var> will be translated to
+ <samp>repos</samp> and <var>{2}</var> will be translated to
+ <samp>path/to/a/file</samp>. A revision is not given in this case, and so
+ <var>{3}</var> is inserted with an empty string. The result is
+ <samp>http://repos/intertrac/path/to/a/file</samp>. If the revision is
+ <samp>1357</samp>, the <var>browser.rev-tmpl</var> will be used to translate
+ it to <samp>@1357</samp>, which is then inserted to <var>{3}</var> of the
+ <var>browser.loc-tmpl</var>. The result is therefore
+ <samp>http://repos/intertrac/path/to/a/file@1357</samp>.</p>
+
+ <p>For more information on how to set up the keywords, please refer to
+ <a href="code_management.html#svn_basic_keywords">Repository & Revision
+ Keywords</a> and the <a href="annex_cfg.html">Annex: FCM Configuration
+ File</a>.</p>
+
+ <h2 id="make-cfg">FCM Make Configuration</h2>
+
+ <p>The <code>fcm make</code> command (for invoking the extract and build
+ systems) is very flexibile and can be used in lots of different ways. It is
+ therefore difficult to give specific advice explaining how to configure them.
+ However, based on experience with a number of systems, the following general
+ advice can be offered.</p>
+
+ <ul>
+ <li>Standard FCM Make configuration files should be defined and stored
+ within the repository. Users then include these files into their
+ configurations, before applying their local changes.</li>
+
+ <li>The files should be designed to include one another in a hierarchy. For
+ example, you may have one core file which defines all the repository and
+ source locations plus a series of platform/compiler specific files which
+ include the core file. More complex setups are also possible if you need to
+ cater for other options such as different optimisation levels, 32/64 bit,
+ etc.</li>
+
+ <li>When including other configuration files, always make use of the
+ special <var>$HERE</var> variable (rather than, for instance, referring to
+ a fixed repository location). When your configuration file is parsed, this
+ special variable is normally expanded into the container directory of the
+ current configuration file. This means that the include statements should
+ work correctly whether you are referring to configuration files in the
+ repository trunk, in a branch or in a local working copy.</li>
+
+ <li>Make good use of variables (e.g. <samp>$name_spaces</samp>) to simplify
+ repetitive declarations and make your configuration files easier to
+ maintain.</li>
+
+ <li>Use continuation lines to split long lines and make them easier to
+ read.</li>
+ </ul>
+
+ <p>Probably the best advice is to look at what has already been set up for
+ other systems. The FCM team can advise on the best systems to examine.</p>
+
+ <p>When you create a stable build you should keep a FCM Make configuration
+ file that can reproduce the build. One easy way to do this is to create your
+ build using the standard configuration files and the latest versions of the
+ code. You can then save the configuration file which is created on
+ success.</p>
+
+ <h2 id="alternate_versions">Maintaining Alternate Versions of Namelists and
+ Data Files</h2>
+
+ <p>Sometimes it is useful to be able to access particular revisions of some
+ directories from a FCM repository without having to go via Subversion.
+ Typical examples are namelist or data files used as inputs to a program. The
+ <code>fcm export-items</code> command is designed to help with this. It can
+ be used to maintain a set of extracted version directories from a FCM
+ repository. The command has the following options:</p>
+
+ <dl>
+ <dt><code>--config-file=PATH</code>, <code>--file=PATH</code>, <code>-f
+ PATH</code></dt>
+
+ <dd>Specifies the path to the configuration file.
+ (default=<samp>$PWD/fcm-export-items.cfg</samp>)</dd>
+
+ <dt><code>--directory=PATH</code>, <code>-C PATH</code></dt>
+
+ <dd>Specifies the path to the destination. (default=<samp>$PWD</samp>)</dd>
+
+ <dt><code>--new</code></dt>
+
+ <dd>Specifies the new mode. In this mode, everything is re-exported.
+ Otherwise, the system runs in incremental mode, in which the version
+ directories are only updated if they do not already exist.</dd>
+ </dl>
+
+ <p>The 1st argument SOURCE should be the URL of a branch in a Subversion
+ repository with the standard FCM layout.</p>
+
+ <p>The configuration file should be in the deprecated FCM 1 configuration
+ format. The label in each entry should be a path relative to the source URL.
+ If the path ends in <samp>*</samp> then the path is expanded recursively and
+ any sub-directories containing regular files are added to the list of relative
+ paths to extract. The value may be empty, or it may be a list of space
+ separated <em>conditions</em>. Each condition is a conditional operator
+ (<code>></code>, <code>>=</code>, <code><</code>,
+ <code><=</code>, <code>==</code> or <code>!=</code>) followed by a
+ revision number. The command uses the revision log to determine the revisions
+ at which the relative path has been updated in the source URL. If these
+ revisions also satisfy the conditions set by the user, they will be
+ considered in the export.</p>
+
+ <p>Example:</p>
+ <pre>
+(SHELL PROMPT)$ cat >fcm-export-items.cfg <<EOF
+namelists/VerNL_AreaDefinition >1000 !=1234
+namelists/VerNL_GRIBToPPCode >=600 <3000
+namelists/VerNL_StationList
+elements/* >1000
+EOF
+(SHELL PROMPT)$ fcm export-items fcm:ver_tr
+</pre>
+
+ <p>N.B.</p>
+
+ <ol>
+ <li>Each time a sub-directory is revised, the script assigns a sequential
+ <em>v</em> number for the item. Each <em>v</em> number for a sub-directory,
+ therefore, is associated with a revision number. For each exported
+ revision directory, there is a corresponding <em>v</em> number symbolic
+ link pointing to it.</li>
+
+ <li>The system also creates a symbolic link <samp>latest</samp> to point to
+ the latest exported revision directory.</li>
+ </ol>
+
+ <h2 id="work-practice">Defining Working Practices and Policies</h2>
+
+ <p>Some options on working practices and policies are defined in the chapter
+ on <a href="working_practices.html">Code Management Working Practices</a>.
+ Individual projects should document the approach they have adopted. In
+ addition, each project may also need to define its own working practices and
+ policies to suit its local need. For example each project may need to
+ specify:</p>
+
+ <ul>
+ <li>Whether changes are allowed directly on the trunk or whether branches
+ have to be used in all cases.</li>
+
+ <li>Whether all users are allowed to make changes to the trunk.</li>
+
+ <li>Whether Trac tickets have to be raised for all changes to the
+ trunk.</li>
+
+ <li>Whether Trac tickets should be raised for all support queries or
+ whether a Trac ticket should only be raised once there is an agreed
+ "issue".</li>
+
+ <li>Whether branches should normally be made from the latest code or from a
+ stable release.</li>
+
+ <li>Whether a user is allowed to resolve conflicts directly when merging a
+ branch into the trunk or whether he/she should merge the trunk into the
+ branch and resolve the conflicts in the branch first.</li>
+
+ <li>Whether all code changes to the trunk need to be reviewed.</li>
+
+ <li>What testing is required before changes can be merged to the
+ trunk.</li>
+
+ <li>Whether history entries are maintained in source files or whether
+ individual source files changes need to be described in the Subversion log
+ message.</li>
+
+ <li>Branch deletion policy.</li>
+
+ <li>Whether any files in the project require locking before being
+ changed.</li>
+ </ul>
+
+ </div>
+ </div>
+ </div>
+
+ <hr/>
+ <div class="container-fluid text-center">
+ <div class="row-fluid"><div class="span12">
+ <address><small>
+ © British Crown Copyright 2006-14
+ <a href="http://www.metoffice.gov.uk">Met Office</a>.
+ See <a href="../etc/fcm-terms-of-use.html">Terms of Use</a>.<br />
+ This document is released under the British <a href=
+ "http://www.nationalarchives.gov.uk/doc/open-government-licence/" rel=
+ "license">Open Government Licence</a>.<br />
+ </small></address>
+ </div></div>
+ </div>
+
+ <script type="text/javascript" src="../etc/jquery.min.js"></script>
+ <script type="text/javascript" src="../etc/bootstrap/js/bootstrap.min.js"></script>
+ <script type="text/javascript" src="../etc/fcm.js"></script>
+ <script type="text/javascript" src="../etc/fcm-version.js"></script>
+</body>
+</html>
diff --git a/doc/user_guide/working_practices.html b/doc/user_guide/working_practices.html
new file mode 100644
index 0000000..a6a4204
--- /dev/null
+++ b/doc/user_guide/working_practices.html
@@ -0,0 +1,822 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>FCM: User Guide: Code Management Working Practices</title>
+ <meta name="author" content="FCM team" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link rel="icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link rel="shortcut icon" href="../etc/fcm-icon.png" type="image/png" />
+ <link href="../etc/bootstrap/css/bootstrap.min.css" rel="stylesheet" media="screen" />
+ <link href="../etc/fcm.css" rel="stylesheet" media="screen" />
+</head>
+<body>
+ <div class="navbar navbar-inverse">
+ <div class="navbar-inner">
+ <a class="brand" href=".."><span class="fcm-version">FCM</span></a>
+ <ul class="nav">
+ <li><a href="../installation/">Installation</a></li>
+
+ <li class="active"><a href=".">User Guide</a></li>
+ </ul>
+ </div>
+ </div>
+
+ <div class="page-header">
+ <div class="fcm-page-content pull-right well well-small"></div>
+ <h1>FCM: User Guide: Code Management Working Practices</h1>
+ </div>
+
+ <div class="container">
+ <div class="row">
+ <div class="span12">
+
+ <h2 id="introduction">Introduction</h2>
+
+ <p>The previous chapter described how to use the various parts of the FCM
+ code management system. They also described aspects of working practices
+ which are enforced by the system. This section discusses other recommended
+ working practices. They are optional in the sense that you don't have to
+ follow them to use FCM. It is a matter for individual projects to decide
+ which working practices to adopt (although we expect most projects/systems
+ using FCM to adopt similar practices).</p>
+
+ <h2 id="changes">Making Changes</h2>
+
+ <p>This sub-section gives an overview of the recommended approach for
+ preparing changes. Particular topics are discussed in more detail in later
+ sub-sections where appropriate.</p>
+
+ <p>The recommended process for making a change is as follows:</p>
+
+ <ol>
+ <li>Before work starts on any coding you should make sure that there is a
+ Trac ticket open which explains the purpose of the change.
+
+ <ul>
+ <li>Make sure that you set the ticket milestone to indicate which
+ release of the system you are aiming to include your change in.</li>
+
+ <li>Accept the ticket to indicate that you are working on the
+ change.</li>
+
+ <li>For further advice on using tickets see <a href="#tickets">Trac
+ Tickets</a> later in this section.</li>
+ </ul>
+ </li>
+
+ <li>Create a branch
+
+ <ul>
+ <li>For very simple changes you may be happy to prepare your changes
+ directly on the trunk. For further details see <a href=
+ "#branching_when">When to Branch</a> later in this section.</li>
+
+ <li>Create your branch either from the latest revision or from a stable
+ release (see <a href="#branching_where">Where to Branch From</a> later
+ in this section).</li>
+ </ul>
+ </li>
+
+ <li>Prepare your code changes on the branch
+
+ <ul>
+ <li>Commit interim versions to your branch on a regular basis as you
+ develop your change. This makes it much easier to keep track of what
+ you're changing and to revert changes if necessary.</li>
+
+ <li>You may wish to merge in changes from the trunk. For further
+ details see <a href="#branching_update">Merging From the Trunk</a>
+ later in this section.
+
+ <ul>
+ <li>Make sure that you always commit any local changes to your
+ branch before doing a merge. Otherwise it becomes impossible to
+ distinguish your changes from those you have merged in. It is also
+ impossible to revert the merge without losing your local
+ changes.</li>
+
+ <li>Likewise, always commit the merge to your branch (after
+ resolving any conflicts) before making any further changes.</li>
+ </ul>
+ </li>
+
+ <li>Don't include unrelated changes. If you want to make some changes
+ which aren't really associated with your other changes then use a
+ separate ticket and branch for these changes.</li>
+ </ul>
+ </li>
+
+ <li>Once your changes are ready for review, update the Trac ticket to
+ record which revision of the branch is to be reviewed and assign the ticket
+ to your reviewer.</li>
+
+ <li>If the reviewer is happy with the change then he/she should update the
+ ticket to record that the change is approved and assign the ticket back to
+ you.
+
+ <ul>
+ <li>The reviewer can use the command <code>fcm branch-diff
+ <branch_name></code> to examine all of the changes on the
+ branch.</li>
+
+ <li>If changes are necessary then these should be prepared and then the
+ ticket updated to refer to the new revision under review.</li>
+ </ul>
+ </li>
+
+ <li>Once the change is approved it can be merged back to the trunk
+
+ <ul>
+ <li>If you have been merging the latest changes from the trunk onto
+ your branch then the merge should be automatic. If not you may have
+ conflicts to resolve.</li>
+
+ <li>Make sure that each merge is a separate commit to the trunk. i.e.
+ Don't combine changes from several branches in one commit. This makes
+ it easier to reverse changes if necessary. It also makes the changeset
+ easier to understand.</li>
+
+ <li>Make sure that you use a good log message to describe your change.
+ For further details see <a href="#messages">Commit Log Messages</a>
+ later in this section.</li>
+
+ <li>Once the changes are commited, update the ticket to refer to the
+ changeset. Then the ticket can be closed.</li>
+ </ul>
+ </li>
+
+ <li>Once you are finished with the branch it should be deleted.</li>
+ </ol>
+
+ <h2 id="wc">Working Copies</h2>
+
+ <p>Some points to consider regarding working copies:</p>
+
+ <ol>
+ <li>If the size of your project is small then you will probably find it
+ easiest to work with a complete copy of the project (either the trunk or
+ your branch). This means that you always have immediate access to all the
+ files and that you are always able to perform merges using your normal
+ working copy.</li>
+
+ <li>If you have a large project then you may prefer to work on a sub-tree
+ of your project.
+
+ <p><dfn>Pros:</dfn></p>
+
+ <ul>
+ <li>Subversion operations on your working copy are faster.</li>
+
+ <li>Your working copies use up less disk space. Remember that you may
+ be working on several changes at once on separate branches so you may
+ wish to have several working copies.</li>
+ </ul>
+
+ <p><dfn>Cons:</dfn></p>
+
+ <ul>
+ <li>You cannot always perform merge operations in sub-trees (if the
+ changes which need to be merged include files outside of your
+ sub-tree). To handle this we suggest that if you need to perform a
+ merge using a complete copy of your project you check it out in your
+ <var>$LOCALDATA</var> area (local disk space which is not backed up) to
+ be used purely for doing the merge.</li>
+
+ <li>You may find that your change involves more files than you
+ originally thought and that some of the files to be changed lie outside
+ of your working copy. You then have to make sure that you have
+ committed any changes before checking out a larger working copy.</li>
+ </ul>
+ </li>
+ </ol>
+
+ <h2 id="branching">Branching & Merging</h2>
+
+ <h3 id="branching_when">When to Branch</h3>
+
+ <p>If you are making a reasonably large change which will take more than a
+ hour or two to prepare then there are clear advantages to doing this work on
+ a branch.</p>
+
+ <ul>
+ <li>You can commit intermediate versions to the branch.</li>
+
+ <li>If you need to merge in changes from the trunk then you have a record
+ of your files prior to the merge.</li>
+
+ <li>The version of the code which gets reviewed is recorded. If subsequent
+ changes are required then only those changes will need reviewing.</li>
+ </ul>
+
+ <p>However, if you are only making a small change (maybe only one line)
+ should you create a branch for this? There are two possible approaches:</p>
+
+ <dl>
+ <dt>Always Branch</dt>
+
+ <dd>
+ <p>ALL coding changes are prepared on branches.</p>
+
+ <p><dfn>Pros:</dfn> Same process is followed in all cases.</p>
+
+ <p><dfn>Cons:</dfn> The extra work required to create the branch and
+ merge it back to the trunk may seem unnecessary for a very small
+ change.</p>
+ </dd>
+
+ <dt>Branch When Needed</dt>
+
+ <dd>
+ <p>Small changes can be committed directly to the trunk (after testing
+ and code review).</p>
+
+ <p><dfn>Pros:</dfn> Avoids the overhead of using branches.</p>
+
+ <p><dfn>Cons:</dfn> Danger of underestimating the size of a change. What
+ you thought was a small change may turn out to be larger than you thought
+ (although you can always move it onto a branch if this happens).</p>
+ </dd>
+ </dl>
+
+ <p>This is a matter for project policy although, in general, we would
+ recommend the <cite>Branch When Needed</cite> approach.</p>
+
+ <h3 id="branching_where">Where to Branch From</h3>
+
+ <p>When you create a new branch you have two choices for which revision to
+ create the branch from:</p>
+
+ <dl>
+ <dt>The latest revision of the trunk</dt>
+
+ <dd>
+ <p>This is the preferred choice where possible. It minimised the chances
+ of conflicts when you need to incorporate your changes back onto the
+ trunk.</p>
+ </dd>
+
+ <dt>An older revision of the trunk</dt>
+
+ <dd>
+ <p>There are a number of reasons why you may need to do this. For
+ example:</p>
+
+ <ul>
+ <li>You are using a stable version to act as your <em>control</em>
+ data.</li>
+
+ <li>You need to know that your baseline is well tested (e.g. scientific
+ changes).</li>
+
+ <li>Your change may need to be merged with other changes relative to a
+ stable version for testing purposes or for use in a package (see
+ <a href="#packages">Creating Packages</a> later in this section).</li>
+ </ul>
+ </dd>
+ </dl>
+
+ <h3 id="branching_update">Merging From the Trunk</h3>
+
+ <p>Once you've created your branch you need to decide whether you now work in
+ isolation or whether you periodically merge in the latest changes from the
+ trunk.</p>
+
+ <ul>
+ <li>Regularly merging from the trunk minimises the work involved when you
+ are ready to merge back to the trunk. You deal with any merge issues as you
+ go along rather than all at the end (by which time your branch and the
+ trunk could have diverged significantly).</li>
+
+ <li>One downside of merging from the trunk is that the baseline for your
+ changes is a moving target. This may not be what you want if you have some
+ <em>control</em> results that you have generated.</li>
+
+ <li>Another downside of merging from the trunk is that it may introduce
+ bugs. Although any code on the trunk should have been tested and reviewed
+ it is unlikely to be as well tested as code from a stable release.</li>
+
+ <li>Unless you originally created your branch from the latest revision of
+ the trunk it is unlikely that you are going to want to merge in changes
+ from the trunk. The exception to this is once your change is complete when
+ it may make sense to merge all the changes on the trunk into your branch as
+ a final step. This is discussed in <a href="#branching_commit">Merging Back
+ to the Trunk</a> below.</li>
+ </ul>
+
+ <p>So, there are basically three methods of working:</p>
+
+ <dl>
+ <dt>Branch from a stable version and prepare all your changes in
+ isolation</dt>
+
+ <dd>Necessary if you need to make your change relative to a well tested
+ release.</dd>
+
+ <dt>Branch from the latest code but then prepare all your changes in
+ isolation</dt>
+
+ <dd>Necessary if you need a stable baseline for your <em>control</em>
+ data.</dd>
+
+ <dt>Branch from the latest code and then update your branch from the trunk
+ on a regular basis</dt>
+
+ <dd>This is considered <em>best practice</em> for parallel working and
+ should be used where possible.</dd>
+ </dl>
+
+ <h3 id="branching_commit">Merging Back to the Trunk</h3>
+
+ <p>Before merging your change back to the trunk you will need to test your
+ change and get it reviewed. There are two options for what code to test and
+ review:</p>
+
+ <dl>
+ <dt>Test and review your changes in isolation, then merge to the trunk and
+ deal with any conflicts</dt>
+
+ <dd>
+ <p>This may be the best method if:</p>
+
+ <ul>
+ <li>Your changes have already been tested against a stable baseline and
+ re-testing after merging would be impracticable.</li>
+
+ <li>Your branch needs to be available for others to merge in its
+ changes in isolation.</li>
+ </ul>
+ </dd>
+
+ <dt>Merge in the latest code from the trunk before your final test and
+ review</dt>
+
+ <dd>
+ <p>This has the advantage that you are testing and reviewing the actual
+ code which will be committed to the trunk. However, it is possible that
+ other changes could get committed to the trunk whilst you are completing
+ your testing and review. There are several ways of dealing with this:</p>
+
+ <ul>
+ <li>Use locking to prevent it happening. The danger with this is that
+ you may prevent others from being able to get their change tested and
+ reviewed, hence inhibiting parallel devlopment.</li>
+
+ <li>Insist that the change is re-tested and reviewed. The problem with
+ this is that there is no guarantee that the same thing won't happen
+ again.</li>
+
+ <li>Merge in the new changes but don't insist on further testing or
+ review.
+
+ <ul>
+ <li>In most cases any changes won't clash so there is little to
+ worry about.</li>
+
+ <li>Where there are clashes then, in most cases, they will be
+ trivial with little danger of any side-effects.</li>
+
+ <li>Where the clashes are significant then, in most cases, this
+ will be very obvious whilst you are resolving the conflicts. In
+ this case you should repeat the testing and get the updates
+ reviewed.</li>
+ </ul>This is the recommended approach since it doesn't inhibit
+ parallel development and yet the chances of a bad change being
+ committed to the trunk are still very small.
+ </li>
+ </ul>
+
+ <p>You should also consider what can be done to minimise the time taken
+ for testing and review.</p>
+
+ <ul>
+ <li>Try to keep your changes small by breaking them down where
+ possible. Smaller changes are easier and quicker to review. This also
+ helps to minimise merge problems by getting changes back onto the trunk
+ earlier.</li>
+
+ <li>Automate your testing as far as possible to speed up the
+ process.</li>
+ </ul>
+ </dd>
+ </dl>
+
+ <p>Most projects will require the developer who prepared the change to merge
+ it back to the trunk once it is complete. However, larger projects may wish
+ to consider restricting this to a number of experienced / trusted
+ developers.</p>
+
+ <ul>
+ <li>This makes it easier to control and prioritise the merges.</li>
+
+ <li>It applies an extra level of quality control.</li>
+
+ <li>It minimises the risk of mistakes being merged back on to the trunk by
+ less experienced developers</li>
+
+ <li>Scientific developers can concentrate on the scientific work.</li>
+
+ <li>One issue is that the person doing the merge to the trunk may need help
+ from the original developer to prepare a suitable log message.</li>
+ </ul>
+
+ <h3 id="branching_delete">When to Delete Branches</h3>
+
+ <p>Once you are finished with your branch it is best to delete it to avoid
+ cluttering up the directory tree (remember that the branch and all its
+ history will still be available). There are two obvious approaches to
+ deleting branches:</p>
+
+ <dl>
+ <dt>Delete the branch as soon as it has been merged back to the trunk
+ (prior to closing any associated Trac ticket)</dt>
+
+ <dd>This is the tidiest approach which minimises the chances of old
+ branches being left around.</dd>
+
+ <dt>Delete the branch once a stable version of the system has been released
+ which incorporates your change</dt>
+
+ <dd>If a bug is found in your change during integration testing then you
+ can prepare the fix on the original branch (without having to do any
+ additional work to restore the branch).</dd>
+ </dl>
+
+ <h2 id="binary">Working with Binary Files</h2>
+
+ <p>The <code>fcm conflicts</code> command and <code>xxdiff</code> can only
+ help you resolve conflicts in text files. If you have binary files in your
+ repository you need to consider whether conflicts in these files would cause
+ a problem.</p>
+
+ <h3 id="binary_conflicts">Resolving Conflicts in Binary Files</h3>
+
+ <p>Conflicts in some types of binary files can be resolved manually. When you
+ are satisfied that the conflicts are resolved, issue the <code>fcm
+ resolved</code> command on the file to remove the conflict status. (You will
+ be prevented from committing if you have a conflicting file in your working
+ copy.)</p>
+
+ <p>If you have a conflicting MS Office 2003+ document, you may be able to
+ take advantage of the <kbd>Tools > Compare and Merge Documents</kbd>
+ facility. Consider a working copy, which you have just updated from revision
+ 100 to revision 101, and someone else has committed some changes to a file
+ <samp>doument.doc</samp> you are editing, you will get:</p>
+ <pre>
+(SHELL PROMPT)$ fcm conflicts
+Conflicts in file: document.doc
+document.doc: ignoring binary file, please resolve conflicts manually.
+(SHELL PROMPT)$ fcm status
+=> svn st
+? document.doc.r100
+? document.doc.r101
+C document.doc
+</pre>
+
+ <p>Open <samp>document.doc.r101</samp> with MS Word. In <kbd>Tools >
+ Compare and Merge Documents...</kbd>, open <samp>document.doc</samp>. You
+ will be in Track Changes mode automatically. Go through the document to
+ accept, reject or merge any changes. Save the document and exit MS Word when
+ you are ready. Finally, issue the <code>fcm resolved</code> command to remove
+ the conflict status:</p>
+ <pre>
+(SHELL PROMPT)$ fcm resolved document.doc
+=> svn resolved document.doc
+Resolved conflicted state of 'document.doc'
+(SHELL PROMPT)$ fcm status
+=> svn st
+M document.doc
+</pre>
+
+ <p>Another type of conflict that you may be able to resolve manually is where
+ the binary file is generated from another file which can be merged. For
+ instance, some people who use LaTeX also store a PDF version of the document
+ in the repository. In such cases it is easy to resolve the conflict by
+ re-generating the PDF file from the merged LaTeX file and then issuing the
+ <code>fcm resolved</code> command to remove the conflict status. Note that,
+ in this particular case, a better approach might be to automate the
+ generation of the PDF file outside of the repository.</p>
+
+ <h3 id="binary_locking">Using Locking</h3>
+
+ <p>For files with binary formats, such as artwork or sound, it is often
+ impossible to merge conflicting changes. In these situations, it is necessary
+ for users to take strict turns when changing the file in order to prevent
+ time wasted on changes that are ultimately discarded.</p>
+
+ <p>Subversion supports <q title=
+ "http://svnbook.red-bean.com/en/1.8/svn.advanced.locking.html">locking</q> to
+ allow you to prevent other users from modifying a file while you are
+ preparing changes. For details please refer to the chapter <a href=
+ "http://svnbook.red-bean.com/en/1.8/svn.advanced.locking.html">Locking</a>
+ from the Subversion book. Note that:</p>
+
+ <ul>
+ <li>FCM does not add any functionality to the locking commands provided by
+ Subversion.</li>
+
+ <li>If you need to lock a file you must do this in a working copy of the
+ trunk. There is nothing to stop you preparing the changes in a branch
+ (maybe you want to prepare the change in combination with a number of other
+ changes which do not require locking). However, you must always remember to
+ lock the file in the trunk first to prevent other users from preparing
+ changes to the file in parallel.</li>
+
+ <li>Locking isn't the only way of preventing conflicts with binary files.
+ If you only have a small project team and a small number of binary files
+ you may find it easier to use other methods of communication such as emails
+ or just talking to each other. Alternatively, you may have a working
+ practice that particular files are only modified by particular users.</li>
+ </ul>
+
+ <h2 id="messages">Commit Log Messages</h2>
+
+ <p>Certain guidelines should be adhered to when writing log messages for code
+ changes when committing to the trunk:</p>
+
+ <ul>
+ <li>Start with a link to the ticket that raises the issues you are
+ addressing.</li>
+
+ <li>Add a keyword to indicate the command/module affected by this change.</li>
+
+ <li>Add a summary of the change.</li>
+
+ <li>Use Trac wiki syntax that can be displayed nicely in plain text.</li>
+
+ <li>E.g. <samp>#429: user guide: improve commit log guidelines.</samp></li>
+ </ul>
+
+ <p>If you realise that you have made a mistake in the commit log, you can
+ modify it by using the command <code>fcm propedit svn:log --revprop -r REV
+ TARGET</code>. Take care since this is an <a href=
+ "http://svnbook.red-bean.com/en/1.8/svn.advanced.props.html">unversioned</a>
+ property so you run the risk of losing information if you aren't careful with
+ your edits.</p>
+
+ <p>There are two possible approaches to recording the changes to individual
+ files:</p>
+
+ <dl>
+ <dt>Maintain history entries in file headers</dt>
+
+ <dd>
+ <p><dfn>Pros:</dfn> You don't need access to the Subversion repository in
+ order to be able to view a files change history (e.g. external
+ collaborators).</p>
+
+ <p><dfn>Cons:</dfn></p>
+
+ <ul>
+ <li>History entries will produce clashes whenever files are changed in
+ parallel (although these conflicts are trivial to resolve).</li>
+
+ <li>Source files which are changed regularly can become cluttered with
+ very long history entries.</li>
+
+ <li>It is not possible to include history entries in some types of
+ file.</li>
+ </ul>
+ </dd>
+
+ <dt>Record which files have changed in the commit log message</dt>
+
+ <dd>
+ <p>The log message should name every modified file and explain why it was
+ changed. Make sure that the log message includes some sort of description
+ for every change. The value of the log becomes much less if developers
+ cannot rely on its completeness. Even if you've only changed comments,
+ note this in the message. For example:</p>
+ <pre>
+ * working_practices.html:
+ Added guidelines for writing log messages.
+</pre>
+
+ <p>If you make exactly the same change in several files, list all the
+ changed files in one entry. For example:</p>
+ <pre>
+ * code_management.html, system_admin.html, index.html:
+ Ran pages through tidy to fix HTML errors.
+</pre>
+
+ <p>It shouldn't normally be necessary to include the full path in the
+ file name - just make sure it is clear which of the changed files you are
+ referring to. You can get a full list of the files changed using
+ <code>fcm log -v</code>.</p>
+ </dd>
+ </dl>
+
+ <p>When you're committing to your own branch then you can be much more
+ relaxed about log messages. Use whatever level of detail you find helpful.
+ However, if you follow similar guidelines then this will help when it comes
+ to preparing the log message when your change is merged back to the
+ trunk.</p>
+
+ <h2 id="tickets">Trac Tickets</h2>
+
+ <h3 id="tickets_create">Creating Tickets</h3>
+
+ <p>There are two different approaches to using the issue tracker within
+ Trac:</p>
+
+ <dl>
+ <dt>All problems should be reported using Trac tickets</dt>
+
+ <dd>
+ <p><dfn>Pros:</dfn> The issue tracker contains a full record of all the
+ problems reported and enhancements requested.</p>
+
+ <p><dfn>Cons:</dfn> The issue tracker gets cluttered up with lots of
+ inappropriate tickets, (which can make it much harder to search the
+ issues and can slow down the response to simple issues).</p>
+
+ <ul>
+ <li>Duplicate tickets.</li>
+
+ <li>Issues already discussed in the documentation.</li>
+
+ <li>Problems which turn out to be unrelated to the system.</li>
+
+ <li>Problems which are poorly described.</li>
+
+ <li>Things which would be better solved by a quick conversation.</li>
+ </ul>
+ </dd>
+
+ <dt>A Trac ticket shouldn't be created until the issue has been agreed</dt>
+
+ <dd>
+ <p>Problems and issues should first be discussed with the project team /
+ system maintainers. Depending on the project, this could be via email, on
+ the newsgroups or through a quick chat over coffee.</p>
+
+ <p>Nothing is lost this way. Issues which are appropriate for the issue
+ tracker still get filed. It just happens slightly later, after initial
+ discussion has helped to clarify the best description for the issue.</p>
+ </dd>
+ </dl>
+
+ <h3 id="tickets_use">Using Tickets</h3>
+
+ <p>This sub-section provides advice on the best way of using tickets:</p>
+
+ <ol>
+ <li>In general, mature systems will require that there is a Trac ticket
+ related to every changeset made to the trunk. However this doesn't mean
+ that there should be a separate ticket for each change.
+
+ <ul>
+ <li>If a change is made to the trunk and then a bug is subsequently
+ found then, if this happens before the next release of the system, the
+ subsequent change can be recorded on the same ticket.</li>
+
+ <li>There can often be changes which don't really affect the system
+ itself since they are just system administration details. One way of
+ dealing with this is to open a ticket for each release in which to
+ record all such miscellaneous changes. It will probably be acceptable
+ to review these changes after they have been committed, prior to the
+ system release.</li>
+ </ul>
+ </li>
+
+ <li>Whenever you refer to source files/directories in tickets, make sure
+ that you refer to particular revisions of the files. This ensures that the
+ links will work in the future, even if those files are no longer in the
+ latest revision. For example:<br />
+ <samp>Changes now ready for review:
+ source:/OPS/branches/dev/frdm/r123_MyBranch at 234</samp></li>
+
+ <li>For some types of information, simply appending to the ticket may not
+ be the best way of working. For example, design notes or test results may
+ be best recorded elsewhere, preferably in a wiki page. If using wiki pages
+ we recommend using a naming convention to identify the wiki page with the
+ associated ticket, for example:<br />
+ <samp>Please refer to [wiki:ticket/123/Design design notes]</samp><br />
+ <samp>See separate [wiki:ticket/123/TestResults test results]</samp><br />
+ Note that the square brackets have to be used since a page name containing
+ numbers is not recognised automatically.</li>
+ </ol>
+
+ <h2 id="packages">Creating Packages</h2>
+
+ <p>Sometimes you may need to combine the changes from several different
+ branches. For example:</p>
+
+ <ul>
+ <li>Your branch is just part of a larger change which needs to be tested in
+ its entirety before committing to the trunk.</li>
+
+ <li>You have some diagnostic code stored on a branch which you want to
+ combine with another branch for testing purposes.</li>
+ </ul>
+
+ <p>We refer to this as creating a <em>package</em>.</p>
+
+ <p>To create a package you simply create a new branch as normal. The
+ <em>type</em> should be a <em>package</em> or possibly a
+ <em>configuration</em> branch to help you distinguish it from your other
+ branches. You then simply merge in all of the branches that you want to
+ combine using <code>fcm merge</code>.</p>
+
+ <ul>
+ <li>The chance of conflicts will be reduced if the branches you are
+ combining have been created from the same point on the trunk. Your package
+ branch should also be created from the same point on the trunk.
+
+ <ul>
+ <li><em>Currently, <code>fcm merge</code> will not work unless this is
+ true.</em></li>
+ </ul>
+ </li>
+
+ <li>If further changes are made on a branch you are using in a package then
+ you can incorporate these changes into your package using <code>fcm
+ merge</code>. Note, however, that if you have a branch which is being used
+ in a package then you should avoid merging changes from the trunk into your
+ branch. If you do then it will be very difficult to get updates to your
+ branch merged into the package.</li>
+ </ul>
+
+ <p>The <code>fcm branch-info</code> command is very useful for maintaining
+ packages. It tells you all of the branches which have been merged into your
+ package and whether there are any more recent changes on those branches.</p>
+
+ <h2 id="releases">Preparing System Releases</h2>
+
+ <p>There are two ways of preparing system releases:</p>
+
+ <dl>
+ <dt>A system release is simply a particular revision of the trunk</dt>
+
+ <dd>
+ <p>In order to do this it will be necessary to restrict changes on the
+ trunk whilst the release is being prepared.</p>
+
+ <ul>
+ <li>Users can continue to develop changes not intended for inclusion in
+ this release on branches.</li>
+
+ <li>This may be a problem if preparing the release takes too long.</li>
+ </ul>
+ </dd>
+
+ <dt>Create a release branch where the release is finalised</dt>
+
+ <dd>
+ <p>You then lose the ability to be able to branch from the release.</p>
+
+ <p>It may be harder to identify what changes have been made between
+ releases (since you can't simply look at all the changesets made between
+ two revisions of the trunk).</p>
+ </dd>
+ </dl>
+
+ <h2 id="rapid">Rapid vs Staged Development Practices</h2>
+
+ <p>Most of this section on working practices has focussed on projects/systems
+ which are quite mature. Such systems are likely to have regular releases and
+ will, for example, insist that all changes to the trunk are reviewed and
+ tested.</p>
+
+ <p>If your system is still undergoing rapid development and has not yet
+ reached any sort of formal release then you will probably want to adopt a
+ much more relaxed set of working practices. For example:</p>
+
+ <ul>
+ <li>Changes don't need to be reviewed.</li>
+
+ <li>More changes will be committed to the trunk. Only very large changes
+ will be prepared on branches.</li>
+
+ <li>No requirement to have a Trac ticket associated with each change.</li>
+ </ul>
+
+ <p>We have tried to avoid building too many assumptions about working
+ practices into the FCM system. This gives projects the flexibility to decide
+ which working practices are appropriate for their system. Hopefully this
+ means that FCM can be used for large or small systems and for rapidly
+ evolving or very stable systems.</p>
+
+ </div>
+ </div>
+ </div>
+
+ <hr/>
+ <div class="container-fluid text-center">
+ <div class="row-fluid"><div class="span12">
+ <address><small>
+ © British Crown Copyright 2006-14
+ <a href="http://www.metoffice.gov.uk">Met Office</a>.
+ See <a href="../etc/fcm-terms-of-use.html">Terms of Use</a>.<br />
+ This document is released under the British <a href=
+ "http://www.nationalarchives.gov.uk/doc/open-government-licence/" rel=
+ "license">Open Government Licence</a>.<br />
+ </small></address>
+ </div></div>
+ </div>
+
+ <script type="text/javascript" src="../etc/jquery.min.js"></script>
+ <script type="text/javascript" src="../etc/bootstrap/js/bootstrap.min.js"></script>
+ <script type="text/javascript" src="../etc/fcm.js"></script>
+ <script type="text/javascript" src="../etc/fcm-version.js"></script>
+</body>
+</html>
diff --git a/doc/user_guide/xxdiff1.png b/doc/user_guide/xxdiff1.png
new file mode 100644
index 0000000..084b277
Binary files /dev/null and b/doc/user_guide/xxdiff1.png differ
diff --git a/doc/user_guide/xxdiff2.png b/doc/user_guide/xxdiff2.png
new file mode 100644
index 0000000..7ff3350
Binary files /dev/null and b/doc/user_guide/xxdiff2.png differ
diff --git a/doc/user_guide/xxdiff_tutorial.png b/doc/user_guide/xxdiff_tutorial.png
new file mode 100644
index 0000000..f308878
Binary files /dev/null and b/doc/user_guide/xxdiff_tutorial.png differ
diff --git a/etc/fcm/admin.cfg.example b/etc/fcm/admin.cfg.example
new file mode 100644
index 0000000..b29783f
--- /dev/null
+++ b/etc/fcm/admin.cfg.example
@@ -0,0 +1,86 @@
+#-------------------------------------------------------------------------------
+# FCM Admin Configuration Example
+# See also the Perl module FCM::Admin::Config
+#-------------------------------------------------------------------------------
+# To use, copy this file to "admin.cfg".
+# Uncomment a line to activate a setting.
+# Default values are given below.
+#-------------------------------------------------------------------------------
+## Email address of FCM system admin
+# admin_email = $USER
+## Notification email address (for the "From:" field in notification emails).
+# notification_from =
+
+## Location for log files
+# log_dir = /var/log/fcm
+
+## Location where FCM is installed
+# fcm_home = $FCM_HOME
+## Location where FCM site specific items are installed
+# fcm_site_home =
+
+## Locations (space delimited) to mirror items in "mirror_keys"
+# mirror_dests =
+## Items (space delimited) to mirror to "mirror_dests", e.g. "fcm_site_home"
+# mirror_keys =
+
+## Location to backup Subversion repositories
+# svn_backup_dir = /var/svn/backups
+## Location to create dumps for commits to Subversion repositories
+# svn_dump_dir = /var/svn/dumps
+### Name of group where Subversion repositories should be created in
+# svn_group =
+## PATH environment variable for Subversion hooks
+# svn_hook_path_env =
+## Location to serve Subversion repositories
+# svn_live_dir = /srv/svn
+## Name of svnserve password file
+# svn_passwd_file =
+## File name suffix that may be added to each Subversion repository
+# svn_project_suffix =
+
+## List of admin users for all Trac environments
+# trac_admin_users =
+## Location to backup Trac environments
+# trac_backup_dir = /var/trac/backups
+### Name of group where Trac files should be created in
+# trac_group =
+## Host name (from a user's perspective) where Trac environments are served
+# trac_host_name = localhost
+## Name of "trac.ini" in each Trac environment
+# trac_ini_file = trac.ini
+## Location to serve Trac environments
+# trac_live_dir = /srv/trac
+## Template to create a Trac environment URL (from a user's perspective)
+# trac_live_url_tmpl = https://{host}/trac/{project}
+## Name of Trac password file (under Apache)
+# trac_passwd_file =
+
+## Specify the name of the tool for obtaining user info (ldap or passwd)
+# user_info_tool = passwd
+
+## LDAP settings, only relevant if user_info_tool = ldap
+## File containing the password to the LDAP server, if required
+# ldappw = ~/.ldappw
+## The URI of the LDAP server
+# ldap_uri =
+## The DN in the LDAP server to bind with to search the directory
+# ldap_binddn =
+## The DN in the LDAP server that is the base for a search
+# ldap_basedn =
+## The attributes for UID, common name and email in the LDAP directory
+# ldap_attrs = uid cn mail
+## If specified, use the value as extra (AND) filters to an LDAP search
+# ldap_filter_more =
+
+## PASSWD settings, only relevant if user_info_tool = passwd
+## Domain name to suffix user IDs to create an email address
+# passwd_email_domain =
+## Maximum GID considered to be a normal user group.
+# passwd_gid_max =
+## Maximum UID considered to be a normal user.
+# passwd_uid_max =
+## Minimum GID considered to be a normal user group.
+# passwd_gid_min = 1000
+## Minimum UID considered to be a normal user.
+# passwd_uid_min = 1000
diff --git a/etc/fcm/external.cfg.example b/etc/fcm/external.cfg.example
new file mode 100644
index 0000000..3119d15
--- /dev/null
+++ b/etc/fcm/external.cfg.example
@@ -0,0 +1,9 @@
+#-------------------------------------------------------------------------------
+# FCM External Configuration Example
+#-------------------------------------------------------------------------------
+# For detail, please refer to:
+# FCM User Guide Annex: FCM Configuration File > FCM External Configuration
+#-------------------------------------------------------------------------------
+
+# E.g. Web browser
+browser = firefox
diff --git a/etc/fcm/keyword.cfg.example b/etc/fcm/keyword.cfg.example
new file mode 100644
index 0000000..3c9bbae
--- /dev/null
+++ b/etc/fcm/keyword.cfg.example
@@ -0,0 +1,9 @@
+#-------------------------------------------------------------------------------
+# FCM Keyword Configuration Example
+#-------------------------------------------------------------------------------
+# For detail, please refer to:
+# FCM User Guide Annex: FCM Configuration File > FCM Keyword Configuration
+#-------------------------------------------------------------------------------
+
+# E.g. Location of FCM
+location{primary}[fcm] = svn://host/fcm
diff --git a/etc/fcm/make.cfg.example b/etc/fcm/make.cfg.example
new file mode 100644
index 0000000..2a3f028
--- /dev/null
+++ b/etc/fcm/make.cfg.example
@@ -0,0 +1,9 @@
+#-------------------------------------------------------------------------------
+# FCM Make Configuration Example
+#-------------------------------------------------------------------------------
+# For detail, please refer to:
+# FCM User Guide Annex: FCM Configuration File > FCM Make Configuration
+#-------------------------------------------------------------------------------
+
+# E.g. C compiler for the build system
+build.prop{cc} = cc
diff --git a/etc/svn-hooks/post-commit b/etc/svn-hooks/post-commit
new file mode 100755
index 0000000..e486c64
--- /dev/null
+++ b/etc/svn-hooks/post-commit
@@ -0,0 +1,23 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+set -eu
+mkdir -p "$1/log"
+nohup "$FCM_HOME/sbin/post-commit-bg" "$@" \
+ </dev/null >>"$1/log/post-commit.log" 2>&1 &
diff --git a/etc/svn-hooks/post-revprop-change b/etc/svn-hooks/post-revprop-change
new file mode 100755
index 0000000..8240fb2
--- /dev/null
+++ b/etc/svn-hooks/post-revprop-change
@@ -0,0 +1,23 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+set -eu
+mkdir -p "$1/log"
+nohup "$FCM_HOME/sbin/post-revprop-change-bg" "$@" \
+ <&0 >>"$1/log/post-revprop-change.log" 2>&1 &
diff --git a/etc/svn-hooks/pre-commit b/etc/svn-hooks/pre-commit
new file mode 100755
index 0000000..8c618da
--- /dev/null
+++ b/etc/svn-hooks/pre-commit
@@ -0,0 +1,22 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+set -eu
+mkdir -p "$1/log"
+exec "$FCM_HOME/sbin/pre-commit" "$@" >>"$1/log/pre-commit.log"
diff --git a/etc/svn-hooks/pre-revprop-change b/etc/svn-hooks/pre-revprop-change
new file mode 100755
index 0000000..fa0338a
--- /dev/null
+++ b/etc/svn-hooks/pre-revprop-change
@@ -0,0 +1,22 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+set -eu
+mkdir -p "$1/log"
+exec "$FCM_HOME/sbin/pre-revprop-change" "$@" >>"$1/log/pre-revprop-change.log"
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..ec87327
--- /dev/null
+++ b/index.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>FCM</title>
+ <meta http-equiv="refresh" content="0;url=doc/">
+</head>
+
+<body>
+<p>If not automatically redirected, please click
+<a href="doc/">FCM Documentation</a>.</p>
+</body>
+</html>
diff --git a/lib/FCM/Admin/Config.pm b/lib/FCM/Admin/Config.pm
new file mode 100644
index 0000000..caa1358
--- /dev/null
+++ b/lib/FCM/Admin/Config.pm
@@ -0,0 +1,353 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+
+use strict;
+use warnings;
+
+package FCM::Admin::Config;
+use base qw{FCM::Class::HASH};
+
+use FCM::Context::Locator;
+use FCM::Util;
+use File::Basename qw{dirname};
+use File::Spec::Functions qw{catfile};
+use FindBin;
+
+our $UTIL = FCM::Util->new();
+
+my $TRAC_LIVE_URL_TMPL = 'https://{host}/trac/{project}';
+my $USER_ID = (getpwuid($<))[0];
+
+__PACKAGE__->class({
+ # Emails
+ admin_email => {isa => '$', default => $USER_ID},
+ notification_from => {isa => '$'},
+
+ # Location for log files
+ log_dir => {isa => '$', default => '/var/log/fcm'},
+
+ # FCM installation locations
+ fcm_home => {isa => '$', default => dirname($FindBin::Bin)},
+ fcm_site_home => {isa => '$', default => q{}},
+
+ # FCM installation mirror locations
+ mirror_dests => {isa => '$', default => q{}},
+ mirror_keys => {isa => '$', default => q{}},
+
+ # Subversion repositories settings
+ svn_backup_dir => {isa => '$', default => '/var/svn/backups'},
+ svn_dump_dir => {isa => '$', default => '/var/svn/dumps'},
+ svn_group => {isa => '$', default => q{}},
+ svn_hook_path_env => {isa => '$', default => q{}},
+ svn_live_dir => {isa => '$', default => '/srv/svn'},
+ svn_passwd_file => {isa => '$', default => q{}},
+ svn_project_suffix => {isa => '$', default => q{}},
+
+ # Trac environments settings
+ trac_admin_users => {isa => '$', default => q{}},
+ trac_backup_dir => {isa => '$', default => '/var/trac/backups'},
+ trac_group => {isa => '$', default => q{}},
+ trac_host_name => {isa => '$', default => 'localhost'},
+ trac_ini_file => {isa => '$', default => 'trac.ini'},
+ trac_live_dir => {isa => '$', default => '/srv/trac'},
+ trac_live_url_tmpl => {isa => '$', default => $TRAC_LIVE_URL_TMPL},
+ trac_passwd_file => {isa => '$', default => q{}},
+
+ # User information tool settings
+ user_info_tool => {isa => '$', default => 'passwd'},
+
+ # User information tool, LDAP settings
+ ldappw => {isa => '$', default => '~/.ldappw'},
+ ldap_uri => {isa => '$', default => q{}},
+ ldap_binddn => {isa => '$', default => q{}},
+ ldap_basedn => {isa => '$', default => q{}},
+ ldap_attrs => {isa => '$', default => q{uid cn mail}},
+ ldap_filter_more => {isa => '$', default => q{}},
+
+ # User information tool, passwd settings
+ passwd_email_domain => {isa => '$', default => q{}},
+ passwd_gid_max => {isa => '$'},
+ passwd_uid_max => {isa => '$'},
+ passwd_gid_min => {isa => '$', default => 1000},
+ passwd_uid_min => {isa => '$', default => 1000},
+});
+
+
+# Returns a unique instance of this class.
+my $INSTANCE;
+sub instance {
+ my ($class) = @_;
+ if (!defined($INSTANCE)) {
+ $INSTANCE = $class->new();
+ # Load $FCM_HOME/etc/fcm/admin.cfg and $HOME/.metomi/fcm/admin.cfg
+ $UTIL->cfg_init(
+ 'admin.cfg',
+ sub {
+ my $config_reader = shift();
+ while (defined(my $entry = $config_reader->())) {
+ my $label = $entry->get_label();
+ if (exists($INSTANCE->{$label})) {
+ $INSTANCE->{$label} = $entry->get_value();
+ }
+ }
+ },
+ );
+ }
+ return $INSTANCE;
+}
+
+1;
+__END__
+
+=head1 NAME
+
+FCM::Admin::Config
+
+=head1 SYNOPSIS
+
+ $config = FCM::Admin::Config->instance();
+ $dir = $config->get_svn_backup_dir();
+ # ...
+
+=head1 DESCRIPTION
+
+This class is used to retrieve/store configurations required by FCM
+admininstration scripts.
+
+It is a sub-class of L<FCM::Class::HASH|FCM::Class::HASH>.
+
+=head1 METHODS
+
+=over 4
+
+=item FCM::Admin::Config->instance()
+
+Returns a unique instance of this class. On first call, creates the instance
+with the configurations set to their default values; and loads from the
+site/user configuration at $FCM_HOME/etc/fcm/admin.cfg and
+$HOME/.metomi/fcm/admin.cfg.
+
+=back
+
+=head1 ATTRIBUTES
+
+Email addresses.
+
+=over 4
+
+=item admin_email
+
+The e-mail address of the FCM administrator.
+
+=item notification_from
+
+Notification email address (for the "From:" field in notification emails).
+
+=back
+
+Location for log files.
+
+=over 4
+
+=item log_dir
+
+The location for log files.
+
+=back
+
+Locations of FCM installation.
+
+=over 4
+
+=item fcm_home
+
+The source path of the default FCM distribution.
+
+=item fcm_site_home
+
+The source path of the default FCM site distribution.
+
+=back
+
+Settings on how to mirror FCM installation.
+
+=over 4
+
+=item mirror_dests
+
+A space-delimited list of destinations to mirror FCM installation.
+
+=item mirror_keys
+
+A string containing a list of source keys. Each source key should point
+to a source location in this $config. The source locations will be distributed
+to the list of destinations in C<mirror_dests>.
+
+=back
+
+Subversion repositories settings.
+
+=over 4
+
+=item svn_backup_dir
+
+The path to a directory containing the backups of SVN repositories.
+
+=item svn_dump_dir
+
+The path to a directory containing the revision dumps of SVN
+repositories.
+
+=item svn_group
+
+The group name in which Subversion repositories should be created in.
+
+=item svn_hook_dir
+
+The path to a directory containing source files of SVN hook scripts.
+
+=item svn_hook_path_env
+
+The value of the PATH environment variable, in which SVN hook scripts
+should run with.
+
+=item svn_live_dir
+
+The path to a directory containing the live SVN repositories.
+
+=item svn_passwd_file
+
+The base name of the SVN password file.
+
+=item svn_project_suffix
+
+The suffix added to the name of each SVN repository.
+
+=back
+
+Trac environment settings.
+
+=over 4
+
+=item trac_admin_users
+
+A space-delimited list of admin users for all Trac environments.
+
+=item trac_backup_dir
+
+The path to a directory containing the backups of Trac environments.
+
+=item trac_group
+
+The group name in which Trac environment files should be created in.
+
+=item trac_host_name
+
+The host name of the Trac server, from the user's perspective.
+
+=item trac_ini_file
+
+The base name of the Trac INI file.
+
+=item trac_live_dir
+
+The path to a directory containing the live Trac environments.
+
+=item trac_live_url_tmpl
+
+The template string for determining the URL of the Trac environment of a
+project.
+
+=item trac_passwd_file
+
+The base name of the Trac password file.
+
+=back
+
+=over 4
+
+User information tool settings.
+
+=item user_info_tool
+
+The name of the tool for obtaining user information.
+
+=back
+
+LDAP settings, only relevant if C<user_info_tool = ldap>
+
+=over 4
+
+=item ldappw
+
+File containing the password to the LDAP server, if required.
+
+=item ldap_uri
+
+The URI of the LDAP server.
+
+=item ldap_binddn
+
+The DN in the LDAP server to bind with to search the directory.
+
+=item ldap_basedn
+
+The DN in the LDAP server that is the base for a search.
+
+=item ldap_attrs
+
+The attributes for UID, common name and email in the LDAP directory.
+
+=item ldap_filter_more
+
+If specified, use the value as extra (AND) filters to an LDAP search.
+
+=back
+
+PASSWD settings, only relevant if user_info_tool = passwd
+
+=over 4
+
+=item passwd_email_domain
+
+Domain name to suffix user IDs to create an email address.
+
+=item passwd_gid_max
+
+Maximum GID considered to be a normal user group.
+
+=item passwd_uid_max
+
+Maximum UID considered to be a normal user.
+
+=item passwd_gid_min
+
+Minimum GID considered to be a normal user group. (default=1000)
+
+=item passwd_uid_min
+
+Minimum UID considered to be a normal user. (default=1000)
+
+=back
+
+=head1 COPYRIGHT
+
+E<169> Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/Admin/Project.pm b/lib/FCM/Admin/Project.pm
new file mode 100644
index 0000000..45490b5
--- /dev/null
+++ b/lib/FCM/Admin/Project.pm
@@ -0,0 +1,260 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+
+use strict;
+use warnings;
+
+package FCM::Admin::Project;
+
+use overload q{""} => \&get_name;
+use FCM::Admin::Config;
+use File::Spec;
+
+my $ARCHIVE_EXTENSION = q{.tgz};
+
+# ------------------------------------------------------------------------------
+# Creates a new instance of this class.
+sub new {
+ my ($class, $args_ref) = @_;
+ return bless({%{$args_ref}}, $class);
+}
+
+# ------------------------------------------------------------------------------
+# Returns the name of the project.
+sub get_name {
+ my ($self) = @_;
+ return $self->{name};
+}
+
+# ------------------------------------------------------------------------------
+# Returns the base name of the backup archive of the project's SVN repository.
+sub get_svn_archive_base_name {
+ my ($self) = @_;
+ return $self->get_svn_base_name() . $ARCHIVE_EXTENSION;
+}
+
+# ------------------------------------------------------------------------------
+# Returns the path of the backup archive of the project's SVN repository.
+sub get_svn_backup_path {
+ my ($self) = @_;
+ return File::Spec->catfile(
+ FCM::Admin::Config->instance()->get_svn_backup_dir(),
+ $self->get_svn_archive_base_name(),
+ );
+}
+
+# ------------------------------------------------------------------------------
+# Returns the base name of the project's Subversion repository.
+sub get_svn_base_name {
+ my ($self) = @_;
+ return
+ $self->get_name()
+ . FCM::Admin::Config->instance()->get_svn_project_suffix();
+}
+
+# ------------------------------------------------------------------------------
+# Returns the path to the revision dumps of the project's SVN repository.
+sub get_svn_dump_path {
+ my ($self) = @_;
+ return File::Spec->catfile(
+ FCM::Admin::Config->instance()->get_svn_dump_dir(),
+ $self->get_svn_base_name(),
+ );
+}
+
+# ------------------------------------------------------------------------------
+# Returns the path to the project's SVN live repository's hooks directory.
+sub get_svn_live_hook_path {
+ my ($self) = @_;
+ return File::Spec->catfile($self->get_svn_live_path(), q{hooks});
+}
+
+# ------------------------------------------------------------------------------
+# Returns the path to the project's SVN live repository.
+sub get_svn_live_path {
+ my ($self) = @_;
+ return File::Spec->catfile(
+ FCM::Admin::Config->instance()->get_svn_live_dir(),
+ $self->get_svn_base_name(),
+ );
+}
+
+# ------------------------------------------------------------------------------
+# Returns the file:// URI to the project's SVN live repository.
+sub get_svn_file_uri {
+ my ($self) = @_;
+ return q{file://} . $self->get_svn_live_path();
+ # Note: can use URI::file in theory, but it returns file:/path (instead of
+ # file:///path) which Subversion does not like.
+}
+
+# ------------------------------------------------------------------------------
+# Returns the base name of the project's Trac environment backup archive.
+sub get_trac_archive_base_name {
+ my ($self) = @_;
+ return $self->get_name() . $ARCHIVE_EXTENSION;
+}
+
+# ------------------------------------------------------------------------------
+# Returns the path to the project's Trac backup archive.
+sub get_trac_backup_path {
+ my ($self) = @_;
+ return File::Spec->catfile(
+ FCM::Admin::Config->instance()->get_trac_backup_dir(),
+ $self->get_trac_archive_base_name(),
+ );
+}
+
+# ------------------------------------------------------------------------------
+# Returns the path to the project's Trac live environment's database.
+sub get_trac_live_db_path {
+ my ($self) = @_;
+ return File::Spec->catfile($self->get_trac_live_path(), qw{db trac.db});
+}
+
+# ------------------------------------------------------------------------------
+# Returns the path to the project's Trac live environment's INI file.
+sub get_trac_live_ini_path {
+ my ($self) = @_;
+ return File::Spec->catfile($self->get_trac_live_path(), qw{conf trac.ini});
+}
+
+# ------------------------------------------------------------------------------
+# Returns the path to the project's Trac live environment.
+sub get_trac_live_path {
+ my ($self) = @_;
+ return File::Spec->catfile(
+ FCM::Admin::Config->instance()->get_trac_live_dir(),
+ $self->get_name(),
+ );
+}
+
+# ------------------------------------------------------------------------------
+# Returns the URL to the project's Trac live environment.
+sub get_trac_live_url {
+ my ($self) = @_;
+ my $return = FCM::Admin::Config->instance()->get_trac_live_url_tmpl();
+ for (
+ ['{host}', FCM::Admin::Config->instance()->get_trac_host_name()],
+ ['{project}', $self->get_name()],
+ ) {
+ my ($key, $value) = @{$_};
+ my $index = index($return, $key);
+ if ($index > -1) {
+ substr($return, $index, length($key), $value);
+ }
+ }
+ $return;
+}
+
+1;
+__END__
+
+=head1 NAME
+
+FCM::Admin::Project
+
+=head1 SYNOPSIS
+
+ use FCM::Admin::Project;
+ $project = FCM::Admin::Project->new({name => 'foo'});
+ $path = $project->get_svn_live_path();
+
+=head1 DESCRIPTION
+
+An object of this class represents a project hosted/managed by FCM. The methods
+of this class relies on L<FCM::Admin::Config|FCM::Admin::Config> for many of the
+configurations.
+
+=head1 METHODS
+
+=over 4
+
+=item FCM::Admin::Project->new({name => $name})
+
+Returns a new instance. A name of the project must be specified.
+
+=item $project->get_name()
+
+Returns the name of the project.
+
+=item $project->get_svn_archive_base_name()
+
+Returns the base name of the backup archive of the project's Subversion
+repository.
+
+=item $project->get_svn_backup_path()
+
+Returns the path to the backup archive of the project's Subversion repository.
+
+=item $project->get_svn_base_name()
+
+Returns the base name of the project's Subversion repository.
+
+=item $project->get_svn_dump_path()
+
+Returns the path to the revision dumps of the project's Subversion repository.
+
+=item $project->get_svn_live_hook_path()
+
+Returns the path to the project's SVN live repository's hooks directory.
+
+=item $project->get_svn_live_path()
+
+Returns the path to the project's SVN live repository.
+
+=item $project->get_svn_file_uri()
+
+Returns the file:// URI to the project's SVN live repository.
+
+=item $project->get_trac_archive_base_name()
+
+Returns the base name of the project's Trac environment backup archive.
+
+=item $project->get_trac_backup_path()
+
+Returns the path to the project's Trac backup archive.
+
+=item $project->get_trac_live_db_path()
+
+Returns the path to the project's Trac live environment's database.
+
+=item $project->get_trac_live_ini_path()
+
+Returns the path to the project's Trac live environment's INI file.
+
+=item $project->get_trac_live_path()
+
+Returns the path to the project's Trac live environment.
+
+=item $project->get_trac_live_url()
+
+Returns the URL to the project's Trac live environment.
+
+=back
+
+=head1 SEE ALSO
+
+L<FCM::Admin::Config|FCM::Admin::Config>
+
+=head1 COPYRIGHT
+
+E<169> Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/Admin/Runner.pm b/lib/FCM/Admin/Runner.pm
new file mode 100644
index 0000000..ee9a1e5
--- /dev/null
+++ b/lib/FCM/Admin/Runner.pm
@@ -0,0 +1,299 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+
+use strict;
+use warnings;
+
+package FCM::Admin::Runner;
+
+use IO::Handle;
+use POSIX qw{strftime};
+
+# The default values of the attributes
+my %DEFAULT = (
+ exceptions => [],
+ max_attempts => 3,
+ retry_interval => 5,
+ stderr_handle => \*STDERR,
+ stdout_handle => \*STDOUT,
+);
+
+my $INSTANCE;
+
+# ------------------------------------------------------------------------------
+# Returns a unique instance of this class. Creates the instance on first call.
+sub instance {
+ my ($class) = @_;
+ if (!defined($INSTANCE)) {
+ $INSTANCE = bless({%DEFAULT}, $class);
+ }
+ return $INSTANCE;
+}
+
+# ------------------------------------------------------------------------------
+# Adds a new exception to the list of exceptions.
+sub _add_exception {
+ my ($self, $exception) = @_;
+ push(@{$self->get_exceptions()}, $exception);
+}
+
+# ------------------------------------------------------------------------------
+# Returns the list of exceptions (or a reference to the list in scalar context).
+sub get_exceptions {
+ my ($self) = @_;
+ return (wantarray() ? @{$self->{exceptions}} : $self->{exceptions});
+}
+
+# ------------------------------------------------------------------------------
+# Returns the latest exception in the exception list.
+sub get_latest_exception {
+ my ($self) = @_;
+ if (exists($self->get_exceptions()->[-1])) {
+ return $self->get_exceptions()->[-1];
+ }
+ else {
+ return;
+ }
+}
+
+# ------------------------------------------------------------------------------
+# Returns the maximum number of attempts for the "run_with_retries" method.
+sub get_max_attempts {
+ my ($self) = @_;
+ return $self->{max_attempts};
+}
+
+# ------------------------------------------------------------------------------
+# Returns the retry interval for the "run_with_retries" method.
+sub get_retry_interval {
+ my ($self) = @_;
+ return $self->{retry_interval};
+}
+
+# ------------------------------------------------------------------------------
+# Returns the file handle for STDERR.
+sub get_stderr_handle {
+ my ($self) = @_;
+ if (!IO::Handle::opened($self->{stderr_handle})) {
+ $self->{stderr_handle} = $DEFAULT{stderr_handle};
+ }
+ return $self->{stderr_handle};
+}
+
+# ------------------------------------------------------------------------------
+# Returns the file handle for STDOUT.
+sub get_stdout_handle {
+ my ($self) = @_;
+ if (!IO::Handle::opened($self->{stdout_handle})) {
+ $self->{stdout_handle} = $DEFAULT{stdout_handle};
+ }
+ return $self->{stdout_handle};
+}
+
+# ------------------------------------------------------------------------------
+# Runs $sub_ref->(@arguments) with a diagnostic $message. Dies on error.
+sub run {
+ my ($self, $message, $sub_ref, @arguments) = @_;
+ printf(
+ {$self->get_stdout_handle()}
+ qq{%s: %s\n}, strftime("%Y-%m-%dT%H:%M:%SZ", gmtime()), $message,
+ );
+ eval {
+ if (!$sub_ref->(@arguments)) {
+ die(qq{\n});
+ }
+ };
+ if ($@) {
+ my $e = $@;
+ chomp($e);
+ my $exception
+ = sprintf(qq{ERROR %s%s\n}, $message, ($e ? qq{ - $e} : qq{}));
+ $self->_add_exception($exception);
+ die($exception);
+ }
+ return 1;
+}
+
+# ------------------------------------------------------------------------------
+# Runs $sub_ref->(@arguments) with a diagnostic $message. Warns on error.
+sub run_continue {
+ my ($self, $message, $sub_ref, @arguments) = @_;
+ my $rc;
+ eval {
+ $rc = $self->run($message, $sub_ref, @arguments);
+ };
+ if ($@) {
+ print({$self->get_stderr_handle()} $@);
+ return;
+ }
+ return $rc;
+}
+
+# ------------------------------------------------------------------------------
+# Runs $sub_ref->(@arguments) with a diagnostic $message. Retries on error.
+sub run_with_retries {
+ my ($self, $message, $sub_ref, @arguments) = @_;
+ for my $i_attempt (1 .. $self->get_max_attempts()) {
+ my $attempt_message = sprintf(
+ qq{%s, attempt %d of %d},
+ $message, $i_attempt, $self->get_max_attempts(),
+ );
+ if ($i_attempt == $self->get_max_attempts()) {
+ return $self->run($attempt_message, $sub_ref, @arguments);
+ }
+ else {
+ if ($self->run_continue($attempt_message, $sub_ref, @arguments)) {
+ return 1;
+ }
+ sleep($self->get_retry_interval());
+ }
+ }
+}
+
+# ------------------------------------------------------------------------------
+# Sets the maximum number of attempts for the "run_with_retries" method.
+sub set_max_attempts {
+ my ($self, $value) = @_;
+ $self->{max_attempts} = $value;
+}
+
+# ------------------------------------------------------------------------------
+# Sets the retry interval for the "run_with_retries" method.
+sub set_retry_interval {
+ my ($self, $value) = @_;
+ $self->{retry_interval} = $value;
+}
+
+# ------------------------------------------------------------------------------
+# Sets the file handle for STDERR.
+sub set_stderr_handle {
+ my ($self, $value) = @_;
+ if (defined($value) && IO::Handle::opened($value)) {
+ $self->{stderr_handle} = $value;
+ }
+}
+
+# ------------------------------------------------------------------------------
+# Sets the file handle for STDOUT.
+sub set_stdout_handle {
+ my ($self, $value) = @_;
+ if (defined($value) && IO::Handle::opened($value)) {
+ $self->{stdout_handle} = $value;
+ }
+}
+
+1;
+__END__
+
+=head1 NAME
+
+FCM::Admin::Runner
+
+=head1 SYNOPSIS
+
+ $runner = FCM::Admin::Runner->instance();
+ $runner->run($message, sub { ... });
+
+=head1 DESCRIPTION
+
+Provides a simple way to run a piece of code with a time-stamped diagnostic
+message.
+
+=head1 METHODS
+
+=over 4
+
+=item FCM::Admin::Runner->instance()
+
+Returns a unique instance of FCM::Admin::Runner.
+
+=item $runner->get_exceptions()
+
+Returns a list containing all the exceptions captured by the previous
+invocations of the $runner->run() method. In SCALAR context, returns a reference
+to the list.
+
+=item $runner->get_latest_exception()
+
+Returns the latest exception captured by the $runner->run() method. Returns
+undef if there is no captured exception in the list.
+
+=item $runner->get_max_attempts()
+
+Returns the number of maximum retries for the
+$runner->run_with_retries($message,$sub_ref, at arguments) method. (Default: 3)
+
+=item $runner->get_retry_interval()
+
+Returns the interval (in seconds) between retries for the
+$runner->run_with_retries($message,$sub_ref, at arguments) method. (Default: 5)
+
+=item $runner->get_stderr_handle()
+
+Returns the file handle for standard error output. (Default: \*STDERR)
+
+=item $runner->get_stdout_handle()
+
+Returns the file handle for standard output. (Default: \*STDOUT)
+
+=item $runner->run($message,$sub_ref, at arguments)
+
+Prints the diagnostic $message and runs $sub_ref (with extra @arguments).
+Returns true if $sub_ref returns true. die() with a message that looks like
+"ERROR $message\n" if $sub_ref returns false or die().
+
+=item $runner->run_continue($message,$sub_ref, at arguments)
+
+Same as $runner->run($message,$sub_ref, at arguments), but only issue a warning
+(and returns false) if $sub_ref returns false or die().
+
+=item $runner->run_with_retries($message,$sub_ref, at arguments)
+
+Attempts $runner->run($message,$sub_ref, at arguments) for a number of times up to
+$runner->get_max_attempts(), with a delay of $runner->get_retry_interval()
+between each attempt. die() if $sub_ref still returns false in the final
+attempt. Returns true on success.
+
+=item $runner->set_max_attempts($value)
+
+Sets the maximum number of attempts in the
+$runner->run_with_retries($message,$sub_ref, at arguments) method.
+
+=item $runner->set_retry_interval($value)
+
+Sets the interval (in seconds) between retries for the
+$runner->run_with_retries($message,$sub_ref, at arguments) method.
+
+=item $runner->set_stderr_handle($value)
+
+Sets the file handle for standard error output to an alternate file handle. The
+$value must be a valid file descriptor.
+
+=item $runner->set_stdout_handle($value)
+
+Sets the file handle for standard output to an alternate file handle. The $value
+must be a valid file descriptor.
+
+=back
+
+=head1 COPYRIGHT
+
+E<169> Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/Admin/System.pm b/lib/FCM/Admin/System.pm
new file mode 100644
index 0000000..2c87cea
--- /dev/null
+++ b/lib/FCM/Admin/System.pm
@@ -0,0 +1,1387 @@
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+
+use strict;
+use warnings;
+
+package FCM::Admin::System;
+
+use Config::IniFiles;
+use DBI; # See also: DBD::SQLite
+use Exporter qw{import};
+use FCM::Admin::Config;
+use FCM::Admin::Project;
+use FCM::Admin::Runner;
+use FCM::Admin::User;
+use FCM::Admin::Util qw{
+ read_file
+ run_copy
+ run_create_archive
+ run_extract_archive
+ run_mkpath
+ run_rename
+ run_rmtree
+ run_rsync
+ run_symlink
+ write_file
+};
+use Fcntl qw{:mode}; # for S_IRGRP, S_IWGRP, S_IROTH, etc
+use File::Basename qw{basename dirname};
+use File::Find qw{find};
+use File::Spec::Functions qw{catfile rel2abs};
+use File::Temp qw{tempdir tempfile};
+use IO::Compress::Gzip qw{gzip};
+use IO::Dir;
+use IO::Pipe;
+use IO::Zlib;
+use List::Util qw{first};
+use POSIX qw{strftime};
+use Text::ParseWords qw{shellwords};
+
+our @EXPORT_OK = qw{
+ add_svn_repository
+ add_trac_environment
+ backup_svn_repository
+ backup_trac_environment
+ backup_trac_files
+ distribute_wc
+ filter_projects
+ get_projects_from_svn_backup
+ get_projects_from_svn_live
+ get_projects_from_trac_backup
+ get_projects_from_trac_live
+ get_users
+ housekeep_svn_hook_logs
+ install_svn_hook
+ manage_users_in_svn_passwd
+ manage_users_in_trac_passwd
+ manage_users_in_trac_db_of
+ recover_svn_repository
+ recover_trac_environment
+ recover_trac_files
+ vacuum_trac_env_db
+ verify_users
+};
+
+our $NO_OVERWRITE = 1;
+our $BUFFER_SIZE = 4096;
+our @SVN_REPOS_ROOT_HOOK_ITEMS = qw{commit.conf svnperms.conf};
+our %USER_INFO_TOOL_OF = (
+ 'ldap' => 'FCM::Admin::Users::LDAP',
+ 'passwd' => 'FCM::Admin::Users::Passwd',
+);
+our $USER_INFO_TOOL;
+
+our $UTIL = $FCM::Admin::Config::UTIL;
+my $CONFIG = FCM::Admin::Config->instance();
+my $RUNNER = FCM::Admin::Runner->instance();
+
+# ------------------------------------------------------------------------------
+# Adds a new Subversion repository.
+sub add_svn_repository {
+ my ($project_name) = @_;
+ my $project = FCM::Admin::Project->new({name => $project_name});
+ if (-e $project->get_svn_live_path()) {
+ die(sprintf(
+ "%s: Subversion repository already exists at %s.\n",
+ $project_name,
+ $project->get_svn_live_path(),
+ ));
+ }
+ my $repos_path = $project->get_svn_live_path();
+ $RUNNER->run(
+ "creating Subversion repository at $repos_path",
+ sub {!system(qw{svnadmin create}, $repos_path)},
+ );
+ my $group = $CONFIG->get_svn_group();
+ if ($group) {
+ _chgrp_and_chmod($project->get_svn_live_path(), $group);
+ }
+ install_svn_hook($project);
+ housekeep_svn_hook_logs($project);
+}
+
+# ------------------------------------------------------------------------------
+# Adds a new Trac environment.
+sub add_trac_environment {
+ my ($project_name) = @_;
+ my $project = FCM::Admin::Project->new({name => $project_name});
+ if (-e $project->get_trac_live_path()) {
+ die(sprintf(
+ "%s: Trac environment already exists at %s.\n",
+ $project_name,
+ $project->get_trac_live_path(),
+ ));
+ }
+ my @repository_arguments = (q{}, q{});
+ if (-d $project->get_svn_live_path()) {
+ @repository_arguments = (q{svn}, $project->get_svn_live_path());
+ }
+ my $RUN = sub{$RUNNER->run(@_)};
+ my $TRAC_ADMIN = sub {
+ my ($log, @args) = @_;
+ my @command = (q{trac-admin}, $project->get_trac_live_path(), @args);
+ $RUN->($log, sub {!system(@command)});
+ };
+ $TRAC_ADMIN->(
+ "initialising Trac environment",
+ q{initenv},
+ $project_name,
+ q{sqlite:db/trac.db},
+ @repository_arguments,
+ q{--inherit=../../trac.ini},
+ );
+ my $group = $CONFIG->get_trac_group();
+ if ($group) {
+ _chgrp_and_chmod($project->get_trac_live_path(), $group);
+ }
+ for my $item (qw{component1 component2}) {
+ $TRAC_ADMIN->(
+ "removing example component $item", q{component remove}, $item,
+ );
+ }
+ for my $item (qw{1.0 2.0}) {
+ $TRAC_ADMIN->(
+ "removing example version $item", q{version remove}, $item,
+ );
+ }
+ for my $item (qw{milestone1 milestone2 milestone3 milestone4}) {
+ $TRAC_ADMIN->(
+ "removing example milestone $item", q{milestone remove}, $item,
+ );
+ }
+ for my $item (
+ ['major' => 'normal' ],
+ ['critical' => 'major' ],
+ ['blocker' => 'critical'],
+ ) {
+ my ($old, $new) = @{$item};
+ $TRAC_ADMIN->(
+ "changing priority $old to $new", qw{priority change}, $old, $new,
+ );
+ }
+ $TRAC_ADMIN->(
+ "adding admin permission", qw{permission add admin TRAC_ADMIN},
+ );
+ my @admin_users = shellwords($CONFIG->get_trac_admin_users());
+ for my $item (@admin_users) {
+ $TRAC_ADMIN->(
+ "adding admin user $item", qw{permission add}, $item, q{admin},
+ );
+ }
+ $TRAC_ADMIN->(
+ "adding TICKET_EDIT_CC permission to authenticated",
+ qw{permission add}, 'authenticated', qw{TICKET_EDIT_CC},
+ );
+ $TRAC_ADMIN->(
+ "adding TICKET_EDIT_DESCRIPTION permission to authenticated",
+ qw{permission add}, 'authenticated', qw{TICKET_EDIT_DESCRIPTION},
+ );
+ eval {$TRAC_ADMIN->(
+ "adding TICKET_EDIT_COMMENT permission to authenticated",
+ qw{permission add}, 'authenticated', qw{TICKET_EDIT_COMMENT},
+ )};
+ if ($@) {
+ # Expected to fail for Trac < 0.12
+ $@ = undef;
+ }
+ $RUN->(
+ "adding names and emails of users",
+ sub {manage_users_in_trac_db_of($project, {get_users()})},
+ );
+ $RUN->(
+ "updating configuration file",
+ sub {
+ my $trac_ini_path = $project->get_trac_live_ini_path();
+ my $trac_ini = Config::IniFiles->new(q{-file} => $trac_ini_path);
+ if (!$trac_ini) {
+ die("$trac_ini_path: cannot open.\n");
+ }
+ for (
+ #section #key #value
+ ['inherit', 'file' , '../../trac.ini,../../intertrac.ini'],
+ ['project', 'descr' , $project->get_name() ],
+ ['trac' , 'base_url', $project->get_trac_live_url() ],
+ ) {
+ my ($section, $key, $value) = @{$_};
+ if (!$trac_ini->SectionExists($section)) {
+ $trac_ini->AddSection($section);
+ }
+ if (!$trac_ini->newval($section, $key, $value)) {
+ die("$trac_ini_path: $section:$key: cannot set value.\n");
+ }
+ }
+ return $trac_ini->RewriteConfig();
+ },
+ );
+ $RUN->(
+ "updating InterTrac",
+ sub {
+ my $ini_path = catfile(
+ $CONFIG->get_trac_live_dir(),
+ 'intertrac.ini',
+ );
+ if (!-e $ini_path) {
+ open(my $handle, '>', $ini_path) || die("$ini_path: $!\n");
+ close($handle) || die("$ini_path: $!\n");
+ }
+ my $trac_ini = Config::IniFiles->new(
+ q{-allowempty} => 1,
+ q{-file} => $ini_path,
+ );
+ if (!defined($trac_ini)) {
+ die("$ini_path: cannot open.\n");
+ }
+ if (!$trac_ini->SectionExists(q{intertrac})) {
+ $trac_ini->AddSection(q{intertrac});
+ }
+ my $name = $project->get_name();
+ for (
+ [q{title} , $name ],
+ [q{url} , $project->get_trac_live_url()],
+ [q{compat}, 'false' ],
+ ) {
+ my ($key, $value) = @{$_};
+ my $option = lc($name) . q{.} . $key;
+ if (!$trac_ini->newval(q{intertrac}, $option, $value)) {
+ die("$ini_path: intertrac:$option: cannot set value.\n");
+ }
+ }
+ return $trac_ini->RewriteConfig();
+ },
+ );
+ return 1;
+}
+
+# ------------------------------------------------------------------------------
+# Backup the SVN repository of a project.
+sub backup_svn_repository {
+ my ($option_hash_ref, $project) = @_;
+ my $RUN = sub {FCM::Admin::Runner->instance()->run(@_)};
+ if (!exists($option_hash_ref->{'no-pack'})) {
+ $RUN->(
+ sprintf("packing %s", $project->get_svn_live_path()),
+ sub {!system(qw{svnadmin pack}, $project->get_svn_live_path())},
+ );
+ }
+ my $base_name = $project->get_svn_base_name();
+ run_mkpath($CONFIG->get_svn_backup_dir());
+ my $work_dir = tempdir(
+ "$base_name.backup.XXXXXX",
+ DIR => $CONFIG->get_svn_backup_dir(),
+ CLEANUP => 1,
+ );
+ my $work_path = catfile($work_dir, $base_name);
+ $RUN->(
+ sprintf(
+ "hotcopying %s to %s", $project->get_svn_live_path(), $work_path,
+ ),
+ sub {!system(
+ qw{svnadmin hotcopy}, $project->get_svn_live_path(), $work_path,
+ )},
+ # Note: "hotcopy" is not yet possible via SVN::Repos
+ );
+ if (!exists($option_hash_ref->{'no-verify-integrity'})) {
+ my $VERIFIED_REVISION_REGEX = qr{\A\*\s+Verified\s+revision\s+\d+\.}xms;
+ $RUN->(
+ "verifying integrity of SVN repository of $project",
+ sub {
+ my $pipe = IO::Pipe->new();
+ $pipe->reader(sprintf(
+ qq{svnadmin verify %s 2>&1}, $work_path,
+ ));
+ while (my $line = $pipe->getline()) {
+ if ($line !~ $VERIFIED_REVISION_REGEX) { # don't print
+ print($line);
+ }
+ }
+ return $pipe->close();
+ # Note: "verify" is not yet possible via SVN::Repos
+ },
+ );
+ }
+ _create_backup_archive(
+ $work_path,
+ $CONFIG->get_svn_backup_dir(),
+ $project->get_svn_archive_base_name(),
+ );
+ if (!exists($option_hash_ref->{'no-housekeep-dumps'})) {
+ my $base_name = $project->get_svn_base_name();
+ my $dump_path = $CONFIG->get_svn_dump_dir();
+ my $youngest = _svnlook_youngest($work_path);
+ # Note: could use SVN::Repos for "youngest"
+ $RUN->(
+ "housekeeping $dump_path/$base_name-*.gz",
+ sub {
+ my @rev_dump_paths;
+ _get_files_from(
+ $dump_path,
+ sub {
+ my ($dump_base_name, $path) = @_;
+ my ($name, $rev)
+ = $dump_base_name =~ qr{\A(.*)-(\d+)\.gz\z}msx;
+ if ( !$name
+ || !$rev
+ || $name ne $base_name
+ || $rev > $youngest
+ ) {
+ return;
+ }
+ push(@rev_dump_paths, $path);
+ },
+ );
+ for my $rev_dump_path (@rev_dump_paths) {
+ run_rmtree($rev_dump_path);
+ }
+ return 1;
+ }
+ );
+ }
+ run_rmtree($work_dir);
+ return 1;
+}
+
+# ------------------------------------------------------------------------------
+# Backup the Trac environment of a project.
+sub backup_trac_environment {
+ my ($option_hash_ref, $project) = @_;
+ my $trac_live_path = $project->get_trac_live_path();
+ my $base_name = $project->get_name();
+ run_mkpath($CONFIG->get_trac_backup_dir());
+ my $work_dir = tempdir(
+ "$base_name.backup.XXXXXX",
+ DIR => $CONFIG->get_trac_backup_dir(),
+ CLEANUP => 1,
+ );
+ my $work_path = catfile($work_dir, $base_name);
+ $RUNNER->run_with_retries(
+ sprintf(
+ qq{hotcopying %s to %s},
+ $project->get_trac_live_path(),
+ $work_path,
+ ),
+ sub {
+ return !system(
+ q{trac-admin},
+ $project->get_trac_live_path(),
+ q{hotcopy},
+ $work_path,
+ );
+ },
+ );
+ if (!exists($option_hash_ref->{'no-verify-integrity'})) {
+ my $db_path = catfile($work_path, qw{db trac.db});
+ my $db_name = catfile($project->get_name(), qw{db trac.db});
+ $RUNNER->run(
+ "checking $db_name for integrity",
+ sub {
+ my $db_handle
+ = DBI->connect(qq{dbi:SQLite:dbname=$db_path}, q{}, q{});
+ if (!$db_handle) {
+ return;
+ }
+ my $rc = defined($db_handle->do(q{pragma integrity_check;}));
+ $db_handle->disconnect();
+ return $rc;
+ },
+ );
+ }
+ _create_backup_archive(
+ $work_path,
+ $CONFIG->get_trac_backup_dir(),
+ $project->get_trac_archive_base_name(),
+ );
+ run_rmtree($work_dir);
+ return 1;
+}
+
+# ------------------------------------------------------------------------------
+# Backup misc files in the Trac live directory to the Trac backup directory.
+sub backup_trac_files {
+ # (no argument)
+ _copy_files($CONFIG->get_trac_live_dir(), $CONFIG->get_trac_backup_dir());
+}
+
+# ------------------------------------------------------------------------------
+# Distributes the central FCM working copy to standard locations.
+sub distribute_wc {
+ my $rc = 1;
+ my @RSYNC_OPTS = qw{--timeout=1800 --exclude=.*};
+ my @sources;
+ for my $source_key (shellwords($CONFIG->get_mirror_keys())) {
+ my $method = "get_$source_key";
+ if ($CONFIG->can($method)) {
+ push(@sources, $CONFIG->$method());
+ }
+ }
+ for my $dest (shellwords($CONFIG->get_mirror_dests())) {
+ $rc = $RUNNER->run_continue(
+ "distributing FCM to $dest",
+ sub {
+ run_rsync(
+ \@sources, $dest,
+ [@RSYNC_OPTS, qw{-a --delete-excluded}],
+ );
+ },
+ ) && $rc;
+ }
+ return $rc;
+}
+
+# ------------------------------------------------------------------------------
+# Returns a filtered list of projects matching names in a list.
+sub filter_projects {
+ my ($project_list_ref, $filter_list_ref) = @_;
+ if (!@{$filter_list_ref}) {
+ return @{$project_list_ref};
+ }
+ my %project_of = map {($_->get_name(), $_)} @{$project_list_ref};
+ my @projects;
+ my @unmatched_names;
+ for my $name (@{$filter_list_ref}) {
+ if (exists($project_of{$name})) {
+ push(@projects, $project_of{$name});
+ }
+ else {
+ push(@unmatched_names, $name);
+ }
+ }
+ if (@unmatched_names) {
+ die("@unmatched_names: not found\n");
+ }
+ return @projects;
+}
+
+# ------------------------------------------------------------------------------
+# Returns a list of projects by searching the backup SVN directory.
+sub get_projects_from_svn_backup {
+ # (no dummy argument)
+ my $SVN_PROJECT_SUFFIX = $CONFIG->get_svn_project_suffix();
+ my @projects;
+ _get_files_from(
+ $CONFIG->get_svn_backup_dir(),
+ sub {
+ my ($base_name, $path) = @_;
+ my $name = $base_name;
+ if ($name !~ s{$SVN_PROJECT_SUFFIX\.tgz\z}{}xms) {
+ return;
+ }
+ if (!-f $path) {
+ return;
+ }
+ push(@projects, FCM::Admin::Project->new({name => $name}));
+ },
+ );
+ return @projects;
+}
+
+# ------------------------------------------------------------------------------
+# Returns a list of projects by searching the live SVN directory.
+sub get_projects_from_svn_live {
+ # (no dummy argument)
+ my $SVN_PROJECT_SUFFIX = $CONFIG->get_svn_project_suffix();
+ my @projects;
+ _get_files_from(
+ $CONFIG->get_svn_live_dir(),
+ sub {
+ my ($base_name, $path) = @_;
+ my $name = $base_name;
+ $name =~ s{$SVN_PROJECT_SUFFIX\z}{}xms;
+ if (!-d $path) {
+ return;
+ }
+ push(@projects, FCM::Admin::Project->new({name => $name}));
+ },
+ );
+ return @projects;
+}
+
+# ------------------------------------------------------------------------------
+# Returns a list of projects by searching the backup Trac directory.
+sub get_projects_from_trac_backup {
+ # (no dummy argument)
+ my @projects;
+ _get_files_from(
+ $CONFIG->get_trac_backup_dir(),
+ sub {
+ my ($base_name, $path) = @_;
+ my $name = $base_name;
+ if ($name !~ s{\.tgz\z}{}xms) {
+ return;
+ }
+ if (!-f $path) {
+ return;
+ }
+ push(@projects, FCM::Admin::Project->new({name => $name}));
+ },
+ );
+ return @projects;
+}
+
+# ------------------------------------------------------------------------------
+# Returns a list of projects by searching the live Trac directory.
+sub get_projects_from_trac_live {
+ # (no dummy argument)
+ my @projects;
+ _get_files_from(
+ $CONFIG->get_trac_live_dir(),
+ sub {
+ my ($name, $path) = @_;
+ if (!-d $path) {
+ return;
+ }
+ push(@projects, FCM::Admin::Project->new({name => $name}));
+ },
+ );
+ return @projects;
+}
+
+# ------------------------------------------------------------------------------
+# Return a HASH of valid users. If @only_users, then return only users matching
+# these IDs.
+sub get_users {
+ my @only_users = @_;
+ if (!defined($USER_INFO_TOOL)) {
+ my $name = $CONFIG->get_user_info_tool();
+ my $class = $UTIL->class_load($USER_INFO_TOOL_OF{$name});
+ $USER_INFO_TOOL = $class->new({util => $UTIL});
+ }
+ return $USER_INFO_TOOL->get_users_info(@only_users);
+}
+
+# ------------------------------------------------------------------------------
+# Housekeep logs generated by hook scripts of a SVN project.
+sub housekeep_svn_hook_logs {
+ my ($project) = @_;
+ my $project_path = $project->get_svn_live_path();
+ my $hook_source_dir = catfile($CONFIG->get_fcm_home(), 'etc', 'svn-hooks');
+ my $today = strftime("%Y%m%d", gmtime());
+ my $date_p1w = strftime("%Y%m%d", gmtime(time() - 604800)); # 1 week ago
+ my $date_p4w = strftime("%Y%m%d", gmtime(time() - 2419200)); # 4 weeks ago
+ my @hook_names = map {basename($_)} glob(catfile($hook_source_dir, q{*}));
+ for my $hook_name (sort @hook_names) {
+ my $log_path = catfile($project_path, 'log', $hook_name . '.log');
+ my $log_path_cur;
+ # Determine whether log file is more than a week old
+ if ( -l $log_path
+ && index(readlink($log_path), $hook_name . '.log.') == 0
+ ) {
+ my $path = readlink($log_path);
+ my ($date) = $path =~ qr{\.log\.(\d{8}\d*)\z}msx;
+ if ($date && $date > $date_p1w) {
+ $log_path_cur = catfile($project_path, 'log', $path);
+ }
+ }
+ # Create latest log, if necessary
+ if (!$log_path_cur) {
+ $log_path_cur = "$log_path.$today";
+ write_file($log_path_cur);
+ }
+ if ( !-e $log_path
+ || !-l $log_path
+ || readlink($log_path) ne basename($log_path_cur)
+ ) {
+ run_rmtree($log_path);
+ run_symlink(basename($log_path_cur), $log_path);
+ }
+ # Remove logs older than $keep_threshold
+ for my $path (
+ sort glob(catfile($project_path, 'log', $hook_name . '*.log.*'))
+ ) {
+ my ($date, $dot_gz) = $path =~ qr{\.log\.(\d{8}\d*)(\.gz)?\z}msx;
+ if ( $date && $date <= $date_p4w
+ || $date && $date <= $date_p1w && !-s $path
+ ) {
+ run_rmtree($path);
+ }
+ elsif ($date && $date <= $date_p1w && !$dot_gz) {
+ $RUNNER->run(
+ "gzip $path",
+ sub {gzip($path, "$path.gz") && unlink($path)},
+ );
+ }
+ }
+ }
+ my $group = $CONFIG->get_svn_group();
+ if ($group) {
+ _chgrp_and_chmod(catfile($project_path, 'log'), $group);
+ }
+}
+
+# ------------------------------------------------------------------------------
+# Installs hook scripts to a SVN project.
+sub install_svn_hook {
+ my ($project, $clean_mode) = @_;
+ my %path_of;
+ for (
+ [$CONFIG->get_fcm_site_home(), 'svn-hooks', $project->get_name()],
+ [$CONFIG->get_fcm_home(), 'etc', 'svn-hooks'],
+ ) {
+ my $hook_source_dir = catfile(@{$_});
+ _get_files_from(
+ $hook_source_dir,
+ sub {
+ my ($base_name, $path) = @_;
+ if (index($base_name, q{.}) == 0 || -d $path) {
+ return;
+ }
+ $path_of{$base_name} = $path;
+ },
+ );
+ }
+ # Write hook environment configuration
+ my $project_path = $project->get_svn_live_path();
+ my $conf_dest = catfile($project_path, qw{conf hooks-env});
+ write_file(
+ $conf_dest,
+ "[default]\n",
+ map {sprintf("%s=%s\n", @{$_});}
+ grep {$_->[1];} (
+ ['FCM_HOME', $CONFIG->get_fcm_home()],
+ ['FCM_SVN_HOOK_ADMIN_EMAIL', $CONFIG->get_admin_email()],
+ ['FCM_SVN_HOOK_COMMIT_DUMP_DIR', $CONFIG->get_svn_dump_dir()],
+ ['FCM_SVN_HOOK_NOTIFICATION_FROM', $CONFIG->get_notification_from()],
+ ['FCM_SVN_HOOK_REPOS_SUFFIX', $CONFIG->get_svn_project_suffix()],
+ ['FCM_SVN_HOOK_TRAC_ROOT_DIR', $CONFIG->get_trac_live_dir()],
+ ['PATH', $CONFIG->get_svn_hook_path_env()],
+ ['TZ', 'UTC'],
+ )
+ );
+ # Install hook scripts and associated files
+ for my $key (sort keys(%path_of)) {
+ my $hook_source = $path_of{$key};
+ my $hook_dest = catfile($project->get_svn_live_hook_path(), $key);
+ run_copy($hook_source, $hook_dest);
+ }
+ # Install hook configurations from repository root, e.g. svnperms.conf
+ for my $line (qx{svnlook tree -N $project_path}) {
+ chomp($line);
+ my ($name) = $line =~ qr{\A\s*(.*)\z}msx;
+ if (grep {$_ eq $name} @SVN_REPOS_ROOT_HOOK_ITEMS) {
+ my $dest = catfile($project->get_svn_live_hook_path(), $name);
+ $RUNNER->run(
+ "install $dest <- ^/$name",
+ sub {
+ my $source = "file://$project_path/$name";
+ !system(qw{svn export -q --force}, $source, $dest)
+ || die("\n");
+ chmod((stat($dest))[2] | S_IRGRP | S_IROTH, $dest);
+ },
+ );
+ $path_of{$name} = "^/$name";
+ }
+ }
+ # Clean hook destination, if necessary
+ if ($clean_mode) {
+ my $hook_path = $project->get_svn_live_hook_path();
+ for my $path (sort glob(catfile($hook_path, q{*}))) {
+ if (!exists($path_of{basename($path)})) {
+ run_rmtree($path);
+ }
+ }
+ }
+ my $group = $CONFIG->get_svn_group();
+ if ($group) {
+ _chgrp_and_chmod($project->get_svn_live_hook_path(), $group);
+ }
+ return 1;
+}
+
+# ------------------------------------------------------------------------------
+# Updates the SVN password file.
+sub manage_users_in_svn_passwd {
+ my ($user_ref) = @_;
+ if (!$CONFIG->get_svn_passwd_file()) {
+ return 1;
+ }
+ my $svn_passwd_file = catfile(
+ $CONFIG->get_svn_live_dir(),
+ $CONFIG->get_svn_passwd_file(),
+ );
+ $RUNNER->run(
+ "updating $svn_passwd_file",
+ sub {
+ my $USERS_SECTION = q{users};
+ my $svn_passwd_ini;
+ my $is_changed;
+ if (-f $svn_passwd_file) {
+ $svn_passwd_ini
+ = Config::IniFiles->new(q{-file} => $svn_passwd_file);
+ }
+ else {
+ $svn_passwd_ini = Config::IniFiles->new();
+ $svn_passwd_ini->SetFileName($svn_passwd_file);
+ $svn_passwd_ini->AddSection($USERS_SECTION);
+ $is_changed = 1;
+ }
+ for my $name (($svn_passwd_ini->Parameters($USERS_SECTION))) {
+ if (!exists($user_ref->{$name})) {
+ $RUNNER->run(
+ "removing $name from $svn_passwd_file",
+ sub {
+ return
+ $svn_passwd_ini->delval($USERS_SECTION, $name);
+ },
+ );
+ $is_changed = 1;
+ }
+ }
+ for my $user (values(%{$user_ref})) {
+ if (!defined($svn_passwd_ini->val($USERS_SECTION, "$user"))) {
+ $RUNNER->run(
+ "adding $user to $svn_passwd_file",
+ sub {
+ $svn_passwd_ini->newval(
+ $USERS_SECTION, $user->get_name(), q{},
+ ),
+ },
+ );
+ $is_changed = 1;
+ }
+ }
+ return ($is_changed ? $svn_passwd_ini->RewriteConfig() : 1);
+ },
+ );
+ return 1;
+}
+
+# ------------------------------------------------------------------------------
+# Updates the Trac password file.
+sub manage_users_in_trac_passwd {
+ my ($user_ref) = @_;
+ if (!$CONFIG->get_trac_passwd_file()) {
+ return 1;
+ }
+ my $trac_passwd_file = catfile(
+ $CONFIG->get_trac_live_dir(),
+ $CONFIG->get_trac_passwd_file(),
+ );
+ $RUNNER->run(
+ "updating $trac_passwd_file",
+ sub {
+ my %old_names;
+ my %new_names = %{$user_ref};
+ if (-f $trac_passwd_file) {
+ read_file(
+ $trac_passwd_file,
+ sub {
+ my ($line) = @_;
+ chomp($line);
+ if (
+ !$line || $line =~ qr{\A\s*\z}xms # blank line
+ || $line =~ qr{\A\s*\#}xms # comment line
+ ) {
+ return;
+ }
+ my ($name, $passwd) = split(qr{\s*:\s*}xms, $line);
+ if (exists($new_names{$name})) {
+ delete($new_names{$name});
+ }
+ else {
+ $old_names{$name} = 1;
+ }
+ },
+ ) || return;
+ }
+ else {
+ write_file($trac_passwd_file) || return;
+ }
+ if (%old_names || %new_names) {
+ for my $name (keys(%old_names)) {
+ $RUNNER->run(
+ "removing $name from $trac_passwd_file",
+ sub {
+ return !system(
+ qw{htpasswd -D}, $trac_passwd_file, $name,
+ );
+ },
+ );
+ }
+ for my $name (keys(%new_names)) {
+ $RUNNER->run(
+ "adding $name to $trac_passwd_file",
+ sub {
+ return !system(
+ qw{htpasswd -b}, $trac_passwd_file, $name, q{},
+ );
+ },
+ );
+ sleep(1); # ensure the random seed for htpasswd is changed
+ }
+ }
+ return 1;
+ },
+ # Note: can use HTTPD::UserAdmin, if it is installed
+ );
+ return 1;
+}
+
+# ------------------------------------------------------------------------------
+# Manages the session* tables in the DB of a Trac environment.
+sub manage_users_in_trac_db_of {
+ my ($project, $user_ref) = @_;
+ return $RUNNER->run_with_retries(
+ sprintf(
+ qq{checking/updating %s},
+ $project->get_trac_live_db_path(),
+ ),
+ sub {return _manage_users_in_trac_db_of($project, $user_ref)},
+ );
+}
+
+# ------------------------------------------------------------------------------
+# Recovers a SVN repository from its backup.
+sub recover_svn_repository {
+ my ($project, $recover_dumps_option, $recover_hooks_option) = @_;
+ if (-e $project->get_svn_live_path()) {
+ die(sprintf(
+ "%s: live repository exists.\n",
+ $project->get_svn_live_path(),
+ ));
+ }
+ run_mkpath($CONFIG->get_svn_live_dir());
+ my $base_name = $project->get_svn_base_name();
+ my $work_dir = tempdir(
+ qq{$base_name.XXXXXX},
+ DIR => $CONFIG->get_svn_live_dir(),
+ CLEANUP => 1,
+ );
+ my $work_path = catfile($work_dir, $base_name);
+ _extract_backup_archive($project->get_svn_backup_path(), $work_path);
+ if ($recover_dumps_option) {
+ my $youngest = _svnlook_youngest($work_path);
+ my %dump_path_of;
+ _get_files_from(
+ $CONFIG->get_svn_dump_dir(),
+ sub {
+ my ($dump_base_name, $path) = @_;
+ my ($name, $rev) = $dump_base_name =~ qr{\A(.*)-(\d+)\.gz\z}msx;
+ if ( !$name
+ || !$rev
+ || $name ne $base_name
+ || $rev <= $youngest
+ ) {
+ return;
+ }
+ $dump_path_of{$rev} = $path;
+ },
+ );
+ for my $rev (sort {$a <=> $b} keys(%dump_path_of)) {
+ my $dump_path = $dump_path_of{$rev};
+ $RUNNER->run(
+ "loading $dump_path into $work_path",
+ sub {
+ my $pipe = IO::Pipe->new();
+ $pipe->writer(qw{svnadmin load}, $work_path);
+ my $handle = IO::Zlib->new($dump_path, 'rb');
+ if (!$handle) {
+ die("$dump_path: $!\n");
+ }
+ while ($handle->read(my $buffer, $BUFFER_SIZE)) {
+ $pipe->print($buffer);
+ }
+ $handle->close();
+ return ($pipe->close());
+ },
+ );
+ }
+ }
+ run_rename($work_path, $project->get_svn_live_path());
+ my $group = $CONFIG->get_svn_group();
+ if ($group) {
+ _chgrp_and_chmod($project->get_svn_live_path(), $group);
+ }
+ if ($recover_hooks_option) {
+ install_svn_hook($project);
+ housekeep_svn_hook_logs($project);
+ }
+ return 1;
+}
+
+# ------------------------------------------------------------------------------
+# Recovers a Trac environment from its backup.
+sub recover_trac_environment {
+ my ($project) = @_;
+ if (-e $project->get_trac_live_path()) {
+ die(sprintf(
+ "%s: live environment exists.\n",
+ $project->get_trac_live_path(),
+ ));
+ }
+ run_mkpath($CONFIG->get_trac_live_dir());
+ my $base_name = $project->get_name();
+ my $work_dir = tempdir(
+ qq{$base_name.XXXXXX},
+ DIR => $CONFIG->get_trac_live_dir(),
+ CLEANUP => 1,
+ );
+ my $work_path = catfile($work_dir, $base_name);
+ _extract_backup_archive($project->get_trac_backup_path(), $work_path);
+ run_rename($work_path, $project->get_trac_live_path());
+ my $group = $CONFIG->get_trac_group();
+ if ($group) {
+ _chgrp_and_chmod($project->get_trac_live_path(), $group);
+ }
+}
+
+# ------------------------------------------------------------------------------
+# Recover a file from the Trac backup directory to the Trac live directory.
+sub recover_trac_files {
+ # (no argument)
+ _copy_files(
+ $CONFIG->get_trac_backup_dir(),
+ $CONFIG->get_trac_live_dir(),
+ $NO_OVERWRITE,
+ qr{\.tgz\z}msx,
+ );
+}
+
+# ------------------------------------------------------------------------------
+# Vacuum the database of a Trac environment.
+sub vacuum_trac_env_db {
+ my ($project) = @_;
+ $RUNNER->run(
+ "performing vacuum on database of Trac environment for $project",
+ sub {
+ my $db_handle = _get_trac_db_handle_for($project);
+ if (!$db_handle) {
+ return;
+ }
+ $db_handle->do(q{vacuum;}) && $db_handle->disconnect();
+ },
+ );
+}
+
+# ------------------------------------------------------------------------------
+# Verify users. Return a list of bad users from @users.
+sub verify_users {
+ my @users = @_;
+ if (!defined($USER_INFO_TOOL)) {
+ my $name = $CONFIG->get_user_info_tool();
+ my $class = $UTIL->class_load($USER_INFO_TOOL_OF{$name});
+ $USER_INFO_TOOL = $class->new({util => $UTIL});
+ }
+ return $USER_INFO_TOOL->verify_users(@users);
+}
+
+# ------------------------------------------------------------------------------
+# Changes/restores ownership and permission of a given $path to a given $group.
+sub _chgrp_and_chmod {
+ my ($path, $group) = @_;
+ my $gid = $group ? scalar(getgrnam($group)) : -1;
+ find(
+ sub {
+ my $file = $File::Find::name;
+ $RUNNER->run(
+ "changing group ownership for $file",
+ sub {return chown(-1, $gid, $file)},
+ );
+ my $mode = (stat($file))[2] | S_IRGRP | S_IWGRP;
+ $RUNNER->run(
+ "adding group write permission for $file",
+ sub {return chmod($mode, $file)},
+ );
+ },
+ $path,
+ );
+ return 1;
+}
+
+# ------------------------------------------------------------------------------
+# Copies files immediately under $source to $target.
+sub _copy_files {
+ my ($source, $target, $no_overwrite, $re_skip) = @_;
+ my @bases;
+ opendir(my $handle, $source) || die("$source: $!\n");
+ while (my $base = readdir($handle)) {
+ if (-f catfile($source, $base)) {
+ if ($no_overwrite && -f catfile($target, $base)) {
+ warn("[SKIP] $base: already exists in $target.\n");
+ }
+ elsif (!$re_skip || ($base !~ $re_skip)) {
+ push(@bases, $base);
+ }
+ }
+ }
+ closedir($handle);
+ run_mkpath($target);
+ for my $base (@bases) {
+ run_copy(map {catfile($_, $base)} ($source, $target));
+ }
+ return 1;
+}
+
+# ------------------------------------------------------------------------------
+# Creates backup archive from a path.
+sub _create_backup_archive {
+ my ($source_path, $backup_dir, $archive_base_name) = @_;
+ my $source_dir = dirname($source_path);
+ my $source_base_name = basename($source_path);
+ run_mkpath($backup_dir);
+ my ($fh, $work_backup_path)
+ = tempfile(qq{$archive_base_name.XXXXXX}, DIR => $backup_dir);
+ close($fh);
+ run_create_archive($work_backup_path, $source_dir, $source_base_name);
+ my $backup_path = catfile($backup_dir, $archive_base_name);
+ run_rename($work_backup_path, $backup_path);
+ my $mode = (stat($backup_path))[2] | S_IRGRP | S_IROTH;
+ return chmod($mode, $backup_path);
+}
+
+# ------------------------------------------------------------------------------
+# Extracts from a backup archive to a work path.
+sub _extract_backup_archive {
+ my ($archive_path, $work_path) = @_;
+ run_extract_archive($archive_path, dirname($work_path));
+ if (! -e $work_path) {
+ my ($base_name) = basename($work_path);
+ die("$base_name: does not exist in archive $archive_path.\n");
+ }
+ return 1;
+}
+
+# ------------------------------------------------------------------------------
+# Searches a directory for files and invokes a callback on each file.
+sub _get_files_from {
+ my ($dir_path, $callback_ref) = @_;
+ my $dir_handle = IO::Dir->new($dir_path);
+ if (!defined($dir_handle)) {
+ return;
+ }
+ BASE_NAME:
+ while (my $base_name = $dir_handle->read()) {
+ my $path = catfile($dir_path, $base_name);
+ if (index($base_name, q{.}) == 0) {
+ next BASE_NAME;
+ }
+ $callback_ref->($base_name, $path);
+ }
+ return $dir_handle->close();
+}
+
+# ------------------------------------------------------------------------------
+# Returns a database handle for the database of a Trac environment.
+sub _get_trac_db_handle_for {
+ my ($project) = @_;
+ my $db_path = $project->get_trac_live_db_path();
+ return DBI->connect(qq{dbi:SQLite:dbname=$db_path}, q{}, q{});
+}
+
+# ------------------------------------------------------------------------------
+# Manages the session* tables in the DB of a Trac environment.
+sub _manage_users_in_trac_db_of {
+ my ($project, $user_ref) = @_;
+ my $db_handle = _get_trac_db_handle_for($project);
+ if (!$db_handle) {
+ return;
+ }
+ SESSION: {
+ my $session_select_statement = $db_handle->prepare(
+ "SELECT sid FROM session WHERE authenticated == 1",
+ );
+ my $session_insert_statement = $db_handle->prepare(
+ "INSERT INTO session VALUES (?, 1, 0)",
+ );
+ my $session_delete_statement = $db_handle->prepare(
+ "DELETE FROM session WHERE sid == ?",
+ );
+ $session_select_statement->execute();
+ my $is_changed = 0;
+ my %session_old_users;
+ while (my ($sid) = $session_select_statement->fetchrow_array()) {
+ if (exists($user_ref->{$sid})) {
+ $session_old_users{$sid} = 1;
+ }
+ else {
+ $RUNNER->run(
+ "session: removing $sid",
+ sub{return $session_delete_statement->execute($sid)},
+ );
+ $is_changed = 1;
+ }
+ }
+ for my $sid (keys(%{$user_ref})) {
+ if (!exists($session_old_users{$sid})) {
+ $RUNNER->run(
+ "session: adding $sid",
+ sub {return $session_insert_statement->execute($sid)},
+ );
+ $is_changed = 1;
+ }
+ }
+ $session_select_statement->finish();
+ $session_insert_statement->finish();
+ $session_delete_statement->finish();
+ }
+ SESSION_ATTRIBUTE: {
+ my $attribute_select_statement = $db_handle->prepare(
+ "SELECT sid,name,value FROM session_attribute "
+ . "WHERE authenticated == 1 AND (name == ? OR name == ?)",
+ );
+ my $attribute_insert_statement = $db_handle->prepare(
+ "INSERT INTO session_attribute VALUES (?, 1, ?, ?)",
+ );
+ my $attribute_update_statement = $db_handle->prepare(
+ "UPDATE session_attribute SET value = ? "
+ . "WHERE sid = ? AND authenticated == 1 AND name == ?",
+ );
+ my $attribute_delete_statement = $db_handle->prepare(
+ "DELETE FROM session_attribute WHERE sid == ?",
+ );
+ $attribute_select_statement->execute('name', 'email');
+ my %attribute_old_users;
+ ROW:
+ while (my @row = $attribute_select_statement->fetchrow_array()) {
+ my ($sid, $name, $value) = @row;
+ my $user = exists($user_ref->{$sid})? $user_ref->{$sid} : undef;
+ if (defined($user)) {
+ $attribute_old_users{$sid} = 1;
+ my $getter
+ = $name eq 'name' ? 'get_display_name'
+ : $name eq 'email' ? 'get_email'
+ : undef;
+ if (!defined($getter)) {
+ next ROW;
+ }
+ if ($user->$getter() ne $value) {
+ my $new_value = $user->$getter();
+ $RUNNER->run(
+ "session_attribute: updating $name: $sid: $new_value",
+ sub {return $attribute_update_statement->execute(
+ $new_value, $sid, $name,
+ )},
+ );
+ }
+ }
+ else {
+ $RUNNER->run(
+ "session_attribute: removing $sid",
+ sub {return $attribute_delete_statement->execute($sid)},
+ );
+ }
+ }
+ USER:
+ for my $sid (keys(%{$user_ref})) {
+ if (exists($attribute_old_users{$sid})) {
+ next USER;
+ }
+ my $user = $user_ref->{$sid};
+ my $display_name = $user->get_display_name();
+ my $email = $user->get_email();
+ $RUNNER->run(
+ "session_attribute: adding name: $sid: $display_name",
+ sub {return $attribute_insert_statement->execute(
+ $sid, 'name', $display_name,
+ )},
+ );
+ $RUNNER->run(
+ "session_attribute: adding email: $sid: $email",
+ sub {return $attribute_insert_statement->execute(
+ $sid, 'email', $email,
+ )},
+ );
+ }
+ $attribute_select_statement->finish();
+ $attribute_insert_statement->finish();
+ $attribute_update_statement->finish();
+ $attribute_delete_statement->finish();
+ }
+ return $db_handle->disconnect();
+}
+
+# ------------------------------------------------------------------------------
+# Returns the youngest revision of a SVN repository.
+sub _svnlook_youngest {
+ my ($svn_repos_path) = @_;
+ my ($youngest) = qx{svnlook youngest $svn_repos_path};
+ chomp($youngest);
+ return $youngest;
+}
+
+1;
+__END__
+
+=head1 NAME
+
+FCM::Admin::System
+
+=head1 SYNOPSIS
+
+ use FCM::Admin::System qw{ ... };
+ # ... see descriptions of individual functions for detail
+
+=head1 DESCRIPTION
+
+This module contains utility functions for the administration of Subversion
+repositories and Trac environments hosted by the FCM team.
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item add_svn_repository($project_name)
+
+Creates a new Subversion repository.
+
+=item add_trac_environment($project_name)
+
+Creates a new Trac environment.
+
+=item backup_svn_repository(\%option,$project)
+
+Creates an archived hotcopy of $project's live SVN repository, and put it in the
+SVN backup directory. If $option{'no-verify-integrity'} does not exist, it
+verifies the integrity of the live repository before creating the hotcopy. If
+$option{'no-pack'} does not exist, it packs the live repository before creating
+the hotcopy. If $option{'no-housekeep-dumps'} does not exist, it housekeeps the
+revision dumps of $project following a successful backup.
+
+$project should be a L<FCM::Admin::Project|FCM::Admin::Project> object.
+
+=item backup_trac_environment(\%option,$project)
+
+Creates an archived hotcopy of $project's live Trac environment, and put it in
+the Trac backup directory. If $option{'no-verify-integrity'} does not exist, it
+verifies the integrity of the database of the live environment before creating
+the hotcopy.
+
+$project should be a L<FCM::Admin::Project|FCM::Admin::Project> object.
+
+=item backup_trac_files()
+
+Copies regular files immediately under the live Trac directory to the Trac
+backup directory.
+
+=item distribute_wc()
+
+Distributes the central FCM working copy to standard locations.
+
+=item filter_projects($project_list_ref,$filter_list_ref)
+
+Filters the project list in $project_list_ref using a list of names in
+$filter_list_ref. Returns a list of projects with names matching those in
+$filter_list_ref. Returns the full list if $filter_list_ref points to an empty
+list.
+
+=item get_projects_from_svn_backup()
+
+Returns a list of L<FCM::Admin::Project|FCM::Admin::Project> objects by
+searching the SVN backup directory. By default, all valid projects are returned.
+
+=item get_projects_from_svn_live()
+
+Similar to get_projects_from_svn_backup(), but it searches the SVN live
+directory.
+
+=item get_projects_from_trac_backup()
+
+Similar to get_projects_from_svn_backup(), but it searches the Trac backup
+directory.
+
+=item get_projects_from_trac_live()
+
+Similar to get_projects_from_svn_backup(), but it searches the Trac live
+directory.
+
+=item get_users(@only_users)
+
+Retrieves a list of users. Store results in a HASH, {user ID => user info, ...}
+where each user info is stored in an instance of
+L<FCM::Admin::System::User|FCM::Admin::System::User>.
+
+If no argument, return all valid users. If @only_users, return only those users
+with matching user ID in @only_users.
+
+=item housekeep_svn_hook_logs($project)
+
+Housekeep logs generated by the hook scripts of the $project's SVN live
+repository.
+
+$project should be a L<FCM::Admin::Project|FCM::Admin::Project> object.
+
+=item install_svn_hook($project, $clean_mode)
+
+Searches for hook scripts in the standard location and install them (as symbolic
+links) in the I<hooks> directory of the $project's SVN live repository.
+
+$project should be a L<FCM::Admin::Project|FCM::Admin::Project> object.
+
+If $clean_mode is specified and is true, remove any items in the I<hooks>
+directory that are not known to this install.
+
+=item manage_users_in_svn_passwd($user_ref)
+
+Using entries in the hash reference $user_ref, sets up or updates the SVN and
+Trac password files. The $user_ref argument should be a reference to a hash, as
+returned by get_users().
+
+=item manage_users_in_trac_passwd($user_ref)
+
+Using entries in the hash reference $user_ref, sets up or updates the Trac
+password files. The $user_ref argument should be a reference to a hash, as
+returned by get_users().
+
+=item manage_users_in_trac_db_of($project, $user_ref)
+
+Using entries in $user_ref, sets up or updates the session/session_attribute
+tables in the databases of the live Trac environments. The $project argument
+should be a L<FCM::Admin::Project|FCM::Admin::Project> object
+and $user_ref should be a reference to a hash, as returned by get_users().
+
+=item recover_svn_repository($project,$recover_dumps_option,$recover_hooks_option)
+
+Recovers a project's SVN repository using its backup. If $recover_dumps_option
+is set to true, it will also attempt to load the latest revision dumps following
+a successful recovery. If $recover_hooks_option is set to true, it will also
+attempt to re-install the hook scripts following a successful recovery.
+
+$project should be a L<FCM::Admin::Project|FCM::Admin::Project> object.
+
+=item recover_trac_environment($project)
+
+Recovers a project's Trac environment using its backup.
+
+$project should be a L<FCM::Admin::Project|FCM::Admin::Project> object.
+
+=item recover_trac_files()
+
+Copies files immediately under the backup Trac directory to the Trac live
+directory (if the files do not already exist).
+
+=item vacuum_trac_env_db($project)
+
+Connects to the database of a project's Trac environment, and issues the
+"VACUUM" SQL command.
+
+$project should be a L<FCM::Admin::Project|FCM::Admin::Project> object.
+
+=back
+
+=head1 SEE ALSO
+
+L<FCM::Admin::Config|FCM::Admin::Config>,
+L<FCM::Admin::Project|FCM::Admin::Project>,
+L<FCM::Admin::Runner|FCM::Admin::Runner>,
+L<FCM::Admin::User|FCM::Admin::User>
+
+=head1 COPYRIGHT
+
+E<169> Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/Admin/User.pm b/lib/FCM/Admin/User.pm
new file mode 100644
index 0000000..2db3303
--- /dev/null
+++ b/lib/FCM/Admin/User.pm
@@ -0,0 +1,90 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+
+use strict;
+use warnings;
+
+package FCM::Admin::User;
+use base qw{FCM::Class::HASH};
+use overload q{""} => \&get_name;
+
+__PACKAGE__->class({
+ name => '$',
+ display_name => '$',
+ email => '$',
+});
+
+1;
+__END__
+
+=head1 NAME
+
+FCM::Admin::User
+
+=head1 SYNOPSIS
+
+ use FCM::Admin::User;
+ $user = FCM::Admin::User->new({name => 'bob'});
+ $user->set_display_name('Robert Smith');
+ $user->set_email('robert.smith at somewhere.org');
+
+=head1 DESCRIPTION
+
+An object of this class is used to store the data model of a user.
+
+=head1 METHODS
+
+=over 4
+
+=item FCM::Admin::User->new(\%arguments)
+
+Creates a new instance. The keys of the %argument hash may contain "name",
+"display_name", and/or "email".
+
+=item $user->get_name()
+
+Returns the name/ID of the user.
+
+=item $user->get_display_name()
+
+Returns the display name of the user.
+
+=item $user->get_email()
+
+Returns the e-mail address of the user.
+
+=item $user->set_name($value)
+
+Sets the name/ID of the user.
+
+=item $user->set_display_name($value)
+
+Sets the display name of the user.
+
+=item $user->set_email($value)
+
+Sets the e-mail address of the user.
+
+=back
+
+=head1 COPYRIGHT
+
+E<169> Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/Admin/Users/LDAP.pm b/lib/FCM/Admin/Users/LDAP.pm
new file mode 100644
index 0000000..a08ad17
--- /dev/null
+++ b/lib/FCM/Admin/Users/LDAP.pm
@@ -0,0 +1,149 @@
+
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+
+use strict;
+use warnings;
+
+package FCM::Admin::Users::LDAP;
+use base qw{FCM::Class::CODE};
+
+use FCM::Admin::Config;
+use FCM::Admin::User;
+use Net::LDAP;
+use Text::ParseWords qw{shellwords};
+
+my %ACTION_OF = (
+ get_users_info => \&_get_users_info,
+ verify_users => \&_verify_users,
+);
+
+__PACKAGE__->class({util => '&'}, {action_of => {%ACTION_OF}});
+
+my $CONFIG = FCM::Admin::Config->instance();
+
+# Gets a HASH of users via LDAP.
+# %user_of = ($name => <FCM::Admin::User instance>, ...)
+sub _get_users_info {
+ my ($attrib_ref, @only_users) = @_;
+ my $res = _ldap_search($attrib_ref, undef, @only_users);
+ my ($uid_attr, $cn_attr, $mail_attr)
+ = shellwords($CONFIG->get_ldap_attrs());
+ my %user_of;
+ for my $entry ($res->entries()) {
+ my $name = $entry->get_value($uid_attr);
+ my $display_name = $entry->get_value($cn_attr);
+ my $email = $entry->get_value($mail_attr);
+ if ($display_name && $email) {
+ $user_of{$name} = FCM::Admin::User->new({
+ name => $name,
+ display_name => $display_name,
+ email => $email,
+ });
+ }
+ }
+ return (wantarray() ? %user_of : \%user_of);
+}
+
+# Return a list of bad users.
+sub _verify_users {
+ my ($attrib_ref, @users) = @_;
+ my $res = _ldap_search($attrib_ref, 0, @users); # 0 == $uid_attr
+ my ($uid_attr, $cn_attr, $mail_attr)
+ = shellwords($CONFIG->get_ldap_attrs());
+ my %bad_users = map {($_ => 1)} @users;
+ for my $entry ($res->entries()) {
+ my $name = $entry->get_value($uid_attr);
+ if (exists($bad_users{$name})) {
+ delete($bad_users{$name});
+ }
+ }
+ return sort(keys(%bad_users));
+}
+
+# Bind to the LDAP server. Return a Net::LDAP instance.
+sub _ldap_search {
+ my ($attrib_ref, $attr_index, @users) = @_;
+
+ my $ldap_uri = $CONFIG->get_ldap_uri();
+ my $ldap = Net::LDAP->new($CONFIG->get_ldap_uri());
+ my $password_file = $CONFIG->get_ldappw();
+ $password_file = $attrib_ref->{util}->file_tilde_expand($password_file);
+ my $password = $password_file
+ ? $attrib_ref->{util}->file_load($password_file)
+ : undef;
+ my %ldap_options = $password ? (password => $password) : ();
+ $ldap->bind($CONFIG->get_ldap_binddn(), %ldap_options);
+
+ my @attrs = shellwords($CONFIG->get_ldap_attrs());
+ my ($uid_attr) = @attrs;
+ my $filter = @users
+ ? "(|($uid_attr=" . join(")($uid_attr=", @users) . '))'
+ : "(&($uid_attr=*))";
+ my $ldap_filter_more = $CONFIG->get_ldap_filter_more();
+ if ($ldap_filter_more) {
+ $filter = '(&' . $filter . $ldap_filter_more . ')';
+ }
+ my $res = $ldap->search(
+ base => $CONFIG->get_ldap_basedn(),
+ filter => $filter,
+ attrs => [$attr_index ? ($attrs[$attr_index]) : @attrs],
+ );
+ $ldap->unbind();
+ return $res;
+}
+
+1;
+__END__
+
+=head1 NAME
+
+FCM::Admin::Users::LDAP
+
+=head1 SYNOPSIS
+
+ use FCM::Admin::Users::LDAP;
+ my $users_info_util = FCM::Admin::Users::LDAP->new();
+ $users_info_util->get_users();
+
+=head1 DESCRIPTION
+
+Utility for obtaining user information via LDAP.
+
+=head1 METHODS
+
+=over 4
+
+=item $util->get_users_info()
+
+Return a HASH (in list context) or a reference to a HASH (in scalar context)
+{name => <FCM::Admin::User instance>, ...}. The HASH should contain all entries
+in the passwd database that appear to be real users.
+
+=item $util->verify_users(@users)
+
+Return a list of bad users in @users.
+
+=back
+
+=head1 COPYRIGHT
+
+E<169> Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/Admin/Users/Passwd.pm b/lib/FCM/Admin/Users/Passwd.pm
new file mode 100644
index 0000000..769f799
--- /dev/null
+++ b/lib/FCM/Admin/Users/Passwd.pm
@@ -0,0 +1,141 @@
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+
+use strict;
+use warnings;
+
+package FCM::Admin::Users::Passwd;
+use base qw{FCM::Class::CODE};
+
+use FCM::Admin::Config;
+use FCM::Admin::User;
+use Text::ParseWords qw{shellwords};
+
+my %ACTION_OF = (
+ get_users_info => \&_get_users_info,
+ verify_users => \&_verify_users,
+);
+
+__PACKAGE__->class({}, {action_of => {%ACTION_OF}});
+
+my $CONFIG = FCM::Admin::Config->instance();
+
+# Gets a HASH of users using the POSIX password DB.
+# %user_of = ($name => <FCM::Admin::User instance>, ...)
+sub _get_users_info {
+ my ($attrib_ref, @only_users) = @_;
+ if (@only_users) {
+ return _get_only_users_info($attrib_ref, @only_users);
+ }
+ my $domain = $CONFIG->get_passwd_email_domain() || q{};
+ if ($domain) {
+ $domain = '@' . $domain;
+ }
+ my $gid_max = $CONFIG->get_passwd_gid_max();
+ my $uid_max = $CONFIG->get_passwd_uid_max();
+ my $gid_min = $CONFIG->get_passwd_gid_min();
+ my $uid_min = $CONFIG->get_passwd_uid_min();
+ my %user_of;
+ USER:
+ while (my ($name, $uid, $gid, $gecos) = (getpwent())[0, 2, 3, 6]) {
+ if ( exists($user_of{$name})
+ || defined($uid_max) && $uid > $uid_max || $uid < $uid_min
+ || defined($gid_max) && $gid > $gid_max || $gid < $gid_min
+ || !$gecos
+ || (@only_users && grep {$_ eq $name} @only_users)
+ ) {
+ next USER;
+ }
+ $user_of{$name} = FCM::Admin::User->new({
+ name => $name,
+ display_name => $gecos,
+ email => $gecos . $domain,
+ });
+ }
+ endpwent();
+ return (wantarray() ? %user_of : \%user_of);
+}
+
+# Gets a HASH of users matching @only_users using the POSIX password DB.
+# %user_of = ($name => <FCM::Admin::User instance>, ...)
+sub _get_only_users_info {
+ my ($attrib_ref, @only_users) = @_;
+ my $domain = $CONFIG->get_passwd_email_domain() || q{};
+ if ($domain) {
+ $domain = '@' . $domain;
+ }
+ my @ok_uids = shellwords($CONFIG->get_passwd_ok_uids());
+ my %user_of;
+ for my $user (@only_users) {
+ my ($name, $gecos) = (getpwnam($user))[0, 6];
+ if ($name && $gecos && $gecos =~ qr{\A[\w\.\-]+\.[\w\.\-]+\z}msx) {
+ $user_of{$name} = FCM::Admin::User->new({
+ name => $name,
+ display_name => $gecos,
+ email => $gecos . $domain,
+ });
+ }
+ }
+ return (wantarray() ? %user_of : \%user_of);
+}
+
+# Return a list of bad users in @users.
+sub _verify_users {
+ my ($attrib_ref, @users) = @_;
+ grep {!getpwnam($_)} @users;
+}
+
+1;
+__END__
+
+=head1 NAME
+
+FCM::Admin::Users::Passwd
+
+=head1 SYNOPSIS
+
+ use FCM::Admin::Users::Passwd;
+ my $users_info_util = FCM::Admin::Users::Passwd->new();
+ $users_info_util->get_users();
+
+=head1 DESCRIPTION
+
+Utility for obtaining user information from passwd information.
+
+=head1 METHODS
+
+=over 4
+
+=item $util->get_users_info()
+
+Return a HASH (in list context) or a reference to a HASH (in scalar context)
+{name => <FCM::Admin::User instance>, ...}. The HASH should contain all entries
+in the passwd database that appear to be real users.
+
+=item $util->verify_users(@users)
+
+Return a list of bad users in @users.
+
+=back
+
+=head1 COPYRIGHT
+
+E<169> Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/Admin/Util.pm b/lib/FCM/Admin/Util.pm
new file mode 100644
index 0000000..524339d
--- /dev/null
+++ b/lib/FCM/Admin/Util.pm
@@ -0,0 +1,386 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+
+use strict;
+use warnings;
+
+package FCM::Admin::Util;
+
+use Exporter qw{import};
+use FCM::Admin::Config;
+use FCM::Admin::Runner;
+use File::Basename qw{dirname};
+use File::Copy qw{copy};
+use File::Path qw{mkpath rmtree};
+use IO::File;
+use SVN::Client;
+
+our @EXPORT_OK = qw{
+ option2config
+ read_file
+ run_copy
+ run_create_archive
+ run_extract_archive
+ run_mkpath
+ run_rename
+ run_rmtree
+ run_rsync
+ run_svn_info
+ run_svn_update
+ run_symlink
+ sed_file
+ write_file
+};
+
+my @HTML2PS = qw{html2ps -n -U -W b};
+my @PS2PDF = qw{
+ ps2pdf
+ -dMaxSubsetPct=100
+ -dCompatibilityLevel=1.3
+ -dSubsetFonts=true
+ -dEmbedAllFonts=true
+ -dAutoFilterColorImages=false
+ -dAutoFilterGrayImages=false
+ -dColorImageFilter=/FlateEncode
+ -dGrayImageFilter=/FlateEncode
+ -dMonoImageFilter=/FlateEncode
+ -sPAPERSIZE=a4
+};
+
+# ------------------------------------------------------------------------------
+# Loads values of an option hash into the configuration.
+sub option2config {
+ my ($option_ref) = @_;
+ my $config = FCM::Admin::Config->instance();
+ for my $key (keys(%{$option_ref})) {
+ my $method = $key;
+ $method =~ s{-}{_}gxms;
+ $method = "set_$method";
+ if ($config->can($method)) {
+ $config->$method($option_ref->{$key});
+ }
+ }
+ return 1;
+}
+
+# ------------------------------------------------------------------------------
+# Reads lines from a file.
+sub read_file {
+ my ($path, $sub_ref) = @_;
+ my $file = IO::File->new($path);
+ if (!defined($file)) {
+ die("$path: cannot open for reading ($!).\n");
+ }
+ while (my $line = $file->getline()) {
+ $sub_ref->($line);
+ }
+ $file->close() || die("$path: cannot close for reading ($!).\n");
+ return 1;
+}
+
+# ------------------------------------------------------------------------------
+# Runs copy with checks and diagnostics.
+sub run_copy {
+ my ($source_path, $dest_path) = @_;
+ FCM::Admin::Runner->instance()->run(
+ "copy $source_path to $dest_path",
+ sub {
+ my $mode = (stat($source_path))[2];
+ my $rc = copy($source_path, $dest_path) && chmod($mode, $dest_path);
+ if (!$rc) {
+ die($!);
+ }
+ return $rc;
+ },
+ );
+}
+
+# ------------------------------------------------------------------------------
+# Creates a TAR-GZIP archive.
+sub run_create_archive {
+ my ($archive_path, $work_dir, @base_names) = @_;
+ FCM::Admin::Runner->instance()->run(
+ "creating archive $archive_path",
+ sub {
+ return !system(
+ qw{tar -c -z},
+ q{-C} => $work_dir,
+ q{-f} => $archive_path,
+ @base_names,
+ );
+ # Note: can use Archive::Tar, but "tar" is much faster.
+ },
+ );
+}
+
+# ------------------------------------------------------------------------------
+# Extracts from a TAR-GZIP archive.
+sub run_extract_archive {
+ my ($archive_path, $work_dir) = @_;
+ FCM::Admin::Runner->instance()->run(
+ "extracting archive $archive_path",
+ sub {
+ return !system(
+ qw{tar -x -z},
+ q{-C} => $work_dir,
+ q{-f} => $archive_path,
+ );
+ # Note: can use Archive::Tar, but "tar" is much faster.
+ },
+ );
+}
+
+# ------------------------------------------------------------------------------
+# Runs mkpath with checks and diagnostics.
+sub run_mkpath {
+ my ($path) = @_;
+ if (!-d $path) {
+ FCM::Admin::Runner->instance()->run(
+ "creating $path",
+ sub {return mkpath($path)},
+ );
+ }
+ return 1;
+}
+
+# ------------------------------------------------------------------------------
+# Runs rename with checks and diagnostics.
+sub run_rename {
+ my ($source_path, $dest_path) = @_;
+ FCM::Admin::Runner->instance()->run(
+ "renaming $source_path to $dest_path",
+ sub {
+ run_mkpath(dirname($dest_path));
+ my $rc = rename($source_path, $dest_path);
+ if (!$rc) {
+ die($!);
+ }
+ return $rc;
+ },
+ );
+ return 1;
+}
+
+# ------------------------------------------------------------------------------
+# Runs rmtree with checks and diagnostics.
+sub run_rmtree {
+ my ($path) = @_;
+ if (-e $path) {
+ FCM::Admin::Runner->instance()->run(
+ "removing $path",
+ sub {
+ rmtree($path);
+ return !-e $path;
+ },
+ );
+ }
+ return 1;
+}
+
+# ------------------------------------------------------------------------------
+# Runs rsync.
+sub run_rsync {
+ my ($sources_ref, $dest_path, $option_list_ref) = @_;
+ FCM::Admin::Runner->instance()->run(
+ sprintf('mirroring %s <- %s', $dest_path, join(q{ }, @{$sources_ref})),
+ sub {return !system(
+ q{rsync},
+ ($option_list_ref ? @{$option_list_ref} : ()),
+ @{$sources_ref},
+ $dest_path,
+ )},
+ );
+ return 1;
+}
+
+# ------------------------------------------------------------------------------
+# Runs "svn info".
+sub run_svn_info {
+ my ($path) = @_;
+ my $return;
+ my $ctx = SVN::Client->new();
+ $ctx->info($path, undef, 'WORKING', sub {$return = $_[1]}, 0);
+ return $return;
+}
+
+# ------------------------------------------------------------------------------
+# Runs "svn update".
+sub run_svn_update {
+ my ($path) = @_;
+ my @return;
+ my $ctx = SVN::Client->new(
+ notify => sub {
+ if ($path ne $_[0]) {
+ push(@return, $_[0]);
+ }
+ }
+ );
+ $ctx->update($path, 'HEAD', 1);
+ return @return;
+}
+
+# ------------------------------------------------------------------------------
+# Runs symlink with checks and diagnostics.
+sub run_symlink {
+ my ($source_path, $dest_path) = @_;
+ FCM::Admin::Runner->instance()->run(
+ "creating symlink: $source_path -> $dest_path",
+ sub {
+ my $rc = symlink($source_path, $dest_path);
+ if (!$rc) {
+ die($!);
+ }
+ return $rc;
+ },
+ );
+ return 1;
+}
+
+# ------------------------------------------------------------------------------
+# Edits content of a file.
+sub sed_file {
+ my ($path, $sub_ref) = @_;
+ my @lines;
+ read_file(
+ $path,
+ sub {
+ my ($line) = @_;
+ $line = $sub_ref->($line);
+ push(@lines, $line);
+ },
+ );
+ write_file($path, @lines);
+}
+
+# ------------------------------------------------------------------------------
+# Writes content to a file.
+sub write_file {
+ my ($path, @contents) = @_;
+ mkpath(dirname($path));
+ my $file = IO::File->new($path, q{w});
+ if (!defined($file)) {
+ die("$path: cannot open for writing ($!).\n");
+ }
+ for my $content (@contents) {
+ $file->print($content);
+ }
+ $file->close() || die("$path: cannot close for writing ($!).\n");
+ return 1;
+}
+
+1;
+__END__
+
+=head1 NAME
+
+FCM::Admin::Util
+
+=head1 SYNOPSIS
+
+ use FCM::Admin::Util qw{ ... };
+ # ... see descriptions of individual functions for detail
+
+=head1 DESCRIPTION
+
+This module contains utility functions for the administration of Subversion
+repositories and Trac environments hosted by the FCM team.
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item option2config($option_ref)
+
+Loads the values of an option hash into
+L<FCM::Admin::Config|FCM::Admin::Config>.
+
+=item read_file($path,$sub_ref)
+
+Reads from $path. For each $line the file, calls $sub_ref->($line).
+
+=item run_copy($source_path,$dest_path)
+
+Copies $source_path to $dest_path, with diagnostic.
+
+=item run_create_archive($archive_path,$work_dir, at base_names)
+
+Creates a TAR-GZIP archive at $archive_path using $work_dir as the working
+directory and @base_names as members of the archive. Depends on GNU "tar" or
+compatible.
+
+=item run_extract_archive($archive_path,$work_dir)
+
+Extracts a TAR-GZIP archive at $archive_path using $work_dir as the working
+directory. Depends on GNU "tar" or compatible.
+
+=item run_mkpath($path)
+
+Creates $path if it does not already exist, with diagnostic.
+
+=item run_rename($source_path,$dest_path)
+
+Same as the core I<rename>, but with diagnostic.
+
+=item run_rmtree($path)
+
+Removes $path, with diagnostic.
+
+=item run_rsync(\@sources,$dest_path,$option_list_ref)
+
+Invokes the "rsync" shell command with diagnostics to mirror the paths in
+ at sources to $dest_path. Command line options can be specified in a list with
+$option_list_ref. Depends on "rsync".
+
+=item run_svn_info($path)
+
+Wrapper of the info() method of L<SVN::Client|SVN::Client>. Expects $path to be
+a Subversion working copy. Returns the C<svn_info_t> object as described by the
+info() method of L<SVN::Client|SVN::Client>.
+
+=item run_svn_update($path)
+
+Wrapper of the update() method of L<SVN::Client|SVN::Client>. Expects $path to be
+a Subversion working copy. Returns a list of updated paths.
+
+=item run_symlink($source_path,$dest_path)
+
+Same as the core I<symlink>, but with diagnostic.
+
+=item sed_file($path,$sub_ref)
+
+For each $line in $path, runs $line = $sub_ref->($line). Writes results back to
+$path.
+
+=item write_file($path,$content)
+
+Writes $content to $path.
+
+=back
+
+=head1 SEE ALSO
+
+L<FCM::Admin::Config|FCM::Admin::Config>,
+L<FCM::Admin::Runner|FCM::Admin::Runner>,
+L<SVN::Client|SVN::Client>
+
+=head1 COPYRIGHT
+
+E<169> Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/CLI.pm b/lib/FCM/CLI.pm
new file mode 100644
index 0000000..c4fd6fd
--- /dev/null
+++ b/lib/FCM/CLI.pm
@@ -0,0 +1,291 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+
+# ------------------------------------------------------------------------------
+package FCM::CLI;
+use base qw{FCM::Class::CODE};
+
+use FCM::CLI::Exception;
+use FCM::CLI::Parser;
+use FCM::Context::Event;
+use FCM::System;
+use FindBin;
+use File::Basename qw{dirname};
+use File::Spec::Functions qw{catfile rel2abs};
+use Pod::Usage qw{pod2usage};
+
+my $E = 'FCM::CLI::Exception';
+our $EVENT;
+our $S;
+our %ACTION_OF = (
+ # Commands handled by FCM
+ 'add' => _opt_func('check', sub {$S->cm_check_unknown(@_)}),
+ 'branch' => \&_branch,
+ 'branch-create' => _sys_func(sub {$S->cm_branch_create(@_)}),
+ 'branch-delete' => _sys_func(sub {$S->cm_branch_delete(@_)}),
+ 'branch-diff' => _sys_func(sub {$S->cm_branch_diff(@_)}),
+ 'branch-info' => _sys_func(sub {$S->cm_branch_info(@_)}),
+ 'branch-list' => _sys_func(sub {$S->cm_branch_list(@_)}),
+ 'browse' => _sys_func(sub {$S->browse(@_)}),
+ 'build' => _sys_func(sub {$S->build(@_)}),
+ 'cfg-print' => _sys_func(sub {$S->config_parse(@_)}),
+ 'checkout' => _sys_func(sub {$S->cm_checkout(@_)}),
+ 'cmp-ext-cfg' => _sys_func(sub {$S->config_compare(@_)}),
+ 'commit' => _sys_func(sub {$S->cm_commit(@_)}),
+ 'conflicts' => _sys_func(sub {$S->cm_resolve_conflicts(@_)}),
+ 'delete' => _opt_func('check', sub {$S->cm_check_missing(@_)}),
+ 'diff' => _opt_func(
+ 'branch', sub {$S->cm_branch_diff(@_)}, sub {$S->cm_diff(@_)},
+ ),
+ 'export-items' => _sys_func(sub {$S->export_items(@_)}),
+ 'extract' => _sys_func(sub {$S->extract(@_)}),
+ 'gui' => \&_gui,
+ 'help' => \&_help,
+ 'keyword-print' => _sys_func(sub {$S->keyword_find(@_)}),
+ 'loc-layout' => _sys_func(sub {$S->cm_loc_layout(@_)}),
+ 'merge' => _sys_func(sub {$S->cm_merge(@_)}),
+ 'mkpatch' => _sys_func(sub {$S->cm_mkpatch(@_)}),
+ 'make' => _sys_func(sub {$S->make(@_)}),
+ 'project-create'=> _sys_func(sub {$S->cm_project_create(@_)}),
+ 'switch' => _opt_func(
+ 'relocate', sub {$S->svn(@_)}, sub {$S->cm_switch(@_)},
+ ),
+ 'test-battery' => \&_test_battery,
+ 'update' => _sys_func(sub {$S->cm_update(@_)}),
+ 'version' => _sys_func(sub {$S->version(@_)}),
+ # Commands passed directly to "svn"
+ map {($_ => _sys_func())} qw{
+ blame
+ cat
+ cleanup
+ copy
+ export
+ import
+ info
+ list
+ lock
+ log
+ mergeinfo
+ mkdir
+ move
+ patch
+ propdel
+ propedit
+ propget
+ proplist
+ propset
+ relocate
+ resolve
+ resolved
+ revert
+ status
+ unlock
+ upgrade
+ },
+);
+# List of overridden subcommands that need to display "svn help"
+our %CLI_MORE_HELP_FOR = map {($_, 1)} (qw{add delete diff switch update});
+
+# Creates the class.
+__PACKAGE__->class(
+ {'gui' => '$', 'parser' => 'FCM::CLI::Parser', 'system' => 'FCM::System'},
+ { init => sub {
+ my $attrib_ref = shift();
+ $attrib_ref->{parser} ||= FCM::CLI::Parser->new();
+ $attrib_ref->{system}
+ ||= FCM::System->new({'gui' => $attrib_ref->{'gui'}});
+ },
+ action_of => {main => \&_main},
+ },
+);
+
+# The main CLI action.
+sub _main {
+ my ($attrib_ref, @argv) = @_;
+ local($EVENT) = sub {$attrib_ref->{system}->util()->event(@_)};
+ my ($app, $option_ref, @args) = eval {$attrib_ref->{parser}->parse(@argv)};
+ if (my $e = $@) {
+ _err($attrib_ref, \@argv, $e);
+ }
+ if (!$app || $option_ref->{help}) {
+ return _help($attrib_ref, $app);
+ }
+ $option_ref ||= {};
+ my $q = $option_ref->{quiet} || 0;
+ my $v = $option_ref->{verbose} || 0;
+ my $reporter = $attrib_ref->{system}->util()->util_of_report();
+ my $verbosity = $reporter->DEFAULT + $v - $q;
+ if (exists($ENV{FCM_DEBUG}) && $ENV{FCM_DEBUG} eq 'true') {
+ $verbosity = $reporter->DEBUG;
+ }
+ $reporter->get_ctx_of_stderr()->set_verbosity($verbosity);
+ $reporter->get_ctx_of_stdout()->set_verbosity($verbosity);
+ my @context = eval {
+ if (!exists($ACTION_OF{$app})) {
+ return $E->throw($E->APP, \@argv);
+ }
+ $ACTION_OF{$app}->($attrib_ref, $app, $option_ref, @args);
+ };
+ if (my $e = $@) {
+ return _err($attrib_ref, \@argv, $e);
+ }
+}
+
+# "fcm branch".
+sub _branch {
+ my ($attrib_ref, $app, $option_ref, @args) = @_;
+ my $method
+ = exists($option_ref->{create}) ? 'cm_branch_create'
+ : exists($option_ref->{delete}) ? 'cm_branch_delete'
+ : exists($option_ref->{list}) ? 'cm_branch_list'
+ : 'cm_branch_info'
+ ;
+ if ($option_ref->{create}) {
+ if (!$option_ref->{name}) {
+ return $E->throw($E->OPT, [$app, @args]);
+ }
+ my $name = delete($option_ref->{name});
+ unshift(@args, $name);
+ }
+ $attrib_ref->{system}->($method, $option_ref, @args);
+}
+
+# Handles FCM::Exception.
+sub _err {
+ my ($attrib_ref, $argv_ref, $e) = @_;
+ $EVENT->(FCM::Context::Event->E, $e) || die($e);
+ die("\n");
+}
+
+# "fcm gui".
+sub _gui {
+ my ($attrib_ref, $app, $option_ref, @args) = @_;
+ exec("$FindBin::Bin/fcm_gui", @args);
+}
+
+# Implements "fcm help" and usage.
+sub _help {
+ my ($attrib_ref, $app, $option_ref, @args) = @_;
+ $app ||= 'help';
+ my @keys = ($app eq 'help' && @args) ? @args : (q{});
+ for my $key (@keys) {
+ if (exists($FCM::CLI::Parser::PREF_NAME_OF{$key})) {
+ $key = $FCM::CLI::Parser::PREF_NAME_OF{$key};
+ }
+ my $pod
+ = $key ? catfile(dirname($INC{'FCM/CLI.pm'}), 'CLI', "fcm-$key.pod")
+ : $0
+ ;
+ if ($pod eq $0) {
+ # Read fcm-version.js file
+ my $version = $attrib_ref->{system}->util()->version();
+ my $bin = rel2abs($0);
+ $EVENT->(FCM::Context::Event->OUT, "$version ($bin)\n");
+ }
+ my $has_pod = -f $pod;
+ if ($has_pod) {
+ my $reporter = $attrib_ref->{system}->util()->util_of_report();
+ my $verbosity = $reporter->get_ctx_of_stdout()->get_verbosity();
+ pod2usage({
+ '-exitval' => 'NOEXIT',
+ '-input' => $pod,
+ '-verbose' => $verbosity,
+ });
+ }
+ if (!$has_pod || exists($CLI_MORE_HELP_FOR{$key})) {
+ $attrib_ref->{system}->svn('help', {}, $key ? $key : ())
+ }
+ }
+ return;
+}
+
+# "fcm test-battery".
+sub _test_battery {
+ my ($attrib_ref, $app, $option_ref, @args) = @_;
+ exec("$FindBin::Bin/fcm_test_battery", @args);
+}
+
+# Returns a function that select the alternate handler for the application. The
+# handler is either $method_id (if $opt_id is set) or "svn".
+sub _opt_func {
+ my ($opt_id, $code0_ref, $code1_ref) = @_;
+ $code0_ref = _sys_func($code0_ref);
+ $code1_ref = _sys_func($code1_ref);
+ sub {
+ my ($attrib_ref, $app, $option_ref, @args) = @_;
+ my $code_ref = exists($option_ref->{$opt_id}) ? $code0_ref : $code1_ref;
+ $code_ref->($attrib_ref, $app, $option_ref, @args);
+ };
+}
+
+# Invokes a system function.
+sub _sys_func {
+ my ($code_ref) = @_;
+ sub {
+ my ($attrib_ref, $app, @args) = @_;
+ local($S) = $attrib_ref->{system};
+ defined($code_ref) ? $code_ref->(@args) : $S->svn($app, @args);
+ };
+}
+
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::CLI
+
+=head1 SYNOPSIS
+
+ my $cli = FCM::CLI->new();
+ $cli->(@ARGV);
+
+=head1 DESCRIPTION
+
+An implementation of the FCM command line interface.
+
+=head1 METHODS
+
+=over 4
+
+=item $class->new()
+
+Returns a new instance.
+
+=item $cli->(@ARGV)
+
+Determines the application using the first element in @ARGV, parses the options
+and arguments according to the application, and invokes the application.
+
+=back
+
+=head1 DIAGNOSTICS
+
+=head2 FCM::CLI::Exception
+
+This exception is thrown when the CLI fails to invoke an application.
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/CLI/Exception.pm b/lib/FCM/CLI/Exception.pm
new file mode 100644
index 0000000..85631ef
--- /dev/null
+++ b/lib/FCM/CLI/Exception.pm
@@ -0,0 +1,57 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+
+# ------------------------------------------------------------------------------
+package FCM::CLI::Exception;
+use base qw{FCM::Exception};
+
+use constant {
+ APP => 'APP',
+ OPT => 'OPT',
+};
+
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::CLI::Exception
+
+=head1 SYNOPSIS
+
+ use FCM::CLI::Exception;
+ FCM::CLI::Exception->throw(FCM::CLI::Exception->APP, \@argv, $e);
+ FCM::CLI::Exception->throw(FCM::CLI::Exception->OPT, \@argv, $e);
+
+=head1 DESCRIPTION
+
+An exception associated with the FCM CLI. It is a sub-class of
+L<FCM::Exception|FCM::Exception>. The $e->get_ctx() method returns an ARRAY
+reference containing the argument list. The $e->get_code() method may return
+either $e->APP (if an unknown application is specified) or $e->OPT (if an
+unknown option is specified, i.e. the option parser returns some errors).
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/CLI/Parser.pm b/lib/FCM/CLI/Parser.pm
new file mode 100644
index 0000000..a85875a
--- /dev/null
+++ b/lib/FCM/CLI/Parser.pm
@@ -0,0 +1,459 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+
+# ------------------------------------------------------------------------------
+package FCM::CLI::Parser;
+use base qw{FCM::Class::CODE};
+
+use FCM::CLI::Exception;
+use Getopt::Long qw{GetOptions :config bundling};
+
+use constant {
+ OPT_INCR => q{+}, # no argument, but incremental
+ OPT_BOOL => q{}, # no argument
+ OPT_SCAL => q{=s}, # single argument
+ OPT_LIST => q{=s@}, # multiple argument
+};
+
+# Option hash, key = preferred name of option, value = HASH reference where:
+# arg => argument flag
+# letters => ARRAY reference of a list of option letters
+# names => ARRAY reference of a list of names
+our %OPTION_OF = map {
+ ($_->[0][0], {arg => $_->[2], letters => $_->[1], names => $_->[0]});
+} (
+ [['archive' , ], ['a'], OPT_BOOL],
+ [['auto-log' , ], [ ], OPT_BOOL],
+ [['branch' , ], ['b'], OPT_BOOL],
+ [['branch-of-branch' , ], [ ], OPT_BOOL],
+ [['browser' , ], ['b'], OPT_SCAL],
+ [['check' , ], ['c'], OPT_BOOL],
+ [['clean' , ], [ ], OPT_BOOL],
+ [['create' , ], ['c'], OPT_BOOL],
+ [['config-file' , 'file' ], ['f'], OPT_LIST],
+ [['config-file-path' , ], ['F'], OPT_LIST],
+ [['custom' , ], [ ], OPT_BOOL],
+ [['delete' , ], ['d'], OPT_BOOL],
+ [['diff-cmd' , ], [ ], OPT_SCAL],
+ [['directory' , ], ['C'], OPT_SCAL],
+ [['dry-run' , ], [ ], OPT_BOOL],
+ [['exclude' , ], [ ], OPT_LIST],
+ [['extensions' , ], ['x'], OPT_SCAL],
+ [['graphical' , ], ['g'], OPT_BOOL],
+ [['fcm1' , ], ['1'], OPT_BOOL],
+ [['full' , ], ['f'], OPT_BOOL],
+ [['help' , 'usage' ], ['h'], OPT_BOOL],
+ [['ignore-lock' , ], [ ], OPT_BOOL],
+ [['info' , ], ['i'], OPT_BOOL],
+ [['jobs' , ], ['j'], OPT_SCAL],
+ [['list' , ], ['l'], OPT_BOOL],
+ [['name' , ], ['n'], OPT_SCAL],
+ [['new' , ], ['n'], OPT_BOOL],
+ [['non-interactive' , ], [ ], OPT_BOOL],
+ [['only' , ], [ ], OPT_LIST],
+ [['organisation' , ], [ ], OPT_SCAL],
+ [['password' , ], [ ], OPT_SCAL],
+ [['quiet' , ], ['q'], OPT_INCR],
+ [['relocate' , ], [ ], OPT_BOOL],
+ [['reverse' , ], [ ], OPT_BOOL],
+ [['revision' , ], ['r'], OPT_SCAL],
+ [['rev-flag' , ], [ ], OPT_SCAL],
+ [['show-all' , ], ['a'], OPT_BOOL],
+ [['show-children' , ], [ ], OPT_BOOL],
+ [['show-other' , ], [ ], OPT_BOOL],
+ [['show-siblings' , ], [ ], OPT_BOOL],
+ [['stage' , ], ['s'], OPT_SCAL],
+ [['summarize' , 'summarise'], [ ], OPT_BOOL],
+ [['svn-non-interactive', ], [ ], OPT_BOOL],
+ [['switch' , ], ['s'], OPT_BOOL],
+ [['targets' , ], ['t'], OPT_LIST],
+ [['ticket' , ], ['k'], OPT_LIST],
+ [['trac' , ], ['t'], OPT_BOOL],
+ [['type' , ], ['t'], OPT_SCAL],
+ [['url' , ], [ ], OPT_BOOL],
+ [['user' , ], ['u'], OPT_LIST],
+ [['verbose' , ], ['v'], OPT_INCR],
+ [['verbosity' , ], ['v'], OPT_SCAL],
+ [['wiki' , ], ['w'], OPT_BOOL],
+ [['wiki-format' , 'wiki' ], ['w'], OPT_SCAL],
+ [['xml' , ], [ ], OPT_BOOL],
+);
+# Hook command before parsing the options
+our %HOOK_BEFORE_FOR = (
+ 'add' => _get_code_to_match($OPTION_OF{check}),
+ 'delete' => _get_code_to_match($OPTION_OF{check}),
+ 'diff' => sub {
+ _get_code_to_replace(
+ $OPTION_OF{graphical}, [qw{--diff-cmd fcm_graphic_diff}]
+ )->(@_);
+ _get_code_to_replace($OPTION_OF{summarize}, ['--summarize'])->(@_);
+ _get_code_to_match($OPTION_OF{branch})->(@_);
+ },
+ 'switch' => sub {!_get_code_to_match($OPTION_OF{relocate})->(@_)},
+);
+our $HELP_APP = 'help';
+# Options for known applications
+our %OPTIONS_FOR = (
+ 'add' => [$OPTION_OF{check}],
+ 'branch' => [@OPTION_OF{
+ qw{ branch-of-branch create delete info list name non-interactive
+ password quiet revision rev-flag show-all show-children
+ show-siblings svn-non-interactive ticket type user verbose
+ }
+ }],
+ 'branch-create' => [@OPTION_OF{
+ qw{ branch-of-branch non-interactive password rev-flag
+ svn-non-interactive switch ticket type
+ }
+ }],
+ 'branch-delete' => [@OPTION_OF{
+ qw{ non-interactive password quiet show-all show-children show-siblings
+ svn-non-interactive switch verbose
+ }
+ }],
+ 'branch-diff' => [@OPTION_OF{
+ qw{diff-cmd graphical extensions summarize trac wiki xml}
+ }],
+ 'branch-info' => [@OPTION_OF{
+ qw{quiet show-all show-children show-siblings verbose}
+ }],
+ 'branch-list' => [@OPTION_OF{
+ qw{only quiet show-all url user verbose}
+ }],
+ 'browse' => [$OPTION_OF{browser}],
+ 'build' => [@OPTION_OF{
+ qw{archive clean full ignore-lock jobs stage targets verbosity}
+ }],
+ 'cfg-print' => [$OPTION_OF{fcm1}],
+ 'cmp-ext-cfg' => [@OPTION_OF{qw{quiet verbose wiki-format}}],
+ 'commit' => [@OPTION_OF{
+ qw{dry-run password svn-non-interactive}
+ }],
+ 'conflicts' => [],
+ 'delete' => [$OPTION_OF{check}],
+ 'diff' => [@OPTION_OF{
+ qw{branch diff-cmd extensions summarize trac wiki}
+ }],
+ 'export-items' => [@OPTION_OF{qw{directory config-file new}}],
+ 'extract' => [@OPTION_OF{qw{clean full ignore-lock verbosity}}],
+ 'gui' => [],
+ $HELP_APP => [@OPTION_OF{qw{quiet verbose}}],
+ 'keyword-print' => [@OPTION_OF{qw{verbose}}],
+ 'loc-layout' => [@OPTION_OF{qw{verbose}}],
+ 'merge' => [@OPTION_OF{
+ qw{auto-log custom dry-run non-interactive quiet reverse revision verbose}
+ }],
+ 'mkpatch' => [@OPTION_OF{qw{exclude organisation revision}}],
+ 'make' => [@OPTION_OF{
+ qw{ directory ignore-lock jobs config-file config-file-path new quiet
+ verbose
+ }
+ }],
+ 'project-create'=> [@OPTION_OF{
+ qw{non-interactive password svn-non-interactive}
+ }],
+ 'switch' => [@OPTION_OF{qw{non-interactive revision quiet verbose}}],
+ 'update' => [@OPTION_OF{qw{non-interactive revision quiet verbose}}],
+);
+# Preferred names of known applications with aliases
+our %PREF_NAME_OF = (
+ 'ann' => 'blame',
+ 'annotate' => 'blame',
+ 'bcreate' => 'branch-create',
+ 'bc' => 'branch-create',
+ 'bdel' => 'branch-delete',
+ 'bdelete' => 'branch-delete',
+ 'bdi' => 'branch-diff',
+ 'bdiff' => 'branch-diff',
+ 'binfo' => 'branch-info',
+ 'bld' => 'build',
+ 'blist' => 'branch-list',
+ 'bls' => 'branch-list',
+ 'br' => 'branch',
+ 'brm' => 'branch-delete',
+ 'cfg' => 'cfg-print',
+ 'ci' => 'commit',
+ 'cf' => 'conflicts',
+ 'co' => 'checkout',
+ 'cp' => 'copy',
+ 'del' => 'delete',
+ 'di' => 'diff',
+ 'ext' => 'extract',
+ 'h' => $HELP_APP,
+ 'kp' => 'keyword-print',
+ 'ls' => 'list',
+ 'mv' => 'move',
+ 'pd' => 'propdel',
+ 'pdel' => 'propdel',
+ 'pe' => 'propedit',
+ 'pedit' => 'propedit',
+ 'pg' => 'propget',
+ 'pget' => 'propget',
+ 'pl' => 'proplist',
+ 'plist' => 'proplist',
+ 'praise' => 'blame',
+ 'ps' => 'propset',
+ 'pset' => 'propset',
+ 'ren' => 'move',
+ 'rename' => 'move',
+ 'rm' => 'delete',
+ 'remove' => 'delete',
+ 'st' => 'status',
+ 'sw' => 'switch',
+ 'stat' => 'status',
+ 'trac' => 'browse',
+ 'up' => 'update',
+ 'usage' => $HELP_APP,
+ 'www' => 'browse',
+ '?' => $HELP_APP,
+ '-V' => 'version',
+ '--help' => $HELP_APP,
+ '--usage' => $HELP_APP,
+ '--version'=> 'version',
+);
+
+# Creates the class.
+__PACKAGE__->class(
+ { help_app => {isa => '$', default => $HELP_APP },
+ help_option => {isa => '%', default => {%{$OPTION_OF{help}}}},
+ hook_before_for => {isa => '%', default => {%HOOK_BEFORE_FOR} },
+ options_for => {isa => '%', default => {%OPTIONS_FOR} },
+ pref_name_of => {isa => '%', default => {%PREF_NAME_OF} },
+ },
+ {action_of => {parse => \&_parse}},
+);
+
+# Parses the options and arguments.
+sub _parse {
+ my ($attrib_ref, @argv) = @_;
+ my @args = @argv;
+ my $option_hash_ref = {};
+ if (!@args) {
+ return ($attrib_ref->{help_app}, $option_hash_ref);
+ }
+ my $app = shift(@args);
+ if (exists($attrib_ref->{pref_name_of}{$app})) {
+ $app = $attrib_ref->{pref_name_of}{$app};
+ }
+ if (_get_code_to_match($attrib_ref->{help_option})->(\@args)) {
+ return ($attrib_ref->{help_app}, {}, $app);
+ }
+ if (exists($attrib_ref->{hook_before_for}{$app})) {
+ if (!$attrib_ref->{hook_before_for}{$app}->(\@args)) {
+ return ($app, $option_hash_ref, @args);
+ }
+ }
+ if (!exists($attrib_ref->{options_for}{$app})) {
+ return ($app, $option_hash_ref, @args);
+ }
+ my @option_strings = map {
+ join('|', @{$_->{names}}, @{$_->{letters}}) . $_->{arg};
+ } @{$attrib_ref->{options_for}{$app}};
+ local(@ARGV) = @args;
+ my @warnings;
+ local($SIG{__WARN__}) = sub {push(@warnings, @_)};
+ if (!GetOptions($option_hash_ref, @option_strings)) {
+ my $E = 'FCM::CLI::Exception';
+ for (@warnings) {
+ chomp();
+ }
+ return $E->throw($E->OPT, \@argv, join('|', @warnings));
+ }
+ @args = @ARGV;
+ return ($app, $option_hash_ref, @args);
+}
+
+# Returns a CODE reference for matching a simple option to a string.
+sub _get_option_matcher {
+ my ($option_ref) = @_;
+ return sub {
+ grep {$_[0] eq $_} (
+ (map {"--$_"} @{$option_ref->{names} }),
+ (map { "-$_"} @{$option_ref->{letters}}),
+ );
+ };
+}
+
+# Returns a CODE reference for matching a simple option to a string.
+sub _get_code_to_match {
+ my ($option_ref) = @_;
+ my $grepper = _get_option_matcher($option_ref);
+ return sub {grep {$grepper->($_)} @{$_[0]}};
+}
+
+# Returns a CODE reference to replace a simple option in the argument list.
+sub _get_code_to_replace {
+ my ($option_ref, $replacement) = @_;
+ my @replacements = ref($replacement) ? @{$replacement} : $replacement;
+ my $grepper = _get_option_matcher($option_ref);
+ return sub {
+ @{$_[0]} = map {($grepper->($_) ? @replacements : $_)} @{$_[0]};
+ return 1;
+ };
+}
+
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::CLI::Parser
+
+=head1 SYNOPSIS
+
+ use FCM::CLI::Parser;
+ my $cli = FCM::CLI::Parser->new(\%attrib);
+ my ($app, $opt_hash_ref, @args) = $cli->(@ARGV);
+
+=head1 DESCRIPTION
+
+This class provides an option/argument parser for the FCM command line
+interface. The parser, when called with some arguments, returns a list. The 1st
+element is the name of the application, the 2nd element is a HASH reference
+containing the option names and their values. The remaining elements are the
+remaining arguments.
+
+=head1 METHODS
+
+=over 4
+
+=item $class->new(\%attrib)
+
+Returns a new instance. The %attrib HASH may contain the following elements:
+
+=over 4
+
+=item help_app
+
+The name of the I<help> application. Default = $FCM::CLI::Parser::HELP_APP.
+
+=item help_option
+
+An option that represents I<help>. If this option is encountered in the command
+line, the CODE reference returns (help_app, {}, $app) regardless of the other
+command line options and arguments. Default =
+$FCM::CLI::Parser::OPTIONS_FOR{help}.
+
+=item hook_before_for
+
+Hook commands for the applications, which are executed before the option parser.
+See the L</CONFIGURATIONS> section for detail. Default =
+$FCM::CLI::Parser::HOOK_BEFORE_FOR.
+
+=item options_for
+
+The options for each application. See the L</CONFIGURATIONS> section for detail.
+Default = $FCM::CLI::Parser::OPTIONS_FOR.
+
+=item pref_name_of
+
+The preferred names for the applications. See the L</CONFIGURATIONS> section for
+detail. Default = $FCM::CLI::Parser::PREF_NAME_OF.
+
+=back
+
+=item $instance->(@args)
+
+=back
+
+=head1 CONFIGURATIONS
+
+The following should only be used as read-only variables. The
+$class->new(\%attrib) method should be used to configure a parser.
+
+=over 4
+
+=item $FCM::CLI::Parser::HELP_APP
+
+The name of the I<help> application.
+
+=item %FCM::CLI::Parser::HOOK_BEFORE_FOR
+
+A hash containing the hook commands, which are invoked before calling the option
+parser. The hash keys are names of the applications, and the values are CODE
+references to invoke. If a hook exists for an application, it is called as
+$hook->(\@args) where @args is the current command line arguments (with the
+first argument, i.e. the application name removed). If the hook returns a false
+value, the parser will return immediately.
+
+=item %FCM::CLI::Parser::OPTION_OF
+
+A hash containing the known options. The key is the preferred name of the
+option, and the value is a HASH reference, where C<names> (=> ARRAY reference)
+are the long names of the option, C<letters> (=> ARRAY reference) are the
+option letters, C<arg> (=> integer) is a flag. (See L</CONSTANTS> section for
+detail.)
+
+=item %FCM::CLI::Parser::OPTIONS_FOR
+
+A hash containing the known applications. The keys are the names of the
+applications and the values are ARRAY references, each pointing to
+a list of options (as described in %FCM::CLIParser::OPTION_OF) for the
+application.
+
+=item %FCM::CLI::Parser::PREF_NAME_OF
+
+A hash containing the preferred names of an application. The keys are the
+aliases and the values are the preferred names.
+
+=back
+
+=head1 CONSTANTS
+
+=over 4
+
+=item FCM::CLI::Parser->OPT_BOOL
+
+Option flag. Option is a boolean with no argument.
+
+=item FCM::CLI::Parser->OPT_INCR
+
+Option flag. Option has no argument but is incremental.
+
+=item FCM::CLI::Parser->OPT_LIST
+
+Option flag. Option has one or more arguments.
+
+=item FCM::CLI::Parser->OPT_SCAL
+
+Option flag. Option has a single argument.
+
+=back
+
+=head1 DIAGNOSTICS
+
+=over 4
+
+=item FCM::CLI::Parser::Exception
+
+This exception is raised if an invalid command option is given. It inherits from
+L<FCM::Exception>. There is no error code associated with this exception. The
+$e->get_ctx() method returns an ARRAY reference containing the original
+arguments.
+
+=back
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/CLI/fcm-add.pod b/lib/FCM/CLI/fcm-add.pod
new file mode 100644
index 0000000..22279de
--- /dev/null
+++ b/lib/FCM/CLI/fcm-add.pod
@@ -0,0 +1,22 @@
+=head1 NAME
+
+fcm add
+
+=head1 SYNOPSIS
+
+ 1. fcm add --check [PATH ...]
+
+=head1 OPTIONS
+
+=over 4
+
+=item --check, -c
+
+Check for any files or directories reported by "L<svn|svn> status" as not under
+version control and add them.
+
+=back
+
+For other options, see output of "L<svn|svn> help add".
+
+=cut
diff --git a/lib/FCM/CLI/fcm-branch-create.pod b/lib/FCM/CLI/fcm-branch-create.pod
new file mode 100644
index 0000000..5cd802a
--- /dev/null
+++ b/lib/FCM/CLI/fcm-branch-create.pod
@@ -0,0 +1,86 @@
+=head1 NAME
+
+fcm branch-create (bcreate, bc)
+
+=head1 SYNOPSIS
+
+Creates a branch.
+
+ fcm branch-create [OPTIONS] NAME [SOURCE]
+
+=head1 ARGUMENTS
+
+NAME is a short name for the branch, which should contain only characters in the
+set [A-Za-z0-9_\-\.]. If the --ticket=N option is not specified and NAME
+contains only a list of positive integers separated by [_-] (an underscore or a
+hyphen), the command will assume that NAME also specifies the related ticket
+numbers.
+
+SOURCE can be an URL or a Subversion working copy. Otherwise, the current
+working directory must be a working copy. The specified URL (or the URL of the
+working copy) must be URL under a standard FCM project.
+
+=head1 OPTIONS
+
+=over 4
+
+=item --branch-of-branch
+
+If this option is specified and the SOURCE is a branch, it will create a new
+branch from the SOURCE branch. Otherwise, the branch is created from the trunk.
+
+=item --non-interactive
+
+Do no interactive prompting. This option implies --svn-non-interactive.
+
+=item --password=PASSWORD
+
+Specify a password for write access to the repository.
+
+=item --rev-flag=NONE|NORMAL|NUMBER
+
+Specify a flag for determining the prefix of the branch name. The flag can be
+the string "NORMAL", "NUMBER" or "NONE". "NORMAL" is the default behaviour,
+in which the branch name will be prefixed with a Subversion revision number if
+the revision is not associated with a registered FCM revision keyword. If the
+revision is registered with a FCM revision keyword, the keyword will be used in
+place of the number. If "NUMBER" is specified, the branch name will always be
+prefixed with a Subversion revision number. If "NONE" is specified, the branch
+name will not be prefixed by a revision number or keyword.
+
+=item --svn-non-interactive
+
+Do no interactive prompting at commit time. This option is implied by
+--non-interactive.
+
+=item --switch
+
+C<fcm switch> the current working directory (if it contains a relevant working
+copy) to point to the newly created branch after the branch is created.
+
+=item --ticket=N, -k N
+
+Specify one (or more) Trac ticket. If specified, the ticket numbers will be
+included in the commit log message. Multiple tickets can be set by specifying
+this option multiple times, or by specifying the tickets in a comma-separated
+list.
+
+=item --type=TYPE, -t TYPE
+
+Specify the type of the branch to be created. It must be one of the following:
+
+ DEV::USER [DEV, USER] - a development branch for the user
+ DEV::SHARE [SHARE] - a shared development branch
+ TEST::USER [TEST] - a test branch for the user
+ TEST::SHARE - a shared test branch
+ PKG::USER [PKG] - a package branch for the user
+ PKG::SHARE - a shared package branch
+ PKG::CONFIG [CONFIG] - a configuration branch
+ PKG::REL [REL] - a release branch
+
+If not specified, the default is to create a development branch for the current
+user, i.e. DEV::USER.
+
+=back
+
+=cut
diff --git a/lib/FCM/CLI/fcm-branch-delete.pod b/lib/FCM/CLI/fcm-branch-delete.pod
new file mode 100644
index 0000000..b9c8ba1
--- /dev/null
+++ b/lib/FCM/CLI/fcm-branch-delete.pod
@@ -0,0 +1,61 @@
+=head1 NAME
+
+fcm branch-delete (bdelete bdel brm)
+
+=head1 SYNOPSIS
+
+Deletes a branch.
+
+ fcm branch-delete [OPTIONS] [TARGET]
+
+=head1 ARGUMENTS
+
+TARGET can be an URL or a Subversion working copy. Otherwise, the current
+working directory must be a working copy. The specified URL (or the URL of the
+working copy) must be a URL under a valid branch in a standard FCM project.
+
+=head1 OPTIONS
+
+=over 4
+
+=item --verbose, -v
+
+Print extra information.
+
+=item --show-all, -a
+
+Set --show-children, --show-other and --show-siblings.
+
+=item --show-children
+
+Report children of the current branch.
+
+=item --show-other
+
+Report custom/ reverse merges into the current branch.
+
+=item --show-siblings
+
+Report merges with siblings of the current branch.
+
+=item --non-interactive
+
+Do no interactive prompting. This option implies --svn-non-interactive.
+
+=item --password=PASSWORD
+
+Specify a password for write access to the repository.
+
+=item --svn-non-interactive
+
+Do no interactive prompting at commit time. This option is implied by
+--non-interactive.
+
+=item --switch
+
+If TARGET not specified in the argument list, C<fcm switch> the current working
+copy to point to the C<trunk> after the branch deletion.
+
+=back
+
+=cut
diff --git a/lib/FCM/CLI/fcm-branch-diff.pod b/lib/FCM/CLI/fcm-branch-diff.pod
new file mode 100644
index 0000000..5c173f7
--- /dev/null
+++ b/lib/FCM/CLI/fcm-branch-diff.pod
@@ -0,0 +1,52 @@
+=head1 NAME
+
+fcm branch-diff (bdiff, bdi)
+
+=head1 SYNOPSIS
+
+Show differences relative to the base of the target branch, i.e. the changes
+available for merging from the target branch into its parent.
+
+ fcm branch-diff [OPTIONS] [TARGET]
+
+=head1 ARGUMENTS
+
+If TARGET is specified, it must either be a URL or a working copy. Otherwise,
+the target is the current directory which must be a working copy. The target
+URL must be a branch in a standard FCM project.
+
+=head1 OPTIONS
+
+=over 4
+
+=item --diff-cmd=PATH
+
+Use PATH as I<diff> command.
+
+=item --graphical, -g
+
+Equivalent to C<--diff-cmd=fcm_graphic_diff>.
+
+=item --summarize, --summarise
+
+Show a summary of the results.
+
+=item --xml
+
+With --summarise, show a summary of the results in XML format.
+
+=item --trac, -t
+
+If TARGET is a URL, use Trac to display the diff.
+
+=item --wiki
+
+If TARGET is a URL, print Trac link for the diff.
+
+=item --extensions=ARG, -x ARG
+
+Options of the I<diff> command. See the help for "L<svn|svn> diff".
+
+=back
+
+=cut
diff --git a/lib/FCM/CLI/fcm-branch-info.pod b/lib/FCM/CLI/fcm-branch-info.pod
new file mode 100644
index 0000000..b28c211
--- /dev/null
+++ b/lib/FCM/CLI/fcm-branch-info.pod
@@ -0,0 +1,43 @@
+=head1 NAME
+
+fcm branch-info (binfo)
+
+=head1 SYNOPSIS
+
+Displays information of a branch.
+
+ fcm branch-info [OPTIONS] [TARGET]
+
+=head1 ARGUMENTS
+
+TARGET can be an URL or a Subversion working copy. Otherwise, the current
+working directory must be a working copy. The specified URL (or the URL of the
+working copy) must be a URL under a valid branch in a standard FCM project.
+
+=head1 OPTIONS
+
+=over 4
+
+=item --verbose, -v
+
+Print extra information.
+
+=item --show-all, -a
+
+Set --show-children, --show-other and --show-siblings.
+
+=item --show-children
+
+Report children of the current branch.
+
+=item --show-other
+
+Report custom/ reverse merges into the current branch.
+
+=item --show-siblings
+
+Report merges with siblings of the current branch.
+
+=back
+
+=cut
diff --git a/lib/FCM/CLI/fcm-branch-list.pod b/lib/FCM/CLI/fcm-branch-list.pod
new file mode 100644
index 0000000..267c1c1
--- /dev/null
+++ b/lib/FCM/CLI/fcm-branch-list.pod
@@ -0,0 +1,52 @@
+=head1 NAME
+
+fcm branch-list (blist, bls)
+
+=head1 SYNOPSIS
+
+Searches and lists branches in projects. By default, it lists only branches
+created by the current user.
+
+ fcm branch-list [OPTIONS] [TARGET ...]
+
+=head1 ARGUMENTS
+
+If no TARGET is specified, the current working directory is assumed to be the
+target. Each target must either be a URL[@REV] or a PATH[@REV] to a working copy
+of a standard FCM project.
+
+=head1 OPTIONS
+
+=over 4
+
+=item --only=DEPTH:PATTERN, ...
+
+Specify a regular expression to match at various depth. E.g. with the normal
+FCM branch naming convention, C<--only=1:dev --only=2:fred> will display only
+the development branches owned by user ID "fred". (This option is cumalative,
+and overrides the --show-all and --user=PATTERN options.)
+
+=item --quiet, -q
+
+Decreases verbosity. Only prints branches matching the search criteria.
+
+=item --show-all, -a
+
+Print branches of all users. (This option overrides the --user=USER option.)
+
+=item --url
+
+Displays Subversion URL instead of FCM location keywords.
+
+=item --user=PATTERN, -u PATTERN
+
+Equivalent to --only=2:^PATTERN$ for projects with the normal FCM branch naming
+convention. Lists branches created by the specified list of users instead of the
+current user. With the normal FCM branch naming convention, you can also list
+shared branches by specifying the user as "Share", configuration branches by
+specifying the user as "Config" and release branches by specifying the user as
+"Rel". (This option is cumalative.)
+
+=back
+
+=cut
diff --git a/lib/FCM/CLI/fcm-branch.pod b/lib/FCM/CLI/fcm-branch.pod
new file mode 100644
index 0000000..52d484e
--- /dev/null
+++ b/lib/FCM/CLI/fcm-branch.pod
@@ -0,0 +1,37 @@
+=head1 NAME
+
+fcm branch (br)
+
+=head1 SYNOPSIS
+
+ 1. fcm branch --create --name NAME [OPTIONS] [SOURCE]
+ fcm branch -c -n NAME [OPTIONS] [SOURCE]
+ 2. fcm branch --delete [OPTIONS] [TARGET]
+ fcm branch -d [OPTIONS] [TARGET]
+ 3. fcm branch --info [OPTIONS] [TARGET]
+ fcm branch [OPTIONS] [TARGET]
+ 4. fcm branch --list [OPTIONS] [TARGET]
+ fcm branch -l [OPTIONS] [TARGET]
+
+This command is deprecated. The 4 usages are replaced by:
+
+=over 4
+
+=item 1.
+
+C<fcm branch-create>: Type C<fcm help branch-create> for detail.
+
+=item 2.
+
+C<fcm branch-delete>: Type C<fcm help branch-delete> for detail.
+
+=item 3.
+
+C<fcm branch-info>: Type C<fcm help branch-info> for detail.
+
+=item 4.
+
+C<fcm branch-list>: Type C<fcm help branch-list> for detail.
+
+=back
+=cut
diff --git a/lib/FCM/CLI/fcm-browse.pod b/lib/FCM/CLI/fcm-browse.pod
new file mode 100644
index 0000000..c88c7f5
--- /dev/null
+++ b/lib/FCM/CLI/fcm-browse.pod
@@ -0,0 +1,30 @@
+=head1 NAME
+
+fcm browse (trac www)
+
+=head1 SYNOPSIS
+
+Invokes the browser for a version controlled target.
+
+ fcm browse [OPTIONS] [TARGET ...]
+
+=head1 ARGUMENTS
+
+If TARGET is specified, it must be a FCM URL keyword, a Subversion URL or the
+path to a local working copy. If not specified, the current working directory
+is assumed to be a working copy. If the --browser option is specified, the
+specified web browser command is used to launch the repository browser.
+Otherwise, it attempts to use the default browser from the configuration
+setting.
+
+=head1 OPTIONS
+
+=over 4
+
+=item --browser=COMMAND
+
+Specify the web browser command.
+
+=back
+
+=cut
diff --git a/lib/FCM/CLI/fcm-build.pod b/lib/FCM/CLI/fcm-build.pod
new file mode 100644
index 0000000..e897992
--- /dev/null
+++ b/lib/FCM/CLI/fcm-build.pod
@@ -0,0 +1,91 @@
+=head1 NAME
+
+fcm build (bld)
+
+=head1 SYNOPSIS
+
+Invokes the deprecated FCM 1 build system.
+
+ fcm build [OPTIONS] [CFGFILE]
+
+=head1 ARGUMENTS
+
+The path to a CFGFILE may be provided. Otherwise, the build system searches the
+default locations for a bld cfg file.
+
+=head1 OPTIONS
+
+If no option is specified, the options "-s 5 -t all -j 1 -v 1" are assumed.
+
+=over 4
+
+=item --archive, -a
+
+Archive sub-directories on success. If archive mode is switched on, build
+sub-directories that are only used in the build process will be archived to
+TAR-GZIP files.
+
+=item --clean
+
+Clean the destination.
+
+=item --full, -f
+
+Run in full mode. If the option for full build is specified, the
+sub-directories created by previous builds will be removed, so that the current
+build can start cleanly.
+
+=item --ignore-lock
+
+Ignore lock file. When a build is invoked, it sets up a lock file in the build
+root directory. The lock is normally removed at the end of the build. While the
+lock file is in place, the build commands invoked in the same root directory
+will fail. If you need to bypass this check for whatever reason, you can invoke
+the build system with this option.
+
+=item --jobs=N, -j N
+
+Specify the number of parallel jobs.
+
+=item --stage=N, -s N
+
+Run command up to a named stage. The stages are:
+
+=over 4
+
+=item "1", "s" or "setup"
+
+Stage 1, setup.
+
+=item "2", "pp" or "pre_process"
+
+Stage 2, pre-process.
+
+=item "3", "gd" or "generate_dependency"
+
+Stage 3, generate dependency.
+
+=item "4", "gi" or "generate_interface"
+
+Stage 4, generate Fortran 9X interface.
+
+=item "5", "m", "make"
+
+Stage 5, make.
+
+=back
+
+=item --targets=TARGETS, -t TARGETS
+
+List of build targets, delimited by (:). If specified, the default targets
+declared in the configuration file will not be used.
+
+=item --verbose=N, -v N
+
+Specify the verbosity level. If specified, the verbosity level must be an
+integer greater than 0. Verbosity level 0 is the quiet mode. Increasing the
+verbosity level will increase the amount of diagnostic output.
+
+=back
+
+=cut
diff --git a/lib/FCM/CLI/fcm-cfg-print.pod b/lib/FCM/CLI/fcm-cfg-print.pod
new file mode 100644
index 0000000..91dd4a8
--- /dev/null
+++ b/lib/FCM/CLI/fcm-cfg-print.pod
@@ -0,0 +1,23 @@
+=head1 NAME
+
+fcm cfg-print (cfg)
+
+=head1 SYNOPSIS
+
+Parses each FCM configuration file specified in the argument list, and prints
+the result to STDOUT.
+
+ fcm cfg-print [OPTIONS] [TARGET ...]
+
+=head1 OPTIONS
+
+=over 4
+
+=item --fcm1, -1
+
+If specified, targets should be in FCM 1 format. Otherwise, they should be in
+FCM 2 format.
+
+=back
+
+=cut
diff --git a/lib/FCM/CLI/fcm-changelist.pod b/lib/FCM/CLI/fcm-changelist.pod
new file mode 100644
index 0000000..7949dc8
--- /dev/null
+++ b/lib/FCM/CLI/fcm-changelist.pod
@@ -0,0 +1,9 @@
+=head1 NAME
+
+fcm changelist
+
+=head1 SYNOPSIS
+
+ svn changelist is not supported under fcm.
+
+=cut
diff --git a/lib/FCM/CLI/fcm-cmp-ext-cfg.pod b/lib/FCM/CLI/fcm-cmp-ext-cfg.pod
new file mode 100644
index 0000000..f5bb36f
--- /dev/null
+++ b/lib/FCM/CLI/fcm-cmp-ext-cfg.pod
@@ -0,0 +1,30 @@
+=head1 NAME
+
+fcm cmp-ext-cfg
+
+=head1 SYNOPSIS
+
+Compares the extract configurations of two similar FCM 1 extract configuration
+files CFGFILE1 and CFGFILE2. This application is deprecated.
+
+ fcm cmp-ext-cfg [OPTIONS] CFFILE1 CFGFILE2
+
+=head1 OPTIONS
+
+=over 4
+
+=item --verbose=N, -v N
+
+Specify the verbosity level. In normal mode with verbosity level 2 or above,
+displays the change log of each revision.
+
+=item --wiki-format=TARGET
+
+Print revision tables in wiki format. In wiki mode, print revision tables in
+wiki format. The argument to this option must be the Subversion URL or FCM URL
+keyword of a FCM project associated with the intended Trac system. The --verbose
+option has no effect in wiki mode.
+
+=back
+
+=cut
diff --git a/lib/FCM/CLI/fcm-commit.pod b/lib/FCM/CLI/fcm-commit.pod
new file mode 100644
index 0000000..d7ed2d1
--- /dev/null
+++ b/lib/FCM/CLI/fcm-commit.pod
@@ -0,0 +1,31 @@
+=head1 NAME
+
+fcm commit (ci)
+
+=head1 SYNOPSIS
+
+Send changes from your working copy to the repository. Invoke your favourite
+editor to prompt you for a commit log message. Update your working copy
+following the commit.
+
+ fcm commit [OPTIONS] [PATH ...]
+
+=head1 OPTIONS
+
+=over 4
+
+=item --dry-run
+
+Allows you to add to the commit message without committing.
+
+=item --svn-non-interactive
+
+Do no interactive prompting at commit time.
+
+=item --password=PASSWORD
+
+Specify a password ARG.
+
+=back
+
+=cut
diff --git a/lib/FCM/CLI/fcm-conflicts.pod b/lib/FCM/CLI/fcm-conflicts.pod
new file mode 100644
index 0000000..d4a3453
--- /dev/null
+++ b/lib/FCM/CLI/fcm-conflicts.pod
@@ -0,0 +1,17 @@
+=head1 NAME
+
+fcm conflicts (cf)
+
+=head1 SYNOPSIS
+
+Use graphical tool to resolve any conflicts within your working copy.
+
+ fcm conflicts [PATH]
+
+=head1 ARGUMENTS
+
+Invoke a graphical merge tool to help you resolve conflicts in your working copy
+at PATH. It prompts you to run "L<svn|svn> resolved" each time you have resolved
+the conflicts in a text file.
+
+=cut
diff --git a/lib/FCM/CLI/fcm-delete.pod b/lib/FCM/CLI/fcm-delete.pod
new file mode 100644
index 0000000..d551e67
--- /dev/null
+++ b/lib/FCM/CLI/fcm-delete.pod
@@ -0,0 +1,22 @@
+=head1 NAME
+
+fcm delete (del, remove, rm)
+
+=head1 SYNOPSIS
+
+ fcm delete [OPTIONS] [TARGETS]
+
+=head1 OPTIONS
+
+=over 4
+
+=item --check, -c
+
+Check for any files or directories reported by "L<svn|svn> status" as missing
+and schedule them for removal.
+
+=back
+
+For other options, see output of "L<svn|svn> help delete".
+
+=cut
diff --git a/lib/FCM/CLI/fcm-diff.pod b/lib/FCM/CLI/fcm-diff.pod
new file mode 100644
index 0000000..c73a00f
--- /dev/null
+++ b/lib/FCM/CLI/fcm-diff.pod
@@ -0,0 +1,45 @@
+=head1 NAME
+
+fcm diff (di)
+
+=head1 SYNOPSIS
+
+Display the differences between two revisions or paths.
+
+ 1. fcm diff --branch [OPTIONS] [TARGET]
+ fcm diff -b [TARGET]
+ 2. fcm diff [OPTIONS] [ARGS]
+
+=over 4
+
+=item 1.
+
+This usage is deprecated. It is replaced by C<fcm branch-diff>. Type
+C<fcm help branch-diff> for detail.
+
+=item 2.
+
+See the output of "L<svn|svn> help diff".
+
+=back
+
+=head1 OPTIONS
+
+The following are additional options supported by C<fcm diff>.
+
+=over 4
+
+=item --graphical, -g
+
+If this option is specified, the command uses a graphical tool to display the
+differences. This option can be used in combination with all other valid
+options except --diff-cmd and --extensions.
+
+=item --summarise
+
+This option is implemented in FCM as a wrapper to the Subversion --summarize
+option. It prints only a summary of the results.
+
+=back
+
+=cut
diff --git a/lib/FCM/CLI/fcm-export-items.pod b/lib/FCM/CLI/fcm-export-items.pod
new file mode 100644
index 0000000..0a3ffda
--- /dev/null
+++ b/lib/FCM/CLI/fcm-export-items.pod
@@ -0,0 +1,57 @@
+=head1 NAME
+
+fcm export-items
+
+=head1 SYNOPSIS
+
+Exports directories in SOURCE as a list of versioned items.
+
+This command is used to support a legacy working practice, in which directories
+in a source tree are regarded as individual versioned items.
+
+ fcm export-items [OPTIONS] SOURCE
+
+=head1 ARGUMENTS
+
+The SOURCE should be the URL of a branch in a Subversion repository with the
+standard FCM layout.
+
+=head1 OPTIONS
+
+=over 4
+
+=item --config-file=PATH, --file=PATH, -f PATH
+
+Specify the path to the configuration file.
+(default=$PWD/fcm-export-items.cfg)
+
+=item --directory=PATH, -C PATH
+
+Specify the path to the destination. (default=$PWD)
+
+=item --new
+
+Specify the new mode. In this mode, everything is re-exported. Otherwise, the
+system runs in incremental mode, in which the version directories are only
+updated if they do not already exist.
+
+=back
+
+=head1 CONFIGURATION
+
+The configuration file should be in the deprecated FCM 1 configuration format.
+The label in each entry should be a path relative to the source URL. If the path
+ends in * then the path is expanded recursively and any sub-directories
+containing regular files are added to the list of relative paths to export. The
+value may be empty, or it may be a list of space separated "conditions". Each
+condition is a conditional operator (>, >=, <, <=, == or !=) followed by a
+revision number. The command uses the revision log to determine the revisions at
+which the relative path has been updated in the source URL. If these revisions
+also satisfy the "conditions" set by the user, they will be considered in the
+export.
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/CLI/fcm-extract.pod b/lib/FCM/CLI/fcm-extract.pod
new file mode 100644
index 0000000..3ccac10
--- /dev/null
+++ b/lib/FCM/CLI/fcm-extract.pod
@@ -0,0 +1,46 @@
+=head1 NAME
+
+fcm extract (ext)
+
+=head1 SYNOPSIS
+
+Invokes the deprecated FCM 1 extract system.
+
+ fcm extract [OPTIONS] [CFGFILE]
+
+=head1 ARGUMENTS
+
+The path to a CFG file may be provided. Otherwise, the extract system searches
+the default locations for an ext cfg file.
+
+=head1 OPTIONS
+
+=over 4
+
+=item --clean
+
+Clean the destination.
+
+=item --full, -f
+
+Run in full mode. If the option for full extract is specified, the
+sub-directories created by previous extracts will be removed, so that the
+current extract can start cleanly.
+
+=item --ignore-lock
+
+Ignore lock file. When an extract is invoked, it sets up a lock file in the
+extract root directory. The lock is normally removed at the end of the extract.
+While the lock file is in place, the extract commands invoked in the same root
+directory will fail. If you need to bypass this check for whatever reason, you
+can invoke the extract system with this option.
+
+=item --verbose=N, -v N
+
+Specify the verbosity level. If specified, the verbosity level must be an
+integer greater than 0. Verbosity level 0 is the quiet mode. Increasing the
+verbosity level will increase the amount of diagnostic output.
+
+=back
+
+=cut
diff --git a/lib/FCM/CLI/fcm-gui.pod b/lib/FCM/CLI/fcm-gui.pod
new file mode 100644
index 0000000..30496a2
--- /dev/null
+++ b/lib/FCM/CLI/fcm-gui.pod
@@ -0,0 +1,12 @@
+=head1 NAME
+
+fcm gui
+
+=head1 SYNOPSIS
+
+Invokes the GUI wrapper for code management commands. The optional argument PATH
+modifies the initial working directory of the GUI.
+
+ fcm gui [PATH]
+
+=cut
diff --git a/lib/FCM/CLI/fcm-help.pod b/lib/FCM/CLI/fcm-help.pod
new file mode 100644
index 0000000..428cab7
--- /dev/null
+++ b/lib/FCM/CLI/fcm-help.pod
@@ -0,0 +1,25 @@
+=head1 NAME
+
+fcm help (h, usage, --help, --usage)
+
+=head1 SYNOPSIS
+
+Describes the usage of this program or its subcommands.
+
+ fcm help [OPTIONS] [SUBCOMMAND...]
+
+=head1 OPTIONS
+
+=over 4
+
+=item --quiet, -q
+
+Decrease the verbosity level.
+
+=item --verbose, -v
+
+Increase the verbosity level.
+
+=back
+
+=cut
diff --git a/lib/FCM/CLI/fcm-keyword-print.pod b/lib/FCM/CLI/fcm-keyword-print.pod
new file mode 100644
index 0000000..d0a8c9c
--- /dev/null
+++ b/lib/FCM/CLI/fcm-keyword-print.pod
@@ -0,0 +1,27 @@
+=head1 NAME
+
+fcm keyword-print (kp)
+
+=head1 SYNOPSIS
+
+Displays registered location and/or revision keywords.
+
+ fcm keyword-print [OPTIONS] [TARGET ...]
+
+=head1 ARGUMENTS
+
+If no argument is specified, prints registered location keywords. Otherwise,
+prints the location keyword and revision keywords for the specified target. If
+--verbose is specified, prints the implied location keywords as well.
+
+=head1 OPTIONS
+
+=over 4
+
+=item --verbose, -v
+
+Increase the verbosity level.
+
+=back
+
+=cut
diff --git a/lib/FCM/CLI/fcm-loc-layout.pod b/lib/FCM/CLI/fcm-loc-layout.pod
new file mode 100644
index 0000000..730d18b
--- /dev/null
+++ b/lib/FCM/CLI/fcm-loc-layout.pod
@@ -0,0 +1,25 @@
+=head1 NAME
+
+fcm loc-layout
+
+=head1 SYNOPSIS
+
+Parse the URL of a FCM/Subversion TARGET, and print its FCM layout information.
+
+ fcm keyword-print [OPTIONS] [TARGET ...]
+
+=head1 ARGUMENTS
+
+If no argument is specified, TARGET is the current working directory.
+
+=head1 OPTIONS
+
+=over 4
+
+=item --verbose, -v
+
+Increase the verbosity level.
+
+=back
+
+=cut
diff --git a/lib/FCM/CLI/fcm-make.pod b/lib/FCM/CLI/fcm-make.pod
new file mode 100644
index 0000000..fbf6a48
--- /dev/null
+++ b/lib/FCM/CLI/fcm-make.pod
@@ -0,0 +1,60 @@
+=head1 NAME
+
+fcm make
+
+=head1 SYNOPSIS
+
+Invokes the FCM make system. See the user guide for detail.
+
+ fcm make [OPTIONS] [DECLARATION ...]
+
+=head1 ARGUMENTS
+
+Each argument is considered to be a declaration line to append to the
+configuration file.
+
+=head1 OPTIONS
+
+=over 4
+
+=item --config-file-path=PATH, -F PATH
+
+Specify paths for searching configuration files specified in relative paths.
+
+=item --config-file=PATH, --file=PATH, -f PATH
+
+Specify paths to the configuration files. (default = fcm-make.cfg in the
+current working directory)
+
+=item --directory=PATH, -C PATH
+
+Specify the path to the destination. (default = $PWD or whatever is specified
+in the "dest" setting in the configuration file)
+
+=item --ignore-lock
+
+Ignore lock file. When the system is invoked, it sets up a lock file in the
+destination. The lock is normally removed when the system completes the make.
+While the lock file is in place, another make invoked in the same destination
+will fail. This option can be used to bypass this check.
+
+=item --jobs=N, -j N
+
+Specify the number of (child) processes that can be run simultaneously.
+
+=item --new
+
+Remove items in the destination created by the previous make, and starts a new
+make.
+
+=item --quiet, -q
+
+Decrease the verbosity level.
+
+=item --verbose, -v
+
+Increase the verbosity level.
+
+=back
+
+=cut
diff --git a/lib/FCM/CLI/fcm-merge.pod b/lib/FCM/CLI/fcm-merge.pod
new file mode 100644
index 0000000..d49cc58
--- /dev/null
+++ b/lib/FCM/CLI/fcm-merge.pod
@@ -0,0 +1,76 @@
+=head1 NAME
+
+fcm merge
+
+=head1 SYNOPSIS
+
+Merge changes from a source into your working copy.
+
+ 1. fcm merge SOURCE
+ 2. fcm merge --custom --revision N[:M] SOURCE
+ fcm merge --custom URL[\@REV1] URL[\@REV2]
+ 3. fcm merge --reverse --revision [M:]N
+
+=over 4
+
+=item 1.
+
+If neither --custom nor --reverse is specified, the command merges changes
+automatically from SOURCE into your working copy. SOURCE must be a valid
+URL[@REV] of a branch in a standard FCM project. The base of the merge will be
+calculated automatically based on the common ancestor and latest merge
+information between the SOURCE and the branch of the working copy.
+
+=item 2.
+
+If --custom is specified, the command can be used in two forms.
+
+In the first form, it performs a custom merge from the specified changeset(s) of
+SOURCE into your working copy. SOURCE must be a valid URL[@REV] of a branch in
+a standard FCM project. If a single revision is specified, the merge delta is (N
+- 1):N of SOURCE. Otherwise, the merge delta, is N:M of SOURCE, where N < M.
+
+In the second form, it performs a custom merge using the delta between the two
+specified branch URLs. For each URL, if a peg revision is not specified, the
+command will peg the URL with its last changed revision.
+
+=item 3.
+
+If --reverse is specified, the command performs a reverse merge of the
+changeset(s) specified by the --revision option. If a single revision is
+specified, the merge delta is N:(N - 1). Otherwise, the merge delta is M:N,
+where M > N. Note that you do not have to specify a SOURCE for a reverse merge,
+because the SOURCE should always be the branch your working copy is pointing to.
+
+=back
+
+The command provide a commit log message template following the merge.
+
+=head1 OPTIONS
+
+=over 4
+
+=item --auto-log
+
+In automatic mode, adds the log messages of the merged revisions in the commit
+log. Has no effect in other merge modes.
+
+=item --dry-run
+
+Try operation but make no changes.
+
+=item --non-interactive
+
+Do no interactive prompting.
+
+=item --revision=REV, -r REV
+
+Specify a (range of) revision number(s).
+
+=item --verbose, -v
+
+Print extra information.
+
+=back
+
+=cut
diff --git a/lib/FCM/CLI/fcm-mkpatch.pod b/lib/FCM/CLI/fcm-mkpatch.pod
new file mode 100644
index 0000000..3d8c0f2
--- /dev/null
+++ b/lib/FCM/CLI/fcm-mkpatch.pod
@@ -0,0 +1,61 @@
+=head1 NAME
+
+fcm mkpatch
+
+=head1 SYNOPSIS
+
+Create patches from specified revisions of a URL
+
+ fcm mkpatch [OPTIONS] URL [OUTDIR]
+
+=head1 ARGUMENTS
+
+If OUTDIR is specified, the output is sent to OUTDIR. Otherwise, the output will
+be sent to a default location in the current directory ($PWD/fcm-mkpatch-out).
+The output directory will contain the patch for each revision as well as a
+script for importing the patch.
+
+A warning is given if the URL is not of a branch in a FCM project or if it is a
+sub-directory of a branch.
+
+=head1 OPTIONS
+
+=over 4
+
+=item --exclude arg
+
+Exclude a path in the URL. Multiple paths can be specified by using a
+colon-separated list of paths, or by specifying this option multiple times.
+
+The specified path must be a relative path of the URL. Glob patterns such as *
+and ? are acceptable. Changes in an excluded path will not be considered in the
+patch. A changeset containing changes only in the excluded path will not be
+considered at all.
+
+=item --organisation arg
+
+This option can be used to specify the name of your organisation.
+
+The command will attempt to parse the commit log message for each revision in
+the patch. It will remove all merge templates, replace Trac links with a
+modified string, and add information about the original changeset. If you
+specify the name of your organisation, it will replace Trac links such as
+"ticket:123" with "$organisation_ticket:123", and report the original changeset
+with a message such as "$organisation_changeset:1000". If the organisation
+name is not specified then it defaults to "original".
+
+=item --revision=arg, -r arg
+
+Specify a revision number or a revision number range.
+
+If a revision is specified with the --revision option, it will attempt to create
+a patch based on the changes at that revision. If a revision is not specified,
+it will attempt to create a patch based on the changes at the HEAD revision. If
+a revision range is specified, it will attempt to create a patch for each
+revision in that range (including the change in the lower range) where changes
+have taken place in the URL. No output will be written if there is no change in
+the given revision (range).
+
+=back
+
+=cut
diff --git a/lib/FCM/CLI/fcm-project-create.pod b/lib/FCM/CLI/fcm-project-create.pod
new file mode 100644
index 0000000..3fa71e5
--- /dev/null
+++ b/lib/FCM/CLI/fcm-project-create.pod
@@ -0,0 +1,34 @@
+=head1 NAME
+
+fcm project-create
+
+=head1 SYNOPSIS
+
+Creates a new project in a repository.
+
+ fcm project-create [OPTIONS] PROJECT-NAME REPOS-ROOT-URL
+
+=head1 ARGUMENTS
+
+PROJECT-NAME is a valid name of a project for the given repository.
+
+=head1 OPTIONS
+
+=over 4
+
+=item --non-interactive
+
+Do no interactive prompting. This option implies --svn-non-interactive.
+
+=item --password=PASSWORD
+
+Specify a password for write access to the repository.
+
+=item --svn-non-interactive
+
+Do no interactive prompting at commit time. This option is implied by
+--non-interactive.
+
+=back
+
+=cut
diff --git a/lib/FCM/CLI/fcm-switch.pod b/lib/FCM/CLI/fcm-switch.pod
new file mode 100644
index 0000000..f4a2c20
--- /dev/null
+++ b/lib/FCM/CLI/fcm-switch.pod
@@ -0,0 +1,16 @@
+=head1 NAME
+
+fcm switch (sw)
+
+=head1 SYNOPSIS
+
+Update the working copy to a different URL.
+
+ 1. fcm switch URL[@REV1] [PATH]
+ 2. fcm switch --relocate FROM TO [PATH...]
+
+Note: if --relocate is not specified, "fcm switch" will only support the options
+--non-interactive, --revision (-r) and --quiet (-q). For detail, see the output
+of "L<svn|svn> help switch".
+
+=cut
diff --git a/lib/FCM/CLI/fcm-test-battery.pod b/lib/FCM/CLI/fcm-test-battery.pod
new file mode 100644
index 0000000..ce7ae8f
--- /dev/null
+++ b/lib/FCM/CLI/fcm-test-battery.pod
@@ -0,0 +1,25 @@
+=head1 NAME
+
+fcm test-battery (self-tests)
+
+=head1 SYNOPSIS
+
+Invokes the FCM self-tests.
+
+ fcm test-battery [PROVE_ARGS]
+
+=over 4
+
+=item 1. If an environment TEST_REMOTE_HOST is set, run CM tests using an
+ auto-generated Subversion server on $TEST_REMOTE_HOST.
+
+=item 2. If an environment variable TEST_PROJECT is set, run CM tests using a
+ project sub-hierarchy in the test repositories.
+
+=head1 ARGUMENTS
+
+PROVE_ARGS specifies override arguments for the 'prove' command. This is run
+from within the t/ subdirectory at the top of FCM's source tree.
+If not specified, the prove command will be run as 'prove -j 9 -r -s -f'.
+
+=cut
diff --git a/lib/FCM/CLI/fcm-update.pod b/lib/FCM/CLI/fcm-update.pod
new file mode 100644
index 0000000..39672cb
--- /dev/null
+++ b/lib/FCM/CLI/fcm-update.pod
@@ -0,0 +1,14 @@
+=head1 NAME
+
+fcm update (up)
+
+=head1 SYNOPSIS
+
+Bring changes from the repository into the working copy.
+
+ fcm update [PATH...]
+
+Note: "fcm update" only supports --non-interactive, --revision=REV (-r REV) and
+--quiet (-q). For detail, see the output of "L<svn|svn> help update".
+
+=cut
diff --git a/lib/FCM/CLI/fcm-version.pod b/lib/FCM/CLI/fcm-version.pod
new file mode 100644
index 0000000..1550ea8
--- /dev/null
+++ b/lib/FCM/CLI/fcm-version.pod
@@ -0,0 +1,11 @@
+=head1 NAME
+
+fcm version (--version, -V)
+
+=head1 SYNOPSIS
+
+Print FCM version.
+
+ fcm version
+
+=cut
diff --git a/lib/FCM/Class/CODE.pm b/lib/FCM/Class/CODE.pm
new file mode 100644
index 0000000..fda31dd
--- /dev/null
+++ b/lib/FCM/Class/CODE.pm
@@ -0,0 +1,307 @@
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+use strict;
+use warnings;
+
+#-------------------------------------------------------------------------------
+package FCM::Class::CODE;
+use FCM::Class::Exception;
+use Scalar::Util qw{reftype};
+
+# Methods for working out the default value of an attribute.
+my %ATTRIB_DEFAULT_BY = (
+ default => sub {
+ my $opt_ref = shift();
+ my $ret = $opt_ref->{default};
+ return (ref($ret) && reftype($ret) eq 'CODE' ? $ret->() : $ret);
+ },
+ isa => sub {
+ my $opt_ref = shift();
+ return
+ $opt_ref->{isa} eq 'ARRAY' ? []
+ : $opt_ref->{isa} eq 'HASH' ? {}
+ : $opt_ref->{isa} eq 'CODE' ? sub {}
+ : undef
+ ;
+ },
+);
+
+# Checks the value of an attribute.
+my $ATTRIB_CHECK = sub {
+ my ($class, $opt_ref, $key, $value, $caller_ref) = @_;
+ # Note: undef is always OK?
+ if (!defined($value)) {
+ return;
+ }
+ my $expected_isa = $opt_ref->{isa};
+ if (!$expected_isa || $expected_isa eq 'SCALAR' && !ref($value)) {
+ return;
+ }
+ if (!UNIVERSAL::isa($value, $expected_isa)) {
+ return FCM::Class::Exception->throw({
+ 'code' => FCM::Class::Exception->CODE_TYPE,
+ 'caller' => $caller_ref,
+ 'package' => $class,
+ 'key' => $key,
+ 'type' => $expected_isa,
+ 'value' => $value,
+ });
+ }
+};
+
+# Creates the methods of the class.
+sub class {
+ my ($class, $attrib_opt_ref, $class_opt_ref) = @_;
+ my %class_opt = (
+ init => sub {},
+ init_attrib => sub {@_},
+ action_of => {},
+ (defined($class_opt_ref) ? %{$class_opt_ref} : ()),
+ );
+ if (!defined($attrib_opt_ref)) {
+ $attrib_opt_ref = {};
+ }
+ my %attrib_opt;
+ while (my ($key, $item) = each(%{$attrib_opt_ref})) {
+ my %option = (
+ r => 1, # readable?
+ w => 1, # writable?
+ default => undef, # default value or CODE to return it
+ isa => undef, # attribute isa
+ ( defined($item) && ref($item) ? %{$item}
+ : defined($item) ? (isa => $item)
+ : ()
+ ),
+ );
+ if (defined($option{isa})) {
+ $option{isa}
+ = $option{isa} eq '$' ? 'SCALAR'
+ : $option{isa} eq '@' ? 'ARRAY'
+ : $option{isa} eq '%' ? 'HASH'
+ : $option{isa} eq '&' ? 'CODE'
+ : $option{isa} eq '*' ? 'GLOB'
+ : $option{isa}
+ ;
+ }
+ $attrib_opt{$key} = \%option;
+ }
+ my $main_ref = sub {
+ my ($attrib_ref, $key, @args) = @_;
+ if (!exists($class_opt{action_of}{$key})) {
+ return;
+ }
+ $class_opt{action_of}{$key}->($attrib_ref, @args);
+ };
+ no strict qw{refs};
+ # $class->new(\%attrib)
+ *{$class . '::new'} = sub {
+ my $class = shift();
+ my ($attrib_ref) = $class_opt{init_attrib}->(@_);
+ my $caller_ref = [caller()];
+ my %attrib = (defined($attrib_ref) ? %{$attrib_ref} : ());
+ while (my ($key, $value) = each(%attrib)) {
+ if (exists($attrib_opt{$key})) {
+ $ATTRIB_CHECK->(
+ $class, $attrib_opt{$key}, $key, $value, $caller_ref,
+ );
+ }
+ #else {
+ # delete($attrib{$key});
+ #}
+ }
+ my $self = bless(sub {$main_ref->(\%attrib, @_)}, $class);
+ KEY:
+ while (my ($key, $opt_ref) = each(%attrib_opt)) {
+ if (exists($attrib{$key})) {
+ next KEY;
+ }
+ for my $opt_name (qw{default isa}) {
+ if (defined($opt_ref->{$opt_name})) {
+ $attrib{$key} = $ATTRIB_DEFAULT_BY{$opt_name}->($opt_ref);
+ next KEY;
+ }
+ }
+ }
+ $class_opt{init}->(\%attrib, $self);
+ return $self;
+ };
+ # $instance->$key()
+ for my $key (keys(%{$class_opt{action_of}})) {
+ *{$class . '::' . $key}
+ = sub {my $self = shift(); $self->($key, @_)};
+ }
+ return 1;
+}
+
+#-------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::Class::CODE
+
+=head1 SYNOPSIS
+
+ # Example
+ package Bar;
+ use base qw{FCM::Class::CODE};
+ __PACKAGE__->class(
+ {
+ # ...
+ },
+ {
+ action_of => {
+ bend => sub {
+ my ($attrib_ref, @args) = @_;
+ # ...
+ },
+ stretch => sub {
+ my ($attrib_ref, @args) = @_;
+ # ...
+ },
+ },
+ },
+ );
+ # Some time later...
+ $bar = Bar->new(\%attrib);
+ $bar->bend(@args);
+ $bar->stretch(@args);
+
+=head1 DESCRIPTION
+
+Provides a simple method to create CODE-based classes.
+
+=head1 METHODS
+
+=over 4
+
+=item $class->class(\%attrib_opt,\%class_opt)
+
+Creates common methods for a CODE-based class.
+
+The %attrib_opt is used to configure the attributes of an instance of the class.
+The key of each element is the name of the attribute, and the value is a HASH
+containing the options of the attribute, or a SCALAR. (If a SCALAR is specified,
+it is equivalent to {isa => value}.) The options may contain:
+
+=over 4
+
+=item r
+
+(Default=true) If true, the attribute is readable.
+
+=item w
+
+(Default=true) If true, the attribute is writable.
+
+=item default
+
+(Default=undef) The default value of the attribute.
+
+If this option is defined, the attribute will be initialised to the specified
+value when the new() method is called. In the special case where the value of
+this option is a CODE reference, it will be invoked as $code->(\%attrib), and
+the default value will be the returned value of the CODE reference. This is
+useful, for example, if the default value needs to be a new instance of a class.
+If a genuine CODE reference is required as the default, this option should be
+set to a CODE reference that returns the required CODE reference itself.
+
+For example:
+
+ Foo->class({
+ foo => {default => 'foo'}, # 'foo'
+ bar => {default => sub {get_id()}}, # the next id
+ baz => {default => sub {\&code}}, # &code
+ });
+ {
+ my $id = 0;
+ sub get_id {$id++}
+ }
+
+If a default option is not defined, and if the attribute "isa" is ARRAY, HASH or
+CODE, then the default value is [], {} and sub {} respectively.
+
+=item isa
+
+(Default=undef) The expected type of the attribute. If this optioin is defined
+as $type, a new $value of the attribute is only accepted if $value is undef,
+UNIVERSAL::isa($value,$type) returns true or if $type is C<SCALAR> and the new
+value is not a reference.
+
+The attribute accepts $, @, %, & and * as aliases to SCALAR, ARRAY, HASH, CODE
+and GLOB.
+
+=back
+
+The %class_opt is used to configure what methods are created for the class, as
+well as other options for the $class->new() method. It may contain the
+following:
+
+=over 4
+
+=item init
+
+If $class_opt{init} is defined, it should be a CODE reference. If specified, it
+will be called once when $instance->new() is called, with the interface
+$init->(\%attrib,$self).
+
+=item init_attrib
+
+The value of this option must be a CODE. The $class->new() normally expects a
+single HASH reference argument. If an alternate interface to the $class->new()
+is required, this CODE can be used to turn the input argument list to the
+expected HASH reference.
+
+=item action_of
+
+This provides the actions of the class. It should be a HASH. Each $key in the
+HASH will be turned into a method implemented by the CODE reference in the
+corresponding $value: $instance->$key(@args) will call $instance->($key, at args),
+which will call $value->(\%attrib, at args).
+
+=back
+
+=item $class->new(\%attrib)
+
+Creates a new instance with %attrib. Initial values of the attributes can be
+specified using %attrib. Otherwise, the method will attempt to assign the
+default values, as specified in the class() method, to the newly created
+instance.
+
+=item $instance->$key(@args)
+
+A method is created for each $key of the %{$attrib{action_of}}.
+
+=back
+
+=head1 DIAGNOSTICS
+
+L<FCM::Class::Exception|FCM::Class::Exception> is thrown on error.
+
+=head1 SEE ALSO
+
+Inspired by the standard module L<Class::Struct|Class::Struct> and CPAN modules
+such as L<Class::Accessor|Class::Accessor>.
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/Class/Exception.pm b/lib/FCM/Class/Exception.pm
new file mode 100644
index 0000000..0f54896
--- /dev/null
+++ b/lib/FCM/Class/Exception.pm
@@ -0,0 +1,134 @@
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+use strict;
+use warnings;
+
+#-------------------------------------------------------------------------------
+package FCM::Class::Exception;
+
+use constant {
+ CODE_TYPE => 'CODE_TYPE',
+};
+
+sub caught {
+ my ($class, $e) = @_;
+ blessed($e) && $e->isa($class);
+}
+
+sub throw {
+ my ($class, $attrib_ref) = @_;
+ my %e = (
+ 'caller' => [],
+ 'code' => undef,
+ 'key' => undef,
+ 'package' => undef,
+ 'type' => undef,
+ 'value' => undef,
+ (defined($attrib_ref) ? %{$attrib_ref} : ()),
+ );
+ die(bless(\%e, $class));
+}
+
+for my $key (qw{caller code key package type value}) {
+ no strict qw{refs};
+ *{"get_$key"} = sub {$_[0]->{$key}};
+}
+
+#-------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::Class::Exception
+
+=head1 SYNOPSIS
+
+ eval {
+ FCM::Class::Exception->throw({
+ 'caller' => [caller()],
+ 'code' => $code,
+ 'key' => $key,
+ 'package' => $package,
+ 'type' => $type,
+ 'value' => $value,
+ });
+ };
+ if (my $e = $@) {
+ if (FCM::Class::Exception->caught($e)) {
+ # ... handle this exception class
+ }
+ else {
+ # ... do something else
+ }
+ }
+
+=head1 DESCRIPTION
+
+This exception is thrown on incorrect usage of an instance of a sub-class. An
+instance of this exception has the following attributes, which can be accessed
+via $e->get_$attrib():
+
+=head1 ATTRIBUTES
+
+=over 4
+
+=item caller
+
+Returns an ARRAY reference containing the caller (as returned by the built-in
+function in ARRAY context) that triggers the exception. Note: for a CODE-based
+class, this is always the caller when the instance is created.
+
+=item code
+
+The error code, which can be one of the following:
+
+=over 4
+
+=item $e->CODE_TYPE
+
+Attempt to set the value of an attribute to an incorrect type.
+
+=back
+
+=item key
+
+The key of the attribute that triggers this exception.
+
+=item type
+
+The expected data type (for an attempt to set the value of an attribute to an
+incorrect type).
+
+=item value
+
+The value of the attribute that triggers this exception.
+
+=back
+
+=head1 SEE ALSO
+
+L<FCM::Class::CODE|FCM::Class::CODE>
+L<FCM::Class::HASH|FCM::Class::HASH>
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/Class/HASH.pm b/lib/FCM/Class/HASH.pm
new file mode 100644
index 0000000..97cd37f
--- /dev/null
+++ b/lib/FCM/Class/HASH.pm
@@ -0,0 +1,340 @@
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+use strict;
+use warnings;
+
+#-------------------------------------------------------------------------------
+package FCM::Class::HASH;
+use FCM::Class::Exception;
+use Scalar::Util qw{reftype};
+
+# Methods for working out the default value of an attribute.
+my %ATTRIB_DEFAULT_BY = (
+ default => sub {
+ my $opt_ref = shift();
+ my $ret = $opt_ref->{default};
+ return (ref($ret) && reftype($ret) eq 'CODE' ? $ret->() : $ret);
+ },
+ isa => sub {
+ my $opt_ref = shift();
+ return
+ $opt_ref->{isa} eq 'ARRAY' ? []
+ : $opt_ref->{isa} eq 'HASH' ? {}
+ : $opt_ref->{isa} eq 'CODE' ? sub {}
+ : undef
+ ;
+ },
+);
+
+# Checks the value of an attribute.
+my $ATTRIB_CHECK = sub {
+ my ($class, $opt_ref, $key, $value, $caller_ref) = @_;
+ # Note: undef is always OK?
+ if (!defined($value)) {
+ return;
+ }
+ my $expected_isa = $opt_ref->{isa};
+ if (!$expected_isa || $expected_isa eq 'SCALAR' && !ref($value)) {
+ return;
+ }
+ if (!UNIVERSAL::isa($value, $expected_isa)) {
+ return FCM::Class::Exception->throw({
+ 'code' => FCM::Class::Exception->CODE_TYPE,
+ 'caller' => $caller_ref,
+ 'package' => $class,
+ 'key' => $key,
+ 'type' => $expected_isa,
+ 'value' => $value,
+ });
+ }
+};
+
+# Creates the methods of the class.
+sub class {
+ my ($class, $attrib_opt_ref, $class_opt_ref) = @_;
+ my %class_opt = (
+ init => sub {},
+ init_attrib => sub {@_},
+ (defined($class_opt_ref) ? %{$class_opt_ref} : ()),
+ );
+ if (!defined($attrib_opt_ref)) {
+ $attrib_opt_ref = {};
+ }
+ my %attrib_opt;
+ while (my ($key, $item) = each(%{$attrib_opt_ref})) {
+ my %option = (
+ r => 1, # readable?
+ w => 1, # writable?
+ add => undef, # isa eq 'HASH' only, class of HASH element
+ default => undef, # default value or CODE to return it
+ isa => undef, # attribute type
+ ( defined($item) && ref($item) ? %{$item}
+ : defined($item) ? (isa => $item)
+ : ()
+ ),
+ );
+ if (defined($option{isa})) {
+ $option{isa}
+ = $option{isa} eq '$' ? 'SCALAR'
+ : $option{isa} eq '@' ? 'ARRAY'
+ : $option{isa} eq '%' ? 'HASH'
+ : $option{isa} eq '&' ? 'CODE'
+ : $option{isa} eq '*' ? 'GLOB'
+ : $option{isa}
+ ;
+ }
+ $attrib_opt{$key} = \%option;
+ }
+ no strict qw{refs};
+ # $class->new(\%attrib)
+ *{$class . '::new'} = sub {
+ my $class = shift();
+ my ($attrib_ref) = $class_opt{init_attrib}->(@_);
+ my $caller_ref = [caller()];
+ my %attrib = (defined($attrib_ref) ? %{$attrib_ref} : ());
+ while (my ($key, $value) = each(%attrib)) {
+ $ATTRIB_CHECK->($class, $attrib_opt{$key}, $key, $value, $caller_ref);
+ }
+ my $self = bless(\%attrib, $class);
+ KEY:
+ while (my ($key, $opt_ref) = each(%attrib_opt)) {
+ if (exists($self->{$key})) {
+ next KEY;
+ }
+ for my $opt_name (qw{default isa}) {
+ if (defined($opt_ref->{$opt_name})) {
+ $self->{$key} = $ATTRIB_DEFAULT_BY{$opt_name}->($opt_ref);
+ next KEY;
+ }
+ }
+ }
+ $class_opt{init}->($self);
+ return $self;
+ };
+ # $instance->$methods()
+ while (my ($key, $opt_ref) = each(%attrib_opt)) {
+ # $instance->get_$attrib()
+ # $instance->get_$attrib($name)
+ if ($opt_ref->{r}) {
+ *{$class . '::get_' . $key}
+ = defined($opt_ref->{isa}) && $opt_ref->{isa} eq 'HASH'
+ ? sub {
+ my ($self, $name) = @_;
+ if (!defined($name)) {
+ return $self->{$key};
+ }
+ if (exists($self->{$key}{$name})) {
+ return $self->{$key}{$name};
+ }
+ return;
+ }
+ : sub {$_[0]->{$key}}
+ ;
+ }
+ # $instance->set_$attrib($value)
+ if ($opt_ref->{w}) {
+ *{$class . '::set_' . $key} = sub {
+ my ($self, $value) = @_;
+ $ATTRIB_CHECK->(
+ $class, $attrib_opt{$key}, $key, $value, [caller()],
+ );
+ $self->{$key} = $value;
+ return $self;
+ };
+ }
+ # $instance->add_$attrib($name,\%option)
+ if ( defined($opt_ref->{isa}) && $opt_ref->{isa} eq 'HASH'
+ && defined($opt_ref->{add})
+ ) {
+ *{$class . '::add_' . $key} = sub {
+ my ($self, $name, @args) = @_;
+ if (defined($self->{$key}{$name})) {
+ return $self->{$key}{$name};
+ }
+ $self->{$key}{$name} = $opt_ref->{add}->new(@args);
+ };
+ }
+ }
+ return 1;
+}
+
+#-------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::Class::HASH
+
+=head1 SYNOPSIS
+
+ package Breakfast;
+ use base qw{FCM::Class::HASH};
+ __PACKAGE__->class(
+ {
+ eggs => {isa => '@'},
+ ham => {isa => '%'},
+ bacon => '$',
+ # ...
+ },
+ );
+ # Some time later...
+ $breakfast = Breakfast->new(\%attrib);
+ @eggs = @{$breakfast->get_eggs()};
+ $breakfast->set_ham(\%ham);
+
+=head1 DESCRIPTION
+
+Provides a simple method to create HASH-based classes.
+
+The class() method creates the new() method for initiating a new instance. It
+also provides a get_$attrib() and set_$attrib() accessors for each attribute.
+Basic type checkings are performed on writing to the attributes to ensure
+correct usage.
+
+=head1 METHODS
+
+=over 4
+
+=item $class->class(\%attrib_opt,\%class_opt)
+
+Creates the class, using the attribute options in %attrib_opt and %class_opt.
+
+The %attrib_opt is used to configure the attributes of an instance of the class.
+The key of each element is the name of the attribute, and the value is a HASH
+containing the options of the attribute, or a SCALAR. (If a SCALAR is specified,
+it is equivalent to {isa => value}.). The options may contain:
+
+=over 4
+
+=item r
+
+(Default=true) If true, the attribute is readable.
+
+=item w
+
+(Default=true) If true, the attribute is writable.
+
+=item add
+
+(Default=undef) This is only useful for a HASH attribute. If defined, it should
+be the name of a class (e.g. $attrib_class). The HASH attribute will receive an
+extra method $instance->add_$attrib($key, at args). The method will assign the
+$name element of the HASH attribute to the result of $attrib_class->new(@args).
+
+=item default
+
+(Default=undef) The default value of the attribute.
+
+If this option is defined, the attribute will be initialised to the specified
+value when the new() method is called. In the special case where the value of
+this option is a CODE reference, it will be invoked as $code->(\%attrib), and
+the default value will be the returned value of the CODE reference. This is
+useful, for example, if the default value needs to be a new instance of a class.
+If a genuine CODE reference is required as the default, this option should be
+set to a CODE reference that returns the required CODE reference itself.
+
+For example:
+
+ Foo->class({
+ foo => {default => 'foo'}, # 'foo'
+ bar => {default => sub {get_id()}}, # the next id
+ baz => {default => sub {\&code}}, # &code
+ });
+ {
+ my $id = 0;
+ sub get_id {$id++}
+ }
+
+If the default options is not defined, and if the attribute "isa" is ARRAY, HASH
+or CODE, then the default value is [], {} and sub {} respectively.
+
+=item isa
+
+(Default=undef) The expected type of the attribute. If this optioin is defined
+as $type, a new $value of the attribute is only accepted if $value is undef,
+UNIVERSAL::isa($value,$type) returns true or if $type is C<SCALAR> and the new
+value is not a reference.
+
+The attribute accepts $, @, %, & and * as aliases to SCALAR, ARRAY, HASH, CODE
+and GLOB.
+
+=back
+
+The argument %class_opt can have the following elements:
+
+=over 4
+
+=item init
+
+If $class_opt{init} is defined, it should be a CODE reference. If specified, it
+will be called just after the instance is blessed in the $class->new() method,
+with an interface $f->($instance) where $instance is the new instance.
+
+=item init_attrib
+
+The value of this option must be a CODE. The $class->new() normally expects a
+single HASH reference argument. If an alternate interface to the $class->new()
+is required, this CODE can be used to turn the input argument list to the
+expected HASH reference.
+
+=back
+
+=item $class->new(\%attrib)
+
+Creates a new instance with %attrib. Initial values of the attributes can be
+specified using %attrib. Otherwise, the method will attempt to assign the
+default values, as specified in the class() method, to the newly created
+instance.
+
+=item $instance->get_$attrib()
+
+Returns a readable attribute.
+
+=item $instance->get_$attrib($key)
+
+These are available for HASH attributes only. Returns the value of an element in
+a readable attribute.
+
+=item $instance->set_$attrib($value)
+
+Sets the value of a writable attribute. Returns $instance.
+
+=item $instance->add_$attrib($key, at args)
+
+These are available for HASH attributes (with the C<add> attribute option
+defined) only. Adds a new $key element to the HASH attribute. Returns the newly
+added element.
+
+=back
+
+=head1 DIAGNOSTICS
+
+L<FCM::Class::Exception|FCM::Class::Exception> is thrown on error.
+
+=head1 SEE ALSO
+
+Inspired by the standard module L<Class::Struct|Class::Struct> and CPAN modules
+such as L<Class::Accessor|Class::Accessor>.
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/Context/ConfigEntry.pm b/lib/FCM/Context/ConfigEntry.pm
new file mode 100644
index 0000000..090b82a
--- /dev/null
+++ b/lib/FCM/Context/ConfigEntry.pm
@@ -0,0 +1,160 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+
+# ------------------------------------------------------------------------------
+package FCM::Context::ConfigEntry;
+use base qw{FCM::Class::HASH};
+
+use Text::ParseWords qw{shellwords};
+
+__PACKAGE__->class({
+ label => '$',
+ modifier_of => '%',
+ ns_list => '@',
+ stack => '@',
+ value => '$',
+});
+
+# A shorthand for shellwords($entry->get_value()).
+sub get_values {
+ shellwords($_[0]->get_value());
+}
+
+# The config entry's left hand side of the equal sign.
+sub get_lhs {
+ my ($self) = @_;
+ my $modifier = join(
+ q{, },
+ ( map
+ { my $value = $self->{modifier_of}{$_};
+ join(q{:}, $_, (($value && $value eq 1) ? () : $value));
+ }
+ sort keys(%{$self->{modifier_of}})
+ ),
+ );
+ my $ns = join(
+ q{ },
+ (map {my $s = $_; $s =~ s{(["'\s])}{\\$1}gxms; $s} @{$self->{ns_list}}),
+ );
+ sprintf(
+ '%s%s%s',
+ $self->{label},
+ ($modifier ? "{$modifier}" : q{}),
+ ($ns ? "[$ns]" : q{}),
+ );
+}
+
+# The config entry, as a string.
+sub as_string {
+ my ($self, $in_fcm1) = @_;
+ my $value = $self->{value};
+ $value ||= q{};
+ $value =~ s{(\\)+(\$)}{$1$1\\$2}gxms;
+ sprintf(($in_fcm1 ? '%s %s' : '%s = %s'), $self->get_lhs(), $value);
+}
+
+# ------------------------------------------------------------------------------
+
+1;
+__END__
+
+=head1 NAME
+
+FCM::Context::ConfigEntry;
+
+=head1 SYNOPSIS
+
+ my $c_entry = FCM::Context::ConfigEntry->new({
+ label => 'egg',
+ modifier_of => {fried => 1},
+ ns_list => [qw{all day breakfast}],
+ stack => [[$breakfast_menu, 10], [$menu, 20]],
+ value => 2,
+ });
+
+ # ... some time later
+ $label = $c_entry->get_label();
+ %modifier_of = %{$c_entry->get_modifier_of()};
+ @ns_list = @{$c_entry->get_ns_list()};
+ @stack = @{$c_entry->get_stack()};
+ $value = $c_entry->get_value();
+
+ print($c_entry->as_string());
+ # should print: egg{fried: 1}[all day breakfast] = 2
+
+=head1 DESCRIPTION
+
+This class is based on L<FCM::Class::HASH|FCM::Class::HASH> for representing an
+entry in a FCM configuration file. All attributes can be read using the
+$instance->get_$attrib() methods.
+
+=head1 ATTRIBUTES
+
+=over 4
+
+=item label
+
+The label of the entry.
+
+=item modifier_of
+
+A HASH containing the modifiers of this entry.
+
+=item ns_list
+
+An ARRAY containing the namespaces of this entry.
+
+=item stack
+
+An ARRAY containing the locator stack that provides this entry. The first
+element represents the top of the stack. Each element should be a reference to a
+2-element array [RESOURCE, LINE_NUMBER].
+
+=item value
+
+The value of this entry.
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item $instance->as_string($in_fcm1)
+
+Returns a string representation of the config entry. If the optional argument
+$in_fcm1 is specified, it will return the config entry in FCM 1 format.
+
+=item $instance->get_lhs()
+
+Returns a string representation of the left hand side of the config entry.
+
+=item $instance->get_values()
+
+A shorthand for shellwords($instance->get_value()).
+
+=back
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/Context/Event.pm b/lib/FCM/Context/Event.pm
new file mode 100644
index 0000000..5e6a090
--- /dev/null
+++ b/lib/FCM/Context/Event.pm
@@ -0,0 +1,383 @@
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+use strict;
+use warnings;
+#-------------------------------------------------------------------------------
+
+package FCM::Context::Event;
+use base qw{FCM::Class::HASH};
+
+use constant {
+ CM_ABORT => 'CM_ABORT',
+ CM_BRANCH_CREATE_SOURCE => 'CM_BRANCH_CREATE_SOURCE',
+ CM_BRANCH_LIST => 'CM_BRANCH_LIST',
+ CM_COMMIT_MESSAGE => 'CM_COMMIT_MESSAGE',
+ CM_CONFLICT_TEXT => 'CM_CONFLICT_TEXT',
+ CM_CONFLICT_TEXT_SKIP => 'CM_CONFLICT_TEXT_SKIP',
+ CM_CONFLICT_TREE => 'CM_CONFLICT_TREE',
+ CM_CONFLICT_TREE_SKIP => 'CM_CONFLICT_TREE_SKIP',
+ CM_CONFLICT_TREE_TIME_WARN => 'CM_CONFLICT_TREE_TIME_WARN',
+ CM_CREATE_TARGET => 'CM_CREATE_TARGET',
+ CM_LOG_EDIT => 'CM_LOG_EDIT',
+ #CM_WC_STATUS => 'CM_WC_STATUS',
+ #CM_WC_STATUS_PATH => 'CM_WC_STATUS_PATH',
+ CONFIG_OPEN => 'CONFIG_OPEN',
+ CONFIG_ENTRY => 'CONFIG_ENTRY',
+ CONFIG_VAR_UNDEF => 'CONFIG_VAR_UNDEF',
+ E => 'E',
+ EXPORT_ITEM_CREATE => 'EXPORT_ITEM_CREATE',
+ EXPORT_ITEM_DELETE => 'EXPORT_ITEM_DELETE',
+ FCM_VERSION => 'FCM_VERSION',
+ KEYWORD_ENTRY => 'KEYWORD_ENTRY',
+ OUT => 'OUT',
+ MAKE_BUILD_SHELL_OUT => 'MAKE_BUILD_SHELL_OUT',
+ MAKE_BUILD_SOURCE_ANALYSE => 'MAKE_BUILD_SOURCE_ANALYSE',
+ MAKE_BUILD_SOURCE_SUMMARY => 'MAKE_BUILD_SOURCE_SUMMARY',
+ MAKE_BUILD_TARGET_DONE => 'MAKE_BUILD_TARGET_DONE',
+ MAKE_BUILD_TARGET_FAIL => 'MAKE_BUILD_TARGET_FAIL',
+ MAKE_BUILD_TARGET_FROM_NS => 'MAKE_BUILD_TARGET_FROM_NS',
+ MAKE_BUILD_TARGET_MISSING_DEP => 'MAKE_BUILD_TARGET_MISSING_DEP',
+ MAKE_BUILD_TARGET_SELECT => 'MAKE_BUILD_TARGET_SELECT',
+ MAKE_BUILD_TARGET_SELECT_TIMER=> 'MAKE_BUILD_TARGET_SELECT_TIMER',
+ MAKE_BUILD_TARGET_STACK => 'MAKE_BUILD_TARGET_STACK',
+ MAKE_BUILD_TARGET_SUMMARY => 'MAKE_BUILD_TARGET_SUMMARY',
+ MAKE_BUILD_TARGET_TASK_SUMMARY=> 'MAKE_BUILD_TARGET_TASK_SUMMARY',
+ MAKE_BUILD_TARGETS_FAIL => 'MAKE_BUILD_TARGETS_FAIL',
+ MAKE_DEST => 'MAKE_DEST',
+ MAKE_EXTRACT_PROJECT_TREE => 'MAKE_EXTRACT_PROJECT_TREE',
+ MAKE_EXTRACT_RUNNER_SUMMARY => 'MAKE_EXTRACT_RUNNER_SUMMARY',
+ MAKE_EXTRACT_SYMLINK => 'MAKE_EXTRACT_SYMLINK',
+ MAKE_EXTRACT_TARGET => 'MAKE_EXTRACT_TARGET',
+ MAKE_EXTRACT_TARGET_SUMMARY => 'MAKE_EXTRACT_TARGET_SUMMARY',
+ MAKE_MIRROR => 'MAKE_MIRROR',
+ SHELL => 'SHELL',
+ TASK_WORKERS => 'TASK_WORKERS',
+ TIMER => 'TIMER',
+};
+
+__PACKAGE__->class({args => '@', code => '$'});
+
+#-------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::Context::Event
+
+=head1 SYNOPSIS
+
+ use FCM::Context::Event;
+ my $event_ctx = FCM::Context::Event->new($code, @args);
+
+=head1 DESCRIPTION
+
+An instance of this class represents the context of an event. This class is a
+sub-class of L<FCM::Class::HASH|FCM::Class::HASH>.
+
+=head1 ATTRIBUTES
+
+=over 4
+
+=item args
+
+An ARRAY reference that represents the additional arguments/contexts of the
+event.
+
+=item code
+
+The event code. See below
+
+=back
+
+=head1 EVENTS
+
+The following is a list of event codes.
+
+=over 4
+
+=item FCM::Context::Event->CM_ABORT
+
+This event is raised when a code management command aborts. The 1st argument
+should be either "user" (user abort) or "null" (null command).
+
+=item FCM::Context::Event->CM_BRANCH_CREATE_SOURCE
+
+This event is raised to notify the source of a branch create. The 1st argument
+should be the expected source URL, and the 2nd argument is the specified peg
+revision.
+
+=item FCM::Context::Event->CM_BRANCH_LIST
+
+This event is raised when doing a branch listing. The 1st argument should be the
+project location and the rest of the arguments are the branches discovered.
+
+=item FCM::Context::Event->CM_COMMIT_MESSAGE
+
+This event is raised to notify the user the log message to be used for a commit.
+The 1st argument of the event should be an instance of
+FCM::System::CM::CommitMessage::State.
+
+=item FCM::Context::Event->CM_CONFLICT_TEXT
+
+This event is raised to notify the path of a file with a text conflict.
+
+=item FCM::Context::Event->CM_CONFLICT_TEXT_SKIP
+
+This event is raised to notify the path of a file with a text conflict that
+cannot be resolved using a merge tool. E.g. it may be a binary file.
+
+=item FCM::Context::Event->CM_CONFLICT_TREE
+
+This event is raised to notify the path of a node with a tree conflict.
+
+=item FCM::Context::Event->CM_CONFLICT_TREE_SKIP
+
+This event is raised to notify the path of a node with a tree conflict that
+cannot be resolved automatically under current functionality. For example, it
+may be a directory containing multiple conflicts.
+
+=item FCM::Context::Event->CM_CREATE_TARGET
+
+This event is raised to notify the target of a newly created URL. The 1st argument
+should be the target URL.
+
+=item FCM::Context::Event->CM_LOG_EDIT
+
+This event is raised before the system launches an editor to edit a commit log
+message. The 1st argument of the event should be the editor command.
+
+=item FCM::Context::Event->CONFIG_ENTRY
+
+This entry is raised to notify the reading of a configuration file entry. The
+1st argument should be a blessed reference of a
+L<FCM::Context::ConfigEntry|FCM::Context::ConfigEntry>. The second argument
+should be a boolean flag to indicate whether this entry is in FCM 1 format or
+not.
+
+=item FCM::Context::Event->CONFIG_OPEN
+
+This event is raised when a new configuration file is opened for reading. The
+1st argument of this event is an ARRAY that represents the include file stack,
+where the last element is the top of the stack. Each element of the stack is a
+2-element ARRAY reference, where the first element is a
+L<FCM::Context::Locator|FCM::Context::Locator> object and the second element is
+the line number. (At the top of the stack, the line number is set to 0.) The 2nd
+optional argument of this event is a number to adjust the verbosity level of the
+event.
+
+=item FCM::Context::Event->CONFIG_VAR_UNDEF
+
+This event is raised when a variable is undefined. The arguments of this event
+contain 2 elements. The 1st element is the configuration entry as a blessed
+reference of L<FCM::Context::ConfigEntry|FCM::Context::ConfigEntry>. The 2nd
+element is the name of the variable.
+
+=item FCM::Context::Event->E
+
+This event is raised when to notify an exception. The 1st argument of this event
+should be the exception.
+
+=item FCM::Context::Event->EXPORT_ITEM_CREATE
+
+This event is raised when the export-items system creates a link to an item.
+The 1st argument is the namespace of the item, the 2nd argument is the revision
+of the item and the 3rd argument is the name of the link.
+
+=item FCM::Context::Event->EXPORT_ITEM_DELETE
+
+This event is raised when the export-items system deletes a link to an item.
+The 1st argument is the namespace of the item, the 2nd argument is the revision
+of the item and the 3rd argument is the name of the link.
+
+=item FCM::Context::Event->FCM_VERSION
+
+This event is raised to notify the FCM version.
+
+=item FCM::Context::Event->KEYWORD_ENTRY
+
+This event is raised to notify a keyword entry. The 1st argument is the keyword
+entry as a blessed reference of FCM::Context::Keyword::Entry as described in
+L<FCM::Context::Keyword|FCM::Context::Keyword>.
+
+=item FCM::Context::Event->MAKE_BUILD_SHELL_OUT
+
+This event is raised to notify (shell command) output from make/build. The 1st
+argument should be the STDOUT, and the 2nd argument should be the STDERR.
+
+=item FCM::Context::Event->MAKE_BUILD_SOURCE_ANALYSE
+
+This event is raised when the make/build system has analysed a source file. The
+1st argument should be a blessed reference of FCM::Context::Make::Build::Source
+as described in L<FCM::Context::Make::Build|FCM::Context::Make::Build>. The 2nd
+argument should be the time it takes for the analysis.
+
+=item FCM::Context::Event->MAKE_BUILD_SOURCE_SUMMARY
+
+This event is raised when the make/build system has analysed all its source
+files. The 1st argument should be the total number of files. The 2nd argument
+should be the number analysed. The 3rd argument should be the elapsed time. The
+4th argument should be the total time, which may differ from the elapsed time if
+the analysis is run on more than 1 process.
+
+=item FCM::Context::Event->MAKE_BUILD_TARGET_DONE
+
+This event is raised when the make/build system has successfully updated a
+target or an update is unnecessary. The 1st argument is the target (as an
+instance of FCM::Context::Make::Build::Target), the 2nd argument is the elapsed
+time of the update, if relevant.
+
+=item FCM::Context::Event->MAKE_BUILD_TARGET_FAIL
+
+This event is raised when the make/build system failed to update a target or the
+target is failed by its dependencies. The 1st argument is the target (as an
+instance of FCM::Context::Make::Build::Target), the 2nd argument is the elapsed
+time, if the target is failed by its update.
+
+=item FCM::Context::Event->MAKE_BUILD_TARGET_FROM_NS
+
+This event is raised when the make/build system has generated a build target
+from a source or a source name-space. The arguments are: source/target
+namespace, target task, target category, and the target key.
+
+=item FCM::Context::Event->MAKE_BUILD_TARGET_MISSING_DEP
+
+This event is raised when the make/build system has discarded a missing
+dependency from a target. The 1st argument is the target ID, the 2nd argument is
+the dependency ID, and the 3rd argument is the dependency type.
+
+=item FCM::Context::Event->MAKE_BUILD_TARGET_SELECT
+
+This event is raised when the make/build system has selected a set of targets to
+build. The 1st argument is a HASH reference of the target set.
+
+=item FCM::Context::Event->MAKE_BUILD_TARGET_SELECT_TIMER
+
+This event is raised when the make/build system has completed the target select
+and dependency tree analysis. The only argument is the elapsed time.
+
+=item FCM::Context::Event->MAKE_BUILD_TARGET_STACK
+
+This event is raised when make/build system checks a target for cyclic
+dependency. The 1st argument is the key of the task. The 2nd argument is rank
+of the task in the dependency hierarchy. The 3rd argument is the number of
+dependencies the task has if the task has already been checked, or undef if this
+is the first check for the task.
+
+=item FCM::Context::Event->MAKE_BUILD_TARGET_SUMMARY
+
+This event is raised when the make/build system has finished updating its
+targets, and is ready to give a total summary. The 1st argument is the number of
+modified targets, the 2nd argument is the number unchanged, the 3rd argument is
+the number failed, the 4th argument is the elapsed time.
+
+=item FCM::Context::Event->MAKE_BUILD_TARGET_TASK_SUMMARY
+
+This event is raised when the make/build system has finished updating its
+targets, and is ready to give a summary of each type of task. The 1st argument
+is the task type name, the 2nd argument is the number of modified targets, the
+3rd argument is the number unchanged, the 4th argument is the number failed, and
+the 5th argument is the total time spent on this task type.
+
+=item FCM::Context::Event->MAKE_BUILD_TARGETS_FAIL
+
+This event is raised when the make/build system has finished updating its
+targets, but some targets failed to update. The 1st argument is an ARRAY of
+FCM::Context::Make::Build::Target objects representing the failed targets.
+
+=item FCM::Context::Event->MAKE_DEST
+
+This event is raised when the make system sets up the destination. The 1st
+argument of this event is the make system context.
+
+=item FCM::Context::Event->MAKE_EXTRACT_PROJECT_TREE
+
+This event is raised after the make/extract system has finished gathering
+information for the source trees of each project. The 1st argument is a HASH of
+the (keys) project name-spaces and the (values) list (ARRAY) of source tree
+locators L<FCM::Context::Locator|FCM::Context::Locator> in the project.
+
+=item FCM::Context::Event->MAKE_EXTRACT_RUNNER_SUMMARY
+
+This event is raised after the make/extract system has finished using the task
+runner to perform some tasks. The 1st argument is an identifier for the tasks
+performed. The 2nd argument is the number of tasks. The 2nd argument is the
+elapsed time. The 3rd argument is the total time in all processes.
+
+=item FCM::Context::Event->MAKE_EXTRACT_SYMLINK
+
+This event is raised as the make/extract system ignores a source that is a
+symbolic link. The 1st argument of this event should be a blessed reference of
+FCM::Context::Make::Extract::Source as described in
+L<FCM::Context::Make::Extract|FCM::Context::Make::Extract>.
+
+=item FCM::Context::Event->MAKE_EXTRACT_TARGET
+
+This event is raised as the make/extract system updates a target destination. The
+1st argument of this event should be a blessed reference of
+FCM::Context::Make::Extract::Target as described in
+L<FCM::Context::Make::Extract|FCM::Context::Make::Extract>.
+
+=item FCM::Context::Event->MAKE_EXTRACT_TARGET_SUMMARY
+
+This event is raised after the make/extract system has updated all target
+destinations. The 1st argument of this event is a HASH reference, which contains
+2 keys: status and status_of_source, i.e. the destination status of the targets
+and the source status of the targets respectively. The values of both are HASH
+references. The keys are the names of the status, and the values are the number
+of targets with the corresponding status.
+
+=item FCM::Context::Event->MAKE_MIRROR
+
+This event is raised as the make/mirror system updates a target destination. The
+1st argument of this event should be the target URI. The remaining arguments
+should be the source paths.
+
+=item FCM::Context::Event->OUT
+
+This event is raised to notify (shell command) output. The 1st argument should
+be the STDOUT, and the 2nd argument should be the STDERR.
+
+=item FCM::Context::Event->SHELL
+
+This event is raised to notify the completion of a shell command. The 1st
+argument is an ARRAY reference of the shell command. The 2nd argument is an
+integer to override the verbosity level. The 3rd argument is the return code and
+the 4th argument is the elapsed time.
+
+=item FCM::Context::Event->TASK_WORKERS
+
+This event is raised on initialisation and destruction of worker processes for
+the utility task runner. The 1st argument should either be "init" or "destroy".
+The 2nd argument should be the number of workers initialised/destroyed.
+
+=item FCM::Context::Event->TIMER
+
+This event is raised at the start and end of the utility timer. The 1st
+argument is the name of the piece of code to time. The 2nd argument is the start
+the timer. The 3rd argument is the elapsed time at the end. If the 3rd argument
+is not specified, it is the start of the timer.
+
+=back
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/Context/Keyword.pm b/lib/FCM/Context/Keyword.pm
new file mode 100644
index 0000000..af1fcda
--- /dev/null
+++ b/lib/FCM/Context/Keyword.pm
@@ -0,0 +1,221 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+# ------------------------------------------------------------------------------
+package FCM::Context::Keyword;
+use base qw{FCM::Class::HASH};
+
+use constant {
+ BROWSER_CONFIG => 'FCM::Context::Keyword::BrowserConfig',
+ ENTRY => 'FCM::Context::Keyword::Entry',
+ ENTRY_OF_LOCATION => 'FCM::Context::Keyword::Entry::Location',
+};
+
+use Scalar::Util qw{blessed};
+
+__PACKAGE__->class({
+ entry_class => {w => 0, isa => '$', default => ENTRY_OF_LOCATION},
+ entry_by_key => {w => 0, isa => '%'},
+ entry_by_value => {w => 0, isa => '%'},
+});
+
+sub add_entry {
+ my $self = shift();
+ my ($key, $value, $entry);
+ if (blessed($_[0])) {
+ $entry = $_[0];
+ $key = $entry->get_key();
+ $value = $entry->get_value();
+ }
+ else {
+ ($key, $value, my $attrib_ref) = @_;
+ $attrib_ref ||= {};
+ $entry = $self->get_entry_class()->new({
+ key => lc($key), value => $value, %{$attrib_ref},
+ });
+ }
+ $self->{entry_by_key}{lc($key)} = $entry;
+ $self->{entry_by_value}{$value} = $entry;
+ return $entry;
+}
+
+# ------------------------------------------------------------------------------
+package FCM::Context::Keyword::BrowserConfig;
+use base qw{FCM::Class::HASH};
+
+__PACKAGE__->class({comp_pat => undef, loc_tmpl => '$', rev_tmpl => '$'});
+
+# ------------------------------------------------------------------------------
+package FCM::Context::Keyword::Entry;
+use base qw{FCM::Class::HASH};
+
+__PACKAGE__->class({
+ key => {w => 0, isa => '$'},
+ value => {w => 0, isa => '$'},
+});
+
+# ------------------------------------------------------------------------------
+package FCM::Context::Keyword::Entry::Location;
+use base qw{FCM::Context::Keyword::Entry};
+
+my $CTX = 'FCM::Context::Keyword';
+
+__PACKAGE__->class(
+ { browser_config => $CTX->BROWSER_CONFIG,
+ ctx_of_implied => $CTX,
+ ctx_of_rev => $CTX,
+ implied => {isa => '$', default => 0},
+ key => {isa => '$', w => 0},
+ loaded_rev_prop => '$',
+ type => '$',
+ value => {isa => '$', w => 0},
+ },
+ { init => sub {
+ my ($self) = @_;
+ if (!$self->get_implied()) {
+ $self->{browser_config} = $CTX->BROWSER_CONFIG->new();
+ $self->{ctx_of_implied} = $CTX->new();
+ $self->{ctx_of_rev} = $CTX->new({entry_class => $CTX->ENTRY});
+ }
+ },
+ },
+);
+
+# Returns true if this is an implied entry
+sub is_implied {
+ $_[0]->{implied};
+}
+
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::Context::Keyword
+
+=head1 SYNOPSIS
+
+ use FCM::Context::Keyword;
+
+=head1 DESCRIPTION
+
+Provides a context object for the FCM keyword utility. All the classes described
+below are sub-classes of L<FCM::Class::HASH|FCM::Class::HASH>.
+
+=head1 OBJECTS
+
+=head2 FCM::Context::Keyword
+
+An object of this class is used to store a list of keyword entries. It has the
+following methods:
+
+=over 4
+
+=item $instance->add_entry($key,$value,\%attrib)
+
+Creates and adds a new entry.
+
+=item $instance->add_entry($entry)
+
+Adds a new entry.
+
+=item $instance->get_entry_class()
+
+Returns the class of the entry stored by this context.
+
+=item $instance->get_entry_by_key()
+
+Returns a HASH reference to map the entry keys with the entry objects.
+
+=item $instance->get_entry_by_value()
+
+Returns a HASH reference to map the entry values with the entry objects.
+
+=back
+
+=head2 FCM::Context::Keyword::BrowserConfig
+
+An object of this class is used to store the configuration for mapping a
+location to a browser URL. It has the following attributes:
+
+=over 4
+
+=item comp_pat
+
+The pattern for extracting components from a locator, for putting into the
+browser location template.
+
+=item loc_tmpl
+
+The browser location template.
+
+=item rev_tmpl
+
+The browser revision template.
+
+=back
+
+=head2 FCM::Context::Keyword::Entry
+
+This is used to store a simple keyword entry (e.g. for revision keywords). It
+has 2 attributes, the I<key> and the I<value>.
+
+=head2 FCM::Context::Keyword::Entry::Location
+
+This is a sub-class of FCM::Context::Keyword::Entry, and is used to store a
+location keyword entry. It has the following additional attributes:
+
+=over 4
+
+=item browser_config
+
+The configuration L</FCM::Context::Keyword::BrowserConfig> for mapping this
+location to a browser URL.
+
+=item ctx_of_implied
+
+The context </FCM::Context::Keyword> object of the implied entries, if this
+entry is a primary location.
+
+=item ctx_of_rev
+
+The context </FCM::Context::Keyword> object of the revision keywords.
+
+=item implied
+
+A flag to indicate that this is an entry implied by a primary location.
+
+=item loaded_rev_prop
+
+A flag to indicate that a previous attempt is made to load revision keywords
+from the I<property> of the location.
+
+=item type
+
+The location type.
+
+=back
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/Context/Locator.pm b/lib/FCM/Context/Locator.pm
new file mode 100644
index 0000000..5959aac
--- /dev/null
+++ b/lib/FCM/Context/Locator.pm
@@ -0,0 +1,144 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+# ------------------------------------------------------------------------------
+package FCM::Context::Locator;
+use base qw{FCM::Class::HASH};
+
+use constant {
+ L_INIT => -1,
+ L_PARSED => 0,
+ L_NORMALISED => 1,
+ L_INVARIANT => 2,
+};
+
+__PACKAGE__->class(
+ { last_mod_rev => '$',
+ last_mod_time => '$',
+ type => '$',
+ value => '$',
+ value_at_init => {isa => '$', i => 1, w => 0},
+ value_level => {isa => '$', default => L_INIT},
+ },
+ { init_attrib => sub {
+ my ($value, $attrib_ref) = @_;
+ return {
+ (defined($attrib_ref) ? %{$attrib_ref} : ()),
+ value => $value,
+ value_at_init => $value,
+ };
+ },
+ }
+);
+
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::Context::Locator
+
+=head1 SYNOPSIS
+
+ use FCM::Context::Locator;
+ $locator = FCM::Context::Locator->new($value_at_init, {type => $type});
+ $locator->set_value($value);
+ $locator->set_value_level($locator->L_INVARIANT);
+ print($locator->get_value(), "\n");
+
+=head1 DESCRIPTION
+
+A simple structure for storing the values of a FCM locator. It is based on
+L<FCM::Class::HASH|FCM::Class::HASH>.
+
+=head1 ATTRIBUTES
+
+An instance has the following attributes, all of which can be initialised and
+accessed via an $instance->get_$attrib() method:
+
+=over 4
+
+=item last_mod_rev
+
+The last modified revision.
+
+=item last_mod_time
+
+The last modified time (seconds since epoch).
+
+=item type
+
+The locator type.
+
+=item value
+
+The current value of the locator.
+
+=item value_at_init
+
+The value of the locator when the object is initialised.
+
+=item value_level
+
+The value level of the locator. It can be one of the L_* constants. A higher
+level indicates that the value is more processed.
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item $class->new($value,\%attrib)
+
+Returns a new instance.
+
+=head1 CONSTANTS
+
+=over 4
+
+=item FCM::Context::Locator->L_INIT
+
+The lowest value level, i.e. the value has not been processed. (default)
+
+=item FCM::Context::Locator->L_PARSED
+
+The value level is between L_INIT and L_NORMALISED, i.e. where necessary, the
+FCM location keyword is substituted.
+
+=item FCM::Context::Locator->L_NORMALISED
+
+The value level is between L_PARSED and L_INVARIANT, i.e. where necessary, the
+FCM location and revision keywords are substituted and the value has been tidied
+(e.g. extra slashes in the path removed).
+
+=item FCM::Context::Locator->L_INVARIANT
+
+The highest value level, i.e. if the locator points to a version control
+resource, the value is expected to be tagged with a specific revision.
+
+=back
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/Context/Make.pm b/lib/FCM/Context/Make.pm
new file mode 100644
index 0000000..4551144
--- /dev/null
+++ b/lib/FCM/Context/Make.pm
@@ -0,0 +1,164 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+
+# ------------------------------------------------------------------------------
+package FCM::Context::Make;
+use base qw{FCM::Class::HASH};
+
+use constant {
+ ST_UNKNOWN => 0,
+ ST_INIT => 1,
+ ST_OK => 2,
+ ST_FAILED => -1,
+};
+
+__PACKAGE__->class({
+ ctx_of => '%',
+ dest => '$',
+ dest_lock => '$',
+ error => {},
+ inherit_ctx_list => '@',
+ option_of => '%',
+ prev_ctx => __PACKAGE__,
+ status => {isa => '$', default => ST_UNKNOWN},
+ steps => '@',
+});
+
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::Context::Make
+
+=head1 SYNOPSIS
+
+ use FCM::Context::Make;
+ my $ctx = FCM::Context::Make->new();
+
+=head1 DESCRIPTION
+
+Provides a context object for the FCM make system. It is a sub-class of
+L<FCM::Class::HASH|FCM::Class::HASH>.
+
+=head1 OBJECTS
+
+=head2 FCM::Context::Make
+
+An instance of this class represents a make. It has the following
+attributes:
+
+=over 4
+
+=item ctx_of
+
+A HASH containing the (keys) IDs and the (values) context objects of the make.
+
+=item dest
+
+The destination of this make.
+
+=item dest_lock
+
+The destination lock of this make.
+
+=item error
+
+This should be set to the value of the exception, if this make ends in one.
+
+=item inherit_ctx_list
+
+An ARRAY of contexts inherited by this make.
+
+=item option_of
+
+A HASH to store the options of this make. See L</OPTION> for detail.
+
+=item status
+
+The status of the make.
+
+=item steps
+
+The names of the steps to make.
+
+=back
+
+=head1 OPTION
+
+The C<option_of> attribute of a FCM::Context::Make object may contain the
+following elements:
+
+=over 4
+
+=item config-file
+
+An ARRAY of configuration file names.
+
+=item directory
+
+The working directory of the make.
+
+=item ignore-lock
+
+Ignores lock file in the destination.
+
+=item jobs
+
+The number of (child) threads that can be run simultaneously.
+
+=item new
+
+Performs a make in "new" mode (as opposed to the "incremental" mode).
+
+=back
+
+=head1 CONSTANTS
+
+=over 4
+
+=item FCM::Context::Make->ST_UNKNOWN
+
+The status of a make context or the context of a subsystem. Status is unknown.
+
+=item FCM::Context::Make->ST_INIT
+
+The status of a make context or the context of a subsystem. The make or the
+subsystem has initialised, but not completed.
+
+=item FCM::Context::Make->ST_OK
+
+The status of a make context or the context of a subsystem. The make or the
+subsystem has completed successfully.
+
+=item FCM::Context::Make->ST_FAILED
+
+The status of a make context or the context of a subsystem. The make or the
+subsystem has failed.
+
+=back
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/Context/Make/Build.pm b/lib/FCM/Context/Make/Build.pm
new file mode 100644
index 0000000..8489963
--- /dev/null
+++ b/lib/FCM/Context/Make/Build.pm
@@ -0,0 +1,481 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+
+# ------------------------------------------------------------------------------
+package FCM::Context::Make::Build;
+use base qw{FCM::Class::HASH};
+
+use FCM::Context::Make;
+
+use constant {
+ CTX_SOURCE => 'FCM::Context::Make::Build::Source',
+ CTX_TARGET => 'FCM::Context::Make::Build::Target',
+ ID_OF_CLASS => 'build',
+};
+
+my $ST_UNKNOWN = FCM::Context::Make->ST_UNKNOWN;
+
+__PACKAGE__->class(
+ { dest => '$',
+ dests => '@',
+ id => {isa => '$' , default => ID_OF_CLASS},
+ id_of_class => {isa => '$' , default => ID_OF_CLASS},
+ input_ns_excl => '@',
+ input_ns_incl => '@',
+ input_source_of => '%',
+ prop_of => '%',
+ source_of => '%',
+ status => {isa => '$' , default => $ST_UNKNOWN},
+ target_of => '%',
+ target_key_of => '%',
+ target_select_by => '%',
+ },
+);
+
+# ------------------------------------------------------------------------------
+package FCM::Context::Make::Build::Source;
+use base qw{FCM::Class::HASH};
+
+__PACKAGE__->class({
+ checksum => '$',
+ deps => '@',
+ info_of => '%',
+ ns => '$',
+ path => '$',
+ prop_of => '%',
+ type => '$',
+ up_to_date => '$',
+});
+
+# ------------------------------------------------------------------------------
+package FCM::Context::Make::Build::Target;
+use base qw{FCM::Class::HASH};
+
+use constant {
+ CT_BIN => 'bin',
+ CT_ETC => 'etc',
+ CT_INCLUDE => 'include',
+ CT_LIB => 'lib',
+ CT_O => 'o',
+ CT_SRC => 'src',
+ POLICY_CAPTURE => 'POLICY_CAPTURE',
+ POLICY_FILTER => 'POLICY_FILTER',
+ POLICY_FILTER_IMMEDIATE => 'POLICY_FILTER_IMMEDIATE',
+ ST_FAILED => 'ST_FAILED',
+ ST_MODIFIED => 'ST_MODIFIED',
+ ST_OOD => 'ST_OOD',
+ ST_UNCHANGED => 'ST_UNCHANGED',
+ ST_UNKNOWN => 'ST_UNKNOWN',
+};
+
+__PACKAGE__->class(
+ { category => '$',
+ checksum => '$',
+ deps => '@',
+ dep_policy_of => '%',
+ failed_by => '@',
+ info_of => '%',
+ key => '$',
+ ns => '$',
+ path => '$',
+ path_of_prev => '$',
+ path_of_source => '$',
+ prop_of => '%',
+ prop_of_prev_of => '%',
+ status => {isa => '$', default => ST_UNKNOWN},
+ status_of => '%',
+ task => '$',
+ triggers => '@',
+ type => '$',
+ },
+);
+
+# Returns true if target has a usable dest status.
+sub can_be_source {
+ $_[0]->get_category() && $_[0]->get_category() eq CT_SRC;
+}
+
+# Returns true if target has an OK status.
+sub is_ok {
+ $_[0]->get_status() eq ST_MODIFIED || $_[0]->get_status() eq ST_UNCHANGED;
+}
+
+# Returns true if target has a failed status.
+sub is_failed {
+ $_[0]->get_status() eq ST_FAILED;
+}
+
+# Shorthand for $target->get_status() eq $target->ST_MODIFIED.
+sub is_modified {
+ $_[0]->get_status() eq ST_MODIFIED;
+}
+
+# Shorthand for $target->get_status() eq $target->ST_UNCHANGED.
+sub is_unchanged {
+ $_[0]->get_status() eq ST_UNCHANGED;
+}
+
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::Context::Make::Build
+
+=head1 SYNOPSIS
+
+ use FCM::Context::Make::Build;
+ my $ctx = FCM::Context::Make::Build->new();
+
+=head1 DESCRIPTION
+
+Provides a context object for the FCM build system. All the classes described
+below are sub-classes of L<FCM::Class::HASH|FCM::Class::HASH>.
+
+=head1 OBJECTS
+
+=head2 FCM::Context::Make::Build
+
+An instance of this class represents a build. It has the following
+attributes:
+
+=over 4
+
+=item dest
+
+The destination of the build.
+
+=item dests
+
+An ARRAY containing the path for searching items in the current build.
+
+=item id
+
+The ID of the context. (default="build")
+
+=item id_of_class
+
+The class ID of the context. (default="build")
+
+=item input_source_of
+
+A HASH to map a name space to its ARRAY of input sources.
+
+=item input_ns_excl
+
+An ARRAY of source name-spaces to exclude.
+
+=item input_ns_incl
+
+An ARRAY of source name-spaces to include.
+
+=item prop_of
+
+A HASH containing the named properties (i.e. options and settings of named
+external tools). Expects a value to be an instance of
+L<FCM::Context::Make::Share::Property|FCM::Context::Make::Share::Property>.
+
+=item prop_of_prev_of
+
+A HASH containing the named properties (i.e. options and settings of named
+external tools) in the latest successful update of this target. Expects a value
+to be an instance of
+L<FCM::Context::Make::Share::Property|FCM::Context::Make::Share::Property>.
+
+=item source_of
+
+A HASH to map the namespace to the source contexts. Each element is expected to
+be an L</FCM::Context::Make::Build::Source> object.
+
+=item status
+
+The status of this context. See L<FCM::Context::Make|FCM::Context::Make> for the
+status constants.
+
+=item target_of
+
+A HASH to map the namespace to the target contexts. Each element is expected to
+be an L</FCM::Context::Make::Build::Target> object.
+
+=item target_key_of
+
+A HASH to map the automatic key of targets to their desired key.
+
+=item target_select_by
+
+A HASH to allow users to specify how to select from all the targets. The key can
+be "category", "key", "ns" or "task". Each value should be a HASH that
+represents the set of criteria.
+
+=back
+
+=head2 FCM::Context::Make::Build::Source
+
+An instance of this class represents an actual source of the build. It has the
+following attributes:
+
+=over 4
+
+=item checksum
+
+The MD5 checksum of the source file.
+
+=item deps
+
+An ARRAY to contain the dependencies of the source file. Each element of the
+ARRAY is expected to be a reference to a two-element ARRAY [$name, $type] where
+$name is the name of the dependency and $type is its type.
+
+=item info_of
+
+A HASH to contain the extra information of the source file. E.g. If the {main}
+element is true, the source contains a main program. If the {symbols} element
+is defined, it contains a reference to an ARRAY of program unit symbols that has
+been found in the source file.
+
+=item ns
+
+The name-space of the source file.
+
+=item path
+
+The path in the file system pointing to the source file.
+
+=item prop_of
+
+A HASH containing the keys and the values of the build properties (mainly on
+dependency settings) of the source.
+
+=item type
+
+The file type of the source file.
+
+=item up_to_date
+
+A flag to indicate whether the source file is up to date, compared with a
+previous build or the nearest inherited build.
+
+=back
+
+=head2 FCM::Context::Make::Build::Target
+
+An instance of this class represents a target of the build. It has the following
+attributes:
+
+=over 4
+
+=item category
+
+The target category, e.g. bin, etc, include, lib, o, src
+
+=item checksum
+
+The MD5 checksum of the target.
+
+=item deps
+
+An ARRAY containing the dependencies of the target. Each element of the
+ARRAY is expected to be a reference to a two-element ARRAY [$name, $type] where
+$name is the name of the dependency and $type is its type.
+
+=item dep_policy_of
+
+A HASH to contain a map between each relevant dependency type of this target and
+its policy to apply to the dependency type. The policy should take the value of
+POLICY_CAPTURE, POLICY_FILTER or POLICY_FILTER_IMMEDIATE.
+
+=item failed_by
+
+On failure, set to an ARRAY containing the names of the targets that cause the
+failure of this target.
+
+=item info_of
+
+A HASH to contain the extra information of the target. E.g. The {paths} => ARRAY
+reference of include/object search paths for the compile, link and preprocess
+tasks; and {deps}{o} => ARRAY reference and {deps}{o.special} => ARRAY
+reference of object dependency for the link tasks.
+
+=item key
+
+The key (i.e. the base name) of the target.
+
+=item ns
+
+The name-space (of the source file) associated with this target.
+
+=item path
+
+The path in the file system where the target can be located.
+
+=item path_of_prev
+
+The path in the file system where the target in a previous or inherited build
+can be located.
+
+=item path_of_source
+
+The path in the file system where the source file associated with the target can
+be located.
+
+=item prop_of
+
+A HASH containing the keys and the values of the build properties of the target.
+
+=item status
+
+The status of the target.
+
+=item status_of
+
+A HASH containing the status of dependency types that may be relevant to targets
+higher up in the dependency tree.
+
+=item task
+
+The target type, (i.e. the name of the task to update with the target).
+
+=item triggers
+
+An ARRAY reference of the keys of targets that should be automatically triggered
+by this target.
+
+=item type
+
+The type of the source that gives this target.
+
+=back
+
+In addition, an instance of FCM::Context::Make::Extract::Target has the
+following methods:
+
+=over 4
+
+=item $target->can_be_source()
+
+Returns true if the destination status indicates that the target is usable as a
+source file of a subsequent a make (step).
+
+=item $target->is_ok()
+
+Returns true if the target has a OK destination status.
+
+=item $target->is_failed()
+
+Returns true if the target has a failed destination status.
+
+=item $target->is_modified()
+
+Shorthand for $target->get_status() eq $target->ST_MODIFIED.
+
+=item $target->is_unchanged()
+
+Shorthand for $target->get_status() eq $target->ST_UNCHANGED.
+
+=back
+
+=head1 CONSTANTS
+
+The following is a list of constants:
+
+=over 4
+
+=item FCM::Context::Make::Build->CTX_INPUT
+
+Alias of FCM::Context::Make::Build::Input.
+
+=item FCM::Context::Make::Build->CTX_SOURCE
+
+Alias of FCM::Context::Make::Build::Source.
+
+=item FCM::Context::Make::Build->ID_OF_CLASS
+
+The default value of the "id" attribute (of an instance), and the ID of the
+functional class. ("build")
+
+=item FCM::Context::Make::Build::Target->CT_BIN
+
+Target category, "bin", executable.
+
+=item FCM::Context::Make::Build::Target->CT_ETC
+
+Target category, "etc", data and misc file.
+
+=item FCM::Context::Make::Build::Target->CT_INCLUDE
+
+Target category, "include", include file.
+
+=item FCM::Context::Make::Build::Target->CT_LIB
+
+Target category, "lib", program library.
+
+=item FCM::Context::Make::Build::Target->CT_O
+
+Target category, "o", compiled object file.
+
+=item FCM::Context::Make::Build::Target->CT_SRC
+
+Target category, "src", generated source file.
+
+=item FCM::Context::Make::Build::Target->POLICY_CAPTURE
+
+Indicates that the dependency type is relevant to the target, and the build
+engine should stop floating the dependency target up the dependency tree.
+
+=item FCM::Context::Make::Build::Target->POLICY_FILTER
+
+Indicates that the dependency type is relevant to the target, and the build
+engine may float the dependency target up the dependency tree as well.
+
+=item FCM::Context::Make::Build::Target->POLICY_FILTER_IMMEDIATE
+
+Indicates that the dependency type is relevant to the target but only if the
+dependency target is an immediate dependency of this target, and the build
+engine may float the dependency target up the dependency tree as well.
+
+=item FCM::Context::Make::Build::Target->ST_FAILED
+
+Indicates that the build has failed to update the target.
+
+=item FCM::Context::Make::Build::Target->ST_MODIFIED
+
+Indicates that the target is out of date and has been modified by the build.
+
+=item FCM::Context::Make::Build::Target->ST_OOD
+
+Indicates that the target is out of date.
+
+=item FCM::Context::Make::Build::Target->ST_UNCHANGED
+
+Indicates that the target is up to date and unchanged by the build.
+
+=item FCM::Context::Make::Build::Target->ST_UNKNOWN
+
+Indicates an unknown target status.
+
+=back
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/Context/Make/Extract.pm b/lib/FCM/Context/Make/Extract.pm
new file mode 100644
index 0000000..8e81b39
--- /dev/null
+++ b/lib/FCM/Context/Make/Extract.pm
@@ -0,0 +1,494 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+
+# ------------------------------------------------------------------------------
+package FCM::Context::Make::Extract;
+use base qw{FCM::Class::HASH};
+
+use FCM::Context::Make;
+
+use constant {
+ CTX_PROJECT => 'FCM::Context::Make::Extract::Project',
+ CTX_SOURCE => 'FCM::Context::Make::Extract::Source',
+ CTX_TARGET => 'FCM::Context::Make::Extract::Target',
+ CTX_TREE => 'FCM::Context::Make::Extract::Tree',
+ ID_OF_CLASS => 'extract',
+ MIRROR => 1,
+};
+
+__PACKAGE__->class({
+ dest => '$',
+ id => {isa => '$', default => ID_OF_CLASS},
+ id_of_class => {isa => '$', default => ID_OF_CLASS},
+ ns_list => '@',
+ project_of => '%',
+ status => {isa => '$', default => FCM::Context::Make->ST_UNKNOWN},
+ target_of => '%',
+ prop_of => '%',
+});
+
+# ------------------------------------------------------------------------------
+package FCM::Context::Make::Extract::Project;
+use base qw{FCM::Class::HASH};
+
+__PACKAGE__->class({
+ cache => '$',
+ inherited => '$',
+ locator => 'FCM::Context::Locator',
+ ns => '$',
+ path_excl => '@',
+ path_incl => '@',
+ path_root => {isa => '$', default => q{}},
+ trees => '@',
+});
+
+# ------------------------------------------------------------------------------
+package FCM::Context::Make::Extract::Tree;
+use base qw{FCM::Class::HASH};
+
+__PACKAGE__->class({
+ cache => '$',
+ inherited => '$',
+ key => '$',
+ locator => 'FCM::Context::Locator',
+ ns => '$',
+ sources => '@',
+});
+
+# ------------------------------------------------------------------------------
+package FCM::Context::Make::Extract::Source;
+use base qw{FCM::Class::HASH};
+
+use constant {
+ ST_NORMAL => 'ST_NORMAL',
+ ST_UNCHANGED => 'ST_UNCHANGED',
+ ST_MISSING => 'ST_MISSING',
+};
+
+__PACKAGE__->class({
+ cache => '$',
+ key_of_tree => '$',
+ locator => 'FCM::Context::Locator',
+ ns => '$',
+ ns_in_tree => '$',
+ status => {isa => '$', default => ST_NORMAL},
+});
+
+# Shorthand for $source->get_status() eq $source->ST_MISSING.
+sub is_missing {
+ $_[0]->get_status() eq ST_MISSING;
+}
+
+# Shorthand for $source->get_status() eq $source->ST_UNCHANGED.
+sub is_unchanged {
+ $_[0]->get_status() eq ST_UNCHANGED;
+}
+
+# ------------------------------------------------------------------------------
+package FCM::Context::Make::Extract::Target;
+use base qw{FCM::Class::HASH};
+
+use constant {
+ ST_ADDED => 'ST_ADDED',
+ ST_DELETED => 'ST_DELETED',
+ ST_MERGED => 'ST_MERGED',
+ ST_MODIFIED => 'ST_MODIFIED',
+ ST_O_ADDED => 'ST_O_ADDED',
+ ST_O_DELETED => 'ST_O_DELETED',
+ ST_UNCHANGED => 'ST_UNCHANGED',
+ ST_UNKNOWN => 'ST_UNKNOWN',
+ can_be_source => 1,
+};
+
+__PACKAGE__->class({
+ dests => '@',
+ ns => '$',
+ path => '$',
+ source_of => '%',
+ status => {isa => '$', default => ST_UNKNOWN},
+ status_of_source => {isa => '$', default => ST_UNKNOWN},
+});
+
+# Returns true if target has an OK status.
+sub is_ok {
+ my ($self) = @_;
+ my $status = $self->get_status();
+ grep {$_ eq $status}
+ (ST_ADDED, ST_MERGED, ST_MODIFIED, ST_O_ADDED, ST_UNCHANGED);
+}
+
+# Shorthand for $target->get_status() eq $target->ST_UNCHANGED.
+sub is_unchanged {
+ $_[0]->get_status() eq ST_UNCHANGED;
+}
+
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::Context::Make::Extract
+
+=head1 SYNOPSIS
+
+ use FCM::Context::Make::Extract;
+ my $ctx = FCM::Context::Make::Extract->new();
+
+=head1 DESCRIPTION
+
+Provides a context object for the FCM extract system. All the classes described
+below are sub-classes of L<FCM::Class::HASH|FCM::Class::HASH>.
+
+=head1 OBJECTS
+
+=head2 FCM::Context::Make::Extract
+
+An instance of this class represents an extract. It has the following
+attributes:
+
+=over 4
+
+=item dest
+
+The destination of the extract.
+
+=item id
+
+The ID of the current context. (default="extract")
+
+=item id_of_class
+
+The class ID of the current context. (default="extract")
+
+=item ns_list
+
+An ARRAY of name-spaces of the projects to extract.
+
+=item project_of
+
+A HASH to map (key) the name-spaces of the projects in this extract to (value)
+their corresponding contexts.
+
+=item prop_of
+
+A HASH containing the named properties (i.e. options and settings of named
+external tools). Expects a value to be an instance of
+L<FCM::Context::Make::Share::Property|FCM::Context::Make::Share::Property>.
+
+=item status
+
+The status of the extract. See L<FCM::Context::Make|FCM::Context::Make> for the
+status constants.
+
+=item target_of
+
+A HASH to map (key) the name-spaces of the targets in this extract to (value)
+their corresponding contexts.
+
+=back
+
+=head2 FCM::Context::Make::Extract::Project
+
+An instance of this class represents a project in an extract. It has the
+following attributes:
+
+=over 4
+
+=item cache
+
+The file system location (cache) of this project.
+
+=item inherited
+
+This project is inherited?
+
+=item locator
+
+An instance of L<FCM::Context::Locator|FCM::Context::Locator> that represents
+the locator of this project.
+
+=item ns
+
+The name-space of this project.
+
+=item path_excl
+
+An ARRAY of patterns to match the names of the paths that will be excluded in
+this project.
+
+=item path_incl
+
+An ARRAY of patterns to match the names of the paths that will always be
+included in this project.
+
+=item path_root
+
+The relative path in a project tree for the root name-space. If this is
+specified, the system will extract only files under this path, and their
+name-spaces will be adjusted to be relative to this path.
+
+=item trees
+
+An ARRAY of the tree contexts in this project. By convention, the 0th element is
+the base tree.
+
+=back
+
+=head2 FCM::Context::Make::Extract::Tree
+
+An instance of this class represents a tree in a project. It has the following
+attributes:
+
+=over 4
+
+=item cache
+
+The file system location (cache) of this tree.
+
+=item inherited
+
+A flag to indicate whether this tree is provided by an inherited extract.
+
+=item key
+
+The key of this tree. By convention, the base tree is the 0th key.
+
+=item locator
+
+An instance of L<FCM::Context::Locator|FCM::Context::Locator> that represents
+the locator of this tree.
+
+=item ns
+
+The name-space of the project in which this tree belongs.
+
+=item sources
+
+An ARRAY of source file contexts provided by this tree.
+
+=back
+
+=head2 FCM::Context::Make::Extract::Source
+
+An instance of this class represents a source file provided by a project tree.
+It has the following attributes:
+
+=over 4
+
+=item cache
+
+The file system location (cache) of this source file.
+
+=item key_of_tree
+
+The key of the tree that provides this source file.
+
+=item locator
+
+An instance of L<FCM::Context::Locator|FCM::Context::Locator> that represents
+the locator of the source file.
+
+=item ns
+
+The full (mapped) name-space of the source file, (including the leading project
+name-space).
+
+=item ns_in_tree
+
+The original name-space of the source file, relative to its path in the tree.
+
+=item status
+
+The status of the source file. It can take the value of one of the
+FCM::Context::Make::Extract::Source->ST_* constants. See </CONSTANTS> for detail.
+
+=back
+
+In addition, an instance of FCM::Context::Make::Extract::Source has the
+following methods:
+
+=over 4
+
+=item $source->is_missing()
+
+Shorthand for $source->get_status() eq $source->ST_MISSING.
+
+=item $source->is_unchanged()
+
+Shorthand for $source->get_status() eq $source->ST_UNCHANGED.
+
+=back
+
+=head2 FCM::Context::Make::Extract::Target
+
+An instance of this class represents an extract target. It has the following
+attributes:
+
+=over 4
+
+=item dests
+
+An ARRAY containing the destination search path of this target. The first
+element is the path to the destination of the current extract, and the rest are
+destinations to inherited extracts.
+
+=item ns
+
+The full name-space of this target.
+
+=item path
+
+Returns the actual destination path of this target.
+
+=item source_of
+
+A HASH for mapping (key) the keys of the trees to (value) the corresponding
+contexts of the source files provided by the trees to this target.
+
+=item status
+
+The status of the target destination. It can take the value of one of the
+FCM::Context::Make::Extract::Target->ST_* constants. See </CONSTANTS> for detail.
+
+=item status_of_source
+
+The status of the target, with respect to its sources. It can take the value of
+one of the FCM::Context::Make::Extract::Target->ST_* constants. See </CONSTANTS>
+for detail.
+
+=back
+
+In addition, an instance of FCM::Context::Make::Extract::Target has the
+following methods:
+
+=over 4
+
+=item $target->can_be_source()
+
+Returns true if the destination status indicates that the target is usable as a
+source file of a subsequent a make (step).
+
+=item $target->is_ok()
+
+Returns true if the target has a OK destination status.
+
+=item $target->is_unchanged()
+
+Shorthand for $target->get_status() eq $target->ST_UNCHANGED.
+
+=back
+
+=head1 CONSTANTS
+
+The following is a list of constants:
+
+=over 4
+
+=item FCM::Context::Make::Extract->CTX_PROJECT
+
+An alias to FCM::Context::Make::Extract::Project.
+
+=item FCM::Context::Make::Extract->CTX_SOURCE
+
+An alias to FCM::Context::Make::Extract::Source.
+
+=item FCM::Context::Make::Extract->CTX_TARGET
+
+An alias to FCM::Context::Make::Extract::Target.
+
+=item FCM::Context::Make::Extract->CTX_TREE
+
+An alias to FCM::Context::Make::Extract::Tree.
+
+=item FCM::Context::Make::Extract->ID_OF_CLASS
+
+The default value of the "id" attribute (of an instance), and the ID of the
+functional class. ("extract")
+
+=item FCM::Context::Make::Extract->MIRROR
+
+A flag to tell the mirror sub-system that the targets of this context can be
+used as inputs sources to subsequent steps for the configuration file in the
+mirror destination.
+
+=item FCM::Context::Make::Extract::Source->ST_NORMAL
+
+Source status: normal.
+
+=item FCM::Context::Make::Extract::Source->ST_UNCHANGED
+
+Source status: source is unchanged (against base).
+
+=item FCM::Context::Make::Extract::Source->ST_MISSING
+
+Source status: source is a placeholder in a target. It does not actually exist
+in the source tree.
+
+=item FCM::Context::Make::Extract::Target->ST_ADDED
+
+As destination status: new file in the target destination. As source status:
+added by a source in a diff tree.
+
+=item FCM::Context::Make::Extract::Target->ST_DELETED
+
+As destination status: file removed from the target destination. As source
+status: target removed by a diff tree.
+
+=item FCM::Context::Make::Extract::Target->ST_MERGED
+
+As source status: modified by 2 or more diff trees.
+
+=item FCM::Context::Make::Extract::Target->ST_MODIFIED
+
+As destination status: target destination is modified. As source status:
+modified by 1 diff tree.
+
+=item FCM::Context::Make::Extract::Target->ST_O_ADDED
+
+As destination status: new file in the target destination, overriding a file in
+an inherited destination.
+
+=item FCM::Context::Make::Extract::Target->ST_O_DELETED
+
+As destination status: target destination should be removed, but there is still
+a file in an inherited destination.
+
+=item FCM::Context::Make::Extract::Target->ST_UNCHANGED
+
+As destination status: target destination is unchanged. As source status:
+unchanged by a diff tree.
+
+=item FCM::Context::Make::Extract::Target->ST_UNKNOWN
+
+Status is unknown.
+
+=back
+
+=head1 SEE ALSO
+
+L<FCM::System::Make::Extract|FCM::System::Make::Extract>
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/Context/Make/Mirror.pm b/lib/FCM/Context/Make/Mirror.pm
new file mode 100644
index 0000000..3f6110b
--- /dev/null
+++ b/lib/FCM/Context/Make/Mirror.pm
@@ -0,0 +1,119 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+# ------------------------------------------------------------------------------
+package FCM::Context::Make::Mirror;
+use base qw{FCM::Class::HASH};
+
+use FCM::Context::Make;
+
+use constant {ID_OF_CLASS => 'mirror'};
+
+__PACKAGE__->class({
+ dest => '$',
+ id => {isa => '$', default => ID_OF_CLASS},
+ id_of_class => {isa => '$', default => ID_OF_CLASS},
+ prop_of => '%',
+ status => {isa => '$', default => FCM::Context::Make->ST_UNKNOWN},
+ target_logname => '$',
+ target_machine => '$',
+ target_path => '$',
+});
+
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::Context::Make::Mirror
+
+=head1 SYNOPSIS
+
+ use FCM::Context::Make::Mirror;
+ my $ctx = FCM::Context::Make::Mirror->new();
+ $ctx->set_dest_path($dest_path);
+ $ctx->set_source_path($source_path);
+ # ...
+
+=head1 DESCRIPTION
+
+Provides a context object for the mirror sub-system.
+
+=head1 ATTRIBUTES
+
+This class is based on L<FCM::Class::HASH|FCM::Class::HASH>. All attributes are
+accessible via $ctx->get_$attrib() and $ctx->set_$attrib($value) methods.
+
+=over 4
+
+=item dest
+
+The local working directory for the mirror sub-system.
+
+=item id
+
+The ID of the context. (default="mirror")
+
+=item id_of_class
+
+The class ID of the context. (default="mirror")
+
+=item prop_of
+
+A HASH containing the named properties (i.e. options and settings of named
+external tools). Expects a value to be an instance of
+L<FCM::Context::Make::Share::Property|FCM::Context::Make::Share::Property>.
+
+=item status
+
+The status of the context. See L<FCM::Context::Make|FCM::Context::Make> for the
+status constants.
+
+=item target_logname
+
+The logname part of the authority of the mirror destination.
+
+=item target_machine
+
+The machine part of the authority of the mirror destination.
+
+=item target_path
+
+The container path of the mirror destination (without the authority).
+
+=back
+
+=head1 CONSTANTS
+
+=over 4
+
+=item ID_OF_CLASS
+
+The default value of the "id" attribute (of an instance), and the ID of the
+functional class. ("mirror")
+
+=back
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/Context/Make/Share/Property.pm b/lib/FCM/Context/Make/Share/Property.pm
new file mode 100644
index 0000000..cc81196
--- /dev/null
+++ b/lib/FCM/Context/Make/Share/Property.pm
@@ -0,0 +1,138 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+# ------------------------------------------------------------------------------
+package FCM::Context::Make::Share::Property;
+use base qw{FCM::Class::HASH};
+
+use constant {
+ CTX_VALUE => 'FCM::Context::Make::Share::Property::Value',
+ NS_OF_ROOT => q{},
+};
+
+__PACKAGE__->class({ctx_of => '%', id => '$'});
+
+sub get_ctx {
+ $_[0]->get_ctx_of(NS_OF_ROOT);
+}
+
+sub set_ctx {
+ $_[0]->get_ctx_of()->{$_[0]->NS_OF_ROOT} = $_[1];
+}
+
+# ------------------------------------------------------------------------------
+package FCM::Context::Make::Share::Property::Value;
+use base qw{FCM::Class::HASH};
+
+__PACKAGE__->class({inherited => '$', value => '$'});
+
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::Context::Make::Share::Property
+
+=head1 SYNOPSIS
+
+ use FCM::Context::Make::Share::Property;
+ $prop = FCM::Context::Make::Share::Property->new(\%attrib);
+
+=head1 DESCRIPTION
+
+Provides a context object to store the property of a named shell command.
+
+=head1 OBJECTS
+
+The classes described below are all sub-classes of
+L<FCM::Class::HASH|FCM::Class::HASH>.
+
+=head2 FCM::Context::Make::Share::Property
+
+This class represents a property. It has the following attributes:
+
+=over 4
+
+=item ctx_of
+
+A HASH to map (keys) the name-spaces to (values) the contexts of this property.
+Expects each context to be an instance of
+L</FCM::Context::Make::Share::Property::Value>.
+
+The context of a simple property is stored in the root (i.e. the empty string)
+name-space.
+
+=item id
+
+The ID of this property.
+
+=back
+
+An instance of FCM::Context::Make::Share::Property has 2 additional methods:
+
+=over 4
+
+=item $instance->get_ctx()
+
+Shorthand for:
+
+ $instance->get_ctx_of(q{}).
+
+=item $instance->set_ctx($ctx)
+
+Shorthand for:
+
+ $instance->get_ctx_of()->{q{}} = $ctx.
+
+=back
+
+=head2 FCM::Context::Make::Share::Property::Value
+
+This class represents a property value (associated with a name-space). It has
+the following attributes:
+
+=over 4
+
+=item inherited
+
+A flag, if true, indicates that this value is inherited.
+
+=item value
+
+The value.
+
+=back
+
+=head1 CONSTANTS
+
+=over 4
+
+=item FCM::Context::Make::Share::Property->NS_OF_ROOT
+
+The root name-space, an empty string.
+
+=back
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/Context/Task.pm b/lib/FCM/Context/Task.pm
new file mode 100644
index 0000000..d49fe6c
--- /dev/null
+++ b/lib/FCM/Context/Task.pm
@@ -0,0 +1,105 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+# ------------------------------------------------------------------------------
+package FCM::Context::Task;
+use base qw{FCM::Class::HASH};
+
+use constant {
+ ST_FAILED => 'ST_FAILED',
+ ST_OK => 'ST_OK',
+ ST_WORKING => 'ST_WORKING',
+};
+
+__PACKAGE__->class({
+ ctx => {},
+ error => {},
+ id => '$',
+ elapse => {isa => '$', default => 0},
+ state => {isa => '$', default => ST_WORKING},
+});
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::Context::Task
+
+=head1 SYNOPSIS
+
+ use FCM::Context::Task;
+ my $task = FCM::Context::Task->new(\%attrib);
+
+=head1 DESCRIPTION
+
+An instance of this class represents the generic context for a task for the
+L<FCM::Util->task_runner()|FCM::Util>. This class is a sub-class of
+L<FCM::Class::HASH|FCM::Class::HASH>.
+
+=head1 ATTRIBUTES
+
+=over 4
+
+=item ctx
+
+The specific context of the task, such as the inputs and the outputs.
+
+=item error
+
+If the task failed, the error/exception will be returned in this attribute.
+
+=item id
+
+The ID of the task.
+
+=item elapse
+
+The amount of time (in seconds) taken to run the task.
+
+=item state
+
+The state of the task. See L</CONSTANTS> for possible variables.
+
+=back
+
+=head1 CONSTANTS
+
+=over 4
+
+=item FCM::Context::Task->ST_FAILED
+
+A status to indicate that the task has failed.
+
+=item FCM::Context::Task->ST_OK
+
+A status to indicate that the task is completed successfully.
+
+=item FCM::Context::Task->ST_WORKING
+
+A status to indicate that the task is bing worked on.
+
+=back
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/Exception.pm b/lib/FCM/Exception.pm
new file mode 100644
index 0000000..edd45c2
--- /dev/null
+++ b/lib/FCM/Exception.pm
@@ -0,0 +1,113 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+
+# ------------------------------------------------------------------------------
+package FCM::Exception;
+
+use Data::Dumper qw{Dumper};
+use Scalar::Util qw{blessed};
+use overload (q{""} => \&Dumper);
+
+use constant {
+ DEFAULT => 'DEFAULT',
+};
+
+# Returns true if $e is a blessed instance of $class.
+sub caught {
+ my ($class, $e) = @_;
+ return (blessed($e) && $e->isa($class));
+}
+
+# Throws the exception.
+sub throw {
+ my ($class, $code, $ctx, $e) = @_;
+ if (defined($e) && !ref($e) && $e =~ qr{\A\s*\z}msx) {
+ $e = undef;
+ }
+ die(bless({code => $code, ctx => $ctx, exception => $e}, $class));
+}
+
+# Attribute accessors.
+for my $name (qw{code ctx exception}) {
+ no strict qw{refs};
+ *{"get_$name"} = sub {$_[0]->{$name}};
+}
+
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::Exception
+
+=head1 SYNOPSIS
+
+ use FCM::Exception;
+ my $E = 'FCM::Exception';
+ eval {
+ # ...
+ if ($some_error_condition) {
+ return $E->throw($code, $ctx);
+ }
+ # ...
+ };
+ if (my $e = $@) {
+ if ($E->caught($e)) {
+ # ...
+ }
+ }
+
+=head1 DESCRIPTION
+
+Exception associated with an FCM operation.
+
+=head1 METHODS
+
+=over 4
+
+=item $class->caught($e)
+
+Returns true if $e is a blessed object of $class.
+
+=item $class->throw($code,$ctx,$e)
+
+Creates an instance and die() with it.
+
+=item $e->get_code()
+
+Returns the code associated with this exception.
+
+=item $e->get_ctx()
+
+Returns the context associated with this exception.
+
+=item $e->get_exception()
+
+Returns the exception that generates this exception.
+
+=back
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/System.pm b/lib/FCM/System.pm
new file mode 100644
index 0000000..5d06d0e
--- /dev/null
+++ b/lib/FCM/System.pm
@@ -0,0 +1,287 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+# ------------------------------------------------------------------------------
+
+package FCM::System;
+use base qw{FCM::Class::CODE};
+
+use FCM::Util;
+use Scalar::Util qw{reftype};
+
+# Alias
+our $S;
+
+# The (keys) named actions of this class and (values) their implementations.
+our %ACTION_OF = (
+ browse => _func('misc', sub {$S->browse(@_)}),
+ build => _func('old' , sub {$S->build(@_)}),
+ config_compare => _func('old' , sub {$S->config_compare(@_)}),
+ config_parse => _func('misc', sub {$S->config_parse(@_)}),
+ cm_branch_create => _func('cm' , sub {$S->cm_branch_create(@_)}),
+ cm_branch_delete => _func('cm' , sub {$S->cm_branch_delete(@_)}),
+ cm_branch_diff => _func('cm' , sub {$S->cm_branch_diff(@_)}),
+ cm_branch_info => _func('cm' , sub {$S->cm_branch_info(@_)}),
+ cm_branch_list => _func('cm' , sub {$S->cm_branch_list(@_)}),
+ cm_commit => _func('cm' , sub {$S->cm_commit(@_)}),
+ cm_checkout => _func('cm' , sub {$S->cm_checkout(@_)}),
+ cm_check_missing => _func('cm' , sub {$S->cm_check_missing(@_)}),
+ cm_check_unknown => _func('cm' , sub {$S->cm_check_unknown(@_)}),
+ cm_diff => _func('cm' , sub {$S->cm_diff(@_)}),
+ cm_loc_layout => _func('cm' , sub {$S->cm_loc_layout(@_)}),
+ cm_merge => _func('cm' , sub {$S->cm_merge(@_)}),
+ cm_mkpatch => _func('cm' , sub {$S->cm_mkpatch(@_)}),
+ cm_project_create => _func('cm' , sub {$S->cm_project_create(@_)}),
+ cm_resolve_conflicts => _func('cm' , sub {$S->cm_resolve_conflicts(@_)}),
+ cm_switch => _func('cm' , sub {$S->cm_switch(@_)}),
+ cm_update => _func('cm' , sub {$S->cm_update(@_)}),
+ export_items => _func('misc', sub {$S->export_items(@_)}),
+ extract => _func('old' , sub {$S->extract(@_)}),
+ keyword_find => _func('misc', sub {$S->keyword_find(@_)}),
+ make => _func('make', sub {$S->main(@_)}),
+ svn => _func('cm' , sub {$S->svn(@_)}),
+ util => sub {$_[0]->{util}},
+ version => _func('misc', sub {$S->version(@_)}),
+);
+# The (keys) named system and their implementation classes.
+our %SYSTEM_CLASS_OF = (
+ cm => 'FCM::System::CM',
+ old => 'FCM::System::Old',
+ make => 'FCM::System::Make',
+ misc => 'FCM::System::Misc',
+);
+
+# Creates the class.
+__PACKAGE__->class(
+ { gui => '$',
+ system_class_of => {isa => '%', default => {%SYSTEM_CLASS_OF}},
+ system_of => '%',
+ util => '&',
+ },
+ {init => \&_init, action_of => {%ACTION_OF}},
+);
+
+# Initialises attributes.
+sub _init {
+ my $attrib_ref = shift();
+ $attrib_ref->{util} = FCM::Util->new();
+}
+
+# Generates main functions.
+sub _func {
+ my ($name, $code_ref) = @_;
+ sub {
+ my ($attrib_ref, @args) = @_;
+ if (!defined($attrib_ref->{system_of}{$name})) {
+ my $class_name = $attrib_ref->{system_class_of}{$name};
+ $attrib_ref->{util}->class_load($class_name);
+ $attrib_ref->{system_of}{$name} = $class_name->new({
+ gui => $attrib_ref->{gui},
+ util => $attrib_ref->{util},
+ });
+ }
+ local($S) = $attrib_ref->{system_of}{$name};
+ $code_ref->(@args);
+ };
+}
+
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::System
+
+=head1 SYNOPSIS
+
+ use FCM::System;
+ $fcm = FCM::System->new();
+ # ...
+ $fcm->make(\%option, @args);
+
+=head1 DESCRIPTION
+
+Provides a top level interface to access the functionalities of the FCM system.
+
+=head1 METHODS
+
+=over 4
+
+=item $class->new(\%attrib)
+
+Returns a new instance. It also initialises the utility and sub-system classes.
+The %attrib hash can be used configure the behaviour of the instance:
+
+=over 4
+
+=item event
+
+A CODE to handle event.
+
+=item gui
+
+The GUI geometry of "fcm gui-internal".
+
+=item system_class_of
+
+A HASH to map (keys) sub-system names to (values) their implementation classes.
+See %FCM::System::SYSTEM_CLASS_OF.
+
+=item system_of
+
+A HASH to map (keys) sub-system names to (values) their implementation instances.
+
+=item util
+
+An instance of L<FCM::Util|FCM::Util>.
+
+=back
+
+=item $fcm->browse(\%option, at args)
+
+Invokes a browser to browse the sources in @args.
+
+=item $fcm->build(\%option, at args)
+
+(Obsolete) Invokes the FCM 1 build system.
+
+=item $fcm->config_compare(\%option, at args)
+
+(Obsolete) Compares 2 FCM 1 extract configuration files.
+
+=item $fcm->config_parse(\%option, at args)
+
+Parses a configuration file.
+
+=item $fcm->cm_branch_create(\%option, at args)
+
+Creates of a branch in a project in a Subversion repository with a standard FCM
+layout.
+
+=item $fcm->cm_branch_delete(\%option, at args)
+
+Deletes of a branch in a project in a Subversion repository with a standard FCM
+layout.
+
+=item $fcm->cm_branch_diff(\%option, at args)
+
+Displays the changes between a branch and its parent in a project in a
+Subversion repository with a standard FCM layout.
+
+=item $fcm->cm_branch_info(\%option, at args)
+
+Displays information of a branch in a project in a Subversion repository with a
+standard FCM layout.
+
+=item $fcm->cm_branch_list(\%option, at args)
+
+Lists branches in a project in a Subversion repository with a standard FCM
+layout.
+
+=item $fcm->cm_commit(\%option, at args)
+
+Wraps C<svn commit>.
+
+=item $fcm->cm_checkout(\%option, at args)
+
+Wraps C<svn checkout>.
+
+=item $fcm->cm_check_missing(\%option, at args)
+
+Checks for missing status in a Subversion working copy.
+
+=item $fcm->cm_check_unknown(\%option, at args)
+
+Checks for unknown status in a Subversion working copy.
+
+=item $fcm->cm_diff(\%option, at args)
+
+Wraps C<svn diff>.
+
+=item $fcm->cm_loc_layout(\%option, at args)
+
+Parse and print layout information of each target in @args.
+
+=item $fcm->cm_merge(\%option, at args)
+
+Wraps C<svn merge>.
+
+=item $fcm->cm_mkpatch(\%option, at args)
+
+Creates FCM patches.
+
+=item $fcm->cm_project_create(\%option, at args)
+
+Create a new project in a Subversion repository.
+
+=item $fcm->cm_resolve_conflicts(\%option, at args)
+
+Invokes a graphic merge tool to resolve conflicts.
+
+=item $fcm->cm_switch(\%option, at args)
+
+Wraps C<svn switch>.
+
+=item $fcm->cm_update(\%option, at args)
+
+Wraps C<svn update>.
+
+=item $fcm->export_items(\%option, at args)
+
+Exports directories as versioned items in a branch of a project in a Subversion
+repository with the standard FCM layout.
+
+=item $fcm->extract(\%option, at args)
+
+(Obsolete) Invokes the FCM 1 extract system.
+
+=item $fcm->keyword_find(\%option, at args)
+
+If @args is empty, search for all known FCM location keyword entries. Otherwise,
+search for FCM location keyword entries matching the locations specified in
+ at args.
+
+=item $fcm->make(\%option, at args)
+
+Invokes the FCM make system.
+
+=item $fcm->svn(\%option, at args)
+
+Invokes C<svn> with @args. %option is ignored.
+
+=item $fcm->util()
+
+Returns the L<FCM::Util|FCM::Util> object.
+
+=back
+
+=head1 DIAGNOSTICS
+
+=head2 FCM::System::Exception
+
+This exception is a sub-class of L<FCM::Exception|FCM::Exception> and is thrown
+by methods of this class on error.
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/System/CM.pm b/lib/FCM/System/CM.pm
new file mode 100644
index 0000000..51aba6a
--- /dev/null
+++ b/lib/FCM/System/CM.pm
@@ -0,0 +1,709 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+
+# ------------------------------------------------------------------------------
+package FCM::System::CM;
+use base qw{FCM::Class::CODE};
+
+use Cwd qw{cwd};
+use FCM1::Cm;
+use FCM1::Interactive;
+use FCM::Context::Event;
+use FCM::Context::Locator;
+use FCM::System::CM::CommitMessage;
+use FCM::System::CM::Prompt;
+use FCM::System::CM::ResolveConflicts qw{_cm_resolve_conflicts};
+use FCM::System::CM::SVN;
+use FCM::System::Exception;
+use FCM::Util::Exception;
+use File::Spec::Functions qw{catfile};
+use List::Util qw{first};
+use Storable qw{dclone};
+
+# The (keys) named actions of this class and (values) their implementations.
+our %ACTION_OF = (
+ cm_branch_create => \&_cm_branch_create,
+ cm_branch_delete => _fcm1_func(\&FCM1::Cm::cm_branch_delete),
+ cm_branch_diff => _fcm1_func(\&FCM1::Cm::cm_branch_diff),
+ cm_branch_info => _fcm1_func(\&FCM1::Cm::cm_branch_info),
+ cm_branch_list => \&_cm_branch_list,
+ cm_commit => _fcm1_func(\&FCM1::Cm::cm_commit),
+ cm_checkout => \&_cm_checkout,
+ cm_check_missing => _fcm1_func(
+ \&FCM1::Cm::cm_check_missing,
+ _opt_mod_st_check_handler_func('WC_STATUS_PATH'),
+ ),
+ cm_check_unknown => _fcm1_func(
+ \&FCM1::Cm::cm_check_unknown,
+ _opt_mod_st_check_handler_func('WC_STATUS_PATH'),
+ ),
+ cm_diff => \&_cm_diff,
+ cm_loc_layout => \&_cm_loc_layout,
+ cm_merge => _fcm1_func(\&FCM1::Cm::cm_merge),
+ cm_mkpatch => _fcm1_func(\&FCM1::Cm::cm_mkpatch),
+ cm_project_create => \&_cm_project_create,
+ cm_resolve_conflicts => \&_cm_resolve_conflicts,
+ cm_switch => _fcm1_func(
+ \&FCM1::Cm::cm_switch, _opt_mod_st_check_handler_func('WC_STATUS'),
+ ),
+ cm_update => _fcm1_func(
+ \&FCM1::Cm::cm_update, _opt_mod_st_check_handler_func('WC_STATUS'),
+ ),
+ svn => \&_svn,
+);
+
+# Alias
+my $E = 'FCM::System::Exception';
+
+# Creates the class.
+__PACKAGE__->class(
+ { commit_message_util => '&',
+ gui => '$',
+ prompt => '&',
+ svn => '&',
+ util => '&',
+ },
+ {init => \&_init, action_of => \%ACTION_OF},
+);
+
+sub _init {
+ my ($attrib_ref) = @_;
+ if (!defined(FCM1::Keyword::get_util())) {
+ FCM1::Keyword::set_util($attrib_ref->{util});
+ }
+ if ($attrib_ref->{'gui'}) {
+ FCM1::Interactive::set_impl(
+ 'FCM1::Interactive::InputGetter::GUI',
+ {geometry => $attrib_ref->{gui}},
+ );
+ }
+ $attrib_ref->{prompt} = FCM::System::CM::Prompt->new({
+ gui => $attrib_ref->{gui}, util => $attrib_ref->{util},
+ });
+ $attrib_ref->{commit_message_util} = FCM::System::CM::CommitMessage->new({
+ gui => $attrib_ref->{gui},
+ util => $attrib_ref->{util},
+ });
+ $attrib_ref->{svn} = FCM::System::CM::SVN->new({util => $attrib_ref->{util}});
+ FCM1::Cm::set_util($attrib_ref->{util});
+ FCM1::Cm::set_commit_message_util($attrib_ref->{commit_message_util});
+ FCM1::Cm::set_svn_util($attrib_ref->{svn});
+}
+
+# Create a branch in a project.
+sub _cm_branch_create {
+ my ($attrib_ref, $option_ref, @args) = @_;
+ _parse_args($attrib_ref, $option_ref, \@args);
+ my ($name, $source) = @args;
+ # Check branch name
+ if (!$name || $name !~ qr{\A[\w\.\-/]+\z}msx) {
+ return $E->throw($E->CM_BRANCH_NAME, $name ? $name : q{});
+ }
+ # Determine ticket list with name
+ if (!$option_ref->{ticket} && $name =~ qr{\A[1-9]\d*([_\-][1-9]\d*)*\z}msx) {
+ $option_ref->{ticket} = [split(qr{[_\-]}msx, $name)];
+ }
+ # Check source
+ $source ||= cwd() . '@HEAD';
+ my $layout = $attrib_ref->{svn}->get_layout($source);
+ my $root = $layout->get_root();
+ my $source_rev = $layout->get_peg_rev();
+ my $project = $layout->get_project();
+ my $source_branch = $layout->get_branch();
+ if (!defined($project)) {
+ return $E->throw($E->CM_BRANCH_SOURCE, $source);
+ }
+ my @project_paths = split(qr{/}msx, $project);
+
+ # Determine whether to create a branch of a branch
+ if (!$option_ref->{'branch-of-branch'} || !$source_branch) {
+ $source_branch = 'trunk';
+ }
+ $source = join('/', $root, @project_paths, $source_branch)
+ . '@' . $source_rev;
+ my $source_commit_rev
+ = $attrib_ref->{svn}->get_info($source)->[0]->{'commit:revision'};
+ $source = join('/', $root, @project_paths, $source_branch)
+ . '@' . $source_commit_rev;
+ $attrib_ref->{util}->event(
+ FCM::Context::Event->CM_BRANCH_CREATE_SOURCE, $source, $source_rev,
+ );
+
+ # Handle multiple tickets
+ $option_ref->{ticket} ||= [];
+ $option_ref->{ticket} = [
+ sort
+ {$a <=> $b}
+ map
+ {s{\A#}{}msx; $_}
+ split(qr{,}msx, join(q{,}, @{$option_ref->{ticket}}))
+ ];
+
+ # Determine the sub-directory names of the branch
+ # FIXME: hard coded legacy!
+ my %layout_config = %{$layout->get_config()};
+ my @names;
+ if ($layout_config{'template-branch'}) {
+ my $template = $layout_config{'template-branch'};
+ if ( index($template, '{category}') >= 0
+ || index($template, '{owner}') >= 0
+ ) {
+ $option_ref->{type} ||= 'dev::user';
+ $option_ref->{type} = lc($option_ref->{type});
+ $option_ref->{type}
+ = $option_ref->{type} eq 'user' ? 'dev::user'
+ : $option_ref->{type} eq 'share' ? 'dev::share'
+ : $option_ref->{type} eq 'config' ? 'pkg::config'
+ : $option_ref->{type} eq 'rel' ? 'pkg::rel'
+ : $option_ref->{type} eq 'dev' ? 'dev::user'
+ : $option_ref->{type} eq 'test' ? 'test::user'
+ : $option_ref->{type} eq 'pkg' ? 'pkg::user'
+ : $option_ref->{type}
+ ;
+ if (!grep {$option_ref->{type} eq $_} qw{
+ dev::share dev::user test::share test::user
+ pkg::config pkg::rel pkg::share pkg::user
+ }) {
+ return $E->throw($E->CM_OPT_ARG, ['type', $option_ref->{type}]);
+ }
+ my %set = map {$_ => 1} split('::', $option_ref->{type});
+ if (index($template, '{category}') >= 0) {
+ my $index = index($template, '{category}');
+ my $category = first {exists($set{$_})} qw{dev test pkg};
+ substr($template, $index, length('{category}'), $category);
+ }
+ if (index($template, '{owner}') >= 0) {
+ my $index = index($template, '{owner}');
+ my $owner = exists($set{user})
+ ? $attrib_ref->{svn}->get_username($root)
+ : first {exists($set{lc($_)})} qw{Share Config Rel};
+ substr($template, $index, length('{owner}'), $owner);
+ }
+ }
+ if (index($template, '{name_prefix}') >= 0) {
+ my $index = index($template, '{name_prefix}');
+ # Check revision flag is valid
+ $option_ref->{'rev-flag'} ||= 'normal';
+ $option_ref->{'rev-flag'} = lc($option_ref->{'rev-flag'});
+ if (!grep {$_ eq $option_ref->{'rev-flag'}} qw{normal number none}) {
+ return $E->throw(
+ $E->CM_OPT_ARG, ['rev-flag', $option_ref->{'rev-flag'}]);
+ }
+ my $name_prefix = q{};
+ if ($option_ref->{'rev-flag'} ne 'none') {
+ $name_prefix = 'r' . $source_commit_rev;
+ if ($option_ref->{'rev-flag'} eq 'normal') {
+ # Attempt to replace revision number with a keyword
+ my $locator = FCM::Context::Locator->new($source);
+ my $as_keyword = $attrib_ref->{util}->loc_as_keyword($locator);
+ my ($u, $r) = $attrib_ref->{svn}->split_by_peg($as_keyword);
+ if ($source_commit_rev ne $r) {
+ $name_prefix = $r;
+ }
+ }
+
+ # Add an underscore
+ $name_prefix .= '_';
+ }
+ substr($template, $index, length('{name_prefix}'), $name_prefix);
+ }
+ if (index($template, '{name}') >= 0) {
+ my $index = index($template, '{name}');
+ substr($template, $index, length('{name}'), $name);
+ }
+ push(@names, split(qr{/+}msx, $template));
+ }
+ else {
+ push(@names, split(qr{/+}msx, $name));
+ }
+ if ($layout_config{'depth-branch'} != scalar(@names)) {
+ return $E->throw($E->CM_BRANCH_NAME, join('/', @names));
+ }
+ if ($layout_config{'dir-branch'}) {
+ unshift(@names, $layout_config{'dir-branch'});
+ }
+ # Check whether the branch already exists
+ my $target = join('/', $root, @project_paths, @names);
+ my $target_url = eval {$attrib_ref->{svn}->get_info($target)->[0]->{url}};
+ $@ = undef;
+ if ($target_url) {
+ return $E->throw($E->CM_ALREADY_EXIST, $target_url);
+ }
+
+ # Message for the commit log
+ my @tickets = @{$option_ref->{ticket}};
+ my @message = sprintf('%sCreated %s from %s@%d.' . "\n",
+ (@tickets ? join(q{,}, map {'#' . $_} @tickets) . q{: } : q{}),
+ join('/', q{}, @project_paths, @names),
+ join('/', q{}, @project_paths, $source_branch), $source_commit_rev,
+ );
+
+ # Create a temporary file for the commit log message
+ my $commit_message_ctx = $attrib_ref->{commit_message_util}->ctx();
+ $commit_message_ctx->set_auto_part(join(q{}, @message));
+ $commit_message_ctx->set_info_part(sprintf("%s %s\n", 'A', $target));
+ if (!$option_ref->{'non-interactive'}) {
+ $attrib_ref->{commit_message_util}->edit($commit_message_ctx);
+ }
+ $attrib_ref->{commit_message_util}->notify($commit_message_ctx);
+ my $temp_handle
+ = $attrib_ref->{commit_message_util}->temp($commit_message_ctx);
+
+ # Check with the user to see if he/she wants to go ahead
+ if ( !$option_ref->{'non-interactive'}
+ && !$attrib_ref->{prompt}->question('BRANCH_CREATE')
+ ) {
+ return;
+ }
+
+ # Create the branch
+ $attrib_ref->{svn}->call(
+ 'copy',
+ '--file', $temp_handle->filename(),
+ '--parents',
+ ($option_ref->{'svn-non-interactive'} ? '--non-interactive' : ()),
+ ( defined($option_ref->{'password'})
+ ? ('--password', $option_ref->{'password'}) : ()
+ ),
+ $source,
+ $target,
+ );
+ $attrib_ref->{util}->event(FCM::Context::Event->CM_CREATE_TARGET, $target);
+
+ # Switch working copy to point to newly created branch
+ if ($option_ref->{'switch'}) {
+ $ACTION_OF{'cm_switch'}->($attrib_ref, $option_ref, $target);
+ }
+
+ $target;
+}
+
+# Filter lists branches in projects.
+sub _cm_branch_list {
+ my ($attrib_ref, $option_ref, @args) = @_;
+ _parse_args($attrib_ref, $option_ref, \@args);
+ if (!@args) {
+ @args = cwd() . '@HEAD';
+ }
+ my %common_patterns_at;
+ if ($option_ref->{'only'} && @{$option_ref->{'only'}}) {
+ for (@{$option_ref->{'only'}}) {
+ my ($depth, $pattern) = split(qr{:}msx, $_, 2);
+ $common_patterns_at{$depth} ||= [];
+ push(@{$common_patterns_at{$depth}}, $pattern);
+ }
+ }
+ my $UTIL = $attrib_ref->{'util'};
+ ARG:
+ for my $arg (@args) {
+ my %patterns_at = %{dclone(\%common_patterns_at)};
+ my %info = eval {%{$attrib_ref->{svn}->get_info($arg)->[0]}};
+ if ($@) {
+ return $E->throw($E->CM_ARG, $arg);
+ }
+ my $url = $info{'url'} . '@' . $info{'revision'};
+ my $layout = $attrib_ref->{svn}->get_layout($url);
+ my $root = $layout->get_root();
+ my $rev = $layout->get_peg_rev();
+ my $project = $layout->get_project();
+ if (!defined($project)) {
+ next ARG;
+ }
+ my $url_project = $root . ($project ? '/' . $project : q{});
+ my %layout_config = %{$layout->get_config()};
+ if ($layout_config{'level-owner-branch'} && !$option_ref->{'show-all'}) {
+ my $level = $layout_config{'level-owner-branch'};
+ if ($option_ref->{'user'} && @{$option_ref->{'user'}}) {
+ $patterns_at{$level} = [
+ map {'^' . $_ . '$'}
+ map {split(qr{[,:]}msx, $_)}
+ @{$option_ref->{'user'}}
+ ];
+ }
+ elsif (!%patterns_at) {
+ my $owner = $attrib_ref->{svn}->get_username($root);
+ $patterns_at{$level} = ['^' . $owner . '$'];
+ }
+ }
+ my $url0 = $url_project;
+ if ($layout_config{'dir-branch'}) {
+ $url0 .= '/' . $layout_config{'dir-branch'};
+ }
+ else {
+ for my $key (qw{trunk tag}) {
+ if ($layout_config{"dir-$key"}) {
+ $patterns_at{1} ||= [];
+ push(
+ @{$patterns_at{1}},
+ '^(?!' . $layout_config{"dir-$key"} . '$)',
+ );
+ }
+ }
+ }
+ my @branches = $attrib_ref->{svn}->get_list(
+ $url0 . '@' . $rev,
+ sub {
+ my ($this_url, $this_name, $is_dir, $depth) = @_;
+ if ( exists($patterns_at{$depth})
+ && !grep {$this_name =~ /$_/} @{$patterns_at{$depth}}
+ ) {
+ return (0, 0);
+ }
+ my $can_return = $depth >= $layout_config{'depth-branch'};
+ ($can_return, ($is_dir && !$can_return));
+ },
+ );
+ if ($option_ref->{'url'}) {
+ $UTIL->event(
+ FCM::Context::Event->CM_BRANCH_LIST,
+ $url_project . '@' . $rev, @branches,
+ );
+ }
+ else {
+ $UTIL->event(
+ FCM::Context::Event->CM_BRANCH_LIST,
+ map {$UTIL->loc_as_keyword(FCM::Context::Locator->new($_))}
+ ($url_project . '@' . $rev, @branches),
+ );
+ }
+ }
+}
+
+# Wraps "svn checkout".
+sub _cm_checkout {
+ my ($attrib_ref, $option_ref, @args) = @_;
+ _parse_args($attrib_ref, $option_ref, \@args);
+ my $target = @args && !$attrib_ref->{util}->uri_match($args[-1])
+ ? $args[-1] : cwd();
+ my $info_entry = eval {$attrib_ref->{svn}->get_info($target)->[0]};
+ if ($@) {
+ $@ = undef; # OK, not a working copy
+ }
+ elsif (grep {index($_, 'wc-info:') == 0} keys(%{$info_entry})) {
+ return $E->throw($E->CM_CHECKOUT, [$target, $info_entry->{url}]);
+ }
+ $attrib_ref->{svn}->call('checkout', @args);
+}
+
+# Wraps "svn diff".
+sub _cm_diff {
+ my ($attrib_ref, $option_ref, @args) = @_;
+ _parse_args($attrib_ref, $option_ref, \@args);
+ local(%ENV) = %ENV;
+ $ENV{FCM_GRAPHIC_DIFF}
+ ||= $attrib_ref->{util}->external_cfg_get('graphic-diff');
+ $attrib_ref->{svn}->call('diff', @args);
+}
+
+# Parse and print layout information of each target in @args.
+sub _cm_loc_layout {
+ my ($attrib_ref, $option_ref, @args) = @_;
+ _parse_args($attrib_ref, $option_ref, \@args);
+ if (!@args) {
+ @args = qw{.};
+ }
+ my $OUT = sub {
+ $attrib_ref->{util}->event(FCM::Context::Event->OUT, @_);
+ };
+ my $not_first;
+ for my $arg (@args) {
+ if ($not_first) {
+ $OUT->("\n");
+ }
+ $not_first = 1;
+ $OUT->("target: $arg\n");
+ my $layout = $attrib_ref->{svn}->get_layout($arg);
+ $OUT->($layout->as_string());
+ }
+}
+
+# Create a new project in a repository.
+sub _cm_project_create {
+ my ($attrib_ref, $option_ref, @args) = @_;
+ _parse_args($attrib_ref, $option_ref, \@args);
+ my ($name, $root_arg) = @args;
+ # Check project name
+ if (!$name || $name !~ qr{\A[\w\.\-/]+\z}msx) {
+ return $E->throw($E->CM_PROJECT_NAME, $name);
+ }
+ # Check root
+ if (!$root_arg) {
+ return $E->throw($E->CM_REPOSITORY, q{});
+ }
+ my $layout = $attrib_ref->{svn}->get_layout($root_arg);
+ my $root = $layout->get_root();
+ if (!$root) {
+ return $E->throw($E->CM_REPOSITORY, $root_arg);
+ }
+
+ # Check whether the depth of the project name is valid
+ my %layout_config = %{$layout->get_config()};
+ my @names = split(qr{/+}msx, $name);
+ my $depth_expected = $layout_config{'depth-project'};
+ if (defined($depth_expected) && $depth_expected != scalar(@names)) {
+ return $E->throw($E->CM_PROJECT_NAME, join('/', @names));
+ }
+ # Check whether the project (trunk) already exists
+ my $target = join('/', $root, @names, $layout_config{'dir-trunk'});
+ my $target_url = eval {$attrib_ref->{svn}->get_info($target)->[0]->{url}};
+ $@ = undef;
+ if ($target_url) {
+ return $E->throw($E->CM_ALREADY_EXIST, $target_url);
+ }
+
+ # Message for the commit log
+ my @message = sprintf("%s: new project.\n", join('/', @names));
+
+ # Create a temporary file for the commit log message
+ my $commit_message_ctx = $attrib_ref->{commit_message_util}->ctx();
+ $commit_message_ctx->set_auto_part(join(q{}, @message));
+ $commit_message_ctx->set_info_part(sprintf("%s %s\n", 'A', $target));
+ if (!$option_ref->{'non-interactive'}) {
+ $attrib_ref->{commit_message_util}->edit($commit_message_ctx);
+ }
+ $attrib_ref->{commit_message_util}->notify($commit_message_ctx);
+ my $temp_handle
+ = $attrib_ref->{commit_message_util}->temp($commit_message_ctx);
+
+ # Check with the user to see if he/she wants to go ahead
+ if ( !$option_ref->{'non-interactive'}
+ && !$attrib_ref->{prompt}->question('PROJECT_CREATE')
+ ) {
+ return;
+ }
+
+ # Create the branch
+ $attrib_ref->{svn}->call(
+ 'mkdir',
+ '--file', $temp_handle->filename(),
+ '--parents',
+ ($option_ref->{'svn-non-interactive'} ? '--non-interactive' : ()),
+ ( defined($option_ref->{'password'})
+ ? ('--password', $option_ref->{'password'}) : ()
+ ),
+ $target,
+ );
+ $attrib_ref->{util}->event(FCM::Context::Event->CM_CREATE_TARGET, $target);
+
+ $target;
+}
+
+# Returns a simple wrapper to FCM 1 FCM1::Cm functions.
+sub _fcm1_func {
+ my ($action_ref, $opt_mod_ref) = @_;
+ $opt_mod_ref ||= sub {};
+ sub {
+ my ($attrib_ref, $option_ref, @args) = @_;
+ _parse_args($attrib_ref, $option_ref, \@args);
+ local(@ARGV) = @args;
+ $opt_mod_ref->($option_ref);
+ eval {$action_ref->($option_ref, @args)};
+ if ($@) {
+ if (!FCM1::Cm::Abort->caught($@)) {
+ die($@);
+ }
+ if (!($@->get_code() eq $@->NULL || $@->get_code() eq $@->USER)) {
+ die($@);
+ }
+ $attrib_ref->{util}->event(
+ FCM::Context::Event->CM_ABORT, lc($@->get_code()),
+ );
+ $@ = undef;
+ }
+ return;
+ };
+}
+
+# Generate an option modifier to st_check_handler.
+sub _opt_mod_st_check_handler_func {
+ my $key = shift();
+ sub {
+ my $option_ref = shift();
+ if (!$option_ref->{'non-interactive'}) {
+ $option_ref->{st_check_handler} = $FCM1::Cm::CLI_HANDLER_OF{$key};
+ }
+ };
+}
+
+# Expands keywords in arguments.
+sub _parse_args {
+ my ($attrib_ref, $option_ref, $args_ref) = @_;
+ # Location keywords
+ my $UTIL = $attrib_ref->{util};
+ my $url;
+ for my $arg (@{$args_ref}) {
+ eval {
+ my $locator = FCM::Context::Locator->new($arg);
+ if ($UTIL->loc_what_type($locator) eq 'svn') {
+ my $new_arg = $UTIL->loc_as_normalised($locator);
+ my $SVN = $attrib_ref->{svn};
+ my ($new_arg_url, $new_arg_rev) = $SVN->split_by_peg($new_arg);
+ my ( $arg_url, $arg_rev) = $SVN->split_by_peg($arg);
+ if (index($arg_url, $UTIL->loc_kw_prefix() . ':') == 0) {
+ $arg_url = $new_arg_url;
+ }
+ if ($arg_rev && $new_arg_rev && $arg_rev ne $new_arg_rev) {
+ $arg_rev = $new_arg_rev;
+ }
+ $arg = $arg_url . ($arg_rev ? '@' . $arg_rev : q{});
+ $url ||= $new_arg_url;
+ }
+ };
+ if (my $e = $@) {
+ if ( !FCM::Util::Exception->caught($e)
+ || index($e->get_code(), 'LOCATOR_') != 0
+ ) {
+ die($e);
+ }
+ $@ = undef;
+ }
+ }
+ # Revision keywords
+ $url ||= cwd();
+ my $in_opt_rev;
+ for my $arg (@{$args_ref}) {
+ my ($opt, $opt_arg);
+ if ($in_opt_rev) {
+ $in_opt_rev = 0;
+ ($opt, $opt_arg) = (q{}, $arg);
+ }
+ elsif (grep {$_ eq $arg} qw{-c --change -r --revision}) {
+ $in_opt_rev = 1;
+ }
+ else {
+ ($opt, $opt_arg)
+ = $arg =~ qr{\A(-[cr]|--(?:change|revision)=)(.*)\z}msx;
+ }
+ if ($opt_arg) {
+ $arg = $opt . _parse_args_rev($attrib_ref, $url, $opt_arg);
+ }
+ }
+ for my $key (grep {exists($option_ref->{$_})} qw{change revision}) {
+ $option_ref->{$key}
+ = _parse_args_rev($attrib_ref, $url, $option_ref->{$key});
+ }
+}
+
+# Expands revision keywords in an argument.
+sub _parse_args_rev {
+ my ($attrib_ref, $url, $arg) = @_;
+ my $UTIL = $attrib_ref->{util};
+ join(
+ ':',
+ map {
+ my $rev = $_;
+ my $locator = FCM::Context::Locator->new($url . '@' . $rev);
+ local($@);
+ my $value = eval{$UTIL->loc_as_normalised($locator)};
+ if ($value) {
+ (my $url, $rev) = $attrib_ref->{svn}->split_by_peg($value);
+ }
+ $rev;
+ } split(qr{:}msx, $arg, 2)
+ );
+}
+
+# Invokes a system "svn" call.
+sub _svn {
+ my ($attrib_ref, $app, $option_ref, @args) = @_;
+ _parse_args($attrib_ref, $option_ref, \@args);
+ $attrib_ref->{svn}->call($app, @args);
+}
+
+#-------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::System::CM
+
+=head1 SYNOPSIS
+
+ use FCM::System::CM;
+ my $system = FCM::System::CM->new(\%attrib);
+ my ($out, $err) = $system->svn({}, @args);
+
+=head1 DESCRIPTION
+
+The FCM code management sub-system. This is currently a thin adaptor of
+L<FCM1::Cm|FCM1::Cm>.
+
+=head1 METHODS
+
+=over 4
+
+=item $class->new(\%attrib)
+
+Returns a new instance. This class should normally be initialised by
+L<FCM::System|FCM::System>.
+
+=item $system->cm_branch_create(\%option, at args)
+
+Implement the C<fcm branch-create> command. On success, return the branch name
+created.
+
+=item $system->cm_branch_list(\%option, at args)
+
+Implement the C<fcm branch-list> command.
+
+=item $system->cm_checkout(\%option, at args)
+
+Thin wrapper of the C<svn checkout> command. Ensure checkout to clean location.
+
+=item $system->cm_diff(\%option, at args)
+
+Thin wrapper of the C<svn diff> command. Allow --graphical option.
+
+=item $system->cm_loc_layout(\%option, at args)
+
+Implement the C<fcm loc-layout> command.
+
+=item $system->cm_project_create(\%option, at args)
+
+Implement the C<fcm project-create> command.
+
+=item $system->cm_branch_delete(\%option, at args)
+=item $system->cm_branch_info(\%option, at args)
+=item $system->cm_commit(\%option, at args)
+=item $system->cm_check_missing(\%option, at args)
+=item $system->cm_check_unknown(\%option, at args)
+=item $system->cm_merge(\%option, at args)
+=item $system->cm_mkpatch(\%option, at args)
+=item $system->cm_resolve_conflicts(\%option, at args)
+=item $system->cm_switch(\%option, at args)
+=item $system->cm_update(\%option, at args)
+
+Thin adaptors for the corresponding code management functions in
+L<FCM1::Cm|FCM1::Cm>.
+
+=item $system->svn($app,\%option, at args)
+
+Invokes a system call to L<svn|svn> $app with @args. %option is not currently
+used, but is left in the argument list for compatibility with the other methods.
+
+=back
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/System/CM/CommitMessage.pm b/lib/FCM/System/CM/CommitMessage.pm
new file mode 100644
index 0000000..a09fbb4
--- /dev/null
+++ b/lib/FCM/System/CM/CommitMessage.pm
@@ -0,0 +1,312 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+
+# Utility to manipulate FCM commit messages.
+package FCM::System::CM::CommitMessage;
+use base qw{FCM::Class::CODE};
+
+use Cwd qw{cwd};
+use FCM::Context::Event;
+use FCM::System::Exception;
+use File::Spec::Functions qw{catfile};
+use File::Temp;
+use Text::ParseWords qw{shellwords};
+
+my $CTX = 'FCM::System::CM::CommitMessage::State';
+my $E = 'FCM::System::Exception';
+
+our $COMMIT_MESSAGE_BASE = '#commit_message#';
+our $DELIMITER_USER
+ = '--Add your commit message ABOVE - do not alter this line or those below--'
+ . "\n";
+our $DELIMITER_AUTO
+ = '--FCM message (will be inserted automatically)--'
+ . "\n";
+our $DELIMITER_INFO
+ = '--Change summary (not part of commit message)--'
+ . "\n";
+our $EDITOR = 'vi';
+our $GEDITOR = 'gedit';
+our $SUBVERSION_CONFIG_FILE = catfile((getpwuid($<))[7], qw{.subversion/config});
+
+__PACKAGE__->class({gui => '$', util => '&'},
+ {action_of => {
+ 'ctx' => sub {$CTX->new()},
+ 'edit' => \&_edit,
+ 'load' => \&_load,
+ 'notify' => \&_notify,
+ 'path' => \&_path,
+ 'path_base' => sub {$COMMIT_MESSAGE_BASE},
+ 'save' => \&_save,
+ 'temp' => \&_temp,
+ }},
+);
+
+# Invokes an editor to edit the commit message context.
+sub _edit {
+ my ($attrib_ref, $commit_message_ctx) = @_;
+ my $UTIL = $attrib_ref->{'util'};
+ my $temp = File::Temp->new();
+ if ($commit_message_ctx->get_user_part()) {
+ print($temp $commit_message_ctx->get_user_part());
+ }
+ else {
+ print($temp "\n");
+ }
+ print($temp $DELIMITER_USER);
+ if ($commit_message_ctx->get_auto_part()) {
+ print($temp $DELIMITER_AUTO . $commit_message_ctx->get_auto_part());
+ }
+ print($temp $DELIMITER_INFO . $commit_message_ctx->get_info_part());
+ close($temp) || die("$temp: $!\n");
+ my $config_value;
+ my $editor_command
+ = $ENV{'SVN_EDITOR'} ? $ENV{'SVN_EDITOR'}
+ : ($config_value = _svn_config_get($attrib_ref, 'helpers', 'editor-cmd'))
+ ? $config_value
+ : $ENV{'VISUAL'} ? $ENV{'VISUAL'}
+ : $ENV{'EDITOR'} ? $ENV{'EDITOR'}
+ : $attrib_ref->{gui} ? $GEDITOR
+ : $EDITOR
+ ;
+ $UTIL->event(FCM::Context::Event->CM_LOG_EDIT, $editor_command);
+ my @command = (shellwords($editor_command), $temp->filename());
+ !system(@command)
+ || return $E->throw($E->SHELL, {command_list => \@command, rc => $?});
+ # Note: cannot use FCM::Util->shell method for terminal based editor.
+ #my %value_of = %{$attrib_ref->{'util'}->shell_simple(\@command)};
+ #if ($value_of{'rc'}) {
+ # return $E->throw($E->SHELL, {command_list => \@command, %value_of});
+ #}
+ my $user_part = _parse(
+ $attrib_ref,
+ scalar($UTIL->file_load($temp->filename())),
+ $DELIMITER_USER,
+ );
+ $commit_message_ctx->set_user_part($user_part);
+ if (($user_part . $commit_message_ctx->get_auto_part()) =~ qr{\A\s*\z}msx) {
+ return $E->throw($E->CM_LOG_EDIT_NULL);
+ }
+}
+
+# Reads a commit message file from $path or the standard location. Returns a
+# commit message context object.
+sub _load {
+ my ($attrib_ref, $path) = @_;
+ $path ||= _path($attrib_ref);
+ my ($user_part, $auto_part) = eval {
+ _parse($attrib_ref, scalar($attrib_ref->{'util'}->file_load($path)));
+ };
+ if (my $e = $@) {
+ $user_part = q{};
+ $auto_part = q{};
+ $@ = undef; # TODO: should raise a high verbosity event?
+ }
+ $CTX->new({'user_part' => $user_part, 'auto_part' => $auto_part});
+}
+
+# Raises an CM_COMMIT_MESSAGE event for the commit message.
+sub _notify {
+ my ($attrib_ref, $commit_message_ctx) = @_;
+ $attrib_ref->{util}->event(
+ FCM::Context::Event->CM_COMMIT_MESSAGE, $commit_message_ctx,
+ );
+}
+
+# Parses a commit message into the user and auto parts. Returns the user part in
+# scalar context. Returns (user_part, auto_part) in list context.
+sub _parse {
+ my ($attrib_ref, $message, $no_delimiter_user) = @_;
+ my @parts = (q{}, q{});
+ my $state = 0;
+ LINE:
+ for my $line (split("\n", $message)) {
+ if ($state && !wantarray()) {
+ last LINE;
+ }
+ $line .= "\n";
+ if ($line eq $DELIMITER_INFO) {
+ last LINE;
+ }
+ elsif ($line eq $DELIMITER_AUTO) {
+ $state = 1;
+ next LINE;
+ }
+ elsif ($line eq $DELIMITER_USER) {
+ $no_delimiter_user = undef;
+ $state = -1;
+ next LINE;
+ }
+ if ($state >= 0) {
+ $parts[$state] .= $line;
+ }
+ }
+ if ($no_delimiter_user) {
+ return $E->throw($E->CM_LOG_EDIT_DELIMITER, $DELIMITER_USER);
+ }
+ for my $part (@parts) {
+ $part =~ s{\A\s*(.*?)\s*\z}{$1}msx;
+ if ($part) {
+ $part .= "\n";
+ }
+ }
+ wantarray() ? @parts : $parts[0];
+}
+
+# Returns the path to the commit message file in the current working directory
+# or the commit message file in $dir if $dir is set.
+sub _path {
+ my ($attrib_ref, $dir) = @_;
+ catfile(($dir ? $dir : cwd()), $COMMIT_MESSAGE_BASE);
+}
+
+# Saves the commit message to $path or the standard location for later
+# retrieval.
+sub _save {
+ my ($attrib_ref, $commit_message_ctx, $path) = @_;
+ $path ||= _path($attrib_ref);
+ my $string = $commit_message_ctx->get_user_part();
+ if ($commit_message_ctx->get_auto_part()) {
+ $string .= $DELIMITER_AUTO . $commit_message_ctx->get_auto_part();
+ }
+ $attrib_ref->{'util'}->file_save($path, $string);
+}
+
+# Returns a File::Temp object containing a commit message ready for the VCS.
+sub _temp {
+ my ($attrib_ref, $commit_message_ctx) = @_;
+ my $temp = File::Temp->new();
+ print($temp $commit_message_ctx->get_user_part());
+ print($temp $commit_message_ctx->get_auto_part());
+ close($temp) || die("$temp: $!\n");
+ $temp;
+}
+
+# Loads a setting from $HOME/.subversion/config, and returns its value.
+sub _svn_config_get {
+ my ($attrib_ref, $section, $key) = @_;
+ # Note: can use Config::IniFiles, but best to avoid another dependency.
+ # Note: not very efficient logic here, but should not yet matter.
+ my $handle = $attrib_ref->{'util'}->file_load_handle($SUBVERSION_CONFIG_FILE);
+ my $is_in_section;
+ my $value;
+ LINE:
+ while (my $line = readline($handle)) {
+ chomp($line);
+ if ($line =~ qr{\A\s*(?:[#;]|\z)}msx) {
+ next LINE;
+ }
+ if ($line =~ qr{\A\s*\[\s*$section\s*\]\s*\z}msx) {
+ $is_in_section = 1;
+ }
+ elsif ($line =~ qr{\A\s*\[}msx) {
+ $is_in_section = 0;
+ }
+ elsif ($is_in_section) {
+ my ($rhs) = $line =~ qr{\A\s*$key\s*=\s*(.*)\z}msx;
+ if (defined($rhs)) {
+ $value = $rhs;
+ }
+ }
+ }
+ close($handle);
+ $value;
+}
+
+#-------------------------------------------------------------------------------
+package FCM::System::CM::CommitMessage::State;
+use base qw{FCM::Class::HASH};
+
+__PACKAGE__->class({
+ (map {($_ . '_part' => {isa => '$', default => q{}})} qw{auto info user}),
+});
+
+#-------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::System::CM::CommitMessage
+
+=head1 SYNOPSIS
+
+ use FCM::System::CM::CommitMessage;
+ my $commit_message_util = FCM::System::CM::CommitMessage->new(\%attrib);
+ my $commit_message_ctx = $commit_message_util->ctx();
+ $commit_message_util->edit($ctx);
+
+=head1 DESCRIPTION
+
+The commit message dumper, editor, loader, parser, etc for the FCM code
+management sub-system.
+
+=head1 METHODS
+
+=over 4
+
+=item $class->new(\%attrib)
+
+Return a new instance. This class should normally be initialised by
+L<FCM::System::CM|FCM::System::CM>.
+
+=item $commit_message_util->ctx()
+
+Return a new and empty commit message context.
+
+=item $commit_message_util->edit($commit_message_ctx)
+
+Invoke an editor to edit the commit message context.
+
+=item $commit_message_util->load($path)
+
+Load the content of a commit message file in $path, and return the result in a
+new commit message context.
+
+=item $commit_message_util->notify($commit_message_ctx)
+
+Raise a CM_COMMIT_MESSAGE event with the $commit_message_ctx.
+
+=item $commit_message_util->path($dir)
+
+Return the path to the commit message file in $dir or the current working
+directory if $dir is not specified.
+
+=item $commit_message_util->path($dir)
+
+Return the base name of the commit message file.
+
+=item $commit_message_util->save($commit_message_ctx, $path)
+
+Save the commit message to $path (or the standard location if $path is not
+specified).
+
+=item $commit_message_util->temp()
+
+Return a File::Temp object containing a commit message ready for the VCS.
+
+=back
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/System/CM/Prompt.pm b/lib/FCM/System/CM/Prompt.pm
new file mode 100644
index 0000000..1217c4f
--- /dev/null
+++ b/lib/FCM/System/CM/Prompt.pm
@@ -0,0 +1,221 @@
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+use strict;
+use warnings;
+
+#-------------------------------------------------------------------------------
+package FCM::System::CM::Prompt;
+use base qw{FCM::Class::CODE};
+
+use FCM::Context::Event;
+
+our $TYPE_YN = 'TYPE_YN';
+
+# Format string table
+my %S = (
+ 'BRANCH_CREATE' => 'Create the branch?',
+ 'OVERWRITE' => '%s: file exists, overwrite?',
+ 'PROJECT_CREATE' => 'Create the project?',
+ 'RESOLVE' => 'Run "svn resolve --accept working %s"?',
+ 'TC' => "Locally: %s.\n"
+ . "Externally: %s.\n"
+ . "Answer (y) to %s.\n"
+ . "Answer (n) to %s.\n"
+ . '%s'
+ . 'Keep the local version?',
+ 'TC_ACTION' => 'accept the %s %s',
+ 'TC_ACTION_ADD' => 'keep the %s file filename',
+ 'TC_ACTION_EDIT' => 'keep the file',
+ 'TC_FROM_LOC' => 'local',
+ 'TC_FROM_INC' => 'external',
+ 'TC_MERGE' => "You can then merge in changes.\n",
+ 'TC_ST_ADD' => 'added',
+ 'TC_ST_DELETE' => 'deleted',
+ 'TC_ST_EDIT' => 'edited',
+ 'TC_ST_RENAME' => 'renamed to %s',
+);
+
+# Configuration for questions
+# KEY => {'format' => $|&, 'type' => $}
+my %Q_CONF = (
+ # Simple question prompts
+ ( map {($_ => {'format' => $S{$_}, 'type' => q{}})}
+ qw{BRANCH_CREATE OVERWRITE PROJECT_CREATE RESOLVE}
+ ),
+ # Tree conflicts prompts: TC_LxIy, for local x, incoming y
+ # where x and y correspond to:
+ # A => add,
+ # D => delete,
+ # E => edit,
+ # M => missing,
+ # R => rename
+ ( map {('TC_' . $_ => {'format' => \&_q_tree_conflict, 'type' => $TYPE_YN})}
+ qw(LAIA LDID LDIE LDIR LEID LEIR LRID LRIE LRIR)
+ ),
+);
+
+__PACKAGE__->class(
+ {gui => '$', util => '&'},
+ {init => \&_init, action_of => {question => \&_q}},
+);
+
+sub _init {
+ my $attrib_ref = shift();
+ my $class = $attrib_ref->{gui}
+ ? 'FCM::System::CM::Prompt::Zenity' : 'FCM::System::CM::Prompt::Simple';
+ $attrib_ref->{impl} = $class->new({util => $attrib_ref->{util}});
+}
+
+sub _q {
+ my ($attrib_ref, $key, @args) = @_;
+ my $format = $Q_CONF{$key}{'format'};
+ my $prompt = ref($format) ? $format->(@args) : sprintf($format, @args);
+ $attrib_ref->{'impl'}->question($Q_CONF{$key}{'type'}, $prompt);
+}
+
+# Tree conflict prompt.
+# $tree_key is the FCM::System::CM::TreeConflictKey for the conflict.
+# $rename_loc is the new local name for the conflict file, if any.
+# $rename_inc is the new incoming name for the conflict file, if any.
+sub _q_tree_conflict {
+ my ($tree_key, $rename_loc, $rename_inc) = @_;
+ my %opt_of = (
+ 'loc' => {'key' => $tree_key->get_local() , 'rename' => $rename_loc},
+ 'inc' => {'key' => $tree_key->get_incoming(), 'rename' => $rename_inc},
+ );
+ sprintf($S{'TC'}, (
+ ( map {
+ my $opt = $_;
+ my $message = $S{'TC_ST_' . uc($opt->{'key'})};
+ if ($opt->{'key'} eq 'rename') {
+ $message = sprintf($message, $opt->{'rename'});
+ }
+ $message;
+ }
+ @opt_of{'loc', 'inc'}
+ ),
+ ( map {
+ my $location_key = $_;
+ my $from = $S{'TC_FROM_' . uc($location_key)};
+ my $key = $opt_of{$location_key}->{'key'};
+ $key eq 'add' ? sprintf($S{'TC_ACTION_ADD'}, $from)
+ : $key eq 'edit' ? $S{'TC_ACTION_EDIT'}
+ : sprintf($S{'TC_ACTION'}, $from, $key)
+ ;
+ }
+ ('loc', 'inc')
+ ),
+ ( ( (grep {$opt_of{'loc'}{'key'} eq $_} qw{rename edit})
+ && (grep {$opt_of{'inc'}{'key'} eq $_} qw{rename edit})
+ )
+ ? $S{'TC_MERGE'} : q{}
+ ),
+ ));
+}
+
+#-------------------------------------------------------------------------------
+package FCM::System::CM::Prompt::Simple;
+use base qw{FCM::Class::CODE};
+
+use FCM::Context::Event;
+
+our %SETTING_OF = (
+ q{} => {'choices' => [qw{y n}], 'default' => 'n', 'positive' => 'y'},
+ 'TYPE_YN' => {'choices' => [qw{y n}], 'default' => 'n', 'positive' => 'y'},
+);
+
+__PACKAGE__->class({util => '&'}, {action_of => {question => \&_question}});
+
+sub _question {
+ my ($attrib_ref, $type, $question) = @_;
+ my %setting = %{$SETTING_OF{$type}};
+ _prompt($attrib_ref, $question, $setting{'choices'}, $setting{'default'})
+ eq $setting{'positive'};
+}
+
+sub _prompt {
+ my ($attrib_ref, $question, $choices_ref, $default) = @_;
+ my ($tail, @heads) = reverse(@{$choices_ref});
+ my $prompt
+ = $question . "\n"
+ . sprintf('Enter "%s" or "%s"', join(q{, }, reverse(@heads)), $tail)
+ . sprintf(' (or just press <return> for "%s") ', $default);
+ my $answer;
+ while (!defined($answer)) {
+ $attrib_ref->{util}->event(FCM::Context::Event->OUT, $prompt);
+ $answer = readline(STDIN);
+ chomp($answer);
+ if (!$answer) {
+ $answer = $default;
+ }
+ if (!grep {$_ eq $answer} @{$choices_ref}) {
+ $answer = undef;
+ }
+ }
+ return $answer;
+}
+
+#-------------------------------------------------------------------------------
+package FCM::System::CM::Prompt::Zenity;
+use base qw{FCM::Class::CODE};
+
+our %OPTIONS_OF = (
+ q{} => [],
+ 'TYPE_YN' => ['--ok-label=_Yes', '--cancel-label=_No'],
+);
+
+__PACKAGE__->class({util => '&'}, {action_of => {question => \&_question}});
+
+sub _question {
+ my ($attrib_ref, $type, $question) = @_;
+ _zenity($attrib_ref, qw{--question --text}, $question, @{$OPTIONS_OF{$type}});
+}
+
+sub _zenity {
+ my ($attrib_ref, @args) = @_;
+ my @command = ('zenity', @args);
+ my %value_of = %{$attrib_ref->{util}->shell_simple(\@command)};
+ !$value_of{rc};
+}
+
+1;
+__END__
+
+=head1 NAME
+
+FCM::System::CM::Prompt
+
+=head1 SYNOPSIS
+
+ use FCM::System::CM::Prompt;
+ my $prompt = FCM::System::CM::Prompt->new(\%attrib);
+ if ($prompt->question($key, @args)) {
+ # do something
+ }
+
+=head1 DESCRIPTION
+
+Helper module for prompts in the FCM code management sub-system.
+See L<FCM::System::CM|FCM::System::CM> for detail.
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/System/CM/ResolveConflicts.pm b/lib/FCM/System/CM/ResolveConflicts.pm
new file mode 100644
index 0000000..da9e96a
--- /dev/null
+++ b/lib/FCM/System/CM/ResolveConflicts.pm
@@ -0,0 +1,669 @@
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+use strict;
+use warnings;
+
+#-------------------------------------------------------------------------------
+package FCM::System::CM::ResolveConflicts;
+use base qw{Exporter};
+our @EXPORT_OK = qw{_cm_resolve_conflicts};
+
+use Cwd qw{cwd};
+use FCM::Context::Event;
+use FCM::System::Exception;
+use File::Basename qw{basename dirname};
+use File::Copy qw{copy};
+use File::Spec::Functions qw{abs2rel catfile rel2abs};
+use File::Temp;
+
+# LxIy stands for local x, incoming y in the tree conflict description.
+# The letters of x and y correspond to:
+# A => add,
+# D => delete,
+# E => edit,
+# M => missing,
+# R => rename,
+# although the 'rename' has to be detected by our code below.
+
+our %TREE_CONFLICT_GET_GRAPHIC_SOURCES_FUNC_FOR = (
+ LEIR => \&_cm_tree_conflict_get_graphic_sources_for_leir,
+ LRIE => \&_cm_tree_conflict_get_graphic_sources_for_lrie,
+ LRIR => \&_cm_tree_conflict_get_graphic_sources_for_lrir,
+);
+
+# A tree conflict key must be present here for auto-resolving.
+our %TREE_CONFLICT_GET_FINAL_ACTIONS_FUNC_FOR = (
+ LAIA => \&_cm_tree_conflict_get_actions_for_laia,
+ LDID => sub {},
+ LDIE => \&_cm_tree_conflict_get_actions_for_ldie,
+ LDIR => \&_cm_tree_conflict_get_actions_for_ldir,
+ LEID => \&_cm_tree_conflict_get_actions_for_leid,
+ LEIR => \&_cm_tree_conflict_get_actions_for_leir,
+ LRID => \&_cm_tree_conflict_get_actions_for_lrid,
+ LRIE => \&_cm_tree_conflict_get_actions_for_lrie,
+ LRIR => \&_cm_tree_conflict_get_actions_for_lrir,
+);
+
+# Handle aliases for actions.
+our %TREE_CONFLICT_GET_UNALIAS_FOR = (
+ 'obstruction' => 'add',
+ 'missing' => 'delete',
+);
+
+# Number of renamed files that triggers a time warning.
+our $TREE_CONFLICT_WARN_FILES_THRESHOLD = 10;
+
+my $E = 'FCM::System::Exception';
+# Regular expressions
+my %RE = (
+ # determines if a file was copied, i.e. added with history from "svn status"
+ ST_COPIED => qr{^A..\+....(.*)}msx,
+);
+
+# Resolve conflicts.
+sub _cm_resolve_conflicts {
+ my ($attrib_ref, $option_ref, @args) = @_;
+ my $UTIL = $attrib_ref->{util};
+ my $pwd = cwd();
+ if (!@args) {
+ push(@args, '.');
+ }
+ for my $arg (@args) {
+ if (!-e $arg) {
+ die("$arg: $!\n");
+ }
+ chdir($attrib_ref->{svn}->get_wc_root($arg)) || die("$arg: $!\n");
+ my @command = qw{svn status};
+ my %value_of = %{$UTIL->shell_simple(\@command)};
+ if ($value_of{rc}) {
+ return $E->throw($E->SHELL, {command_list => \@command, %value_of});
+ }
+ my @status_lines = grep {$_} split("\n", $value_of{o});
+ local(%ENV) = %ENV;
+ $ENV{FCM_GRAPHIC_MERGE} ||= $UTIL->external_cfg_get('graphic-merge');
+ for my $path (map {($_ =~ qr{\AC.{6}\s(.*)\z}msx)} @status_lines) {
+ _cm_resolve_text_conflict(
+ $attrib_ref,
+ $option_ref,
+ $path,
+ @status_lines,
+ );
+ }
+ for my $path (map {($_ =~ qr{\A.{6}C\s(.*)\z}msx)} @status_lines) {
+ _cm_resolve_tree_conflict(
+ $attrib_ref,
+ $option_ref,
+ $path,
+ @status_lines,
+ );
+ }
+ }
+ chdir($pwd);
+}
+
+# Helper for _cm_resolve_conflicts, launch graphic merge tool.
+sub _cm_graphic_merge {
+ my $attrib_ref = shift();
+ my @command = ('fcm_graphic_merge', @_);
+ my $UTIL = $attrib_ref->{util};
+ my %value_of = %{$UTIL->shell_simple(\@command)};
+ # rc==0: all conflicts resovled
+ # rc==1: some conflicts not resolved
+ # rc==2: trouble
+ if (!grep {$_ eq $value_of{rc}} (0, 1)) {
+ return $E->throw(
+ $E->SHELL, {command_list => \@command, %value_of}, $value_of{e},
+ );
+ }
+ $UTIL->event(FCM::Context::Event->OUT, $value_of{o});
+ $value_of{rc};
+}
+
+# Resolve a text conflict.
+sub _cm_resolve_text_conflict {
+ my ($attrib_ref, $option_ref, $path) = @_;
+ my $PROMPT = $attrib_ref->{prompt};
+ my $UTIL = $attrib_ref->{util};
+ if (-B $path) {
+ $UTIL->event(FCM::Context::Event->CM_CONFLICT_TEXT_SKIP, $path);
+ return;
+ }
+ $UTIL->event(FCM::Context::Event->CM_CONFLICT_TEXT, $path);
+
+ # Get conflicts markers files
+ my %info = %{$attrib_ref->{svn}->get_info($path)->[0]};
+ my @keys = map {"conflict:$_-file"} qw{prev-wc prev-base cur-base};
+ # Subversion 1.6: conflict filenames are relative paths.
+ # Subversion 1.8: conflict filenames are absolute paths.
+ my ($mine, $older, $yours) = map {
+ rel2abs($_, rel2abs(dirname($path)))
+ } @info{@keys};
+
+ # If $path is newer (by more than a second), it may contain saved changes.
+ if ( -f $path && (stat($path))[9] > (stat($mine))[9] + 1
+ && !$PROMPT->question('OVERWRITE', $path)
+ ) {
+ return;
+ }
+
+ # Launch graphic merge tool
+ if (_cm_graphic_merge($attrib_ref, $path, $mine, $older, $yours)) {
+ return; # rc==1, some conflicts not resolved
+ }
+
+ # Prompt user to run "svn resolve --accept working" on the file
+ if ($PROMPT->question('RESOLVE', $path)) {
+ $attrib_ref->{svn}->call(qw{resolve --accept working}, $path);
+ }
+}
+
+# Resolve a tree conflict.
+sub _cm_resolve_tree_conflict {
+ my ($attrib_ref, $option_ref, $path, @status_lines) = @_;
+ my $PROMPT = $attrib_ref->{prompt};
+ my $UTIL = $attrib_ref->{util};
+
+ # Skip directories - too complex for now.
+ if (-d $path) {
+ $UTIL->event(FCM::Context::Event->CM_CONFLICT_TREE_SKIP, $path);
+ return;
+ }
+
+ # Get basic information about the tree conflict, and the filename.
+ my %info = %{$attrib_ref->{svn}->get_info($path)->[0]};
+
+ # Skip non-existent or unhandled tree conflicts.
+ if (!exists($info{'tree-conflict:operation'})) {
+ return
+ }
+ if ($info{'tree-conflict:operation'} ne 'merge') {
+ $UTIL->event(FCM::Context::Event->CM_CONFLICT_TREE_SKIP, $path);
+ return;
+ }
+
+ my $tree_reason = $info{'tree-conflict:reason'};
+ if (grep {$tree_reason eq $_} keys(%TREE_CONFLICT_GET_UNALIAS_FOR)) {
+ $tree_reason = $TREE_CONFLICT_GET_UNALIAS_FOR{$tree_reason};
+ }
+ my $tree_key = FCM::System::CM::TreeConflictKey->new(
+ { 'local' => $tree_reason,
+ 'incoming' => $info{'tree-conflict:action'},
+ 'type' => $info{'tree-conflict:operation'},
+ },
+ );
+ my $tree_filename = $info{'path'};
+
+ my %wc_info = %{$attrib_ref->{svn}->get_info()->[0]};
+
+ my $repos_root = $wc_info{'repository:root'};
+ my $wc_branch = substr($wc_info{'url'}, length($repos_root) + 1);
+
+ my $tree_full_filename = '/' . $wc_branch . '/' . $tree_filename;
+
+ # Check for external renaming, by examining files added with history
+ my $ext_renamed_file = '';
+ my $ext_branch = '';
+ COPIED_FILE:
+ for my $copied_file (map {($_ =~ $RE{ST_COPIED})} @status_lines) {
+ my %copy_info = %{$attrib_ref->{svn}->get_info($copied_file)->[0]};
+ my $url = (
+ $copy_info{'wc-info:copy-from-url'}
+ . '@' . $copy_info{'wc-info:copy-from-rev'}
+ );
+ my $copy_log_ref = $attrib_ref->{svn}->get_log($url);
+ if (!$ext_branch) {
+ my $copy_full_path = substr(
+ $copy_info{'wc-info:copy-from-url'},
+ length($repos_root) + 1,
+ );
+ $ext_branch = substr(
+ $copy_full_path,
+ 0,
+ -length($copied_file) - 1,
+ );
+ }
+ my $copied_full_filename = '/' . $ext_branch . '/' . $copied_file;
+ my $tree_ext_name
+ = ('/' . $info{'tree-conflict:source-right:path-in-repos'});
+ my $search_name = $tree_ext_name;
+ for my $log_entry_ref (reverse(@{$copy_log_ref})) {
+ for my $path_entry (@{$log_entry_ref->{'paths'}}) {
+ if ( exists $path_entry->{'copyfrom-path'}
+ && $path_entry->{'copyfrom-path'} eq $search_name
+ ) {
+ $search_name = $path_entry->{'path'};
+ if ($search_name eq $copied_full_filename) {
+ $ext_renamed_file = $copied_file;
+ last COPIED_FILE;
+ }
+ }
+ }
+ }
+ }
+
+ # Check for local renaming of the tree conflict file
+ my $local_renamed_file = '';
+ if ($tree_reason eq 'delete') {
+ $local_renamed_file = _cm_tree_conflict_get_local_rename(
+ $attrib_ref,
+ $wc_branch,
+ $tree_full_filename,
+ $ext_renamed_file
+ );
+ }
+
+ # The tree conflict identifier (tree_key) needs to be adjusted for reality
+ if ($local_renamed_file) {
+ $tree_key->set_local('rename');
+ }
+ if ($ext_renamed_file) {
+ $tree_key->set_incoming('rename');
+ }
+
+ # Skip and return if the tree key does not match a key in final cmds.
+ my $cmds_getter
+ = $TREE_CONFLICT_GET_FINAL_ACTIONS_FUNC_FOR{$tree_key->as_string()};
+ if (!$cmds_getter) {
+ $UTIL->event(FCM::Context::Event->CM_CONFLICT_TREE_SKIP, $path);
+ return;
+ }
+
+ # Print the tree conflict event message
+ $UTIL->event(FCM::Context::Event->CM_CONFLICT_TREE, $path);
+
+ # These are the relevant files for this tree conflict.
+ my @file_args = grep {$_} ($path, $local_renamed_file, $ext_renamed_file);
+
+ # Prompt which version of events to accept - local or incoming.
+ my $keep_local = $PROMPT->question(
+ 'TC_' . $tree_key->as_string(),
+ $tree_key, $local_renamed_file, $ext_renamed_file,
+ );
+
+ # Add any graphic merge commands
+ my @cmds = _cm_tree_conflict_get_graphic_cmds(
+ $attrib_ref, $tree_key->as_string(), $keep_local, \@file_args,
+ );
+ # Now load any miscellaneous actions or commands - for example 'svn delete'
+ if ($tree_key->get_local() eq 'add' && $tree_key->get_incoming() eq 'add') {
+ # We need to generate a new filename and a temporary one in this case.
+ @file_args = ($path);
+ my $tree_dir = dirname($path);
+ my $newfile_handle = File::Temp->new(
+ DIR => $tree_dir,
+ TEMPLATE => basename($path) . '.XXXX',
+ UNLINK => 0,
+ );
+ unlink("$newfile_handle"); # Delete it, or it will block the copy.
+ push(@file_args, "$newfile_handle");
+ }
+ push(@cmds, $cmds_getter->($attrib_ref, $keep_local, \@file_args));
+
+ # Run the actions, including any subroutine references.
+ for my $cmd_ref (@cmds) {
+ $cmd_ref->();
+ }
+ # svn resolve.
+ $attrib_ref->{svn}->call(qw{resolve --accept working}, $path);
+}
+
+# Tree conflicts: check if a file was renamed locally.
+sub _cm_tree_conflict_get_local_rename {
+ my ($attrib_ref, $wc_branch, $tree_full_filename, $ext_renamed_file) = @_;
+ my $UTIL = $attrib_ref->{util};
+
+ # Get the verbose log for the working copy.
+ # Find the revision where the file was deleted, and store any copied
+ # filenames at that revision, and since that revision.
+ my ($d_rev, @rev_copied_filenames, $found_delete);
+ my @since_copied_filenames;
+ my $wc_log_ref = $attrib_ref->{svn}->get_log();
+ ENTRY:
+ for my $log_entry_ref (@{$wc_log_ref}) {
+ $d_rev = $log_entry_ref->{'revision'};
+ @rev_copied_filenames = ();
+ for my $path_entry (@{$log_entry_ref->{'paths'}}) {
+ if ($path_entry->{'copyfrom-path'}) {
+ push(@rev_copied_filenames, $path_entry->{'path'});
+ push(@since_copied_filenames, $path_entry->{'path'});
+ }
+ if ( $path_entry->{'path'} eq $tree_full_filename
+ && $path_entry->{'action'} eq 'D'
+ ) {
+ $found_delete = 1;
+ }
+ }
+ if ($found_delete) {
+ last ENTRY;
+ }
+ }
+
+ # We need to detect a copy and a deletion of the file to continue.
+ if (!$found_delete || !@rev_copied_filenames) {
+ return;
+ }
+
+ # Get rid of the branch name in our copied files
+ for my $copied_file (@rev_copied_filenames) {
+ $copied_file =~ s{^/$wc_branch/}{}msx;
+ }
+ for my $copied_file (@since_copied_filenames) {
+ $copied_file =~ s{^/$wc_branch/}{}msx;
+ }
+
+ # Get the pre-existing working copy files.
+ my @wc_files
+ = grep {$_ && $_ =~ qr{[^/]$}msx}
+ $attrib_ref->{svn}->stdout(qw{svn ls -R});
+
+ # Examine the files added with history at the revision log.
+ # A single rename will be detected here.
+ my @result;
+ COPIED_AT_REV_FILE:
+ for my $file (@rev_copied_filenames) {
+ if (!grep {$_ eq $file} @wc_files) {
+ next COPIED_AT_REV_FILE;
+ }
+ my @log_entry_refs
+ = @{$attrib_ref->{svn}->get_log($file . '@' . $d_rev)};
+ my $search_name = $tree_full_filename;
+ my $full_potential_rename = '/' . $wc_branch . '/' . $file;
+ for my $log_entry_ref (@log_entry_refs) {
+ for my $path_entry (@{$log_entry_ref->{'paths'}}) {
+ if ( exists($path_entry->{'copyfrom-path'})
+ && $path_entry->{'copyfrom-path'} eq $tree_full_filename
+ && $path_entry->{'action'} eq 'A'
+ && $path_entry->{'path'} eq $full_potential_rename
+ ) {
+ return $file;
+ }
+ }
+ }
+ }
+
+ # If no rename was detected, there may have been more than one.
+ # Get the logs for all current filenames that match copied filenames
+ # since the deletion, according to the working copy log.
+
+ # Warn if the number of files to be examined > the threshold.
+ if ( @wc_files > $TREE_CONFLICT_WARN_FILES_THRESHOLD
+ && @since_copied_filenames > $TREE_CONFLICT_WARN_FILES_THRESHOLD
+ ) {
+ my $tree_path = substr($tree_full_filename, length($wc_branch) + 2);
+ $UTIL->event(
+ FCM::Context::Event->CM_CONFLICT_TREE_TIME_WARN,
+ $tree_path,
+ );
+ }
+ WC_FILE:
+ for my $file (@wc_files) {
+ if (!grep {$_ eq $file} @since_copied_filenames) {
+ next WC_FILE;
+ }
+ my @log_entry_refs = @{$attrib_ref->{svn}->get_log($file)};
+ my $search_name = $tree_full_filename;
+ my $full_potential_rename = '/' . $wc_branch . '/' . $file;
+ for my $log_entry_ref (reverse(@log_entry_refs)) {
+ my $revision = $log_entry_ref->{'revision'};
+ for my $path_entry (@{$log_entry_ref->{'paths'}}) {
+ if ( exists($path_entry->{'copyfrom-path'})
+ && $path_entry->{'copyfrom-path'} eq $search_name
+ && $path_entry->{'action'} eq 'A'
+ ) {
+ $search_name = $path_entry->{'path'};
+ if ( $search_name eq $full_potential_rename
+ && $revision >= $d_rev
+ ) {
+ return $file;
+ }
+ }
+ }
+ }
+ }
+ return;
+}
+
+# Return the tree conflict command related to fcm_graphic_merge.
+sub _cm_tree_conflict_get_graphic_cmds {
+ my ($attrib_ref, $key, $keep_local, $files_ref) = @_;
+ my ($cfile, @rename_args) = @{$files_ref};
+ if (!@rename_args) {
+ return;
+ }
+ # Get the source argument subroutine reference, if it exists.
+ my $get_srcs_func_ref = $TREE_CONFLICT_GET_GRAPHIC_SOURCES_FUNC_FOR{$key};
+ if (!$get_srcs_func_ref) {
+ return;
+ }
+ # Get the sources for the graphic merge files.
+ my ($older_src, $merge_src, $working_src, $base)
+ = $get_srcs_func_ref->($cfile, $keep_local, \@rename_args);
+
+ # Set up the filenames.
+ my ($older_url, $older_peg)
+ = _cm_tree_conflict_source($attrib_ref, 'left', $older_src);
+ my $mine = $base . '.working';
+ my $older = $base . '.merge-left.r' . $older_peg;
+ my ($merge_url, $merge_peg)
+ = _cm_tree_conflict_source($attrib_ref, 'right', $merge_src);
+ my $yours = $base . '.merge-right.r' . $merge_peg;
+ # Set up the conflict files as in a text conflict.
+ sub {
+ $attrib_ref->{svn}->call(qw{export -q}, $older_url, $older);
+ $attrib_ref->{svn}->call(qw{export -q}, $merge_url, $yours);
+ copy($working_src, $mine);
+ _cm_graphic_merge($attrib_ref, $base, $mine, $older, $yours);
+ unlink($mine, $older, $yours);
+ };
+}
+
+# Return the source-left or source-right url from svn info.
+sub _cm_tree_conflict_source {
+ my ($attrib_ref, $direction, $info_filename) = @_;
+ my %info = %{$attrib_ref->{svn}->get_info($info_filename)->[0]};
+ my ($source_url, $source_peg);
+ if ($info{"tree-conflict:source-$direction:repos-url"}) {
+ $source_url
+ = $info{"tree-conflict:source-$direction:repos-url"}
+ . '/'
+ . $info{"tree-conflict:source-$direction:path-in-repos"};
+ $source_peg = $info{"tree-conflict:source-$direction:revision"};
+ }
+ elsif ($direction eq 'right') {
+ $source_url = $info{'wc-info:copy-from-url'};
+ $source_peg = $info{'wc-info:copy-from-rev'};
+ }
+ ($source_url . '@' . $source_peg, $source_peg);
+}
+
+# Select the files needed for the xxdiff (local edit, incoming rename)
+sub _cm_tree_conflict_get_graphic_sources_for_leir {
+ my ($cfile, $keep_local, $renames) = @_;
+ my $ext_rename = shift(@{$renames});
+ ( $cfile,
+ $ext_rename,
+ $cfile,
+ ($keep_local ? $cfile : $ext_rename),
+ );
+}
+
+# Select the files needed for the xxdiff (local rename, incoming edit)
+sub _cm_tree_conflict_get_graphic_sources_for_lrie {
+ my ($cfile, $keep_local, $renames) = @_;
+ my $local_rename = shift(@{$renames});
+ ( $cfile,
+ $cfile,
+ $local_rename,
+ $local_rename,
+ );
+}
+
+# Select the files needed for the xxdiff (local rename, incoming rename)
+sub _cm_tree_conflict_get_graphic_sources_for_lrir {
+ my ($cfile, $keep_local, $renames) = @_;
+ my ($local_rename, $ext_rename) = @{$renames};
+ ( $cfile,
+ $ext_rename,
+ $local_rename,
+ ($keep_local ? $local_rename : $ext_rename),
+ );
+}
+
+# Return the actions needed to resolve 'local add, incoming add'
+sub _cm_tree_conflict_get_actions_for_laia {
+ my ($attrib_ref, $keep_local, $files_ref) = @_;
+ my ($cfile, $new_name) = @{$files_ref};
+ my ($url, $url_peg) = _cm_tree_conflict_source($attrib_ref, 'right', $cfile);
+ my ($basename) = basename($cfile);
+ my $cdir = dirname($cfile);
+ sub {
+ if (!$keep_local) {
+ my $content = $attrib_ref->{svn}->stdout(qw{svn cat}, $url);
+ $attrib_ref->{util}->file_save($cfile, $content);
+ }
+ };
+}
+
+
+# Return the actions needed to resolve 'local missing, incoming edit'
+sub _cm_tree_conflict_get_actions_for_ldie {
+ my ($attrib_ref, $keep_local, $files_ref) = @_;
+ my ($cfile) = @{$files_ref};
+ my ($url, $url_peg) = _cm_tree_conflict_source($attrib_ref, 'right', $cfile);
+ my $cdir = dirname($cfile);
+ sub {
+ if (!$keep_local) {
+ $attrib_ref->{svn}->call('copy', $url, "$cdir/");
+ }
+ };
+}
+
+# Return the actions needed to resolve 'local delete, incoming rename'
+sub _cm_tree_conflict_get_actions_for_ldir {
+ my ($attrib_ref, $keep_local, $files_ref) = @_;
+ my ($cfile, $ext_rename) = @{$files_ref};
+ sub {
+ if ($keep_local) {
+ $attrib_ref->{svn}->call('revert', $ext_rename);
+ unlink($ext_rename);
+ }
+ };
+}
+
+# Return the actions needed to resolve 'local edit, incoming delete'
+sub _cm_tree_conflict_get_actions_for_leid {
+ my ($attrib_ref, $keep_local, $files_ref) = @_;
+ my ($cfile) = @{$files_ref};
+ sub {
+ if (!$keep_local) {
+ $attrib_ref->{svn}->call('delete', $cfile);
+ }
+ };
+}
+
+# Return the actions needed to resolve 'local edit, incoming rename'
+sub _cm_tree_conflict_get_actions_for_leir {
+ my ($attrib_ref, $keep_local, $files_ref) = @_;
+ my ($cfile, $ext_rename) = @{$files_ref};
+ sub {
+ if ($keep_local) {
+ $attrib_ref->{svn}->call('revert', $ext_rename);
+ unlink($ext_rename);
+ }
+ else {
+ $attrib_ref->{svn}->call('delete', $cfile);
+ }
+ };
+}
+
+# Return the actions needed to resolve 'local rename, incoming delete'
+sub _cm_tree_conflict_get_actions_for_lrid {
+ my ($attrib_ref, $keep_local, $files_ref) = @_;
+ my ($cfile, $lcl_rename) = @{$files_ref};
+ sub {
+ if (!$keep_local) {
+ $attrib_ref->{svn}->call('delete', $lcl_rename);
+ }
+ };
+}
+
+# Return the actions needed to resolve 'local rename, incoming edit'
+sub _cm_tree_conflict_get_actions_for_lrie {
+ my ($attrib_ref, $keep_local, $files_ref) = @_;
+ my ($cfile, $lcl_rename) = @{$files_ref};
+ sub {
+ if (!$keep_local) {
+ $attrib_ref->{svn}->call('rename', $lcl_rename, $cfile)
+ }
+ };
+}
+
+# Return the actions needed to resolve 'local rename, incoming rename'
+sub _cm_tree_conflict_get_actions_for_lrir {
+ my ($attrib_ref, $keep_local, $files_ref) = @_;
+ my ($cfile, $lcl_rename, $ext_rename) = @{$files_ref};
+ sub {
+ if ($keep_local) {
+ $attrib_ref->{svn}->call('revert', $ext_rename);
+ unlink($ext_rename);
+ }
+ else {
+ $attrib_ref->{svn}->call('delete', $lcl_rename);
+ }
+ };
+}
+
+# -----------------------------------------------------------------------------
+# Stores the identifier of the type of tree conflict.
+package FCM::System::CM::TreeConflictKey;
+use base qw{FCM::Class::HASH};
+
+# Creates the class.
+# 'local' is the local change (e.g. edit or delete),
+# 'incoming' is the external change (e.g. add or rename),
+# 'type' is one of merge, switch, or update.
+__PACKAGE__->class({'local' => '$', 'incoming' => '$', 'type' => '$'});
+
+# Returns a label string of the form LXIY e.g. LEID for local edit,
+# incoming delete.
+sub as_string {
+ my ($self) = shift();
+ sprintf(
+ 'L%sI%s',
+ uc(substr($self->get_local(), 0, 1)),
+ uc(substr($self->get_incoming(), 0, 1)),
+ );
+}
+
+1;
+__END__
+
+=head1 NAME
+
+FCM::System::CM::ResolveConflicts
+
+=head1 DESCRIPTION
+
+Part of L<FCM::System::CM|FCM::System::CM>.
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/System/CM/SVN.pm b/lib/FCM/System/CM/SVN.pm
new file mode 100644
index 0000000..8e84e8f
--- /dev/null
+++ b/lib/FCM/System/CM/SVN.pm
@@ -0,0 +1,1003 @@
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+use strict;
+use warnings;
+
+#-------------------------------------------------------------------------------
+package FCM::System::CM::SVN;
+use base qw{FCM::Class::CODE};
+
+use Cwd qw{cwd};
+use FCM::Context::Event;
+use FCM::Context::Locator;
+use FCM::System::Exception;
+use Memoize qw{memoize};
+use File::Basename qw{dirname};
+use File::Spec::Functions qw{catfile rel2abs};
+use Time::Piece;
+use XML::Parser;
+
+my $E = 'FCM::System::Exception';
+
+# Settings for the default repository layout
+our %LAYOUT_CONFIG = (
+ 'depth-project' => undef,
+ 'depth-branch' => 3,
+ 'depth-tag' => 1,
+ 'dir-trunk' => 'trunk',
+ 'dir-branch' => 'branches',
+ 'dir-tag' => 'tags',
+ 'level-owner-branch' => 2,
+ 'level-owner-tag' => undef,
+ 'template-branch' => '{category}/{owner}/{name_prefix}{name}',
+ 'template-tag' => undef,
+);
+
+# Layout configuration file basename
+our $LAYOUT_CFG_BASE = 'svn-repos-layout.cfg';
+
+# "svn log --xml" handlers.
+# -> element node start tag handlers
+my %SVN_LOG_ELEMENT_START_HANDLER_FOR = (
+# tag => handler
+ 'logentry' => \&_get_log_handle_element_enter_logentry,
+ 'path' => \&_get_log_handle_element_enter_path,
+);
+# -> text node (after a start tag) handlers
+my %SVN_LOG_TEXT_HANDLER_FOR = (
+# tag => handler
+ 'date' => \&_get_log_handle_text_date,
+ 'path' => \&_get_log_handle_text_path,
+);
+
+our $SUBVERSION_SERVERS_CONF = catfile((getpwuid($<))[7], qw{.subversion/servers});
+
+my %ACTION_OF = (
+ 'call' => \&_call,
+ 'get_info' => \&_get_info,
+ 'get_layout' => \&_get_layout,
+ 'get_layout_common' => \&_get_layout_common,
+ 'get_list' => \&_get_list,
+ 'get_log' => \&_get_log,
+ 'get_username' => \&_get_username,
+ 'get_wc_root' => \&_get_wc_root,
+ 'load_layout_config' => \&_load_layout_config,
+ 'split_by_peg' => \&_split_by_peg,
+ 'stdout' => \&_stdout,
+);
+
+# Creates the class.
+__PACKAGE__->class(
+ { layout_cfg_base => {isa => '$', default => $LAYOUT_CFG_BASE},
+ layout_config_of=> '%',
+ util => '&',
+ },
+ {action_of => \%ACTION_OF},
+);
+
+# Calls "svn".
+sub _call {
+ my ($attrib_ref, @args) = @_;
+ my @command = ('svn', @args);
+ my $timer = $attrib_ref->{util}->timer();
+ my $rc = system(@command);
+ $attrib_ref->{util}->event(
+ FCM::Context::Event->SHELL, \@command, $rc, $timer->());
+ if ($rc) {
+ $rc = $? == -1 ? $!
+ : $? & 127 ? $? & 127
+ : $? >> 8
+ ;
+ return $E->throw($E->SHELL, {command_list => \@command, rc => $rc});
+ }
+ return;
+}
+
+# Invokes "svn info --xml @paths", and returns a LIST of info entries.
+memoize('_get_info');
+sub _get_info {
+ my $attrib_ref = shift();
+ my %option = ('recursive' => undef, 'revision' => undef);
+ if (@_ && ref($_[0]) && ref($_[0]) eq 'HASH') {
+ %option = (%option, %{shift()});
+ }
+ my @paths = @_;
+ if (!@paths) {
+ @paths = (q{.});
+ }
+ my (@entries, @stack);
+ my $parser = XML::Parser->new(Handlers => {
+ 'Start' => sub {_get_info_handle_element_enter(\@entries, \@stack, @_)},
+ 'End' => sub {_get_info_handle_element_leave(\@entries, \@stack, @_)},
+ 'Char' => sub {_get_info_handle_text( \@entries, \@stack, @_)},
+ });
+ $parser->parse(scalar(_stdout(
+ $attrib_ref,
+ qw{svn info --xml},
+ ($option{'recursive'} ? '--recursive' : ()),
+ ($option{'revision'} ? ('--revision', $option{'revision'}) : ()),
+ @paths,
+ )));
+ \@entries;
+}
+
+# Helper for _get_info. Handle the start tag of an XML element.
+sub _get_info_handle_element_enter {
+ my ($entries_ref, $stack_ref, $expat, $tag, %attrib) = @_;
+ # "entry": create a new entry in the list
+ if ($tag eq 'entry') {
+ push(@{$entries_ref}, {});
+ }
+ # "tree-conflict:version": need to handle differently
+ if ( $tag eq 'version'
+ && @{$stack_ref}
+ && $stack_ref->[-1] eq 'tree-conflict'
+ ) {
+ my (undef, undef, @names) = @{$stack_ref};
+ push(@names, delete($attrib{side}));
+ while (my ($key, $value) = each(%attrib)) {
+ my $name = join(':', @names, $key);
+ $entries_ref->[-1]->{$name} = delete($attrib{$key});
+ }
+ }
+ # Add current tag to stack
+ push(@{$stack_ref}, $tag);
+ # Add attributes to current entry, if appropriate
+ if (@{$entries_ref} && @{$stack_ref} >= 2 && %attrib) {
+ my (undef, undef, @names) = @{$stack_ref};
+ while (my ($key, $value) = each(%attrib)) {
+ my $name = join(':', @names, $key);
+ $entries_ref->[-1]->{$name} = $value;
+ }
+ }
+}
+
+# Helper for _get_info. Handle the end tag of an XML element.
+sub _get_info_handle_element_leave {
+ my ($entries_ref, $stack_ref, $expat, $tag) = @_;
+ pop(@{$stack_ref}) eq $tag;
+}
+
+# Helper for _get_info. Handle an XML text node.
+sub _get_info_handle_text {
+ my ($entries_ref, $stack_ref, $expat, $text) = @_;
+ if (@{$stack_ref} <= 2 || !@{$entries_ref} || $text eq "\n") {
+ return;
+ }
+ my (undef, undef, @names) = @{$stack_ref};
+ my $name = join(':', @names);
+ $entries_ref->[-1]->{$name} .= $text;
+}
+
+# Return an object containing the repository layout information of a URL.
+sub _get_layout {
+ my ($attrib_ref, $url_arg) = @_;
+ my %info = %{_get_info($attrib_ref, $url_arg)->[0]};
+ my ($url, $root, $peg_rev) = @info{'url', 'repository:root', 'revision'};
+ my $path = substr($url, length($root));
+ my $layout = _get_layout_common($attrib_ref, $root, $peg_rev, $path);
+ $layout->set_url($root . $path . '@' . $peg_rev);
+ $layout->set_username(_get_username($attrib_ref, $root));
+ $layout;
+}
+
+# Return an object containing the repository layout information of a URL.
+sub _get_layout_common {
+ my ($attrib_ref, $root, $rev, $path, $is_local) = @_;
+
+ my %layout_config = _load_layout_config(
+ $attrib_ref, ($is_local ? 'file://' . $root : $root),
+ );
+ my ($project, $branch, $category, $owner, $sub_tree);
+ my @names = split(qr{/+}msx, $path);
+ shift(@names); # element 1 should be an empty string
+
+ # Search for the project
+ my $depth = $layout_config{'depth-project'};
+ if (defined($depth)) {
+ if (@names >= $depth) {
+ my @project_names = ();
+ for (1 .. $layout_config{'depth-project'}) {
+ push(@project_names, shift(@names));
+ }
+ $project = join('/', @project_names);
+ }
+ }
+ elsif (!grep {!defined($layout_config{"dir-$_"})} qw{trunk branch tag}) {
+ # trunk, branches and tags are ALL in specific sub-directories under
+ # the project
+ my @dirs = map {$layout_config{"dir-$_"}} qw{trunk branch tag};
+ my @head = ();
+ my @tail = @names;
+ while (my $name = shift(@tail)) {
+ if (grep {$_ eq $name} @dirs) {
+ $project = join('/', @head);
+ @names = ($name, @tail);
+ last;
+ }
+ push(@head, $name);
+ }
+ if (!defined($project)) {
+ # $path does not contain the specific sub-directories that
+ # contain the trunk, branches and tags, but $path itself may be
+ # the project
+ my $target = $path . '/' . $layout_config{'dir-trunk'};
+ if (_verify_path($attrib_ref, $root, $rev, $target, $is_local)) {
+ $project = join('/', @names);
+ }
+ @names = ();
+ }
+ }
+ else {
+ # Can only assume that trunk is in a specific sub-directory under the
+ # project
+ my @head = ();
+ my @tail = @names;
+ while (my $name = shift(@tail)) {
+ if ($name eq $layout_config{'dir-trunk'}) {
+ $project = join('/', @head);
+ @names = ($name, @tail);
+ last;
+ }
+ push(@head, $name);
+ }
+ if (!defined($project)) {
+ # $path does not contain the trunk sub-directory, need to search
+ # for it
+ my @head = ();
+ my @tail = @names;
+ while (@head <= @names) {
+ my $target = join('/', @head, $layout_config{'dir-trunk'});
+ if (_verify_path($attrib_ref, $root, $rev, $target, $is_local)) {
+ $project = join('/', @head);
+ @names = @tail;
+ last;
+ }
+ push(@head, shift(@tail));
+ }
+ }
+ }
+
+ # Search for the branch
+ if (defined($project) && @names) {
+ KEY:
+ for my $key (qw{trunk branch tag}) {
+ my @branch_names;
+ if ($layout_config{"dir-$key"}) {
+ if ($names[0] eq $layout_config{"dir-$key"}) {
+ @branch_names = (shift(@names));
+ }
+ else {
+ next KEY;
+ }
+ }
+ my $depth = $layout_config{"depth-$key"}
+ ? $layout_config{"depth-$key"} : 0;
+ if (@names >= $depth) {
+ for my $i (1 .. $depth) {
+ my $name = shift(@names);
+ push(@branch_names, $name);
+ if ( $layout_config{"level-owner-$key"}
+ && $layout_config{"level-owner-$key"} == $i
+ ) {
+ $owner = $name;
+ }
+ }
+ $branch = join('/', @branch_names);
+ $category = $key;
+ }
+ last KEY;
+ }
+ }
+ # Remainder is the sub-tree under the branch
+ if (defined($branch)) {
+ $sub_tree = join('/', @names);
+ }
+ FCM::System::CM::SVN::Layout->new({
+ config => \%layout_config,
+ root => $root,
+ path => $path,
+ peg_rev => $rev,
+ project => $project,
+ branch => $branch,
+ branch_category => $category,
+ branch_owner => $owner,
+ sub_tree => $sub_tree,
+ });
+}
+
+# Return a (filtered) recursive listing of $url_arg.
+sub _get_list {
+ my ($attrib_ref, $url_arg, $filter_func) = @_;
+ my @list;
+ my ($url0, $rev) = _split_by_peg($attrib_ref, $url_arg);
+ my @items = ([$url0, 0]);
+ while (my $item = shift(@items)) {
+ my ($url, $depth) = @{$item};
+ ++$depth;
+ my @lines = _stdout($attrib_ref, qw{svn list}, $url . '@' . $rev);
+ for my $line (@lines) {
+ my ($this_name, $is_dir) = $line =~ qr{\A(.*?)(/?)\z};
+ my $this_url = $url . '/' . $this_name ;
+ my ($can_return, $can_recurse) = (1, $is_dir);
+ if (defined($filter_func)) {
+ ($can_return, $can_recurse)
+ = $filter_func->($this_url, $this_name, $is_dir, $depth);
+ }
+ if ($can_return) {
+ push(@list, $this_url . '@' . $rev);
+ }
+ if ($can_recurse && $is_dir) {
+ push(@items, [$this_url, $depth]);
+ }
+ }
+ }
+ @list;
+}
+
+# Invokes "svn log --xml".
+sub _get_log {
+ my $attrib_ref = shift();
+ my %option = ('revision' => undef, 'stop-on-copy' => undef);
+ if (@_ && ref($_[0]) && ref($_[0]) eq 'HASH') {
+ %option = (%option, %{shift()});
+ }
+ my @paths = @_;
+ if (!@paths) {
+ @paths = (q{.});
+ }
+ my (@entries, @stack);
+ my $parser = XML::Parser->new(Handlers => {
+ 'Start' => sub {_get_log_handle_element_enter(\@entries, \@stack, @_)},
+ 'End' => sub {_get_log_handle_element_leave(\@entries, \@stack, @_)},
+ 'Char' => sub {_get_log_handle_text( \@entries, \@stack, @_)},
+ });
+ $parser->parse(scalar(_stdout(
+ $attrib_ref,
+ qw{svn log --xml -v},
+ ($option{'revision'} ? ('--revision', $option{'revision'}) : ()),
+ ($option{'stop-on-copy'} ? ('--stop-on-copy') : ()),
+ @paths,
+ )));
+ \@entries;
+}
+
+# Helper for "_get_log", handle beginning of an XML element.
+sub _get_log_handle_element_enter {
+ my ($entries_ref, $stack_ref, $expat, $tag, %attrib) = @_;
+ push(@{$stack_ref}, $tag);
+ if (exists($SVN_LOG_ELEMENT_START_HANDLER_FOR{$tag})) {
+ $SVN_LOG_ELEMENT_START_HANDLER_FOR{$tag}->(
+ $entries_ref,
+ $tag,
+ %attrib,
+ );
+ }
+}
+
+# Helper for "_get_log", handle beginning of the "logentry" element.
+sub _get_log_handle_element_enter_logentry {
+ my ($entries_ref, $tag, %attrib) = @_;
+ push(
+ @{$entries_ref},
+ { 'author' => q{},
+ 'date' => q{},
+ 'msg' => q{},
+ 'paths' => [],
+ 'revision' => $attrib{'revision'},
+ },
+ );
+}
+
+# Helper for "_get_log", handle beginning of the "path" element.
+sub _get_log_handle_element_enter_path {
+ my ($entries_ref, $tag, %attrib) = @_;
+ push(@{$entries_ref->[-1]->{'paths'}}, {%attrib, 'path' => q{}});
+}
+
+# Helper for "_get_log", handle end of an element.
+sub _get_log_handle_element_leave {
+ my ($entries_ref, $stack_ref, $expat, $tag) = @_;
+ pop(@{$stack_ref}) eq $tag;
+}
+
+# Helper for "_get_log", handle text node.
+sub _get_log_handle_text {
+ my ($entries_ref, $stack_ref, $expat, $text) = @_;
+ if (!exists($stack_ref->[-1])) {
+ return;
+ }
+ if (exists($SVN_LOG_TEXT_HANDLER_FOR{$stack_ref->[-1]})) {
+ $SVN_LOG_TEXT_HANDLER_FOR{$stack_ref->[-1]}->($entries_ref, $text);
+ }
+ elsif ( @{$entries_ref}
+ && exists($entries_ref->[-1]->{$stack_ref->[-1]})
+ && !ref($entries_ref->[-1]->{$stack_ref->[-1]})
+ ) {
+ $entries_ref->[-1]->{$stack_ref->[-1]} .= $text;
+ }
+}
+
+# Helper for "_get_log", handle text node in a "date" element.
+sub _get_log_handle_text_date {
+ my ($entries_ref, $text) = @_;
+ # "svn log --xml" may return a date with trailing spaces!
+ $text =~ s{\s+\z}{}gmsx;
+ my $head = Time::Piece->strptime(substr($text, 0, -8), '%Y-%m-%dT%H:%M:%S');
+ my $tail = substr($text, -8, -1);
+ $entries_ref->[-1]->{'date'} = $head->epoch() + $tail;
+}
+
+# Helper for "_get_log", handle text node in a "path" element.
+sub _get_log_handle_text_path {
+ my ($entries_ref, $text) = @_;
+ $entries_ref->[-1]->{'paths'}->[-1]->{'path'} .= $text;
+}
+
+# Return the username of the host of a given target URL.
+memoize('_get_username');
+sub _get_username {
+ my ($attrib_ref, $target) = @_;
+ my ($scheme, $sps) = $attrib_ref->{util}->uri_match($target);
+ my ($host) = $sps =~ qr{\A//([^/]+)(?:/|\z)}msx;
+ # Note: can use Config::IniFiles, but best to avoid another dependency.
+ # Note: not very efficient logic here, but should not yet matter.
+ my $subversion_servers_conf = exists($ENV{'FCM_SUBVERSION_SERVERS_CONF'})
+ ? $ENV{'FCM_SUBVERSION_SERVERS_CONF'} : $SUBVERSION_SERVERS_CONF;
+ my $handle
+ = $attrib_ref->{'util'}->file_load_handle($subversion_servers_conf);
+ my $is_in_section;
+ my $group;
+ LINE:
+ while (my $line = readline($handle)) {
+ chomp($line);
+ if ($line =~ qr{\A\s*(?:[#;]|\z)}msx) {
+ next LINE;
+ }
+ if ($line =~ qr{\A\s*\[\s*groups\s*\]\s*\z}msx) {
+ $is_in_section = 1;
+ }
+ elsif ($line =~ qr{\A\s*\[}msx) {
+ $is_in_section = 0;
+ }
+ elsif ($is_in_section) {
+ my ($lhs, $rhs) = $line =~ qr{\A\s*(\S+)\s*=\s*(\S+)\s*\z}msx;
+ if ($rhs) {
+ $rhs =~ s{[.]}{\\.}gmsx;
+ $rhs =~ s{[*]}{.*}gmsx;
+ $rhs =~ s{[?]}{.?}gmsx;
+ if ($host && $host =~ qr{\A$rhs\z}msx) {
+ $group = $lhs;
+ last LINE;
+ }
+ }
+ }
+ }
+ my $username = scalar(getpwuid($<)); # current user ID
+ if ($group) {
+ seek($handle, 0, 0);
+ LINE:
+ while (my $line = readline($handle)) {
+ chomp($line);
+ if ($line =~ qr{\A\s*(?:[#;]|\z)}msx) {
+ next LINE;
+ }
+ if ($line =~ qr{\A\s*\[\s*$group\s*\]\s*\z}msx) {
+ $is_in_section = 1;
+ }
+ elsif ($line =~ qr{\A\s*\[}msx) {
+ $is_in_section = 0;
+ }
+ elsif ($is_in_section) {
+ my ($rhs) = $line =~ qr{\A\s*username\s*=\s*(\S+)\s*\z}msx;
+ if ($rhs) {
+ $username = $rhs;
+ last LINE;
+ }
+ }
+ }
+ }
+ close($handle);
+ return $username;
+}
+
+# Return path to the root working copy directory of the argument.
+sub _get_wc_root {
+ my ($attrib_ref, $path) = @_;
+ $path ||= cwd();
+ my ($entries_ref) = _get_info($attrib_ref, $path);
+ if ( defined($entries_ref)
+ && @{$entries_ref}
+ && exists($entries_ref->[0]->{'wc-info:wcroot-abspath'})
+ ) {
+ return $entries_ref->[0]->{'wc-info:wcroot-abspath'};
+ }
+ if (-f $path) {
+ $path = dirname($path);
+ }
+ $path = rel2abs($path);
+ my $return;
+ if (-e catfile($path, qw{.svn entries})) {
+ while ( -e catfile($path, qw{.svn entries})
+ && $path ne dirname($path)
+ ) {
+ $return = $path;
+ $path = dirname($path);
+ }
+ }
+ else {
+ while ( !-e catfile($path, qw{.svn entries})
+ && $path ne dirname($path)
+ ) {
+ $path = dirname($path);
+ $return = $path;
+ }
+ }
+ return $return;
+}
+
+# Load layout related configuration for a given URL root.
+memoize('_load_layout_config');
+sub _load_layout_config {
+ my ($attrib_ref, $root) = @_;
+ if (exists($attrib_ref->{layout_config_of}{$root})) {
+ return %{$attrib_ref->{layout_config_of}{$root}};
+ }
+ my %site_layout_config;
+ if (exists($attrib_ref->{layout_config_of}{q{}})) {
+ %site_layout_config = %{$attrib_ref->{layout_config_of}{q{}}};
+ }
+ else {
+ %site_layout_config = %LAYOUT_CONFIG;
+ $attrib_ref->{util}->cfg_init(
+ $attrib_ref->{layout_cfg_base},
+ sub {
+ my $config_reader = shift();
+ my @unknown_entries;
+ while (defined(my $entry = $config_reader->())) {
+ if (exists($site_layout_config{$entry->get_label()})) {
+ my $value
+ = $entry->get_value() ? $entry->get_value() : undef;
+ $site_layout_config{$entry->get_label()} = $value;
+ }
+ else {
+ push(@unknown_entries, $entry);
+ }
+ }
+ if (@unknown_entries) {
+ return $E->throw($E->CONFIG_UNKNOWN, \@unknown_entries);
+ }
+ },
+ );
+ $attrib_ref->{layout_config_of}{q{}} = {%site_layout_config};
+ }
+ $attrib_ref->{layout_config_of}{$root} = {%site_layout_config};
+ my @prop_lines = eval {
+ _stdout($attrib_ref, qw{svn propget fcm:layout}, $root);
+ };
+ if ($@) {
+ $@ = undef;
+ }
+ PROP_LINE:
+ while (defined(my $prop_line = shift(@prop_lines))) {
+ chomp($prop_line);
+ if ($prop_line =~ qr{\A\s*(?:\#|\z)}msx) { # comment line
+ next PROP_LINE;
+ }
+ ($prop_line) = $prop_line =~ qr{\A\s*(.+?)\s*\z}msx; # trim
+ my ($key, $value) = split(qr{\s*=\s*}msx, $prop_line, 2);
+ if (exists($attrib_ref->{layout_config_of}{$root}{$key})) {
+ $attrib_ref->{layout_config_of}{$root}{$key} = $value;
+ }
+ }
+ %{$attrib_ref->{layout_config_of}{$root}};
+}
+
+# Splits a URL at REV by the @.
+sub _split_by_peg {
+ my ($attrib_ref, $url) = @_;
+ $url =~ qr{\A(.*?)(?:@([^@/]+))?\z}msx;
+}
+
+# Calls "svn", return its standard output.
+sub _stdout {
+ my ($attrib_ref, @command) = @_;
+ my %value_of = %{$attrib_ref->{util}->shell_simple(\@command)};
+ if ($value_of{rc}) {
+ return $E->throw(
+ $E->SHELL,
+ {command_list => \@command, %value_of}
+ );
+ }
+ wantarray() ? split("\n", $value_of{o}) : $value_of{o};
+}
+
+# Return true if $path is in $repos for this $rev
+sub _verify_path {
+ my ($attrib_ref, $root, $rev, $path, $is_local) = @_;
+ if ($is_local) {
+ my $opt = $rev =~ qr{\A\d+\z}msx ? '-r' : '-t';
+ eval {
+ _stdout($attrib_ref, qw{svnlook tree -N}, $opt, $rev, $root, $path);
+ };
+ if ($@) {
+ $@ = q{};
+ return;
+ }
+ return ($root, $rev, $path);
+ }
+ else {
+ my $target = $root . '/' . $path . '@' . $rev;
+ my $url = eval {_get_info($attrib_ref, $target)->[0]->{url}};
+ if ($@ || !$url) {
+ $@ = q{};
+ return;
+ }
+ return ($root, $rev, $path);
+ }
+}
+
+#-------------------------------------------------------------------------------
+# Represent the layout information of a Subversion URL.
+package FCM::System::CM::SVN::Layout;
+use base qw{FCM::Class::HASH};
+
+__PACKAGE__->class({
+ config => '%',
+ url => '$',
+ root => '$',
+ path => '$',
+ peg_rev => '$',
+ project => '$',
+ branch => '$',
+ branch_category => '$',
+ branch_owner => '$',
+ sub_tree => '$',
+ username => {isa => '$', default => scalar(getpwuid($<))},
+});
+
+sub is_trunk {
+ $_[0]->{branch_category} && $_[0]->{branch_category} eq 'trunk';
+}
+
+sub is_branch {
+ $_[0]->{branch_category} && $_[0]->{branch_category} eq 'branch';
+}
+
+sub is_tag {
+ $_[0]->{branch_category} && $_[0]->{branch_category} eq 'tag';
+}
+
+sub is_owned_by_user {
+ my ($self, $user) = @_;
+ $user ||= $self->get_username();
+ $self->{branch_owner} && $self->{branch_owner} eq $user;
+}
+
+sub is_shared {
+ my ($self) = @_;
+ $self->{branch_owner}
+ && grep {$_ eq $self->{branch_owner}} qw{Share Config Rel};
+}
+
+sub as_string {
+ my ($self) = @_;
+ my $return = q{};
+ for my $key (qw{
+ url
+ root
+ path
+ peg_rev
+ project
+ branch
+ branch_category
+ branch_owner
+ sub_tree
+ }) {
+ my $value = $self->{$key};
+ if ($key ne 'config' && defined($value)) {
+ $return .= "$key: $value\n";
+ }
+ }
+ return $return;
+}
+
+1;
+__END__
+
+=head1 NAME
+
+FCM::System::CM::SVN
+
+=head1 DESCRIPTION
+
+Part of L<FCM::System::CM|FCM::System::CM>. Provides an interface for common SVN
+functionalities used in the FCM CM sub-system.
+
+=head1 METHODS
+
+This is a sub-class of L<FCM::Class::CODE|FCM::Class::CODE>.
+
+=over 4
+
+=item $class->new(\%attrib)
+
+Return a new instance of this class. %attrib accepts a single "util" key for an
+instance of an L<FCM::Util|FCM::Util> object.
+
+=item $instance->call(@args)
+
+Call the command line "svn" with a list of arguments in @args.
+
+=item $instance->get_info(@path)
+=item $instance->get_info(\%option, @path)
+
+Invokes "svn info --xml @paths", and returns a LIST of info entries. If @paths
+is not specified, use ("."). If %option is specified, it may contain the keys:
+
+=over 4
+
+=item recursive
+
+If value of this key is not undef, add --recursive to "svn info".
+
+=item revision
+
+If value of this key is not undef, add --revision VALUE to "svn info".
+
+=back
+
+Each info entry is a HASH with keys reflecting the tag or attribute name in an
+entry element. The original hierarchy below the entry element is delimited by a
+colon in the name. For example, a return structure may look like this:
+ [ { 'commit:author' => 'fred',
+ 'commit:date' => '2011-11-09T15:41:14.514665Z',
+ 'commit:revision' => '4549',
+ 'kind' => 'dir',
+ 'path' => 'trunk',
+ 'revision' => '4552',
+ 'repository:root' => 'svn://host/my-repos',
+ 'repository:uuid' => '91f685bf-fbee-0310-99e6-f3aa9e660bd5'
+ 'url' => 'svn://host/my-repos/FCM/trunk',
+ },
+ ]
+
+=item $instance->get_layout($url)
+
+Return an instance of L<FCM::System::CM::SVN::Layout|/FCM::System::CM::SVN::Layout>
+containing the repository layout information of $url.
+
+=item $instance->get_layout_common($root, $rev, $path, $is_local)
+
+Return an instance of L<FCM::System::CM::SVN::Layout|/FCM::System::CM::SVN::Layout>
+containing the repository layout information for $path in $root at $rev. If
+$is_local is true, use "svnlook" to verify the existence of $path in $root
+at $rev. Otherwise, it uses "svn info" instead. If $rev is assumed to be a
+transaction if it is not numeric.
+
+=item $instance->get_list($url_arg, $filter_func)
+
+Call "svn list" multiple times to obtain a recursive listing of files and
+directories under $url_arg. Return a list containing the listing. If
+$filter_func is defined, it should be a CODE reference, which would be invoked
+for each file/directory found. It should have the interface:
+
+ ($can_return, $can_recurse)
+ = $filter_func->($this_url, $this_name, $is_dir, $depth);
+
+where $this_url is the URL of the file/directory found, $this_name is the
+base name of the file/directory found, $is_dir is true if it is a directory,
+$depth is the directory depth of $this_url relative to $url_arg.
+
+The $filter_func CODE reference should return a 2-element list ($can_return,
+$can_recurse). The get_list method will only return $this_url in the listing
+if $can_return is set to true. If $is_dir is true and $can_recurse is true, the
+get_list method will go down to do more listing in $this_url.
+
+=item $instance->get_log(@path)
+=item $instance->get_log(\%option, @path)
+
+Invokes "svn log --xml". If @paths is not specified, use ("."). If %option is
+specified, it may contain the keys:
+
+=over 4
+
+=item revision
+
+If value of this key is not undef, add --revision VALUE to "svn log".
+
+=item stop-on-copy
+
+If value of this key is not undef, add --stop-on-copy to "svn log".
+
+=back
+
+Returns an ARRAY reference. Each element is a data structure that represents a
+log entry. The data structure should look like:
+ [ { 'author' => $author,
+ 'date' => $date, # seconds since epoch
+ 'msg' => $msg,
+ 'paths' => [
+ { 'path' => $path,
+ 'action' => $action,
+ 'copyfrom-path' => $p,
+ 'copyfrom-rev' => $r,
+ },
+ # ...
+ ],
+ 'revision' => $revision,
+ },
+ ]
+
+=item $instance->get_username($target)
+
+Return the user name associated with $target.
+
+=item $instance->get_wc_root($path)
+
+Return the path to the root working copy directory of the argument.
+
+=item $instance->load_layout_config($root)
+
+Return a HASH (not a reference) containing the layout configuration of $root.
+See %LAYOUT_CONFIG for default settings. $root should be the URL to a
+repository root.
+
+=item $instance->split_by_peg($location)
+
+Split a location string (either a URL at PEG or a PATH at PEG) and return a
+two-element list: either (URL, PEG) or (PATH, PEG).
+
+=item $instance->stdout(@command)
+
+Call a @command, capture and return the STDOUT on success. In scalar context,
+return the STDOUT as-is. In array context, return it as a list of lines with the
+new line characters removed.
+
+=back
+
+=head1 EXCEPTION
+
+Methods in this class may throw an
+L<FCM::System::Exception|FCM::System::Exception> on error.
+
+=head1 FCM::System::CM::SVN::Layout
+
+The FCM::System::CM::SVN::Layout class inherits from
+L<FCM::Class::HASH|FCM::Class::HASH>. An instance represents the layout
+information in a Subversion URL based on the default or specified FCM layout
+information. It has the following attributes:
+
+=over 4
+
+=item config
+
+is a HASH containing the layout configuration applied to this URL.
+Valid keys and their default values are:
+
+=over 4
+
+=item depth-project => undef
+Number of sub-directories used by the name of a project.
+
+=item depth-branch => 3
+Number of sub-directories (under "branches") used by the name of branch.
+
+=item depth-tag => 1
+Number of sub-directories (under "tags") used by the name of branch.
+
+=item dir-trunk => 'trunk'
+Name of the master/trunk directory.
+
+=item dir-branch => 'branches'
+Name of the directory where all branches live. May be empty.
+
+=item dir-tag => 'tags'
+Name of the directory where all tags live. May be empty.
+
+=item level-owner-branch => 2
+Sub-directory level in the name of a branch containing the its owner.
+
+=item level-owner-branch => undef
+Sub-directory level in the name of a tag containing the its owner.
+
+=item template-branch => '{category}/{owner}/{name_prefix}{name}'
+Branch name template.
+
+=item template-tag => undef
+Tag name template.
+
+=back
+
+=item url
+
+is the full URL at PEG.
+
+=item root
+
+is the repository root.
+
+=item path
+
+is the path below the repository root.
+
+=item peg_rev
+
+is the (peg) revision of the URL.
+
+=item project
+
+is the project name in the URL. It is undef if the URL does not contain a valid
+project name for the given repository. An empty string is possible, for example,
+if the layout means that the trunk is at the root level.
+
+=item branch
+
+is the "branch" name in the URL, (which may be the name of the master/trunk
+branch or the name of a tag). It is undef if the URL does not contain a valid
+branch name for the given repository.
+
+=item branch_category
+
+is the category (i.e. "trunk", "branch" or "tag") of the branch.
+
+=item branch_owner
+
+is the owner of the branch, if it can be derived from the URL.
+
+=item sub_tree
+
+is the path in the URL under the branch of a project tree. It is undef if the
+URL is not at or below the level of a branch of the project tree. An empty
+string means the that the URL is at root level of the project tree.
+
+=back
+
+An FCM::System::CM::SVN::Layout instance has the following convenient methods:
+
+=over 4
+
+=item $layout->is_trunk()
+
+The URL is in the trunk of a project.
+
+=item $layout->is_branch()
+
+The URL is in a branch of a project.
+
+=item $layout->is_tag()
+
+The URL is in a tag of a project.
+
+=item $layout->is_owned_by_user($user)
+
+The URL is in a branch owned by $user. If $user is not defined, it defaults to
+the current user ID.
+
+=item $layout->is_shared()
+
+The URL is in a shared branch.
+
+=back
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/System/Exception.pm b/lib/FCM/System/Exception.pm
new file mode 100644
index 0000000..b7a356e
--- /dev/null
+++ b/lib/FCM/System/Exception.pm
@@ -0,0 +1,421 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+
+package FCM::System::Exception;
+use base qw{FCM::Exception};
+use Scalar::Util qw{blessed};
+
+use constant {
+ BUILD_SOURCE => 'BUILD_SOURCE',
+ BUILD_SOURCE_SYN => 'BUILD_SOURCE_SYN',
+ BUILD_TARGET => 'BUILD_TARGET',
+ BUILD_TARGET_BAD => 'BUILD_TARGET_BAD',
+ BUILD_TARGET_CYC => 'BUILD_TARGET_CYC',
+ BUILD_TARGET_DEP => 'BUILD_TARGET_DEP',
+ BUILD_TARGET_DUP => 'BUILD_TARGET_DUP',
+ CACHE_LOAD => 'CACHE_LOAD',
+ CACHE_TYPE => 'CACHE_TYPE',
+ CM_ALREADY_EXIST => 'CM_ALREADY_EXIST',
+ CM_ARG => 'CM_ARG',
+ CM_BRANCH_NAME => 'CM_BRANCH_NAME',
+ CM_BRANCH_SOURCE => 'CM_BRANCH_SOURCE',
+ CM_CHECKOUT => 'CM_CHECKOUT',
+ CM_LOG_EDIT_DELIMITER => 'CM_LOG_EDIT_DELIMITER',
+ CM_LOG_EDIT_NULL => 'CM_LOG_EDIT_NULL',
+ CM_OPT_ARG => 'CM_OPT_ARG',
+ CM_PROJECT_NAME => 'CM_PROJECT_NAME',
+ CM_REPOSITORY => 'CM_REPOSITORY',
+ CONFIG_CONFLICT => 'CONFIG_CONFLICT',
+ CONFIG_INHERIT => 'CONFIG_INHERIT',
+ CONFIG_MODIFIER => 'CONFIG_MODIFIER',
+ CONFIG_NS => 'CONFIG_NS',
+ CONFIG_NS_VALUE => 'CONFIG_NS_VALUE',
+ CONFIG_UNKNOWN => 'CONFIG_UNKNOWN',
+ CONFIG_VALUE => 'CONFIG_VALUE',
+ COPY => 'COPY',
+ DEST_CLEAN => 'DEST_CLEAN',
+ DEST_CREATE => 'DEST_CREATE',
+ DEST_LOCKED => 'DEST_LOCKED',
+ EXPORT_ITEMS_SRC => 'EXPORT_ITEMS_SRC',
+ EXTRACT_LOC_BASE => 'EXTRACT_LOC_BASE',
+ EXTRACT_MERGE => 'EXTRACT_MERGE',
+ EXTRACT_NS => 'EXTRACT_NS',
+ MIRROR => 'MIRROR',
+ MIRROR_NULL => 'MIRROR_NULL',
+ MIRROR_SOURCE => 'MIRROR_SOURCE',
+ MIRROR_TARGET => 'MIRROR_TARGET',
+ MAKE => 'MAKE',
+ MAKE_ARG => 'MAKE_ARG',
+ MAKE_CFG => 'MAKE_CFG',
+ MAKE_CFG_FILE => 'MAKE_CFG_FILE',
+ MAKE_PROP_NS => 'MAKE_PROP_NS',
+ MAKE_PROP_VALUE => 'MAKE_PROP_VALUE',
+ SHELL => 'SHELL',
+};
+
+1;
+__END__
+
+=head1 NAME
+
+FCM::System::Exception
+
+=head1 SYNOPSIS
+
+ eval {
+ # ...
+ FCM::System::Exception->throw($code, $ctx);
+ # ...
+ FCM::System::Exception->throw($code, $ctx, {exception => $e});
+ # ...
+ };
+ if (my $e = $@) {
+ if (FCM::System::Exception->caught($e)) {
+ # do something ...
+ }
+ else {
+ # do something else ...
+ }
+ }
+
+=head1 DESCRIPTION
+
+This exception represents an error condition in an FCM sub-system. It is a
+sub-class of L<FCM::Exception|FCM::Exception>.
+
+=head1 CONSTANTS
+
+The following are known error code:
+
+=over 4
+
+=item FCM::System::Exception->BUILD_SOURCE
+
+The build sub-system fails because a specified source does not exist. Expects
+$e->get_ctx() to return the source path.
+
+=item FCM::System::Exception->BUILD_SOURCE_SYN
+
+The build sub-system fails because a specified source has a syntax error.
+Expects $e->get_ctx() to return an ARRAY reference containing the source path
+and the line number where the error occurs.
+
+=item FCM::System::Exception->BUILD_TARGET
+
+The build sub-system fails because a target does not exist when it is supposed
+to be updated. Expects $e->get_ctx() to return an instance of
+L<FCM::Context::Make::Build::Target|FCM::Context::Make::Build/FCM::Context::Make::Build::Target>.
+
+=item FCM::System::Exception->BUILD_TARGET_BAD
+
+The build sub-system fails because the user has specified invalid targets.
+Expects $e->get_ctx() to return an ARRAY reference of the bad target keys.
+
+=item FCM::System::Exception->BUILD_TARGET_CYC
+
+The build sub-system fails due to cyclic dependency in a target. Expects
+$e->get_ctx() to return a HASH {$key => {'keys' => \@stack}, ...}, where each
+$key is the ID of a problematic target and the @stack is an ARRAY reference of a
+stack of target keys where the problem is detected.
+
+=item FCM::System::Exception->BUILD_TARGET_DEP
+
+The build sub-system fails because some targets have missing dependencies.
+Expects $e->get_ctx() to return a HASH
+{$key => {'keys' => \@stack, 'values' => [$dep_key, $dep_type]}, ...}, where each
+$key is the ID of a problematic target, the @stack is an ARRAY reference of a
+stack of target keys where the problem is detected, and the
+[$dep_key, $dep_type] ARRAY contains the key and type of the dependency.
+
+=item FCM::System::Exception->BUILD_TARGET_DUP
+
+The build sub-system fails because there are multiple versions of a build
+target. Expects $e->get_ctx() to return a HASH
+{$key => {'keys' => \@stack, 'values' => \@ns}, ...} where each $key is the ID of a
+problematic target, the @stack is an ARRAY
+reference of a stack of target keys where the problem is detected,
+and @ns contains the name-spaces of the sources that give the same target key.
+
+=item FCM::System::Exception->CACHE_LOAD
+
+The system is unable to load a cache from a make destination. Expects
+$e->get_ctx() to return the path it fails to load; and the $e->get_exception()
+to return the original exception that triggers this failure.
+
+=item FCM::System::Exception->CACHE_TYPE
+
+The system loaded a cache into a data structure, but the data structure is not
+the expected object type. Expects $e->get_ctx() to return the path to the cache.
+
+=item FCM::System::Exception->CM_ALREADY_EXIST
+
+Attempt to create a target that already exists. Expects $e->get_ctx() to return the
+target URL.
+
+=item FCM::System::Exception->CM_ARG
+
+Attempt to supply a bad argument. Expects $e->get_ctx() to return the bad value.
+
+=item FCM::System::Exception->CM_BRANCH_NAME
+
+Attempt to create a branch with a bad name. Expects $e->get_ctx() to return the
+bad name.
+
+=item FCM::System::Exception->CM_BRANCH_SOURCE
+
+Attempt to create a branch with an invalid source. Expects $e->get_ctx() to
+return the source.
+
+=item FCM::System::Exception->CM_CHECKOUT
+
+Attempt to checkout to an existing working copy. Expects $e->get_ctx() to return
+an ARRAY containing the target path and the URL it is pointing to.
+
+=item FCM::System::Exception->CM_LOG_EDIT_DELIMITER
+
+The commit message delimiter is modified after an edit.
+
+=item FCM::System::Exception->CM_LOG_EDIT_NULL
+
+The commit message is empty after an edit.
+
+=item FCM::System::Exception->CM_OPT_ARG
+
+Attempt to supply a bad argument to a valid option. Expects $e->get_ctx() to
+return the option key and the bad value.
+
+=item FCM::System::Exception->CM_PROJECT_NAME
+
+Attempt to create a project with a bad name. Expects $e->get_ctx() to return the
+bad name.
+
+=item FCM::System::Exception->CM_REPOSITORY
+
+Attempt to access an invalid repository. Expects $e->get_ctx() to return the
+bad repository.
+
+=item FCM::System::Exception->CONFIG_CONFLICT
+
+In a make configuration file, a declaration attempts to modify a value that is
+inherited from a previous make, and considered read-only. Expects $e->get_ctx()
+to return a L<FCM::Context::ConfigEntry|FCM::Context::ConfigEntry> object.
+
+=item FCM::System::Exception->CONFIG_INHERIT
+
+In a make configuration file, a declaration attempts to inherit from a make that
+is either incomplete or failed. Expects $e->get_ctx() to return a
+L<FCM::Context::ConfigEntry|FCM::Context::ConfigEntry> object.
+
+=item FCM::System::Exception->CONFIG_MODIFIER
+
+In a make configuration file, a modifier of in a declaration is incorrect.
+Expects $e->get_ctx() to return a
+L<FCM::Context::ConfigEntry|FCM::Context::ConfigEntry> object and
+$e->get_exception() to return (if any) the original exception that triggers this
+failure.
+
+=item FCM::System::Exception->CONFIG_NS
+
+In a make configuration file, a declaration is missing a required name-space, or
+the name-space declaration is incorrect. Expects $e->get_ctx() to return a
+L<FCM::Context::ConfigEntry|FCM::Context::ConfigEntry> object and
+$e->get_exception() to return (if any) the original exception that triggers this
+failure.
+
+=item FCM::System::Exception->CONFIG_NS_VALUE
+
+In a make configuration file, the name-space of a declaration is incompatible
+with the value. (E.g. the number of name-space elements does not match with the
+number of words in a value.) Expects $e->get_ctx() to return a
+L<FCM::Context::ConfigEntry|FCM::Context::ConfigEntry> object and
+$e->get_exception() to return (if any) the original exception that triggers this
+failure.
+
+=item FCM::System::Exception->CONFIG_UNKNOWN
+
+In a make configuration file, the label of a declaration is unrecognised by the
+system. Expects $e->get_ctx() to return a reference to an ARRAY containing
+L<FCM::Context::ConfigEntry|FCM::Context::ConfigEntry> objects.
+
+=item FCM::System::Exception->CONFIG_VALUE
+
+In a make configuration file, the value of a declaration is incorrect. Expects
+$e->get_ctx() to return a L<FCM::Context::ConfigEntry|FCM::Context::ConfigEntry>
+object and $e->get_exception() to return (if any) the original exception that
+triggers this failure.
+
+=item FCM::System::Exception->COPY
+
+The system fails to perform a file copy. Expects $e->get_ctx() to return a
+2-element ARRAY reference to represent the source and the target, and the
+$e->get_exception() to return the original exception that triggers this failure.
+
+=item FCM::System::Exception->DEST_CLEAN
+
+A destination path cannot be removed. Expects $e->get_ctx() to return the path
+that the system fails to remove, and $e->get_exception() to return the original
+exception that triggers this failure.
+
+=item FCM::System::Exception->DEST_CREATE
+
+The system is unable to create a path at a make destination. Expects
+$e->get_ctx() to return the path it fails to create; and the $e->get_exception()
+to return the original exception that triggers this failure.
+
+=item FCM::System::Exception->DEST_LOCKED
+
+A lock file exists at the destination. Expects $e->get_ctx() to return the
+path to the lock file.
+
+=item FCM::System::Exception->EXPORT_ITEMS_SRC
+
+The system fails because the source location is not specified.
+
+=item FCM::System::Exception->EXTRACT_LOC_BASE
+
+The system fails to determine the location of a base tree of a project. Expects
+$e->get_ctx() to return the name-space of the project.
+
+=item FCM::System::Exception->EXTRACT_MERGE
+
+The system fails to merge the sources of an extract target. Expects
+$e->get_ctx() to return a HASH reference with the following keys:
+
+=over 4
+
+=item target
+
+The FCM::Context::Make::Extract::Target object associated with this failure.
+
+=item output
+
+The path to a file containing the failed merge output.
+
+=item keys_done
+
+The keys of the source trees providing the source files for this target that
+have already been merged.
+
+=item key
+
+The key of the source tree providing the source file for this target that causes
+the merge conflict.
+
+=item keys_left
+
+The keys of the source trees providing the source files for this target that are
+yet to be merged.
+
+=back
+
+=item FCM::System::Exception->EXTRACT_NS
+
+The system fails because there are some extract declarations for the name-spaces
+but the settings are not used. Expects $e->get_ctx() to return an ARRAY of bad
+name-spaces.
+
+=item FCM::System::Exception->MIRROR
+
+The mirror operation failed. Expects $e->get_ctx() to return a reference of a
+2-element ARRAY containing the source and the target of the mirror, and
+$e->get_exception() to return the original exception that triggers this failure.
+
+=item FCM::System::Exception->MIRROR_NULL
+
+The mirror step failed because a target is not specified. The $e->get_ctx() is
+undefined.
+
+=item FCM::System::Exception->MIRROR_SOURCE
+
+The mirror step failed because the destination of a completed step in the make
+is not suitable for mirroring. Expects $e->get_ctx() to return an ARRAY
+reference containing the names of the unsuitable steps.
+
+=item FCM::System::Exception->MIRROR_TARGET
+
+The mirror step failed to create the target. Expects $e->get_ctx() to
+return the target of the mirror, and $e->get_exception() to return the original
+exception that triggers this failure.
+
+=item FCM::System::Exception->MAKE
+
+A named step in a make is not implemented. Expects $e->get_ctx() to return the
+name of the step.
+
+=item FCM::System::Exception->MAKE_ARG
+
+A make sub-system fails because of bad command line arguments. Expects
+$e->get_ctx() to return an ARRAY reference of something like this:
+
+ my @list = @{$e->get_ctx()};
+ for (@list) {
+ my ($arg_index, $arg) = @{$_};
+ warn("Argument $arg_index ($arg) is invalid\n");
+ }
+
+=item FCM::System::Exception->MAKE_CFG
+
+A make sub-system fails because it can find no configuration.
+
+=item FCM::System::Exception->MAKE_CFG_FILE
+
+A make sub-system fails because it cannot file a named configuration file.
+Expects $e->get_ctx() to return the configuration file name.
+
+=item FCM::System::Exception->MAKE_PROP_NS
+
+A make sub-system fails because a property is specified with an invalid
+name-space. Expects $e->get_ctx() to return an ARRAY reference of something like
+this:
+
+ my @list = @{$e->get_ctx()};
+ for (@list) {
+ my ($step_name, $prop_name, $ns, $value) = @{$_};
+ warn("{$prop_name}[$ns]: prop ns is invalid\n");
+ }
+
+=item FCM::System::Exception->MAKE_PROP_VALUE
+
+A make sub-system fails because a property is specified with an invalid
+value. Expects $e->get_ctx() to return an ARRAY reference of something like
+this:
+
+ my @list = @{$e->get_ctx()};
+ for (@list) {
+ my ($step_name, $prop_name, $ns, $value) = @{$_};
+ warn("{$prop_name}[$ns] = $value: prop value is bad\n");
+ }
+
+=item FCM::System::Exception->SHELL
+
+A shell command returns an error. Expects $e->get_ctx() to return a HASH
+reference containing {command_list}, an ARRAY reference representing the
+command; {rc}, the return code; {out}, the standard output of the command and
+{err}, the standard error of the command. Expects $e->get_exception() to return
+the standard error of the command.
+
+=back
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/System/Make.pm b/lib/FCM/System/Make.pm
new file mode 100644
index 0000000..3e13021
--- /dev/null
+++ b/lib/FCM/System/Make.pm
@@ -0,0 +1,451 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+# ------------------------------------------------------------------------------
+package FCM::System::Make;
+use base qw{FCM::Class::CODE};
+
+use FCM::Context::ConfigEntry;
+use FCM::Context::Event;
+use FCM::System::Exception;
+use FCM::System::Make::Build;
+use FCM::System::Make::Extract;
+use FCM::System::Make::Mirror;
+use FCM::System::Make::Preprocess;
+use FCM::System::Make::Share::Config;
+use FCM::System::Make::Share::Dest;
+use File::Path qw{rmtree};
+use File::Spec::Functions qw{catfile};
+use File::Copy qw{copy};
+use File::Temp;
+use POSIX qw{strftime};
+use Sys::Hostname qw{hostname};
+
+# Actions of the named common steps
+my %ACTION_OF = (
+ 'config-parse' => \&_config_parse,
+ 'dest-init' => \&_dest_init ,
+);
+# Alias to class name
+my $E = 'FCM::System::Exception';
+# The initial steps to run
+my @INIT_STEPS = (qw{config-parse dest-init});
+# The name of the system
+our $NAME = 'make';
+# Base name of common configuration file
+our $CFG_BASE = 'make.cfg';
+# A map of named helper utilities
+our %SHARED_UTIL_OF = (
+ 'config' => 'FCM::System::Make::Share::Config',
+ 'dest' => 'FCM::System::Make::Share::Dest' ,
+);
+# A map of named subsystems
+our %SUBSYSTEM_OF = (
+ 'build' => 'FCM::System::Make::Build' ,
+ 'extract' => 'FCM::System::Make::Extract' ,
+ 'mirror' => 'FCM::System::Make::Mirror' ,
+ 'preprocess' => 'FCM::System::Make::Preprocess',
+);
+
+# Creates the class.
+__PACKAGE__->class(
+ { cfg_base => {isa => '$', default => $CFG_BASE},
+ name => {isa => '$', default => $NAME},
+ shared_util_of => '%',
+ subsystem_of => '%',
+ util => '&',
+ },
+ {init => \&_init, action_of => {main => \&_main}},
+);
+
+# Initialises an instance.
+sub _init {
+ my $attrib_ref = shift();
+ for (
+ ['shared_util_of', \%SHARED_UTIL_OF],
+ ['subsystem_of' , \%SUBSYSTEM_OF ],
+ ) {
+ my ($key, $hash_ref) = @{$_};
+ while (my ($id, $class) = each(%{$hash_ref})) {
+ if (!exists($attrib_ref->{$key}{$id})) {
+ $attrib_ref->{$key}{$id} = $class->new({
+ 'shared_util_of' => $attrib_ref->{'shared_util_of'},
+ 'subsystem_of' => $attrib_ref->{'subsystem_of'},
+ 'util' => $attrib_ref->{'util'},
+ });
+ }
+ }
+ }
+ $attrib_ref->{util}->cfg_init(
+ $attrib_ref->{cfg_base},
+ sub {
+ my $config_reader = shift();
+ my @unknown_entries;
+ while (defined(my $entry = $config_reader->())) {
+ my ($id, $label) = split(qr{\.}msx, $entry->get_label(), 2);
+ if (exists($attrib_ref->{subsystem_of}{$id})) {
+ my $subsystem = $attrib_ref->{subsystem_of}{$id};
+ if (!$subsystem->config_parse_class_prop($entry, $label)) {
+ push(@unknown_entries, $entry);
+ }
+ }
+ else {
+ push(@unknown_entries, $entry);
+ }
+ }
+ if (@unknown_entries) {
+ return $E->throw($E->CONFIG_UNKNOWN, \@unknown_entries);
+ }
+ },
+ );
+}
+
+# Sets up the destination.
+sub _config_parse {
+ my ($attrib_ref, $m_ctx, @args) = @_;
+ my $entry_callback_ref = sub {
+ my ($entry) = @_;
+ print({$attrib_ref->{handle_cfg}} $entry->as_string(), "\n");
+ };
+ $attrib_ref->{shared_util_of}{config}->parse(
+ $entry_callback_ref, $m_ctx, @args,
+ );
+}
+
+# Sets up the destination.
+sub _dest_init {
+ my ($attrib_ref, $m_ctx) = @_;
+ $attrib_ref->{shared_util_of}{dest}->dest_init($m_ctx);
+
+ # Move temporary log file to destination
+ my $now = strftime("%Y%m%dT%H%M%S", gmtime());
+ my $log = $attrib_ref->{shared_util_of}{dest}->path($m_ctx, 'sys-log');
+ my $log_actual = sprintf("%s-%s", $log, $now);
+ _symlink($log_actual, $log);
+ ( close($attrib_ref->{handle_log})
+ && copy($attrib_ref->{handle_log}->filename(), $log)
+ && open(my $handle_log, '>>', $log)
+ ) || return $E->throw($E->DEST_CREATE, $log, $!);
+ _symlink(
+ $FCM::System::Make::Share::Dest::PATH_OF{'sys-log'},
+ $attrib_ref->{shared_util_of}{dest}->path($m_ctx, 'sys-log-symlink'),
+ );
+ my $log_ctx = $attrib_ref->{util}->util_of_report()->get_ctx($m_ctx);
+ $log_ctx->set_handle($handle_log);
+
+ # Saves as parsed config
+ my $cfg = $attrib_ref->{shared_util_of}{dest}->path(
+ $m_ctx, 'sys-config-as-parsed',
+ );
+ ( close($attrib_ref->{handle_cfg})
+ && copy($attrib_ref->{handle_cfg}->filename(), $cfg)
+ ) || return $E->throw($E->DEST_CREATE, $cfg, $!);
+ _symlink(
+ $FCM::System::Make::Share::Dest::PATH_OF{'sys-config-as-parsed'},
+ $attrib_ref->{shared_util_of}{dest}->path(
+ $m_ctx,
+ 'sys-config-as-parsed-symlink',
+ ),
+ );
+}
+
+# The main function of an instance of this class.
+sub _main {
+ my ($attrib_ref, $option_hash_ref, @args) = @_;
+ my @bad_args;
+ for my $i (0 .. $#args) {
+ if (index($args[$i], "=") < 0) {
+ push(@bad_args, [$i, $args[$i]]);
+ }
+ }
+ if (@bad_args) {
+ return $E->throw($E->MAKE_ARG, \@bad_args);
+ }
+ # Starts the system
+ my $m_ctx = FCM::Context::Make->new({option_of => $option_hash_ref});
+ my $T = sub {_timer_wrap($attrib_ref, @_)};
+ eval {$T->(
+ sub {
+ my %attrib = (
+ %{$attrib_ref},
+ handle_log => File::Temp->new(),
+ handle_cfg => File::Temp->new(),
+ );
+ $attrib_ref->{util}->util_of_report()->add_ctx(
+ $m_ctx, # key
+ { handle => $attrib{handle_log},
+ type => undef,
+ verbosity => $attrib_ref->{util}->util_of_report()->HIGH,
+ },
+ );
+ $attrib_ref->{util}->event(
+ FCM::Context::Event->FCM_VERSION,
+ $attrib_ref->{util}->version(),
+ );
+ for my $step (@INIT_STEPS) {
+ $T->(sub {$ACTION_OF{$step}->(\%attrib, $m_ctx, @args)}, $step);
+ }
+ my $prev_m_ctx = $m_ctx->get_prev_ctx();
+ if (defined($prev_m_ctx)) {
+ for my $step (keys(%{$prev_m_ctx->get_ctx_of()})) {
+ if (!grep {$_ eq $step} @{$m_ctx->get_steps()}) {
+ delete($prev_m_ctx->get_ctx_of()->{$step});
+ }
+ }
+ }
+ for my $step (@{$m_ctx->get_steps()}) {
+ my $ctx = $m_ctx->get_ctx_of($step);
+ if (!defined($ctx)) {
+ return $E->throw($E->MAKE, $step);
+ }
+ my $id_of_class = $ctx->get_id_of_class();
+ if (!exists($attrib_ref->{subsystem_of}{$id_of_class})) {
+ return $E->throw($E->MAKE, $step);
+ }
+ my $impl = $attrib_ref->{subsystem_of}{$id_of_class};
+ $ctx->set_status($m_ctx->ST_INIT);
+ if ($ctx->can('set_dest')) {
+ $ctx->set_dest(
+ $attrib_ref->{shared_util_of}{dest}->path(
+ $m_ctx, 'target', $ctx->get_id(),
+ ),
+ );
+ }
+ eval {$T->(sub {$impl->main($m_ctx, $ctx)}, $step)};
+ if (my $e = $@) {
+ $ctx->set_status($m_ctx->ST_FAILED);
+ die($e);
+ }
+ $ctx->set_status($m_ctx->ST_OK);
+ if ( defined($prev_m_ctx)
+ && exists($prev_m_ctx->get_ctx_of()->{$step})
+ ) {
+ delete($prev_m_ctx->get_ctx_of()->{$step});
+ }
+ }
+ },
+ )};
+ if (my $e = $@) {
+ $m_ctx->set_status($m_ctx->ST_FAILED);
+ $m_ctx->set_error($e);
+ $attrib_ref->{util}->event(FCM::Context::Event->E, $e);
+ _main_finally($attrib_ref, $m_ctx);
+ die("\n");
+ }
+ $m_ctx->set_status($m_ctx->ST_OK);
+ $attrib_ref->{shared_util_of}{dest}->save(
+ [$attrib_ref->{shared_util_of}{config}->unparse($m_ctx)],
+ $m_ctx,
+ 'sys-config-on-success',
+ );
+ _symlink(
+ $FCM::System::Make::Share::Dest::PATH_OF{'sys-config-on-success'},
+ $attrib_ref->{shared_util_of}{dest}->path(
+ $m_ctx,
+ 'sys-config-on-success-symlink',
+ ),
+ );
+ _main_finally($attrib_ref, $m_ctx);
+ return $m_ctx;
+}
+
+# Helper to run the "finally" part of "_main".
+sub _main_finally {
+ my ($attrib_ref, $m_ctx) = @_;
+ $m_ctx->set_inherit_ctx_list([]);
+ $m_ctx->set_prev_ctx(undef);
+ $attrib_ref->{shared_util_of}{dest}->dest_done($m_ctx);
+ my $log_ctx = $attrib_ref->{util}->util_of_report()->del_ctx($m_ctx);
+ close($log_ctx->get_handle());
+}
+
+# Wrap "symlink".
+sub _symlink {
+ my ($source, $target) = @_;
+ if (-l $target && readlink($target) eq $source) {
+ return;
+ }
+ if (-e $target || -l $target) {
+ rmtree($target);
+ }
+ symlink($source, $target) || return $E->throw($E->DEST_CREATE, $target, $!);
+}
+
+# Wraps a piece of code with timer events.
+sub _timer_wrap {
+ my ($attrib_ref, $code_ref, @names) = @_;
+ my @event_args = (
+ FCM::Context::Event->TIMER,
+ join(q{ }, $attrib_ref->{name}, @names),
+ time(),
+ );
+ $attrib_ref->{util}->event(@event_args);
+ my $timer = $attrib_ref->{util}->timer();
+ my $return = eval {wantarray() ? [$code_ref->()] : $code_ref->()};
+ my $e = $@;
+ $attrib_ref->{util}->event(@event_args, $timer->(), $e);
+ if ($e) {
+ die($e);
+ }
+ return (wantarray() ? @{$return} : $return);
+}
+
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::System::Make
+
+=head1 SYNOPSIS
+
+ use FCM::System::Make;
+ my $system = FCM::System::Make->new(\%attrib);
+ $system->(\%option);
+
+
+=head1 DESCRIPTION
+
+Invokes the FCM make system.
+
+=head1 METHODS
+
+=over 4
+
+=item $class->new(\%attrib)
+
+Creates and returns a new instance. The %attrib may contain the following:
+
+=over 4
+
+=item cfg_base
+
+The base name of the common (site/user) configuration file. (default="make.cfg")
+
+=item name
+
+The name of this sub-system. (default="make")
+
+=item shared_util_of
+
+A HASH to map the names to the classes of the named helper utilities for the
+make system and its sub-systems. (default = %FCM::System::Make::SHARED_UTIL_OF)
+
+=item subsystem_of
+
+A HASH to map the names to the classes of the subsystems. (default =
+%FCM::System::Make::SUBSYSTEM_OF)
+
+=item util
+
+An instance of L<FCM::Util|FCM::Util>.
+
+=back
+
+=item $system->(\%option)
+
+Invokes a make. The %option may contain the following:
+
+=over 4
+
+=item config-file
+
+The path to the configuration file. (default = $PWD/fcm-make.cfg)
+
+=item ignore-lock
+
+This flag can be used to ignore the lock file. The system creates a lock file in
+the destination to prevent another command from running in the same destination.
+If this flag is set, the system will continue even if it encounters a lock file
+in the destination. (default = false)
+
+=item jobs
+
+The number of (child) jobs that can be used to run parallel tasks.
+
+=item new
+
+A flag to tell the system to perform a new make. (default = false, i.e.
+incremental make)
+
+=back
+
+Throws L<FCM::System::Exception|FCM::System::Exception> on error.
+
+=back
+
+=head1 SUBSYSTEMS
+
+A subsystem of the make system should be a CODE-based class that implements a
+particular set of methods. (Some of these methods can be imported from
+L<FCM::System::Make::Share::Subsystem|FCM::System::Make::Share::Subsystem>.) The
+methods that should be implemented are:
+
+=over 4
+
+=item $subsystem_class->new(\%attrib)
+
+Creates a new instance of the subsystem. The make system passes the
+I<shared_util_of>, I<subsystem_of> and I<util> attributes to this method.
+
+=item $subsystem->config_parse($ctx,$entry,$label)
+
+Reads the settings of $entry into the $ctx. The $label is the configuration
+entry label in the context of the subsystem. (This is normally the
+$entry->get_label() but with the context ID prefix removed.). Returns true on
+success.
+
+=item $subsystem->config_parse_inherit_hook($ctx,$i_ctx)
+
+This method is called when the make inherits from an existing make. The $ctx is
+the current subsystem context, and the $i_ctx is the inherited subsystem
+context. This method allows the subsystem to make use of the inherited settings
+in the current context.
+
+=item $subsystem->config_unparse($ctx)
+
+Returns a list of L<FCM::Context::ConfigEntry|FCM::Context::ConfigEntry> to
+represent the settings of the $ctx.
+
+=item $subsystem->ctx($id_of_class,$id)
+
+Returns a new context for the subsystem. The $id_of_class is the ID of the
+subsystem class. The $id is the step ID of the context.
+
+=item $subsystem->config_parse_class_prop($entry,$label)
+
+Reads a configuration $entry into the subsystem default property. The $label is
+the label of the $entry, but with the prefix (the subsystem ID plus a dot)
+removed.
+
+=item $subsystem->main($m_ctx,$ctx)
+
+Invokes the subsystem. The $m_ctx is the current context of the make (as a
+blessed reference of L<FCM::Context::Make|FCM::Context::Make>). The $ctx is the
+context of the subsystem.
+
+=back
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/System/Make/Build.pm b/lib/FCM/System/Make/Build.pm
new file mode 100644
index 0000000..8e55c03
--- /dev/null
+++ b/lib/FCM/System/Make/Build.pm
@@ -0,0 +1,1650 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+# ------------------------------------------------------------------------------
+package FCM::System::Make::Build;
+use base qw{FCM::Class::CODE};
+
+use Cwd qw{cwd realpath};
+use FCM::Context::ConfigEntry;
+use FCM::Context::Event;
+use FCM::Context::Make::Build;
+use FCM::Context::Task;
+use FCM::System::Exception;
+use FCM::System::Make::Build::FileType::C;
+use FCM::System::Make::Build::FileType::CXX;
+use FCM::System::Make::Build::FileType::Data;
+use FCM::System::Make::Build::FileType::Fortran;
+use FCM::System::Make::Build::FileType::H;
+use FCM::System::Make::Build::FileType::NS;
+use FCM::System::Make::Build::FileType::Script;
+use FCM::System::Make::Share::Subsystem;
+use File::Basename qw{basename dirname};
+use File::Find qw{find};
+use File::Path qw{mkpath};
+use File::Spec::Functions qw{abs2rel catfile rel2abs splitdir splitpath};
+use Storable qw{dclone};
+use Text::ParseWords qw{shellwords};
+
+# Aliases
+our ($EVENT, $UTIL);
+my $E = 'FCM::System::Exception';
+my $STATE = 'FCM::System::Make::Build::State';
+
+# Classes for working with typed source files
+our @FILE_TYPE_UTILS = (
+ 'FCM::System::Make::Build::FileType::C',
+ 'FCM::System::Make::Build::FileType::CXX',
+ 'FCM::System::Make::Build::FileType::Data',
+ 'FCM::System::Make::Build::FileType::Fortran',
+ 'FCM::System::Make::Build::FileType::H',
+ 'FCM::System::Make::Build::FileType::NS',
+ 'FCM::System::Make::Build::FileType::Script',
+);
+
+# Default target selection
+our %TARGET_SELECT_BY = (task => {});
+
+# Configuration parser label to action map
+our %CONFIG_PARSER_OF = (
+ 'ns-excl' => _config_parse_ns_filter_func(sub {$_[0]->get_input_ns_excl()}),
+ 'ns-incl' => _config_parse_ns_filter_func(sub {$_[0]->get_input_ns_incl()}),
+ 'source' => \&_config_parse_source,
+ 'target' => \&_config_parse_target,
+ 'target-rename' => \&_config_parse_target_rename,
+);
+
+# Default properties
+our %PROP_OF = (
+ # [default , ns-ok]
+ 'ignore-missing-dep-ns' => [q{} , undef],
+ 'no-step-source' => [q{} , undef],
+ 'no-inherit-source' => [q{} , undef],
+ 'no-inherit-target-category' => [q{bin etc lib}, undef],
+);
+
+# Creates the class.
+__PACKAGE__->class(
+ { config_parser_of => {isa => '%', default => {%CONFIG_PARSER_OF}},
+ file_type_utils => {isa => '@', default => [@FILE_TYPE_UTILS]},
+ file_type_util_of => '%',
+ prop_of => {isa => '%', default => {%PROP_OF}},
+ target_select_by => {isa => '%', default => {%TARGET_SELECT_BY}},
+ util => '&',
+ },
+ { init => \&_init,
+ action_of => {
+ config_parse => \&_config_parse,
+ config_parse_class_prop => \&_config_parse_class_prop,
+ config_parse_inherit_hook => \&_config_parse_inherit_hook,
+ config_unparse => \&_config_unparse,
+ config_unparse_class_prop => \&_config_unparse_class_prop,
+ ctx => \&_ctx,
+ main => \&_main,
+ },
+ },
+);
+
+# Initialises the helpers of the class.
+sub _init {
+ my ($attrib_ref) = @_;
+ # Initialises file type utilities, if necessary
+ for my $class (@{$attrib_ref->{file_type_utils}}) {
+ $attrib_ref->{util}->class_load($class);
+ my $file_type_util = $class->new({util => $attrib_ref->{util}});
+ my $id = $file_type_util->id();
+ if (!defined($attrib_ref->{file_type_util_of}{$id})) {
+ $attrib_ref->{file_type_util_of}{$id} = $file_type_util;
+ }
+ }
+ # Initialises properties derived from the file type utilities
+ # TBD: warn if a property is already set and is different from previous?
+ while (
+ my ($id, $file_type_util) = each(%{$attrib_ref->{file_type_util_of}})
+ ) {
+ # File name extension, name pattern and she-bang pattern
+ for my $key (qw{ext pat she}) {
+ my $method = 'file_' . $key;
+ if ($file_type_util->can($method)) {
+ my $value = $file_type_util->$method();
+ if (defined($value)) {
+ $attrib_ref->{prop_of}{"file-$key.$id"} = [$value];
+ }
+ }
+ }
+ # Dependency types
+ if ($file_type_util->can('source_analyse_deps')) {
+ for my $name ($file_type_util->source_analyse_deps()) {
+ $attrib_ref->{prop_of}{"dep.$name"} = [q{}, 1];
+ $attrib_ref->{prop_of}{"no-dep.$name"} = [q{}, 1];
+ }
+ }
+ # Name-space dependency types
+ if ($file_type_util->can('ns_targets_deps')) {
+ for my $name ($file_type_util->ns_targets_deps()) {
+ $attrib_ref->{prop_of}{"ns-dep.$name"} = [q{}, 1];
+ }
+ }
+ # Target extensions
+ if ($file_type_util->can('target_file_ext_of')) {
+ while (my ($key, $value)
+ = each(%{$file_type_util->target_file_ext_of()})
+ ) {
+ $attrib_ref->{prop_of}{"file-ext.$key"} = [$value, 1];
+ }
+ }
+ # Target file naming options
+ if ($file_type_util->can('target_file_name_option_of')) {
+ while (my ($key, $value)
+ = each(%{$file_type_util->target_file_name_option_of()})
+ ) {
+ $attrib_ref->{prop_of}{"file-name-option.$key"} = [$value, 1];
+ }
+ }
+ # Task properties
+ my %task_of = %{$file_type_util->task_of()};
+ while (my ($name, $task) = each(%task_of)) {
+ if ($task->can('prop_of')) {
+ my %prop_of = %{$task->prop_of()};
+ while (my ($key, $value) = each(%prop_of)) {
+ $attrib_ref->{prop_of}{$key} = [$value, 1];
+ }
+ }
+ }
+ }
+}
+
+# A hook command for the "inherit/use" declaration.
+sub _config_parse_inherit_hook {
+ my ($attrib_ref, $ctx, $i_ctx) = @_;
+ push(@{$ctx->get_input_ns_excl()}, @{$i_ctx->get_input_ns_excl()});
+ push(@{$ctx->get_input_ns_incl()}, @{$i_ctx->get_input_ns_incl()});
+ while (my ($key, $value) = each(%{$i_ctx->get_target_key_of()})) {
+ $ctx->get_target_key_of()->{$key} = $value;
+ }
+ while (my ($key, $value) = each(%{$i_ctx->get_target_select_by()})) {
+ $ctx->get_target_select_by()->{$key} = dclone($value);
+ }
+ _config_parse_inherit_hook_prop($attrib_ref, $ctx, $i_ctx);
+}
+
+# Returns a function to parse a build/preprocess.ns-??cl declaration.
+sub _config_parse_ns_filter_func {
+ my ($getter) = @_;
+ sub {
+ my ($attrib_ref, $ctx, $entry) = @_;
+ if (@{$entry->get_ns_list()}) {
+ return $E->throw($E->CONFIG_NS, $entry);
+ }
+ @{$getter->($ctx)} = map {$_ eq q{/} ? q{} : $_} $entry->get_values();
+ };
+}
+
+# Parses a build/preprocess.source declaration.
+sub _config_parse_source {
+ my ($attrib_ref, $ctx, $entry) = @_;
+ my ($ns) = @{$entry->get_ns_list()};
+ $ns ||= q{};
+ $ctx->get_input_source_of()->{$ns} = [$entry->get_values()];
+}
+
+# Parses a build/preprocess.target declaration.
+sub _config_parse_target {
+ my ($attrib_ref, $ctx, $entry) = @_;
+ my %modifier_of = %{$entry->get_modifier_of()};
+ if (!keys(%modifier_of)) {
+ %modifier_of = (key => 1);
+ }
+ while (my $name = each(%modifier_of)) {
+ if (!grep {$_ eq $name} qw{category key ns task}) {
+ return $E->throw($E->CONFIG_MODIFIER, $entry);
+ }
+ $ctx->get_target_select_by()->{$name}
+ = {map {$_ eq q{/} ? (q{} => 1) : ($_ => 1)} $entry->get_values()};
+ }
+}
+
+# Parses a build/preprocess.target-rename declaration.
+sub _config_parse_target_rename {
+ my ($attrib_ref, $ctx, $entry) = @_;
+ $ctx->set_target_key_of({
+ map {
+ my ($old, $new) = split(qr{:}msx, $_, 2);
+ if (!$old || !$new) {
+ return $E->throw($E->CONFIG_VALUE, $entry);
+ }
+ ($old => $new);
+ } ($entry->get_values()),
+ });
+}
+
+# Turns a context into a list of configuration entries.
+sub _config_unparse {
+ my ($attrib_ref, $ctx) = @_;
+ my %LABEL_OF
+ = map {($_ => $ctx->get_id() . q{.} . $_)} keys(%CONFIG_PARSER_OF);
+ ( ( @{$ctx->get_input_ns_excl()}
+ ? FCM::Context::ConfigEntry->new({
+ label => $LABEL_OF{'ns-excl'},
+ value => _config_unparse_join(
+ map {$_ ? $_ : q{/}} @{$ctx->get_input_ns_excl()}
+ ),
+ })
+ : ()
+ ),
+ ( @{$ctx->get_input_ns_incl()}
+ ? FCM::Context::ConfigEntry->new({
+ label => $LABEL_OF{'ns-incl'},
+ value => _config_unparse_join(
+ map {$_ ? $_ : q{/}} @{$ctx->get_input_ns_incl()}
+ ),
+ })
+ : ()
+ ),
+ ( map {
+ FCM::Context::ConfigEntry->new({
+ label => $LABEL_OF{source},
+ ns_list => [$_],
+ value => _config_unparse_join(
+ sort(@{$ctx->get_input_source_of()->{$_}})
+ ),
+ })
+ }
+ sort keys(%{$ctx->get_input_source_of()})
+ ),
+ ( keys(%{$ctx->get_target_key_of()})
+ ? FCM::Context::ConfigEntry->new({
+ label => $LABEL_OF{'target-rename'},
+ value => _config_unparse_join(
+ map {$_ . ':' . $ctx->get_target_key_of()->{$_}}
+ sort keys(%{$ctx->get_target_key_of()})
+ ),
+ })
+ : ()
+ ),
+ ( map {
+ FCM::Context::ConfigEntry->new({
+ label => $LABEL_OF{'target'},
+ modifier_of => {$_ => 1},
+ value => _config_unparse_join(
+ keys(%{$ctx->get_target_select_by()->{$_}}),
+ ),
+ });
+ }
+ sort keys(%{$ctx->get_target_select_by()})
+ ),
+ _config_unparse_prop($attrib_ref, $ctx),
+ );
+}
+
+# Returns a new context.
+sub _ctx {
+ my ($attrib_ref, $id_of_class, $id) = @_;
+ FCM::Context::Make::Build->new({
+ id => $id,
+ id_of_class => $id_of_class,
+ target_select_by => dclone($attrib_ref->{target_select_by}),
+ });
+}
+
+# The main function of the class.
+sub _main {
+ my ($attrib_ref, $m_ctx, $ctx) = @_;
+ local($UTIL) = $attrib_ref->{util};
+ local($EVENT) = sub {$UTIL->event(@_)};
+ for my $function (
+ \&_sources_locate,
+ \&_sources_type,
+ \&_sources_analyse,
+ \&_targets_update,
+ ) {
+ $function->($attrib_ref, $m_ctx, $ctx);
+ }
+}
+
+# Locates the actual source files, and determines their types.
+sub _sources_locate {
+ my ($attrib_ref, $m_ctx, $ctx) = @_;
+ # From inherited
+ my %NO_INHERIT_FROM
+ = map {($_ => 1)} _props($attrib_ref, 'no-inherit-source', $ctx);
+ if (!$NO_INHERIT_FROM{'*'}) {
+ for my $i_ctx (_i_ctx_list($m_ctx, $ctx)) {
+ while (my ($ns, $source) = each(%{$i_ctx->get_source_of()})) {
+ if (!exists($NO_INHERIT_FROM{$ns})) { # exact name-spaces only
+ $ctx->get_source_of()->{$ns} = dclone($source);
+ }
+ }
+ }
+ }
+ # From specified input
+ while (my ($ns, $input_sources_ref) = each(%{$ctx->get_input_source_of()})) {
+ for my $input_source (@{$input_sources_ref}) {
+ my $path = realpath(rel2abs($input_source, $m_ctx->get_dest()));
+ _sources_locate_by_find($attrib_ref, $m_ctx, $ctx, $ns, $path);
+ }
+ }
+ # From completed make destinations
+ my %NO_SOURCE_FROM
+ = map {($_, 1)} _props($attrib_ref, 'no-step-source', $ctx);
+ for my $step (@{$m_ctx->get_steps()}) {
+ my $a_ctx = $m_ctx->get_ctx_of($step);
+ if ( !exists($NO_SOURCE_FROM{$step})
+ && defined($a_ctx)
+ && $a_ctx->get_status() eq $m_ctx->ST_OK
+ && $a_ctx->can('get_target_of')
+ ) {
+ my @target_list
+ = grep {$_->can_be_source()} values(%{$a_ctx->get_target_of()});
+ for my $target (@target_list) {
+ if ($target->is_ok() && -e $target->get_path()) {
+ my $checksum;
+ if ($target->can('get_checksum')) {
+ $checksum = $target->get_checksum();
+ }
+ my $source = $ctx->CTX_SOURCE->new({
+ checksum => $checksum,
+ ns => $target->get_ns(),
+ path => $target->get_path(),
+ });
+ $ctx->get_source_of()->{$target->get_ns()} = $source;
+ }
+ elsif (exists($ctx->get_source_of()->{$target->get_ns()})) {
+ delete($ctx->get_source_of()->{$target->get_ns()});
+ }
+ }
+ }
+ }
+ # Applies filter
+ my %INPUT_NS_EXCL = map {($_, 1)} @{$ctx->get_input_ns_excl()};
+ my %INPUT_NS_INCL = map {($_, 1)} @{$ctx->get_input_ns_incl()};
+ if (keys(%INPUT_NS_EXCL) || keys(%INPUT_NS_INCL)) {
+ while (my ($ns, $source) = each(%{$ctx->get_source_of()})) {
+ my $ns_iter_ref = $UTIL->ns_iter($ns, $UTIL->NS_ITER_UP);
+ NS:
+ while (defined(my $head = $ns_iter_ref->())) {
+ if (exists($INPUT_NS_INCL{$head})) {
+ last NS;
+ }
+ if (exists($INPUT_NS_EXCL{$head})) {
+ delete($ctx->get_source_of()->{$ns});
+ last NS;
+ }
+ }
+ }
+ }
+}
+
+# Locates the actual source files in $path.
+sub _sources_locate_by_find {
+ my ($attrib_ref, $m_ctx, $ctx, $key, $path) = @_;
+ if (!-e $path) {
+ return $E->throw($E->BUILD_SOURCE, $path, $!);
+ }
+ find(
+ sub {
+ my $path_found = $File::Find::name;
+ if (-d $path_found) {
+ return;
+ }
+ my ($vol, $dir_name, $base) = splitpath($path_found);
+ for my $name (splitdir($dir_name), $base) {
+ if (index($name, q{.}) == 0) {
+ return; # ignore Unix hidden/system files
+ }
+ }
+ my $ns = abs2rel($path_found, $path);
+ if ($key) {
+ $ns = $UTIL->ns_cat($key, $ns);
+ }
+ $ctx->get_source_of()->{$ns}
+ = $ctx->CTX_SOURCE->new({ns => $ns, path => $path_found});
+ },
+ $path,
+ );
+}
+
+# Determines source types.
+sub _sources_type {
+ my ($attrib_ref, $m_ctx, $ctx) = @_;
+ my %INPUT_FILE_EXT_TO_TYPE_MAP;
+ my %INPUT_FILE_PAT_TO_TYPE_MAP;
+ my %INPUT_FILE_SHE_TO_TYPE_MAP;
+ for (
+ ['file-ext.', \%INPUT_FILE_EXT_TO_TYPE_MAP, 1],
+ ['file-pat.', \%INPUT_FILE_PAT_TO_TYPE_MAP, 0],
+ ['file-she.', \%INPUT_FILE_SHE_TO_TYPE_MAP, 0],
+ ) {
+ my ($prefix, $map_ref, $value_is_words) = @{$_};
+ for my $id (keys(%{$attrib_ref->{file_type_util_of}})) {
+ my $name = $prefix . $id;
+ my $value = _prop($attrib_ref, $name, $ctx);
+ if (defined($value)) {
+ for my $key (($value_is_words ? shellwords($value) : ($value))) {
+ $map_ref->{$key} = $id;
+ }
+ }
+ }
+ }
+ my $type_func = sub {
+ my ($path) = @_;
+ # Try file name extension
+ my $extension = $UTIL->file_ext($path);
+ $extension = $extension ? q{.} . $extension : undef;
+ if ($extension && exists($INPUT_FILE_EXT_TO_TYPE_MAP{$extension})) {
+ return $INPUT_FILE_EXT_TO_TYPE_MAP{$extension};
+ }
+ # Try she-bang line
+ if (-T $path) {
+ my $line = $UTIL->file_head($path);
+ if ($line) {
+ while (my ($pattern, $type) = each(%INPUT_FILE_SHE_TO_TYPE_MAP)) {
+ if (index($line, '#!') == 0) { # OK to hard code this
+ keys(%INPUT_FILE_SHE_TO_TYPE_MAP); # reset iterator
+ return $type;
+ }
+ }
+ }
+ }
+ # Try file name pattern
+ my $base_name = basename($path);
+ while (my ($pattern, $type) = each(%INPUT_FILE_PAT_TO_TYPE_MAP)) {
+ if ($base_name =~ $pattern) {
+ keys(%INPUT_FILE_PAT_TO_TYPE_MAP); # reset iterator
+ return $type;
+ }
+ }
+ return q{};
+ };
+ while (my ($ns, $source) = each(%{$ctx->get_source_of()})) {
+ if (!defined($source->get_type())) {
+ $source->set_type($type_func->($source->get_path()));
+ }
+ }
+}
+
+# Reads source files to gather dependency and other information.
+sub _sources_analyse {
+ my ($attrib_ref, $m_ctx, $ctx) = @_;
+ my $timer = $UTIL->timer();
+ my %FILE_TYPE_UTIL_OF = %{$attrib_ref->{file_type_util_of}};
+ # Checksum
+ while (my ($ns, $source) = each(%{$ctx->get_source_of()})) {
+ if ( exists($FILE_TYPE_UTIL_OF{$source->get_type()})
+ && !defined($source->get_checksum())
+ ) {
+ $source->set_checksum($UTIL->file_md5($source->get_path()));
+ }
+ }
+ # Source information
+ my $n_jobs = $m_ctx->get_option_of('jobs');
+ my $runner = $UTIL->task_runner(
+ sub {_source_analyse($attrib_ref, @_)},
+ $n_jobs,
+ );
+ my $elapse_tasks = 0;
+ my $n = eval {
+ $runner->main(
+ _source_analyse_get_func($attrib_ref, $m_ctx, $ctx),
+ _source_analyse_put_func($attrib_ref, $m_ctx, $ctx, \$elapse_tasks),
+ );
+ };
+ my $e = $@;
+ $runner->destroy();
+ if ($e) {
+ die($e);
+ }
+ my $n_total = scalar(keys(%{$ctx->get_source_of()}));
+ $EVENT->(
+ FCM::Context::Event->MAKE_BUILD_SOURCE_SUMMARY,
+ $n_total, $n, $timer->(), $elapse_tasks,
+ );
+}
+
+# Reads a source to gather information.
+sub _source_analyse {
+ my ($attrib_ref, $source) = @_;
+ my $FILE_TYPE_UTIL
+ = $attrib_ref->{file_type_util_of}->{$source->get_type()};
+ if (!$FILE_TYPE_UTIL->can('source_analyse')) {
+ return;
+ }
+ $FILE_TYPE_UTIL->source_analyse($source);
+}
+
+# Generates an iterator for each source file requiring information gathering.
+sub _source_analyse_get_func {
+ my ($attrib_ref, $m_ctx, $ctx) = @_;
+ my $P_SOURCE_GETTER
+ = _prev_hash_item_getter($m_ctx, $ctx, sub {$_[0]->get_source_of()});
+ my %FILE_TYPE_UTIL_OF = %{$attrib_ref->{file_type_util_of}};
+ my $exhausted;
+ sub {
+ if ($exhausted) {
+ return;
+ }
+ SOURCE:
+ while (my ($ns, $source) = each(%{$ctx->get_source_of()})) {
+ my $type = $source->get_type();
+ if (!exists($FILE_TYPE_UTIL_OF{$type})) {
+ next SOURCE;
+ }
+ # Stores the current properties relevant to the source
+ for my $dep_type ($FILE_TYPE_UTIL_OF{$type}->source_analyse_deps()) {
+ for my $n (map {$_ . q{.} . $dep_type} qw{dep no-dep}) {
+ $source->get_prop_of()->{$n}
+ = _prop($attrib_ref, $n, $ctx, $ns);
+ }
+ }
+ # Compare with previous source, if possible
+ my $p_source = $P_SOURCE_GETTER->($ns);
+ if (defined($p_source)) {
+ $source->set_up_to_date(
+ $p_source->get_checksum() eq $source->get_checksum());
+ if ( $source->get_up_to_date()
+ && !$UTIL->hash_cmp(
+ map {$_->get_prop_of()} ($source, $p_source)
+ )
+ ) {
+ $source->set_info_of(dclone($p_source->get_info_of()));
+ $source->set_deps( dclone($p_source->get_deps() ));
+ next SOURCE;
+ }
+ }
+ return FCM::Context::Task->new({ctx => $source, id => $ns});
+ }
+ $exhausted = 1;
+ return;
+ };
+}
+
+# Generates a callback when a source read completes.
+sub _source_analyse_put_func {
+ my ($attrib_ref, $m_ctx, $ctx, $elapse_tasks_ref) = @_;
+ my %FILE_TYPE_UTIL_OF = %{$attrib_ref->{file_type_util_of}};
+ sub {
+ my ($task) = @_;
+ if ($task->get_state() eq $task->ST_FAILED) {
+ die($task->get_error());
+ }
+ my $ns = $task->get_id();
+ my $source = $ctx->get_source_of()->{$ns} = $task->get_ctx();
+ for my $type (
+ $FILE_TYPE_UTIL_OF{$source->get_type()}->source_analyse_deps()
+ ) {
+ # Note: "dep" property: use name-space value only
+ my $key = 'dep.' . $type;
+ push(
+ @{$source->get_deps()},
+ (map {[$_, $type]} _props($attrib_ref, $key, $ctx, $ns)),
+ );
+ }
+ ${$elapse_tasks_ref} += $task->get_elapse();
+ $EVENT->(
+ FCM::Context::Event->MAKE_BUILD_SOURCE_ANALYSE,
+ $source, $task->get_elapse(),
+ );
+ }
+}
+
+# Updates the targets.
+sub _targets_update {
+ my ($attrib_ref, $m_ctx, $ctx) = @_;
+ my $timer = $UTIL->timer();
+ # Creates and changes directory to the destination
+ eval {mkpath($ctx->get_dest())};
+ if ($@) {
+ return $E->throw($E->DEST_CREATE, $ctx->get_dest());
+ }
+ my $old_cwd = cwd();
+ chdir($ctx->get_dest()) || die(sprintf("%s: %s\n", $ctx->get_dest(), $!));
+ # Determines the destination search path
+ my $id = $ctx->get_id();
+ @{$ctx->get_dests()} = (
+ $ctx->get_dest(),
+ map {$_->get_ctx_of($id) ? @{$_->get_ctx_of($id)->get_dests()} : ()}
+ @{$m_ctx->get_inherit_ctx_list()}
+ ,
+ );
+ # Performs targets update
+ my %stat_of = ();
+ eval {
+ my $n_jobs = $m_ctx->get_option_of('jobs');
+ my $runner = $UTIL->task_runner(
+ sub {_target_update($attrib_ref, @_)},
+ $n_jobs,
+ );
+ eval {
+ my ($get_ref, $put_ref) = _targets_manager_funcs(
+ $attrib_ref, $m_ctx, $ctx, \%stat_of,
+ );
+ $runner->main($get_ref, $put_ref);
+ };
+ my $e = $@;
+ $runner->destroy();
+ if ($e) {
+ die($e);
+ }
+ };
+ my $e = $@;
+ # Back to original working directory
+ chdir($old_cwd) || die(sprintf("%s: %s\n", $old_cwd, $!));
+ if ($e) {
+ die($e);
+ }
+ # Finally
+ my @targets = values(%{$ctx->get_target_of()});
+ for my $key (sort(keys(%stat_of))) {
+ $stat_of{$key}{n}{$ctx->CTX_TARGET->ST_MODIFIED} ||= 0;
+ $stat_of{$key}{n}{$ctx->CTX_TARGET->ST_UNCHANGED} ||= 0;
+ $stat_of{$key}{n}{$ctx->CTX_TARGET->ST_FAILED} ||= 0;
+ $stat_of{$key}{t} ||= 0.0;
+ $EVENT->(
+ FCM::Context::Event->MAKE_BUILD_TARGET_TASK_SUMMARY,
+ $key,
+ $stat_of{$key}{n}{$ctx->CTX_TARGET->ST_MODIFIED},
+ $stat_of{$key}{n}{$ctx->CTX_TARGET->ST_UNCHANGED},
+ $stat_of{$key}{n}{$ctx->CTX_TARGET->ST_FAILED},
+ $stat_of{$key}{t},
+ );
+ }
+ $EVENT->(
+ FCM::Context::Event->MAKE_BUILD_TARGET_SUMMARY,
+ scalar(grep {$_->is_modified() } @targets),
+ scalar(grep {$_->is_unchanged()} @targets),
+ scalar(grep {$_->is_failed() } @targets),
+ $timer->(),
+ );
+ my @failed_targets = grep {$_->is_failed()} @targets;
+ if (@failed_targets) {
+ $EVENT->(
+ FCM::Context::Event->MAKE_BUILD_TARGETS_FAIL,
+ \@failed_targets
+ );
+ die("\n");
+ }
+}
+
+# Updates a target.
+sub _target_update {
+ my ($attrib_ref, $target) = @_;
+ my $file_type_util = $attrib_ref->{file_type_util_of}{$target->get_type()};
+ eval {$file_type_util->task_of()->{$target->get_task()}->main($target)};
+ if ($@) {
+ if ($target->get_path() && -e $target->get_path()) {
+ unlink($target->get_path());
+ }
+ die($@);
+ }
+ if (! -e $target->get_path()) {
+ return $E->throw($E->BUILD_TARGET, $target);
+ }
+ $target->set_status($target->ST_MODIFIED);
+ my $checksum = $UTIL->file_md5($target->get_path());
+ if ($target->get_checksum() && $checksum eq $target->get_checksum()) {
+ $target->set_status($target->ST_UNCHANGED);
+ if ($target->get_path_of_prev()) {
+ $target->set_path($target->get_path_of_prev());
+ }
+ }
+ $target->set_checksum($checksum);
+ $target->set_prop_of_prev_of({}); # unset
+ $target->set_path_of_prev(undef); # unset
+}
+
+# Returns the get/put functions to send/receive targets to update.
+sub _targets_manager_funcs {
+ my ($attrib_ref, $m_ctx, $ctx, $stat_hash_ref) = @_;
+
+ my @targets;
+ _targets_from_sources($attrib_ref, $m_ctx, $ctx, \@targets);
+ _targets_props_assign($attrib_ref, $m_ctx, $ctx, \@targets);
+
+ my ($stack_ref, $state_hash_ref)
+ = _targets_select($attrib_ref, $m_ctx, $ctx, \@targets);
+
+ my $get_action_ref = sub {
+ STATE:
+ while (my $state = pop(@{$stack_ref})) {
+ if ( !$state->is_ready()
+ || !_target_deps_are_done($state, $state_hash_ref, $stack_ref)
+ ) {
+ next STATE;
+ }
+ my $target = $state->get_target();
+ if (_target_check_failed_dep($state, $state_hash_ref)) {
+ _target_update_failed(
+ $stat_hash_ref, $ctx, $target, $state_hash_ref, $stack_ref,
+ );
+ }
+ elsif (_target_check_ood($state, $state_hash_ref)) {
+ _target_prep($state, $ctx);
+ $state->set_value($STATE->PENDING);
+ # Adds tasks that can be triggered by this task
+ for my $key (@{$target->get_triggers()}) {
+ if ( exists($state_hash_ref->{$key})
+ && !$state_hash_ref->{$key}->is_done()
+ && !grep {$_->get_id() eq $key} @{$stack_ref}
+ ) {
+ my $trigger_target
+ = $state_hash_ref->{$key}->get_target();
+ $trigger_target->set_status($trigger_target->ST_OOD);
+ push(@{$stack_ref}, $state_hash_ref->{$key});
+ }
+ }
+ return FCM::Context::Task->new(
+ {ctx => $target, id => $state->get_id()},
+ );
+ }
+ else {
+ _target_update_ok(
+ $stat_hash_ref, $ctx, $target, $state_hash_ref, $stack_ref,
+ );
+ }
+ }
+ return;
+ };
+ my $put_action_ref = sub {
+ my $task = shift();
+ my $target = $task->get_ctx();
+ if ($task->get_state() eq $task->ST_FAILED) {
+ $EVENT->(FCM::Context::Event->E, $task->get_error());
+ _target_update_failed(
+ $stat_hash_ref, $ctx, $target, $state_hash_ref, $stack_ref,
+ $task->get_elapse(),
+ );
+ }
+ else {
+ my $target = $task->get_ctx();
+ _target_update_ok(
+ $stat_hash_ref, $ctx, $target, $state_hash_ref, $stack_ref,
+ $task->get_elapse(),
+ );
+ }
+ };
+ ($get_action_ref, $put_action_ref);
+}
+
+# Determines and returns the targets from the sources.
+sub _targets_from_sources {
+ my ($attrib_ref, $m_ctx, $ctx, $targets_ref) = @_;
+ my %FILE_TYPE_UTIL_OF = %{$attrib_ref->{file_type_util_of}};
+ my %FILE_EXT_OF;
+ my %FILE_NAME_OPTION_OF;
+ for my $FILE_TYPE_UTIL (values(%FILE_TYPE_UTIL_OF)) {
+ while (my $key = each(%{$FILE_TYPE_UTIL->target_file_ext_of()})) {
+ $FILE_EXT_OF{$key} ||= _prop($attrib_ref, 'file-ext.' . $key, $ctx);
+ }
+ while (my $key = each(%{$FILE_TYPE_UTIL->target_file_name_option_of()})) {
+ $FILE_NAME_OPTION_OF{$key}
+ ||= _prop($attrib_ref, 'file-name-option.' . $key, $ctx);
+ }
+ }
+ # Determine the targets for each source
+ #my %target_ns_list_of;
+ SOURCE:
+ while (my ($ns, $source) = each(%{$ctx->get_source_of()})) {
+ my $type = $source->get_type();
+ $type ||= q{};
+ if (!exists($FILE_TYPE_UTIL_OF{$type})) {
+ next SOURCE;
+ }
+ my $FILE_TYPE_UTIL = $FILE_TYPE_UTIL_OF{$type};
+ if (!$FILE_TYPE_UTIL->can('source_to_targets')) {
+ next SOURCE;
+ }
+ for my $target (
+ $FILE_TYPE_UTIL->source_to_targets(
+ $source, \%FILE_EXT_OF, \%FILE_NAME_OPTION_OF)
+ ) {
+ my $key = $target->get_key();
+ if (exists($ctx->get_target_key_of()->{$key})) {
+ $key = $ctx->get_target_key_of()->{$key};
+ $target->set_key($key);
+ }
+ push(@{$targets_ref}, $target);
+ $target->set_ns($ns);
+ $target->set_path(
+ catfile($ctx->get_dest(), $target->get_category(), $key),
+ );
+ $target->set_path_of_source($source->get_path());
+ $target->set_type($type);
+ if (!$source->get_up_to_date()) {
+ $target->set_status($target->ST_OOD);
+ }
+ }
+ }
+ # Determines name-space dependencies
+ my %deps_in_ns_in_cat_of; # $cat => {$ns => [$targets ...]}
+ FILE_TYPE_UTIL:
+ while (my ($type, $FILE_TYPE_UTIL) = each(%FILE_TYPE_UTIL_OF)) {
+ if (!$FILE_TYPE_UTIL->can('ns_targets')) {
+ next FILE_TYPE_UTIL;
+ }
+ for my $cat ($FILE_TYPE_UTIL->ns_targets_deps()) {
+ $deps_in_ns_in_cat_of{$cat} = {};
+ }
+ for my $target (
+ $FILE_TYPE_UTIL->ns_targets(
+ $targets_ref, \%FILE_EXT_OF, \%FILE_NAME_OPTION_OF)
+ ) {
+ my $key = $target->get_key();
+ if (exists($ctx->get_target_key_of()->{$key})) {
+ $key = $ctx->get_target_key_of()->{$key};
+ $target->set_key($key);
+ }
+ push(@{$targets_ref}, $target);
+ $target->set_type($type);
+ $target->set_path(
+ catfile($ctx->get_dest(), $target->get_category(), $key),
+ );
+ }
+ }
+ for my $target (
+ sort {
+ $a->get_ns() cmp $b->get_ns() || $a->get_key() cmp $b->get_key();
+ } @{$targets_ref}
+ ) {
+ $EVENT->(
+ FCM::Context::Event->MAKE_BUILD_TARGET_FROM_NS,
+ ($target->get_ns() ? $target->get_ns() : '/'),
+ $target->get_task(),
+ $target->get_category(),
+ $target->get_key(),
+ );
+ }
+ # Target categories and name-spaces.
+ for my $target (@{$targets_ref}) {
+ my $cat = $target->get_category();
+ if ($cat && exists($deps_in_ns_in_cat_of{$cat})) {
+ my $ns_iter = $UTIL->ns_iter($target->get_ns(), $UTIL->NS_ITER_UP);
+ # $ns_iter->(); # discard
+ while (defined(my $ns = $ns_iter->())) {
+ $deps_in_ns_in_cat_of{$cat}{$ns} ||= [];
+ push(@{$deps_in_ns_in_cat_of{$cat}{$ns}}, $target->get_key());
+ }
+ }
+ }
+ # Adds categorised name-space dependencies.
+ TARGET:
+ for my $target (@{$targets_ref}) {
+ if (!exists($target->get_info_of()->{'deps'})) {
+ next TARGET;
+ }
+ CATEGORY:
+ while (my ($cat, $deps_in_ns_ref) = each(%deps_in_ns_in_cat_of)) {
+ if (!exists($target->get_info_of()->{'deps'}{$cat})) {
+ next CATEGORY;
+ }
+ my $name = 'ns-dep.' . $cat;
+ my @ns_list = map {$_ eq q{/} ? q{} : $_}
+ _props($attrib_ref, $name, $ctx, $target->get_ns());
+ for my $ns (@ns_list) {
+ if (exists($deps_in_ns_ref->{$ns})) {
+ push(
+ @{$target->get_deps()},
+ ( map {[$_, $cat]}
+ grep {$_ ne $target->get_key()}
+ @{$deps_in_ns_ref->{$ns}}
+ ),
+ );
+ }
+ else {
+ # This will be reported later as missing dependency
+ push(@{$target->get_deps()}, [$ns, $cat, 'ns-dep']);
+ }
+ }
+ }
+ }
+}
+
+# Stores the properties relevant to the target.
+# Assigns previous checksum and properties, where appropriate.
+sub _targets_props_assign {
+ my ($attrib_ref, $m_ctx, $ctx, $targets_ref) = @_;
+ my $P_TARGET_GETTER
+ = _prev_hash_item_getter($m_ctx, $ctx, sub {$_[0]->get_target_of()});
+ my %NO_INHERIT_CATEGORY_IN
+ = map {$_ => 1} _props($attrib_ref, 'no-inherit-target-category', $ctx);
+ my %CTX_PROP_OF = %{$ctx->get_prop_of()};
+ for my $target (@{$targets_ref}) {
+ # Properties
+ my $FILE_TYPE_UTIL
+ = $attrib_ref->{file_type_util_of}->{$target->get_type()};
+ my $task = $FILE_TYPE_UTIL->task_of()->{$target->get_task()};
+ my $key = $target->get_key();
+ if ($task->can('prop_of')) {
+ my %prop_of = %{$task->prop_of($target)};
+ while (my $name = each(%prop_of)) {
+ if ( exists($CTX_PROP_OF{$name})
+ && exists($CTX_PROP_OF{$name}->get_ctx_of()->{$key})
+ ) {
+ $target->get_prop_of()->{$name}
+ = $CTX_PROP_OF{$name}->get_ctx_of()->{$key}->get_value();
+ }
+ else {
+ $target->get_prop_of()->{$name}
+ = _prop($attrib_ref, $name, $ctx, $target->get_ns());
+ }
+ }
+ }
+ if ($FILE_TYPE_UTIL->can('target_deps_filter')) {
+ $FILE_TYPE_UTIL->target_deps_filter($target);
+ }
+ # Path, checksum and previous properties
+ my $p_target = $P_TARGET_GETTER->($key);
+ if (defined($p_target)) {
+ $target->set_checksum($p_target->get_checksum());
+ if ($p_target->is_ok()) {
+ $target->set_path_of_prev($p_target->get_path());
+ $target->set_prop_of_prev_of($p_target->get_prop_of());
+ }
+ else {
+ $target->set_path_of_prev($p_target->get_path_of_prev());
+ $target->set_prop_of_prev_of($p_target->get_prop_of_prev_of());
+ $target->set_status($target->ST_OOD);
+ }
+ if (exists($NO_INHERIT_CATEGORY_IN{$target->get_category()})) {
+ $target->set_path_of_prev($target->get_path());
+ }
+ }
+ }
+}
+
+# Selects targets to update.
+sub _targets_select {
+ my ($attrib_ref, $m_ctx, $ctx, $targets_ref) = @_;
+ my $time = time();
+ my $timer = $UTIL->timer();
+ my %select_by = %{$ctx->get_target_select_by()};
+ my %target_of;
+ my %targets_of;
+ my %target_set;
+ my %has_ns_in; # available sets of name-spaces
+ for my $target (@{$targets_ref}) {
+ if ( exists($select_by{key}{$target->get_key()})
+ || ( !exists($select_by{category})
+ || exists($select_by{category}{$target->get_category()})
+ )
+ && ( !exists($select_by{task})
+ || exists($select_by{task}{$target->get_task()})
+ )
+ && ( !exists($select_by{ns})
+ || $UTIL->ns_in_set($target->get_ns(), $select_by{ns})
+ )
+ ) {
+ $target_set{$target->get_key()} = 1;
+ }
+ if (exists($target_of{$target->get_key()})) {
+ if (!exists($targets_of{$target->get_key()})) {
+ $targets_of{$target->get_key()}
+ = [delete($target_of{$target->get_key()})];
+ }
+ push(@{$targets_of{$target->get_key()}}, $target);
+ }
+ else {
+ $target_of{$target->get_key()} = $target;
+ }
+ # Name-spaces
+ my $ns_iter = $UTIL->ns_iter($target->get_ns(), $UTIL->NS_ITER_UP);
+ NS:
+ while (defined(my $ns = $ns_iter->())) {
+ if (exists($has_ns_in{$ns})) {
+ last NS;
+ }
+ $has_ns_in{$ns} = 1;
+ }
+ }
+ my @target_keys = sort keys(%target_set);
+
+ # Wraps each relevant target with a state object.
+ # Walks the target dependency tree to build a state dependency tree.
+ # Checks for missing dependencies.
+ # Checks for duplicated target.
+ my @items = map {[[$_, undef]]} @target_keys;
+ my %state_of;
+ my %dup_in;
+ my %cyc_in;
+ my %missing_deps_in;
+ ITEM:
+ while (my $item = pop(@items)) {
+ my ($unit, @up_units) = @{$item};
+ my ($key, $type) = @{$unit};
+ my @up_keys = map {$_->[0]} @up_units;
+ if ( exists($cyc_in{$key})
+ || exists($dup_in{$key})
+ || exists($missing_deps_in{$key})
+ ) {
+ next ITEM;
+ }
+ if (exists($state_of{$key})) {
+ # Already visited this ITEM
+ # Detect cyclic dependency
+ if ( !$state_of{$key}->get_cyclic_ok()
+ && grep {$_->[0] eq $key} @up_units
+ ) {
+ my @_up_units = (@up_units, $unit);
+ my $_up_unit_last = pop(@_up_units);
+ DEP_UP_KEY:
+ while (my $_up_unit = pop(@_up_units)) {
+ my ($_up_key, $_up_type) = @{$_up_unit};
+ my @dep_up_deps = @{$state_of{$_up_key}->get_deps()};
+ # If parent of $_up_unit_last does not depend on
+ # $_up_unit_last, chain is broken, and we are OK.
+ my ($_up_key_last, $_up_type_last) = @{$_up_unit_last};
+ if (!grep { $_->[0]->get_key() eq $_up_key_last
+ || $_->[1] eq $_up_type_last
+ } @dep_up_deps
+ ) {
+ last DEP_UP_KEY;
+ }
+ if ($type && $key eq $_up_key && $type eq $_up_type) {
+ $cyc_in{$key} = {'keys' => [@up_keys, $key]};
+ next ITEM;
+ }
+ $_up_unit_last = $_up_unit;
+ }
+ }
+ $state_of{$key}->set_cyclic_ok(1);
+ # Float current target up dependency chain
+ my $is_directly_related = 1;
+ UP_KEY:
+ for my $up_key (reverse(@up_keys)) {
+ if ($state_of{$up_key}->add_visitor(
+ $state_of{$key}->get_target(),
+ $type,
+ $is_directly_related,
+ )) {
+ last UP_KEY;
+ }
+ $is_directly_related = 0;
+ }
+ # Add floatable dependencies up the dependency chain
+ for my $visitor (values(%{$state_of{$key}->get_floatables()})) {
+ UP_KEY:
+ for my $up_key (reverse(@up_keys)) {
+ if ($state_of{$up_key}->add_visitor(@{$visitor})) {
+ last UP_KEY;
+ }
+ }
+ }
+ next ITEM;
+ }
+
+ # First visit to this ITEM
+ # Checks for duplicated target
+ if (exists($targets_of{$key})) {
+ $dup_in{$key} = {
+ 'keys' => [@up_keys, $key],
+ 'values' => [map {$_->get_ns()} @{$targets_of{$key}}],
+ };
+ next ITEM;
+ }
+ # Wraps all required targets with a STATE object
+ $state_of{$key} = $STATE->new(
+ {id => $key, target => $target_of{$key}},
+ );
+ my $target = $target_of{$key};
+ DEP:
+ for (grep {$_->[0] ne $key} @{$target->get_deps()}) {
+ my ($dep_key, $dep_type, $dep_remark) = @{$_};
+ # Duplicated targets
+ if (exists($targets_of{$dep_key})) {
+ $dup_in{$dep_key} = {
+ 'keys' => [@up_keys, $key, $dep_key],
+ 'values' => [map {$_->get_ns()} @{$targets_of{$dep_key}}],
+ };
+ next DEP;
+ }
+ # Missing dependency
+ if (!exists($target_of{$dep_key})) {
+ if (!exists($missing_deps_in{$key})) {
+ $missing_deps_in{$key} = {
+ 'keys' => [@up_keys, $key, $dep_key],
+ 'values' => [],
+ };
+ }
+ push(
+ @{$missing_deps_in{$key}{'values'}},
+ [$dep_key, $dep_type, $dep_remark],
+ );
+ next DEP;
+ }
+ # OK
+ push(@items, [[$dep_key, $dep_type], @up_units, [$key, $type]]);
+ # add_visitor, is_directly_related=1
+ $state_of{$key}->add_visitor($target_of{$dep_key}, $dep_type, 1)
+ }
+ # Float current target up dependency chain
+ my $is_directly_related = 1;
+ UP_KEY:
+ for my $up_key (reverse(@up_keys)) {
+ if ($state_of{$up_key}->add_visitor(
+ $target, $type, $is_directly_related,
+ )) {
+ last UP_KEY;
+ }
+ $is_directly_related = 0;
+ }
+ # Adds triggers
+ for my $trigger_key (@{$target->get_triggers()}) {
+ if (!exists($state_of{$trigger_key})) {
+ unshift(@items, [[$trigger_key, undef]]);
+ }
+ }
+ }
+ # Visitors no longer used
+ for my $state (values(%state_of)) {
+ $state->free_visitors();
+ }
+ # Assigns targets to build context
+ %{$ctx->get_target_of()}
+ = map {($_->get_id() => $_->get_target())} values(%state_of);
+
+ # Report cyclic dependencies
+ # Report duplicated targets
+ # Report missing dependencies
+ # Report bad keys in target select
+ if (keys(%cyc_in)) {
+ return $E->throw($E->BUILD_TARGET_CYC, \%cyc_in);
+ }
+ if (keys(%dup_in)) {
+ return $E->throw($E->BUILD_TARGET_DUP, \%dup_in);
+ }
+ my @ignore_missing_dep_ns_list
+ = _props($attrib_ref, 'ignore-missing-dep-ns', $ctx);
+ KEY:
+ for my $key (sort(keys(%missing_deps_in))) {
+ my $target = $target_of{$key};
+ for my $ns (@ignore_missing_dep_ns_list) {
+ if ($UTIL->ns_common($ns, $target->get_ns()) eq $ns) { # target in ns
+ my $hash_ref = @{delete($missing_deps_in{$key})};
+ my @deps = @{$hash_ref->{"values"}};
+ for my $dep (@deps) {
+ $EVENT->(
+ FCM::Context::Event->MAKE_BUILD_TARGET_MISSING_DEP,
+ $key, @{$dep},
+ );
+ }
+ next KEY;
+ }
+ }
+ }
+ if (keys(%missing_deps_in)) {
+ return $E->throw($E->BUILD_TARGET_DEP, \%missing_deps_in);
+ }
+ if (exists($select_by{key})) {
+ my @bad_keys = grep {!exists($state_of{$_})} keys(%{$select_by{key}});
+ if (@bad_keys) {
+ return $E->throw($E->BUILD_TARGET_BAD, \@bad_keys);
+ }
+ }
+ # Walk the tree and report it
+ my @report_items = map {[$_]} @target_keys;
+ my %reported;
+ ITEM:
+ while (my $item = pop(@report_items)) {
+ my ($key, @stack) = @{$item};
+ my @deps = @{$state_of{$key}->get_deps()};
+ my @more_items = reverse(map {[$_->[0]->get_key(), @stack, $key]} @deps);
+ my $n_more_items;
+ if (exists($reported{$key})) {
+ $n_more_items = scalar(@more_items);
+ }
+ else {
+ push(@report_items, @more_items);
+ }
+ $attrib_ref->{util}->event(
+ FCM::Context::Event->MAKE_BUILD_TARGET_STACK,
+ $key, scalar(@stack), $n_more_items,
+ );
+ $reported{$key} = 1;
+ }
+ $EVENT->(
+ FCM::Context::Event->MAKE_BUILD_TARGET_SELECT,
+ {map {$_ => $target_of{$_}} @target_keys},
+ );
+ # TODO: error if nothing to build?
+
+ # Checks whether properties with name-spaces are valid.
+ my @invalid_prop_ns_list;
+ while (my ($name, $prop) = each(%{$ctx->get_prop_of()})) {
+ while (my ($ns, $prop_ctx) = each(%{$prop->get_ctx_of()})) {
+ if ( !$prop_ctx->get_inherited()
+ && !exists($target_of{$ns})
+ && !exists($has_ns_in{$ns})
+ ) {
+ push(
+ @invalid_prop_ns_list,
+ [$ctx->get_id(), $name, $ns, $prop_ctx->get_value()],
+ );
+ }
+ }
+ }
+ if (@invalid_prop_ns_list) {
+ return $E->throw($E->MAKE_PROP_NS, \@invalid_prop_ns_list);
+ }
+
+ $EVENT->(FCM::Context::Event->MAKE_BUILD_TARGET_SELECT_TIMER, $timer->());
+
+ # Returns list of keys of top targets, and the states
+ ([map {$state_of{$_}} reverse(@target_keys)], \%state_of);
+}
+
+# Returns true if $target dependencies are done.
+sub _target_deps_are_done {
+ my ($state, $state_hash_ref, $stack_ref) = @_;
+ my @deps = map {[$_->[0]->get_key(), $_->[1]]} @{$state->get_deps()};
+ for my $k (grep {$state_hash_ref->{$_}->is_ready()} map {$_->[0]} @deps) {
+ if (!grep {$_->get_id() eq $k} @{$stack_ref}) {
+ push(@{$stack_ref}, $state_hash_ref->{$k});
+ }
+ }
+ my %not_done
+ = map {@{$_}}
+ grep {!$_->[1]->is_done()}
+ map {[$_->[0], $state_hash_ref->{$_->[0]}]}
+ @deps;
+ if (keys(%not_done)) {
+ $state->set_value($STATE->PENDING);
+ while (my ($k, $s) = each(%not_done)) {
+ $state->get_pending_for()->{$k} = $s;
+ $s->get_needed_by()->{$state->get_id()} = $state;
+ }
+ return 0;
+ }
+ return 1;
+}
+
+# Returns true if $target has failed dependencies.
+sub _target_check_failed_dep {
+ my ($state, $state_hash_ref) = @_;
+ my $target = $state->get_target();
+ for my $dep (@{$state->get_deps()}) {
+ my ($target_of_dep, $type_of_dep) = @{$dep};
+ if ($target_of_dep->is_failed()) {
+ return 1;
+ }
+ if ( exists($target_of_dep->get_status_of()->{$type_of_dep})
+ && $target_of_dep->get_status_of()->{$type_of_dep}
+ eq $target->ST_FAILED
+ ) {
+ return 1;
+ }
+ }
+ return 0;
+}
+
+# Returns true if $target is out of date.
+sub _target_check_ood {
+ my ($state, $state_hash_ref) = @_;
+ my $target = $state->get_target();
+ # Dependencies
+ my $rc;
+ for my $dep (@{$state->get_deps()}) {
+ my ($target_of_dep, $type_of_dep) = @{$dep};
+ if ( $target_of_dep->is_modified()
+ || exists($target_of_dep->get_status_of()->{$type_of_dep})
+ && $target_of_dep->get_status_of()->{$type_of_dep}
+ eq $target->ST_MODIFIED
+ ) {
+ if (exists($target->get_status_of()->{$type_of_dep})) {
+ $target->get_status_of()->{$type_of_dep} = $target->ST_MODIFIED;
+ if ( $target->get_path_of_prev()
+ && $target->get_path() ne $target->get_path_of_prev()
+ ) {
+ # Inherited build, cannot just pass on a status
+ $rc = 1;
+ }
+ }
+ else {
+ $rc = 1;
+ }
+ }
+ }
+ if ($rc || $target->get_status() eq $target->ST_OOD) {
+ return 1;
+ }
+ # Dest and properties
+ my $path_of_prev = $target->get_path_of_prev();
+ my $checksum = $target->get_checksum();
+ my $prop_hash_ref = $target->get_prop_of();
+ my $prop_of_prev_hash_ref = $target->get_prop_of_prev_of();
+ ( !$path_of_prev
+ || !-e $path_of_prev
+ || $UTIL->file_md5($path_of_prev) ne $checksum
+ || $UTIL->hash_cmp($prop_hash_ref, $prop_of_prev_hash_ref)
+ );
+}
+
+# Callback to prepare the target for the task.
+sub _target_prep {
+ my ($state, $ctx) = @_;
+ my $target = $state->get_target();
+ # Creates the container directory, where necessary
+ my %paths_of_dirs_set;
+ for my $t (
+ $target,
+ map {$ctx->get_target_of($_)} @{$target->get_triggers()},
+ ) {
+ $paths_of_dirs_set{dirname($t->get_path())} = 1;
+ }
+ for my $path_of_dir (keys(%paths_of_dirs_set)) {
+ if (!-d $path_of_dir) {
+ eval {mkpath($path_of_dir)};
+ if ($@) {
+ return $E->throw($E->DEST_CREATE, $path_of_dir);
+ }
+ }
+ }
+ # Put in required info
+ if ($target->get_info_of('paths')) {
+ @{$target->get_info_of('paths')} = @{$ctx->get_dests()};
+ }
+ if ($target->get_info_of('deps')) {
+ my $info_deps_ref = $target->get_info_of('deps');
+ my %set_of = map {$_ => {}} keys(%{$info_deps_ref});
+ for my $dep (@{$state->get_deps()}) {
+ my ($target_of_dep, $type) = @{$dep};
+ my $key = $target_of_dep->get_key();
+ if (exists($set_of{$type}) && !$set_of{$type}{$key}) {
+ if ($target_of_dep->get_ns() eq $target->get_ns()) {
+ # E.g. main *.o of *.exe
+ unshift(@{$info_deps_ref->{$type}}, $key);
+ }
+ else {
+ push(@{$info_deps_ref->{$type}}, $key);
+ }
+ $set_of{$type}{$key} = 1;
+ }
+ }
+ }
+}
+
+# Sets state and stack when a $target has failed to update or cannot be updated
+# due to failed dependencies.
+sub _target_update_failed {
+ my ($stat_hash_ref,
+ $ctx,
+ $target,
+ $state_hash_ref,
+ $stack_ref,
+ $elapsed_time, # only defined if target update action is done
+ ) = @_;
+ my $key = $target->get_key();
+ my $state = $state_hash_ref->{$key};
+ $state->set_value($STATE->DONE);
+ # If this target is needed by other targets...
+ while (my ($k, $s) = each(%{$state->get_needed_by()})) {
+ my $pending_for_ref = $s->get_pending_for();
+ delete($pending_for_ref->{$key});
+ if (!keys(%{$pending_for_ref})) {
+ $s->set_value($STATE->DONE);
+ # Remove from stack
+ @{$stack_ref} = grep {$_->get_id() ne $k} @{$stack_ref};
+ $s->get_target()->set_status($target->ST_FAILED);
+ push(@{$s->get_target()->get_failed_by()}, $key);
+ }
+ }
+ if (defined($elapsed_time)) { # Done target update
+ my $target0 = $ctx->get_target_of()->{$target->get_key()};
+ $target0->set_info_of({}); # unset
+ $target0->set_checksum(undef);
+ $target0->set_path(undef);
+ $target0->set_prop_of_prev_of({}); # unset
+ $target0->set_path_of_prev(undef); # unset
+ $target0->set_status($target->ST_FAILED);
+ push(@{$target0->get_failed_by()}, $target->get_key());
+ ++$stat_hash_ref->{$target->get_task()}{n}{$target->ST_FAILED};
+ $stat_hash_ref->{$target->get_task()}{t} += $elapsed_time;
+ }
+ else { # No target update required
+ $target->set_path(undef);
+ $target->set_prop_of_prev_of({}); # unset
+ $target->set_path_of_prev(undef); # unset
+ $target->set_status($target->ST_FAILED);
+ for my $dep (@{$state->get_deps()}) {
+ my ($dep_target, $dep_type) = @{$dep};
+ my $dep_key = $dep_target->get_key();
+ if ( $dep_target->is_failed()
+ && !grep {$_ eq $dep_key} @{$target->get_failed_by()}
+ ) {
+ push(@{$target->get_failed_by()}, $dep_key);
+ }
+ }
+ ++$stat_hash_ref->{$target->get_task()}{n}{$target->ST_FAILED};
+ }
+ $EVENT->(
+ FCM::Context::Event->MAKE_BUILD_TARGET_FAIL, $target, $elapsed_time,
+ );
+}
+
+# Sets state and stack when a $target is up to date or updated successfully.
+sub _target_update_ok {
+ my ($stat_hash_ref,
+ $ctx,
+ $target,
+ $state_hash_ref,
+ $stack_ref,
+ $elapsed_time, # only defined if target update action is done
+ ) = @_;
+ my $key = $target->get_key();
+ my $state = $state_hash_ref->{$key};
+ $state->set_value($STATE->DONE);
+ # If this target is needed by other targets...
+ while (my ($k, $s) = each(%{$state->get_needed_by()})) {
+ my $pending_for_ref = $s->get_pending_for();
+ delete($pending_for_ref->{$key});
+ if ($s->is_pending() && !keys(%{$pending_for_ref})) {
+ $s->set_value($STATE->READY);
+ if (!grep {$_->get_id() eq $k} @{$stack_ref}) {
+ push(@{$stack_ref}, $s);
+ }
+ }
+ }
+ if (defined($elapsed_time)) { # Done target update
+ my $target0 = $ctx->get_target_of()->{$target->get_key()};
+ $target0->set_info_of({}); # unset
+ $target0->set_checksum($target->get_checksum());
+ $target0->set_path($target->get_path());
+ $target0->set_prop_of_prev_of({}); # unset
+ $target0->set_path_of_prev(undef); # unset
+ $target0->set_status($target->get_status());
+ ++$stat_hash_ref->{$target->get_task()}{n}{$target->get_status()};
+ $stat_hash_ref->{$target->get_task()}{t} += $elapsed_time;
+ }
+ else { # No target update required
+ if ($target->get_path_of_prev()) {
+ $target->set_path($target->get_path_of_prev());
+ }
+ $target->set_prop_of_prev_of({}); # unset
+ $target->set_path_of_prev(undef); # unset
+ $target->set_status($target->ST_UNCHANGED);
+ ++$stat_hash_ref->{$target->get_task()}{n}{$target->ST_UNCHANGED};
+ }
+ $EVENT->(
+ FCM::Context::Event->MAKE_BUILD_TARGET_DONE, $target, $elapsed_time,
+ );
+}
+
+# Returns a list containing the inherited contexts with the same ID as $ctx.
+sub _i_ctx_list {
+ my ($m_ctx, $ctx) = @_;
+ grep
+ {defined()}
+ map
+ {$_->get_ctx_of($ctx->get_id())}
+ @{$m_ctx->get_inherit_ctx_list()};
+}
+
+# Returns a function that returns the previous source/target of a specified key.
+sub _prev_hash_item_getter {
+ my ($m_ctx, $ctx, $getter_ref) = @_;
+ my $p_m_ctx = $m_ctx->get_prev_ctx();
+ my %p_item_of;
+ my $ctx_id = $ctx->get_id();
+ if (defined($p_m_ctx) && defined($p_m_ctx->get_ctx_of($ctx_id))) {
+ %p_item_of = %{$getter_ref->($p_m_ctx->get_ctx_of($ctx_id))};
+ }
+ else {
+ for my $i_ctx (_i_ctx_list($m_ctx, $ctx)) {
+ %p_item_of = (%p_item_of, %{$getter_ref->($i_ctx)});
+ }
+ }
+ sub {exists($p_item_of{$_[0]}) ? $p_item_of{$_[0]} : undef};
+}
+
+# ------------------------------------------------------------------------------
+package FCM::System::Make::Build::State;
+use base qw{FCM::Class::HASH};
+
+use constant {
+ DONE => 'DONE', # state value
+ READY => 'READY', # state value
+ PENDING => 'PENDING', # state value
+};
+
+__PACKAGE__->class({
+ cyclic_ok => '$',
+ deps => '@',
+ floatables => '%',
+ id => '$',
+ needed_by => '%',
+ pending_for => '%',
+ target => 'FCM::Context::Make::Build::Target',
+ value => {isa => '$', default => READY},
+ visited_by => '%',
+});
+
+sub add_visitor {
+ my ($self, $dep_target, $dep_type, $is_directly_related) = @_;
+ my $dep_key = $dep_target->get_key();
+ my $dep_str = join(':', $dep_key, $dep_type);
+ # Dependency has already visited me, return cached return value
+ if (exists($self->get_visited_by()->{$dep_str})) {
+ return $self->get_visited_by()->{$dep_str};
+ }
+ # Adopt dep_target as my dependency if there is a policy to do so
+ my $target = $self->get_target();
+ my $policy = $target->get_dep_policy_of($dep_type);
+ if ( $policy
+ && ($policy ne $target->POLICY_FILTER_IMMEDIATE || $is_directly_related)
+ && (!grep {$_->[0]->get_key() eq $dep_key} @{$self->get_deps()})
+ && (!grep {$_ eq $dep_key} @{$target->get_triggers()})
+ ) {
+ push(@{$self->get_deps()}, [$dep_target, $dep_type]);
+ }
+ # If target is captured by me, return true.
+ # Otherwise, return false, and the target is a floatable.
+ $self->get_visited_by()->{$dep_str}
+ = ($policy && $policy eq $target->POLICY_CAPTURE);
+ if ( !$self->get_visited_by()->{$dep_str}
+ && !exists($self->get_floatables()->{$dep_str})
+ ) {
+ $self->get_floatables()->{$dep_str} = [$dep_target, $dep_type];
+ }
+ return $self->get_visited_by()->{$dep_str};
+}
+
+sub free_visitors {
+ my ($self) = @_;
+ %{$self->get_floatables()} = ();
+ %{$self->get_visited_by()} = ();
+}
+
+sub is_done {
+ $_[0]->{value} eq DONE;
+}
+
+sub is_pending {
+ $_[0]->{value} eq PENDING;
+}
+
+sub is_ready {
+ $_[0]->{value} eq READY;
+}
+#-------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::System::Make::Build
+
+=head1 SYNOPSIS
+
+ use FCM::System::Make::Build;
+
+=head1 DESCRIPTION
+
+Implements the build sub-system. An instance of this class is expected to be
+initialised and called by L<FCM::System::Make|FCM::System::Make>.
+
+=head1 METHODS
+
+See L<FCM::System::Make|FCM::System::Make> for detail.
+
+=head1 ATTRIBUTES
+
+The $class->new(\%attrib) method of this class supports the following
+attributes:
+
+=over 4
+
+=item config_parser_of
+
+A HASH to map the labels in a configuration file to their parsers. (default =
+%FCM::System::Make::Build::CONFIG_PARSER_OF)
+
+=item target_select_by
+
+A HASH to map the default target selector. The keys should be "category", "key",
+"ns", or "task". (default = %FCM::System::Make::Build::TARGET_SELECT_by)
+
+=item file_type_utils
+
+An ARRAY of file type utility classes to be loaded into the file_type_util_of
+HASH. (default = @FCM::System::Make::Build::FILE_TYPE_UTILS)
+
+=item file_type_util_of
+
+A HASH to map the file type names to the utilities to manipulate the given file
+types. An values in this HASH overrides the classes in I<file_type_utils>.
+(default = determined by I<file_type_utils>)
+
+=item prop_of
+
+A HASH to map the names of the properties to their settings. Each setting
+is a 2-element ARRAY reference, where element [0] is the default setting
+and element [1] is a flag to indicate whether the property accepts a name-space
+or not. (default = %FCM::System::Make::Build::PROP_OF + values loaded from the
+file type utilities)
+
+=item util
+
+See L<FCM::System::Make|FCM::System::Make> for detail.
+
+=back
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/System/Make/Build/FileType.pm b/lib/FCM/System/Make/Build/FileType.pm
new file mode 100644
index 0000000..5eee9b3
--- /dev/null
+++ b/lib/FCM/System/Make/Build/FileType.pm
@@ -0,0 +1,226 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+
+# ------------------------------------------------------------------------------
+package FCM::System::Make::Build::FileType;
+use base qw{FCM::Class::CODE};
+
+use Text::ParseWords qw{shellwords};
+
+# Creates the class.
+__PACKAGE__->class(
+ { id => '$',
+ file_ext => '$',
+ file_pat => '$',
+ file_she => '$',
+ source_analyse_always => '$',
+ source_analyse_dep_of => '%',
+ source_analyse_more => '&',
+ source_analyse_more_init => '&',
+ source_to_targets => '&',
+ target_deps_filter => '&',
+ target_file_ext_of => '%',
+ target_file_name_option_of => '%',
+ task_class_of => '%',
+ task_of => '%',
+ util => '&',
+ },
+ { init => \&_init,
+ action_of => {
+ (map {my $key = $_; ($key => sub {$_[0]->{$key}})}
+ qw{
+ id
+ file_ext
+ file_pat
+ file_she
+ source_analyse_always
+ target_file_ext_of
+ target_file_name_option_of
+ task_of
+ }
+ ),
+ source_analyse => \&_source_analyse,
+ source_analyse_deps => sub {keys(%{$_[0]->{source_analyse_dep_of}})},
+ source_to_targets => sub {$_[0]->{source_to_targets}->(@_)},
+ target_deps_filter => sub {$_[0]->{target_deps_filter}->(@_)},
+ },
+ },
+);
+
+# Initialises some attributes.
+sub _init {
+ my ($attrib_ref) = @_;
+ while (my ($key, $class) = each(%{$attrib_ref->{task_class_of}})) {
+ $attrib_ref->{util}->class_load($class);
+ $attrib_ref->{task_of}{$key}
+ = $class->new({util => $attrib_ref->{util}});
+ }
+}
+
+# Reads information according to the $source.
+sub _source_analyse {
+ my ($attrib_ref, $source) = @_;
+ my %no_dep_of;
+ my %dep_type_of
+ = map {($_ => 1)} keys(%{$attrib_ref->{source_analyse_dep_of}});
+ while (my $type = each(%dep_type_of)) {
+ my $key = 'no-dep.' . $type;
+ if ($source->get_prop_of($key)) {
+ for my $v (shellwords($source->get_prop_of($key))) {
+ if ($v eq '*') {
+ delete($dep_type_of{$type});
+ }
+ else {
+ $no_dep_of{$type}{$v} = 1;
+ }
+ }
+ }
+ }
+ if (!keys(%dep_type_of) && !$attrib_ref->{source_analyse_always}) {
+ return;
+ }
+ my $path = $source->get_path();
+ my $handle = $attrib_ref->{util}->file_load_handle($path);
+
+ my @dep_types = keys(%dep_type_of)
+ ? keys(%dep_type_of) : (_source_analyse_deps($attrib_ref));
+ my (%dep_of, %info_of, %state);
+ $attrib_ref->{source_analyse_more_init}->(\%info_of, \%state);
+ LINE:
+ while (my $line = readline($handle)) {
+ chomp($line);
+ TYPE:
+ for my $type (@dep_types) {
+ my ($item, $can_analyse_more)
+ = $attrib_ref->{source_analyse_dep_of}{$type}->($line);
+ if ($item) {
+ $dep_of{$type}{$item} = 1;
+ if ($can_analyse_more) {
+ last TYPE;
+ }
+ else {
+ next LINE;
+ }
+ }
+ }
+ $attrib_ref->{source_analyse_more}->($line, \%info_of, \%state);
+ }
+
+ close($handle);
+ $source->set_info_of(\%info_of);
+ while (my ($type, $hash_ref) = each(%dep_of)) {
+ while (my $item = each(%{$hash_ref})) {
+ if (!exists($no_dep_of{$type}{$item})) {
+ push(@{$source->get_deps()}, [$item, $type]);
+ }
+ }
+ }
+}
+
+1;
+__END__
+
+=head1 NAME
+
+FCM::System::Make::Build::FileType
+
+=head1 SYNOPSIS
+
+ use FCM::System::Make::Build::FileType;
+ my $file_type_util = FCM::System::Make::Build::FileType->new(\%attrib);
+ $file_type_util->source_analyse($handle);
+
+=head1 DESCRIPTION
+
+An abstract class to implement the shared methods for gathering information to
+build different types of source files.
+
+=head1 METHODS
+
+=over 4
+
+=item $class->new(\%attrib)
+
+Creates and returns a new instance.
+
+=item $instance->id()
+
+Returns the recommended ID of this file type.
+
+=item $instance->file_ext()
+=item $instance->file_pat()
+=item $instance->file_she()
+
+Returns the recommended file name extension, file name pattern and file she-bang
+line pattern of this file type.
+
+=item $instance->source_analyse($source)
+
+Analysis $source for dependencies and other information. Add or modify items in
+@{$source->get_deps()} and %{$source->get_info_of()}.
+
+=item $instance->source_analyse_deps()
+
+Returns a list containing the possible dependency types.
+
+=item $instance->source_analyse_always()
+
+Returns true if $instance->source_analyse($handle,\@dep_types) can read
+information other than dependencies.
+
+=item $instance->source_to_targets($source,\%prop_of)
+
+Using the information in $source, creates and returns the contexts of a list of
+suitable build targets. Where appropriate, the %prop_of should contain a mapping
+of the names of the properties used by this method and their values.
+
+=item $instance->target_deps_filter($target)
+
+This may modify @{$target->get_deps()} in place based on values in
+%{$target->get_prop_of()}. This method is normally implemented by sub-classes.
+
+=item $instance->target_file_ext_of()
+
+Returns a HASH reference containing a map between the named types of file
+extensions used by the $instance->source_to_targets($source,\%prop_of) method
+and their default values.
+
+=item $instance->target_file_name_option_of()
+
+Returns a HASH reference containing a map between the named types of files
+used by the $instance->source_to_targets($source,\%prop_of) method
+and their default settings for other file naming options.
+
+=item $instance->task_of()
+
+Returns a HASH reference containing a map between the named tasks for this file
+type and their implementation objects. Each task should have a
+$task->main($target) method to update a target and optionally a $task->prop_of()
+method to return a HASH reference containing a map between the named properties
+used by the task and their default values.
+
+=back
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/System/Make/Build/FileType/C.pm b/lib/FCM/System/Make/Build/FileType/C.pm
new file mode 100644
index 0000000..bd0a99e
--- /dev/null
+++ b/lib/FCM/System/Make/Build/FileType/C.pm
@@ -0,0 +1,156 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+
+# ------------------------------------------------------------------------------
+package FCM::System::Make::Build::FileType::C;
+use base qw{FCM::System::Make::Build::FileType};
+
+use FCM::Context::Make::Build; # for FCM::Context::Make::Build::Target
+use FCM::System::Make::Build::Task::Compile::C;
+use FCM::System::Make::Build::Task::Install;
+use FCM::System::Make::Build::Task::Link::C;
+use File::Basename qw{basename};
+
+# RE: file (base) name
+my $RE_FILE = qr{[\w\-+.]+}imsx;
+
+# RE: main program
+my $RE_MAIN = qr{int\s*main\b}msx;
+
+my %SOURCE_ANALYSE_DEP_OF = (
+ include => sub { $_[0] =~ qr{\A\#\s*include\s+"($RE_FILE)"}msx },
+ o => sub { lc($_[0]) =~ qr{\A\s*/\*\s*depends\s*on\s*:\s*($RE_FILE)}imsx },
+);
+my $TARGET = 'FCM::Context::Make::Build::Target';
+my %TASK_CLASS_OF = (
+ 'compile' => 'FCM::System::Make::Build::Task::Compile::C',
+ 'install' => 'FCM::System::Make::Build::Task::Install',
+ 'link' => 'FCM::System::Make::Build::Task::Link::C',
+);
+
+sub new {
+ my ($class, $attrib_ref) = @_;
+ bless(
+ FCM::System::Make::Build::FileType->new({
+ id => 'c',
+ file_ext => '.c .i .m .mi',
+ source_analyse_always => 1,
+ source_analyse_dep_of => {%SOURCE_ANALYSE_DEP_OF},
+ source_analyse_more => \&_source_analyse_more,
+ source_analyse_more_init => \&_source_analyse_more_init,
+ source_to_targets => \&_source_to_targets,
+ target_file_ext_of => {bin => '.exe', o => '.o'},
+ task_class_of => {%TASK_CLASS_OF},
+ %{$attrib_ref},
+ }),
+ $class,
+ );
+}
+
+sub _source_analyse_more {
+ my ($line, $info_hash_ref) = @_;
+ if (!$info_hash_ref->{main} && $line =~ $RE_MAIN) {
+ return $info_hash_ref->{main} = 1;
+ }
+ return;
+}
+
+sub _source_analyse_more_init {
+ my ($info_hash_ref) = @_;
+ $info_hash_ref->{main} = 0;
+}
+
+# Returns a list of targets for a given build source.
+sub _source_to_targets {
+ my ($attrib_ref, $source, $prop_hash_ref) = @_;
+ my $key = basename($source->get_path());
+ my ($ext, $root) = $attrib_ref->{util}->file_ext($key);
+ my %dot = %{$prop_hash_ref};
+ my @deps = @{$source->get_deps()};
+ my $key_o = lc($root) . $dot{o}; # lc for legacy
+ my @targets = (
+ $TARGET->new(
+ { category => $TARGET->CT_INCLUDE,
+ deps => [@deps],
+ dep_policy_of => {'include' => $TARGET->POLICY_CAPTURE},
+ key => $key,
+ status_of => {'include' => $TARGET->ST_UNKNOWN},
+ task => 'install',
+ }
+ ),
+ $TARGET->new(
+ { category => $TARGET->CT_O,
+ deps => [@deps],
+ dep_policy_of => {'include' => $TARGET->POLICY_CAPTURE},
+ info_of => {paths => []},
+ key => $key_o,
+ task => 'compile',
+ }
+ ),
+ );
+
+ if ($source->get_info_of()->{'main'}) {
+ my @link_deps = grep {$_->[1] eq 'o'} @deps;
+ push(
+ @targets,
+ $TARGET->new(
+ { category => $TARGET->CT_BIN,
+ deps => [[$key_o, 'o'], @link_deps],
+ dep_policy_of => {
+ map {($_ => $TARGET->POLICY_CAPTURE)} qw{o o.special},
+ },
+ info_of => {
+ paths => [], deps => {o => [], 'o.special' => []},
+ },
+ key => $root . $dot{bin},
+ task => 'link',
+ }
+ )
+ );
+ }
+ return @targets;
+}
+
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::System::Make::Build::FileType::C
+
+=head1 SYNOPSIS
+
+ use FCM::System::Make::Build::FileType::C;
+ my $helper = FCM::System::Make::Build::FileType::C->new();
+ $helper->source_analyse($handle);
+
+=head1 DESCRIPTION
+
+A wrapper of
+L<FCM::System::Make::Build::FileType|FCM::System::Make::Build::FileType> with
+configurations to work with C source files.
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/System/Make/Build/FileType/CPP.pm b/lib/FCM/System/Make/Build/FileType/CPP.pm
new file mode 100644
index 0000000..54587d4
--- /dev/null
+++ b/lib/FCM/System/Make/Build/FileType/CPP.pm
@@ -0,0 +1,92 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+
+# ------------------------------------------------------------------------------
+package FCM::System::Make::Build::FileType::CPP;
+use base qw{FCM::System::Make::Build::FileType};
+
+use FCM::Context::Make::Build; # for FCM::Context::Make::Build::Target
+use FCM::System::Make::Build::Task::Preprocess::C;
+
+# Dependency types and CODE to extract them
+my %SOURCE_ANALYSE_DEP_OF
+ = (include => sub { $_[0] =~ qr{\A\#\s*include\s+"([\w\-+.]+)"}msx });
+
+# Handler of tasks
+my %TASK_CLASS_OF
+ = (process => 'FCM::System::Make::Build::Task::Preprocess::C');
+
+sub new {
+ my ($class, $attrib_ref) = @_;
+ bless(
+ FCM::System::Make::Build::FileType->new({
+ id => 'cpp',
+ file_ext => '.c .m .cc .cp .cxx .cpp .CPP .c++ .C .mm .M',
+ source_analyse_dep_of => {%SOURCE_ANALYSE_DEP_OF},
+ source_to_targets => \&_source_to_targets,
+ target_file_ext_of => {},
+ task_class_of => {%TASK_CLASS_OF},
+ %{$attrib_ref},
+ }),
+ $class,
+ );
+}
+
+# Returns a list of targets for a given build source.
+sub _source_to_targets {
+ my ($attrib_ref, $source) = @_;
+ my $TARGET = 'FCM::Context::Make::Build::Target';
+ $TARGET->new(
+ { category => $TARGET->CT_SRC,
+ deps => [@{$source->get_deps()}],
+ dep_policy_of => {'include' => $TARGET->POLICY_CAPTURE},
+ info_of => {paths => []},
+ key => $source->get_ns(),
+ task => 'process',
+ }
+ );
+}
+
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::System::Make::Build::FileType::CPP
+
+=head1 SYNOPSIS
+
+ use FCM::System::Make::Build::FileType::CPP;
+ my $helper = FCM::System::Make::Build::FileType::CPP->new();
+ $helper->source_analyse($handle);
+
+=head1 DESCRIPTION
+
+A wrapper of
+L<FCM::System::Make::Build::FileType|FCM::System::Make::Build::FileType> with
+configurations to work with C source files for preprocessing.
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/System/Make/Build/FileType/CXX.pm b/lib/FCM/System/Make/Build/FileType/CXX.pm
new file mode 100644
index 0000000..c7d2a8a
--- /dev/null
+++ b/lib/FCM/System/Make/Build/FileType/CXX.pm
@@ -0,0 +1,72 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+# ------------------------------------------------------------------------------
+package FCM::System::Make::Build::FileType::CXX;
+use base qw{FCM::System::Make::Build::FileType::C};
+
+use FCM::System::Make::Build::Task::Compile::CXX;
+use FCM::System::Make::Build::Task::Install;
+use FCM::System::Make::Build::Task::Link::CXX;
+
+my %TASK_CLASS_OF = (
+ 'compile' => 'FCM::System::Make::Build::Task::Compile::CXX',
+ 'install' => 'FCM::System::Make::Build::Task::Install',
+ 'link' => 'FCM::System::Make::Build::Task::Link::CXX',
+);
+
+sub new {
+ my ($class, $attrib_ref) = @_;
+ bless(
+ FCM::System::Make::Build::FileType::C->new({
+ id => 'cxx',
+ file_ext => '.cc .cp .cxx .cpp .CPP .c++ .C .mm .M .mii',
+ task_class_of => {%TASK_CLASS_OF},
+ %{$attrib_ref},
+ }),
+ $class,
+ );
+}
+
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::System::Make::Build::FileType::CXX
+
+=head1 SYNOPSIS
+
+ use FCM::System::Make::Build::FileType::CXX;
+ my $helper = FCM::System::Make::Build::FileType::CXX->new();
+ $helper->source_analyse($handle);
+
+=head1 DESCRIPTION
+
+A wrapper of
+L<FCM::System::Make::Build::FileType::C|FCM::System::Make::Build::FileType::C>
+with configurations to work with C++ source files.
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/System/Make/Build/FileType/Data.pm b/lib/FCM/System/Make/Build/FileType/Data.pm
new file mode 100644
index 0000000..c97e986
--- /dev/null
+++ b/lib/FCM/System/Make/Build/FileType/Data.pm
@@ -0,0 +1,82 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+
+# ------------------------------------------------------------------------------
+package FCM::System::Make::Build::FileType::Data;
+use base qw{FCM::System::Make::Build::FileType};
+
+use FCM::Context::Make::Build; # for FCM::Context::Make::Build::Target
+use FCM::System::Make::Build::Task::Install;
+
+# Handler of tasks
+my %TASK_CLASS_OF = (install => 'FCM::System::Make::Build::Task::Install');
+
+sub new {
+ my ($class, $attrib_ref) = @_;
+ bless(
+ FCM::System::Make::Build::FileType->new({
+ id => q{},
+ source_analyse_dep_of => {},
+ source_to_targets => \&_source_to_targets,
+ target_file_ext_of => {},
+ task_class_of => {%TASK_CLASS_OF},
+ %{$attrib_ref},
+ }),
+ $class,
+ );
+}
+
+# Returns a list of targets for a given build source.
+sub _source_to_targets {
+ my ($attrib_ref, $source) = @_;
+ FCM::Context::Make::Build::Target->new(
+ { category => FCM::Context::Make::Build::Target->CT_ETC,
+ key => $source->get_ns(),
+ task => 'install',
+ }
+ );
+}
+
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::System::Make::Build::FileType::Data
+
+=head1 SYNOPSIS
+
+ use FCM::System::Make::Build::FileType::Data;
+ my $helper = FCM::System::Make::Build::FileType::Data->new();
+ $helper->source_analyse($handle);
+
+=head1 DESCRIPTION
+
+A class based on
+L<FCM::System::Make::Build::FileType|FCM::System::Make::Build::FileType>
+with configurations to install data files to the etc/ sub-directory of a build.
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/System/Make/Build/FileType/FPP.pm b/lib/FCM/System/Make/Build/FileType/FPP.pm
new file mode 100644
index 0000000..61bc67e
--- /dev/null
+++ b/lib/FCM/System/Make/Build/FileType/FPP.pm
@@ -0,0 +1,67 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+# ------------------------------------------------------------------------------
+package FCM::System::Make::Build::FileType::FPP;
+use base qw{FCM::System::Make::Build::FileType::CPP};
+
+use FCM::System::Make::Build::Task::Preprocess::Fortran;
+
+my %TASK_CLASS_OF
+ = (process => 'FCM::System::Make::Build::Task::Preprocess::Fortran');
+
+sub new {
+ my ($class, $attrib_ref) = @_;
+ bless(
+ FCM::System::Make::Build::FileType::CPP->new({
+ id => 'fpp',
+ file_ext => '.F90 .F95 .F .FTN .FOR',
+ task_class_of => {%TASK_CLASS_OF},
+ %{$attrib_ref},
+ }),
+ $class,
+ );
+}
+
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::System::Make::Build::FileType::FPP
+
+=head1 SYNOPSIS
+
+ use FCM::System::Make::Build::FileType::FPP;
+ my $helper = FCM::System::Make::Build::FileType::FPP->new();
+ $helper->source_analyse($handle);
+
+=head1 DESCRIPTION
+
+A wrapper of
+L<FCM::System::Make::Build::FileType::CPP|FCM::System::Make::Build::FileType::CPP>
+with configurations to work with Fortran source files for preprocessing.
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/System/Make/Build/FileType/Fortran.pm b/lib/FCM/System/Make/Build/FileType/Fortran.pm
new file mode 100644
index 0000000..beb6fed
--- /dev/null
+++ b/lib/FCM/System/Make/Build/FileType/Fortran.pm
@@ -0,0 +1,404 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+
+# ------------------------------------------------------------------------------
+package FCM::System::Make::Build::FileType::Fortran;
+use base qw{FCM::System::Make::Build::FileType};
+
+use FCM::Context::Make::Build; # for FCM::Context::Make::Build::Target
+use FCM::System::Make::Build::Task::Compile::Fortran;
+use FCM::System::Make::Build::Task::ExtractInterface;
+use FCM::System::Make::Build::Task::Install;
+use FCM::System::Make::Build::Task::Link::Fortran;
+use File::Basename qw{basename};
+use Text::Balanced qw{extract_bracketed extract_delimited};
+
+# Recommended file extensions of this utility
+our $FILE_EXT = '.F .F90 .F95 .FOR .FTN .f .f90 .f95 .for .ftn .inc';
+
+# List of Fortran intrinsic modules
+our @INTRINSIC_MODULES = qw{
+ ieee_arithmetic
+ ieee_exceptions
+ ieee_features
+ iso_c_binding
+ iso_fortran_env
+ omp_lib
+ omp_lib_kinds
+};
+
+# Prefix for dependency name that is only applicable under OMP
+our $OMP_PREFIX = '!$';
+
+# Regular expressions
+my $RE_FILE = qr{[\w\-+.]+}imsx;
+my $RE_NAME = qr{[A-Za-z]\w*}imsx;
+my $RE_SPEC = qr{
+ character|class|complex|double\s*complex|double\s*precision|integer|
+ logical|procedure|real|type
+}imsx;
+my $RE_UNIT_BASE = qr{block\s*data|module|program|submodule}imsx;
+my $RE_UNIT_CALL = qr{subroutine|function}imsx;
+my %RE = (
+ DEP_O => qr{\A\s*!\s*depends\s*on\s*:\s*($RE_FILE)}imsx,
+ DEP_USE => qr{\A\s*use\s+($RE_NAME)}imsx,
+ DEP_SUBM => qr{\A\s*submodule\s+\(($RE_NAME)\)}imsx,
+ INCLUDE => qr{\#?\s*include\s*}imsx,
+ OMP_SENT => qr{\A(\s*!\$\s+)?(.*)\z}imsx,
+ UNIT_ATTR => qr{\A\s*(?:(?:(?:impure\s+)?elemental|recursive|pure)\s+)+(.*)\z}imsx,
+ UNIT_BASE => qr{\A\s*($RE_UNIT_BASE)\s+($RE_NAME)\s*\z}imsx,
+ UNIT_CALL => qr{\A\s*($RE_UNIT_CALL)\s+($RE_NAME)\b}imsx,
+ UNIT_END => qr{\A\s*(end)(?:\s+($RE_NAME)(?:\s+($RE_NAME))?)?\s*\z}imsx,
+ UNIT_SPEC => qr{\A\s*$RE_SPEC\b(.*)\z}imsx,
+);
+
+# Dependency types and extractors
+my %SOURCE_ANALYSE_DEP_OF = (
+ 'f.module' => \&_source_analyse_dep_module,
+ 'include' => \&_source_analyse_dep_include,
+ 'o' => sub { lc($_[0]) =~ $RE{DEP_O} }, # lc required for legacy
+ 'o.special' => sub {},
+);
+# Alias
+my $TARGET = 'FCM::Context::Make::Build::Target';
+# Classes for tasks used by targets of this file type
+my %TASK_CLASS_OF = (
+ 'compile' => 'FCM::System::Make::Build::Task::Compile::Fortran',
+ 'compile+' => 'FCM::System::Make::Build::Task::Compile::Fortran::Extra',
+ 'ext-iface' => 'FCM::System::Make::Build::Task::ExtractInterface',
+ 'install' => 'FCM::System::Make::Build::Task::Install',
+ 'link' => 'FCM::System::Make::Build::Task::Link::Fortran',
+);
+# Property suffices of output file extensions
+my %TARGET_EXT_OF = (
+ 'bin' => '.exe',
+ 'f90-interface' => '.interface',
+ 'f90-mod' => '.mod',
+ 'o' => '.o',
+);
+
+sub new {
+ my ($class, $attrib_ref) = @_;
+ bless(
+ FCM::System::Make::Build::FileType->new({
+ id => 'fortran',
+ file_ext => $FILE_EXT,
+ source_analyse_always => 1,
+ source_analyse_dep_of => {%SOURCE_ANALYSE_DEP_OF},
+ source_analyse_more => \&_source_analyse_more,
+ source_analyse_more_init => \&_source_analyse_more_init,
+ source_to_targets => \&_source_to_targets,
+ target_deps_filter => \&_target_deps_filter,
+ target_file_ext_of => {%TARGET_EXT_OF},
+ target_file_name_option_of => {'f90-mod' => q{}},
+ task_class_of => {%TASK_CLASS_OF},
+ %{$attrib_ref},
+ }),
+ $class,
+ );
+}
+
+sub _source_analyse_more {
+ my ($line, $info_hash_ref, $state) = @_;
+
+ # End Interface
+ if ($state->{in_interface}) {
+ if ($line =~ qr{\A\s*end\s*interface\b}imsx) {
+ $state->{in_interface} = 0;
+ }
+ return 1;
+ }
+
+ # End Program Unit
+ if (@{$state->{stack}} && $line =~ qr{\A\s*end\b}imsx) {
+ my ($end, $type, $symbol) = lc($line) =~ $RE{UNIT_END};
+ if (!$end) {
+ return 1;
+ }
+ my ($top_type, $top_symbol) = @{$state->{stack}->[-1]};
+ if (!$type
+ || $top_type eq $type && (!$symbol || $top_symbol eq $symbol))
+ {
+ pop(@{$state->{stack}});
+ if ($state->{in_contains} && !@{$state->{stack}}) {
+ $state->{in_contains} = 0;
+ }
+ }
+ return 1;
+ }
+
+ # Interface/Contains
+ if ($line =~ qr{\A\s*contains\b}imsx) {
+ $state->{'in_contains'} = 1;
+ return 1;
+ }
+ if ($line =~ qr{\A\s*(?:abstract\s+)?interface\b}imsx) {
+ $state->{'in_interface'} = 1;
+ return 1;
+ }
+
+ # Program Unit
+ my ($type, $symbol) = _process_prog_unit($line);
+ if ($type) {
+ if (!@{$state->{stack}}) {
+ if ($type eq 'program') {
+ $info_hash_ref->{main} = 1;
+ }
+ $info_hash_ref->{symbols} ||= [];
+ push(@{$info_hash_ref->{symbols}}, [$type, $symbol]);
+ }
+ push(@{$state->{stack}}, [$type, $symbol]);
+ return 1;
+ }
+ return;
+}
+
+sub _source_analyse_more_init {
+ my ($info_ref, $state) = @_;
+ %{$info_ref} = (main => 0, symbols => []);
+ %{$state} = (in_contains => undef, in_interface => undef, stack => []);
+}
+
+# Reads information: extract an include dependency.
+sub _source_analyse_dep_include {
+ my ($line) = @_;
+ my ($omp_sentinel, $extracted);
+ ($omp_sentinel, $line) = $line =~ $RE{OMP_SENT};
+ ($extracted) = extract_delimited($line, q{'"}, $RE{INCLUDE});
+ if (!$extracted) {
+ return;
+ }
+ $extracted = substr($extracted, 1, length($extracted) - 2);
+ if ($omp_sentinel) {
+ $extracted = $OMP_PREFIX . $extracted;
+ }
+ $extracted;
+}
+
+# Reads information: extract a module dependency.
+sub _source_analyse_dep_module {
+ my ($line) = @_;
+ my ($omp_sentinel, $extracted, $can_analyse_more);
+ ($omp_sentinel, $line) = $line =~ $RE{OMP_SENT};
+ ($extracted) = lc($line) =~ $RE{DEP_USE};
+ if (!$extracted) {
+ ($extracted) = lc($line) =~ $RE{DEP_SUBM};
+ $can_analyse_more = 1;
+ }
+ if (!$extracted || grep {$_ eq $extracted} @INTRINSIC_MODULES) {
+ return;
+ }
+ if ($omp_sentinel) {
+ $extracted = $OMP_PREFIX . $extracted;
+ }
+ ($extracted, $can_analyse_more);
+}
+
+# Parse a statement for program unit header. Returns a list containing the type,
+# the symbol and the signature tokens of the program unit.
+sub _process_prog_unit {
+ my ($string) = @_;
+ my ($type, $symbol, @args) = (q{}, q{});
+ ($type, $symbol) = lc($string) =~ $RE{UNIT_BASE};
+ if ($type) {
+ $type = lc($type);
+ $type =~ s{\s*}{}gmsx;
+ return ($type, $symbol);
+ }
+ $string =~ s/$RE{UNIT_ATTR}/$1/;
+ my ($match) = $string =~ $RE{UNIT_SPEC};
+ if ($match) {
+ $string = $match;
+ if ($string =~ qr{\A \s* \(}msx) {
+ extract_bracketed($string);
+ }
+ elsif ($string =~ qr{\A \s* \*}msx) {
+ $string =~ s{\A \s* \* \d+ \s*}{}msx;
+ }
+ }
+ ($type, $symbol) = lc($string) =~ $RE{UNIT_CALL};
+ if (!$type) {
+ return;
+ }
+ return (lc($type), lc($symbol));
+}
+
+# Returns a list of targets for a given build source.
+sub _source_to_targets {
+ my ($attrib_ref, $source, $ext_hash_ref, $option_hash_ref) = @_;
+ my $key = basename($source->get_path());
+ my $TARGET_OF = sub {
+ my ($symbol, $type) = @_;
+ if (exists($option_hash_ref->{$type})) {
+ my $is_upper = index($option_hash_ref->{$type}, 'case=upper') >= 0;
+ $symbol = $is_upper ? uc($symbol) : lc($symbol);
+ }
+ $symbol . $ext_hash_ref->{$type};
+ };
+ my @deps = map {
+ my ($k, $type) = @{$_};
+ my $ext = $attrib_ref->{util}->file_ext($k);
+ $type eq 'f.module' ? [$TARGET_OF->($k, 'f90-mod'), 'include', 1]
+ : $type eq 'o' && !$ext ? [$TARGET_OF->($k, 'o'), $type]
+ : [$k, $type]
+ } @{$source->get_deps()};
+ # All source files can be used as include files
+ my @targets = (
+ $TARGET->new(
+ { category => $TARGET->CT_INCLUDE,
+ deps => [@deps],
+ dep_policy_of => {'include' => $TARGET->POLICY_CAPTURE},
+ key => $key,
+ status_of => {'include' => $TARGET->ST_UNKNOWN},
+ task => 'install',
+ }
+ ),
+ );
+ my ($ext, $root) = $attrib_ref->{util}->file_ext($key);
+ my $symbols_ref = $source->get_info_of()->{symbols};
+ # FIXME: hard code the handling of "*.inc" files as include files
+ if (!defined($symbols_ref) || !@{$symbols_ref} || $ext eq 'inc') {
+ return @targets;
+ }
+ my $key_of_o = $TARGET_OF->($symbols_ref->[0][1], 'o');
+ my @keys_of_mod;
+ for (grep {$_->[0] eq 'module'} @{$symbols_ref}) {
+ my ($type, $symbol) = @{$_};
+ my $key_of_mod = $TARGET_OF->($symbol, 'f90-mod');
+ my @include_deps = grep {$_->[1] eq 'include'} @deps;
+ push(
+ @targets,
+ $TARGET->new(
+ { category => $TARGET->CT_INCLUDE,
+ deps => [[$key_of_o, 'o']],
+ dep_policy_of => {
+ 'include' => $TARGET->POLICY_CAPTURE,
+ 'o' => $TARGET->POLICY_FILTER_IMMEDIATE,
+ },
+ key => $key_of_mod,
+ task => 'compile+',
+ }
+ )
+ );
+ push(@keys_of_mod, $key_of_mod);
+ }
+ push(
+ @targets,
+ $TARGET->new(
+ { category => $TARGET->CT_O,
+ deps => [@deps],
+ dep_policy_of => {'include' => $TARGET->POLICY_CAPTURE},
+ info_of => {paths => []},
+ key => $key_of_o,
+ task => 'compile',
+ triggers => \@keys_of_mod,
+ }
+ ),
+ );
+ if (grep {$_->[0] eq 'subroutine' || $_->[0] eq 'function'} @{$symbols_ref}) {
+ my $target_key = $root . $ext_hash_ref->{'f90-interface'};
+ push(
+ @targets,
+ $TARGET->new(
+ { category => $TARGET->CT_INCLUDE,
+ deps => [[$key_of_o, 'o'], grep {exists($_->[2])} @deps],
+ dep_policy_of => {
+ 'include' => $TARGET->POLICY_FILTER_IMMEDIATE,
+ },
+ key => $target_key,
+ task => 'ext-iface',
+ }
+ )
+ );
+ }
+ if ($source->get_info_of()->{main}) {
+ my @link_deps = grep {$_->[1] eq 'o' || $_->[1] eq 'o.special'} @deps;
+ push(
+ @targets,
+ $TARGET->new(
+ { category => $TARGET->CT_BIN,
+ deps => [[$key_of_o, 'o'], @link_deps],
+ dep_policy_of => {
+ 'o' => $TARGET->POLICY_CAPTURE,
+ 'o.special' => $TARGET->POLICY_CAPTURE,
+ },
+ info_of => {
+ paths => [], deps => {o => [], 'o.special' => []},
+ },
+ key => $root . $ext_hash_ref->{bin},
+ task => 'link',
+ }
+ )
+ );
+ }
+ return @targets;
+}
+
+# If target's fc.flag-omp property is empty, remove !$OMP dependencies.
+# Otherwise, remove !$OMP sentinels from the dependencies.
+sub _target_deps_filter {
+ my ($attrib_ref, $target) = @_;
+ if ($target->get_prop_of()->{'fc.flag-omp'}) {
+ for my $dep_ref (@{$target->get_deps()}) {
+ if (index($dep_ref->[0], $OMP_PREFIX) == 0) {
+ substr($dep_ref->[0], 0, length($OMP_PREFIX), q{});
+ }
+ }
+ }
+ else {
+ $target->set_deps(
+ [grep {index($_->[0], $OMP_PREFIX) == -1} @{$target->get_deps()}],
+ );
+ }
+}
+
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::System::Make::Build::FileType::Fortran
+
+=head1 SYNOPSIS
+
+ use FCM::System::Make::Build::FileType::Fortran;
+ my $file_type_util = FCM::System::Make::Build::FileType::Fortran->new();
+
+ $file_type_util->source_analyse($source);
+
+ my @targets = $file_type_util->source_to_targets($m_ctx, $ctx, $source);
+
+=head1 DESCRIPTION
+
+A wrapper of
+L<FCM::System::Make::Build::FileType|FCM::System::Make::Build::FileType> with
+configurations to work with Fortran source files.
+
+=head1 TODO
+
+Combine the code with FCM::System::Make::Build::Task::ExtractInterface.
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/System/Make/Build/FileType/H.pm b/lib/FCM/System/Make/Build/FileType/H.pm
new file mode 100644
index 0000000..eebef0c
--- /dev/null
+++ b/lib/FCM/System/Make/Build/FileType/H.pm
@@ -0,0 +1,131 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+
+# ------------------------------------------------------------------------------
+package FCM::System::Make::Build::FileType::H;
+use base qw{FCM::System::Make::Build::FileType};
+
+use FCM::Context::Make::Build; # for FCM::Context::Make::Build::Target
+use FCM::System::Make::Build::Task::Install;
+use File::Basename qw{basename};
+
+# RE: file (base) name
+my $RE_FILE = qr{[\w\-+.]+}imsx;
+
+# Dependency types and CODE to extract them
+my %SOURCE_ANALYSE_DEP_OF = (
+ include => sub { $_[0] =~ qr{\A\#\s*include\s+"($RE_FILE)"}msx },
+
+ # Note: handle ! as a comment, for *.h files containing Fortran source
+ o => sub {
+ $_[0] =~ qr{\A\s*(?:!|/\*)\s*depends\s*on\s*:\s*($RE_FILE)}imsx;
+ },
+);
+
+# Alias
+my $TARGET = 'FCM::Context::Make::Build::Target';
+
+# Handler of tasks
+my %TASK_CLASS_OF = (install => 'FCM::System::Make::Build::Task::Install');
+
+# Property suffices of output file extensions
+my %TARGET_EXT_OF = ('o' => '.o');
+
+sub new {
+ my ($class, $attrib_ref) = @_;
+ bless(
+ FCM::System::Make::Build::FileType->new({
+ id => 'h',
+ file_ext => '.h',
+ source_analyse_dep_of => {%SOURCE_ANALYSE_DEP_OF},
+ source_to_targets => \&_source_to_targets,
+ target_file_ext_of => {%TARGET_EXT_OF},
+ task_class_of => {%TASK_CLASS_OF},
+ %{$attrib_ref},
+ }),
+ $class,
+ );
+}
+
+# Returns a list of targets for a given build source.
+sub _source_to_targets {
+ my ($attrib_ref, $source, $prop_hash_ref) = @_;
+ my %dot = %{$prop_hash_ref};
+ my $key = basename($source->get_path());
+ my @deps = map {
+ my $ext = $attrib_ref->{util}->file_ext($_->[0]);
+ $_->[1] eq 'o' && !$ext ? [lc($_->[0]) . $dot{o}, $_->[1]] : $_;
+ } @{$source->get_deps()};
+ $TARGET->new(
+ { category => $TARGET->CT_INCLUDE,
+ deps => [@deps],
+ dep_policy_of => {'include' => $TARGET->POLICY_CAPTURE},
+ key => $key,
+ status_of=> {'include' => $TARGET->ST_UNKNOWN},
+ task => 'install',
+ }
+ );
+}
+
+# ------------------------------------------------------------------------------
+package FCM::System::Make::Build::FileType::HPP;
+use base qw{FCM::System::Make::Build::FileType::H};
+
+sub new {
+ my ($class, $attrib_ref) = @_;
+ bless(
+ FCM::System::Make::Build::FileType::H->new({
+ source_analyse_dep_of => {include => $SOURCE_ANALYSE_DEP_OF{include}},
+ target_file_ext_of => {},
+ %{$attrib_ref},
+ }),
+ $class,
+ );
+}
+
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::System::Make::Build::FileType::H
+
+=head1 SYNOPSIS
+
+ use FCM::System::Make::Build::FileType::H;
+ my $helper = FCM::System::Make::Build::FileType::H->new();
+ $helper->source_analyse($handle);
+
+ my $helper = FCM::System::Make::Build::FileType::HPP->new();
+ $helper->source_analyse($handle);
+
+=head1 DESCRIPTION
+
+A wrapper of
+L<FCM::System::Make::Build::FileType|FCM::System::Make::Build::FileType> with
+configurations to work with C or Fortran preprocessor header files.
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/System/Make/Build/FileType/NS.pm b/lib/FCM/System/Make/Build/FileType/NS.pm
new file mode 100644
index 0000000..e6c1eea
--- /dev/null
+++ b/lib/FCM/System/Make/Build/FileType/NS.pm
@@ -0,0 +1,210 @@
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+use strict;
+use warnings;
+#-------------------------------------------------------------------------------
+
+package FCM::System::Make::Build::FileType::NS;
+use base qw{FCM::Class::CODE};
+
+use FCM::Context::Make::Build; # for FCM::Context::Make::Build::Target
+use FCM::System::Make::Build::Task::Archive;
+use FCM::System::Make::Build::Task::Install;
+use File::Spec::Functions qw{catfile};
+
+my $ID = '/';
+
+my %TARGET_FILE_EXT_OF = (a => '.a', etc => '.etc');
+
+my %TASK_CLASS_OF = (
+ 'archive' => 'FCM::System::Make::Build::Task::Archive',
+ 'install' => 'FCM::System::Make::Build::Task::Install',
+);
+
+# Creates the class.
+__PACKAGE__->class(
+ { id => {isa => '$', default => $ID},
+ target_file_ext_of => {isa => '%', default => {%TARGET_FILE_EXT_OF}},
+ target_file_name_option_of => '%',
+ task_class_of => {isa => '%', default => {%TASK_CLASS_OF}},
+ shared_util_of => '%',
+ task_of => '%',
+ util => '&',
+ },
+ { init => \&_init,
+ action_of => {
+ (map {my $key = $_; ($key => sub {$_[0]->{$key}})}
+ qw{id target_file_ext_of target_file_name_option_of task_of}
+ ),
+ ns_targets_deps => sub {('o')},
+ ns_targets => \&_ns_targets,
+ },
+ },
+);
+
+# Initialises some attributes.
+sub _init {
+ my ($attrib_ref) = @_;
+ while (my ($key, $class) = each(%{$attrib_ref->{task_class_of}})) {
+ $attrib_ref->{util}->class_load($class);
+ $attrib_ref->{task_of}{$key}
+ = $class->new({util => $attrib_ref->{util}});
+ }
+}
+
+# Returns a list of targets for a given build source.
+sub _ns_targets {
+ my ($attrib_ref, $targets_ref, $prop_hash_ref) = @_;
+ my %target_of;
+ TARGET:
+ for my $target (@{$targets_ref}) {
+ my @ns_targets;
+ for (
+ [sub {!$_[0]->get_type()} , \&_ns_target_new_etc],
+ [sub {$_[0]->get_category() eq 'o'}, \&_ns_target_new_lib],
+ ) {
+ my ($test, $new) = @{$_};
+ if ($test->($target)) {
+ my $ns_iter = $attrib_ref->{util}->ns_iter(
+ $target->get_ns(), $attrib_ref->{util}->NS_ITER_UP,
+ );
+ $ns_iter->(); # discard
+ while (defined(my $ns = $ns_iter->())) {
+ my $ns_target = $new->($ns, $prop_hash_ref);
+ my $key = $ns_target->get_key();
+ if (!exists($target_of{$key})) {
+ $target_of{$key} = $ns_target;
+ }
+ push(
+ @{$target_of{$key}->get_deps()},
+ [$target->get_key(), $target->get_category()],
+ );
+ }
+ next TARGET;
+ }
+ }
+ }
+ values(%target_of);
+}
+
+# Returns a new etc target for building data files in a namespace.
+sub _ns_target_new_etc {
+ my ($ns, $prop_hash_ref) = @_;
+ my $DOT_ETC = $prop_hash_ref->{etc};
+ my $TARGET = 'FCM::Context::Make::Build::Target';
+ $TARGET->new(
+ { category => $TARGET->CT_ETC,
+ dep_policy_of => {'etc' => $TARGET->POLICY_CAPTURE},
+ key => ($ns ? catfile($ns, $DOT_ETC) : $DOT_ETC),
+ ns => $ns,
+ task => 'install',
+ }
+ );
+}
+
+# Returns a new archive target for building an object library for a namespace.
+sub _ns_target_new_lib {
+ my ($ns, $prop_hash_ref) = @_;
+ my $NAME = 'libo' . $prop_hash_ref->{a}; # FIXME: libo hard-coded
+ my $TARGET = 'FCM::Context::Make::Build::Target';
+ $TARGET->new(
+ { category => $TARGET->CT_LIB,
+ dep_policy_of => {'o' => $TARGET->POLICY_CAPTURE},
+ info_of => {paths => [], deps => {o => []}},
+ key => ($ns ? catfile($ns, $NAME) : $NAME),
+ ns => $ns,
+ task => 'archive',
+ }
+ );
+}
+
+#-------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::System::Make::Build::FileType::NS
+
+=head1 SYNOPSIS
+
+ use FCM::System::Make::Build::FileType::NS;
+ my $file_type_util = FCM::System::Make::Build::FileType->new(\%attrib);
+ $file_type_util->ns_targets($m_ctx, $ctx, @targets);
+
+=head1 DESCRIPTION
+
+Generates name space level targets.
+
+=head1 METHODS
+
+=over 4
+
+=item $class->new(\%attrib)
+
+Creates and returns a new instance.
+
+=item $instance->id()
+
+Returns the recommended ID of this file type.
+
+=item $instance->ns_targets(\@targets,\%prop_of)
+
+Using the information in the original list of targets, creates and returns the
+contexts of a list of extra targets based on the name spaces of the original
+list. In the current settings, a target with no type (i.e. a data file target)
+will generate a C<.etc> target for the container name spaces; a target in the
+C<o> category will generate a C<libo.a> target for the container name spaces.
+
+=item $instance->ns_targets_deps()
+
+Returns a list of dependency types used by
+$instance->ns_targets(\@targets,\%prop_of).
+
+=item $instance->target_file_ext_of()
+
+Returns a HASH reference containing a map between the named types of file
+extensions used by the $instance->ns_targets(\@targets,\%prop_of) method
+and their default values.
+
+=item $instance->target_file_name_option_of()
+
+Returns a HASH reference containing a map between the named types of files
+used by the $instance->source_to_targets($source,\%prop_of) method
+and their default settings for other file naming options.
+
+=item $instance->task_of()
+
+Returns a HASH reference containing a map between the named tasks for this file
+type and their implementation objects. Each task should have a
+$task->main($target) method to update a target and optionally a $task->prop_of()
+method to return a HASH reference containing a map between the named properties
+used by the task and their default values.
+
+=back
+
+=head1 TODO
+
+The configuration in this module is a bit hard coded. It can do with a refactor.
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/System/Make/Build/FileType/Script.pm b/lib/FCM/System/Make/Build/FileType/Script.pm
new file mode 100644
index 0000000..aa466b3
--- /dev/null
+++ b/lib/FCM/System/Make/Build/FileType/Script.pm
@@ -0,0 +1,100 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+
+# ------------------------------------------------------------------------------
+package FCM::System::Make::Build::FileType::Script;
+use base qw{FCM::System::Make::Build::FileType};
+
+use FCM::Context::Make::Build; # for FCM::Context::Make::Build::Target
+use FCM::System::Make::Build::Task::Install;
+use File::Basename qw{basename};
+
+# RE: file (base) name
+my $RE_FILE = qr{[\w\-+.]+}imsx;
+
+# Dependency types and CODE to extract them
+my %SOURCE_ANALYSE_DEP_OF
+ = (bin => sub { $_[0] =~ qr{\A\s*(?:\#|;)\s*calls\s*:\s*($RE_FILE)}imsx });
+
+# Alias
+my $TARGET = 'FCM::Context::Make::Build::Target';
+
+# Handler of tasks
+my %TASK_CLASS_OF = (install => 'FCM::System::Make::Build::Task::Install');
+
+sub new {
+ my ($class, $attrib_ref) = @_;
+ $attrib_ref->{dest_keys} ||= [$TARGET->CT_INCLUDE, 'o', 'o.special'];
+ my $SOURCE_TO_TARGETS
+ = sub {_source_to_targets($attrib_ref->{dest_keys}, @_)};
+ bless(
+ FCM::System::Make::Build::FileType->new({
+ id => 'script',
+ file_she => q{}, # Value not used, for file type match
+ file_ext => q{},
+ source_analyse_dep_of => {%SOURCE_ANALYSE_DEP_OF},
+ source_to_targets => \&_source_to_targets,
+ task_class_of => {%TASK_CLASS_OF},
+ %{$attrib_ref},
+ }),
+ $class,
+ );
+}
+
+# Returns a list of targets for a given build source.
+sub _source_to_targets {
+ my ($attrib_ref, $source, $prop_hash_ref) = @_;
+ my $key = basename($source->get_path());
+ $TARGET->new(
+ { category => $TARGET->CT_BIN,
+ deps => [@{$source->get_deps()}],
+ dep_policy_of => {'bin', $TARGET->POLICY_CAPTURE},
+ key => $key,
+ task => 'install',
+ }
+ );
+}
+
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::System::Make::Build::FileType::Script
+
+=head1 SYNOPSIS
+
+ use FCM::System::Make::Build::FileType::Script;
+ my $helper = FCM::System::Make::Build::FileType::Script->new();
+ $helper->source_analyse($handle);
+
+=head1 DESCRIPTION
+
+A wrapper of
+L<FCM::System::Make::Build::FileType|FCM::System::Make::Build::FileType>
+with configurations to work with some UKMO script files.
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/System/Make/Build/Task/Archive.pm b/lib/FCM/System/Make/Build/Task/Archive.pm
new file mode 100644
index 0000000..e486483
--- /dev/null
+++ b/lib/FCM/System/Make/Build/Task/Archive.pm
@@ -0,0 +1,133 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+# ------------------------------------------------------------------------------
+package FCM::System::Make::Build::Task::Archive;
+use base qw{FCM::Class::CODE};
+
+use FCM::Context::Event;
+use FCM::System::Exception;
+use File::Spec::Functions qw{abs2rel catfile};
+use List::Util qw{first};
+use Text::ParseWords qw{shellwords};
+
+our %PROP_OF = (ar => 'ar', 'ar.flags' => 'rs');
+my $E = 'FCM::System::Exception';
+
+__PACKAGE__->class(
+ {prop_of => {isa => '%', default => {%PROP_OF}}, util => '&'},
+ {action_of => {main => \&_main, prop_of => sub {\%PROP_OF}}},
+);
+
+sub _main {
+ my ($attrib_ref, $target) = @_;
+ # Selects the correct dependent objects
+ my @paths = @{$target->get_info_of('paths')};
+ my %dep_keys_of = %{$target->get_info_of('deps')};
+ my @paths_of_o = ();
+ my $abs2rel_func
+ = sub {index($_[0], $paths[0]) == 0 ? abs2rel($_[0], $paths[0]) : $_[0]};
+ while (my ($type, $key_list_ref) = each(%dep_keys_of)) {
+ for my $key (@{$key_list_ref}) {
+ my $path = first {-e} map {catfile($_, 'o', $key)} @paths;
+ if ($path) {
+ push(@paths_of_o, $abs2rel_func->($path));
+ }
+ }
+ }
+ my @command_list = (
+ (map {shellwords($target->get_prop_of($_))} qw{ar ar.flags}),
+ $target->get_path(),
+ @paths_of_o,
+ );
+ my %value_of = %{$attrib_ref->{util}->shell_simple(\@command_list)};
+ if ($value_of{rc}) {
+ return $E->throw(
+ $E->SHELL, {command_list => \@command_list, %value_of}, $value_of{e},
+ );
+ }
+ $attrib_ref->{util}->event(
+ FCM::Context::Event->MAKE_BUILD_SHELL_OUT, @value_of{qw{o e}},
+ );
+ $target;
+}
+
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::System::Make::Build::Task::Link
+
+=head1 SYNOPSIS
+
+ use FCM::System::Make::Build::Task::Link;
+ my $build_task = FCM::System::Make::Build::Task::Link->new(\%attrib);
+ $build_task->main($target);
+
+=head1 DESCRIPTION
+
+Invokes the linker to create the target executable.
+
+=head1 METHODS
+
+=over 4
+
+=item $class->new(\%attrib)
+
+Creates and returns a new instance. %attrib should contain:
+
+=over 4
+
+=item {prop_of}
+
+A HASH that maps the property names (used by this task) to their default values.
+
+=item {util}
+
+An instance of L<FCM::Util|FCM::Util>.
+
+=back
+
+=item $instance->main($target)
+
+Invokes the "ar" command to create the $target object archive. It uses the
+$target->get_info_of('deps')->{o} ARRAY. All "o" dependency items are placed in
+the archive.
+
+=item $instance->prop_of()
+
+Returns the HASH that maps the property names (used by this task) to their
+default values.
+
+=back
+
+=head1 CONSTANTS
+
+=item %FCM::System::Make::Build::Task::Link::PROP_OF
+
+A map containing the property names and their default values.
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/System/Make/Build/Task/Compile.pm b/lib/FCM/System/Make/Build/Task/Compile.pm
new file mode 100644
index 0000000..cf33d26
--- /dev/null
+++ b/lib/FCM/System/Make/Build/Task/Compile.pm
@@ -0,0 +1,143 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+
+use FCM::System::Exception;
+my $E = 'FCM::System::Exception';
+
+# ------------------------------------------------------------------------------
+package FCM::System::Make::Build::Task::Compile;
+use base qw{FCM::Class::CODE};
+
+use FCM::Context::Event;
+use FCM::System::Make::Build::Task::Share qw{_props_to_opts};
+use File::Spec::Functions qw{abs2rel catfile};
+use Text::ParseWords qw{shellwords};
+
+__PACKAGE__->class(
+ {name => '$', prop_of => '&', util => '&'},
+ {action_of => {main => \&_main, prop_of => \&_prop_of}},
+);
+
+sub _main {
+ my ($attrib_ref, $target) = @_;
+ my $NAME = $attrib_ref->{name};
+ my $P = sub {scalar($target->get_prop_of($_[0]))};
+ my @paths = @{$target->get_info_of('paths')};
+ my $abs2rel_func
+ = sub {index($_[0], $paths[0]) == 0 ? abs2rel($_[0], $paths[0]) : $_[0]};
+ my @include_paths
+ = map {catfile(($_ eq $paths[0] ? q{.} : $_), 'include')} @paths;
+ my %opt_of = (
+ c => $P->($NAME . '.flag-compile'),
+ D => $P->($NAME . '.flag-define'),
+ I => $P->($NAME . '.flag-include'),
+ M => $P->($NAME . '.flag-module'), # FIXME
+ o => $P->($NAME . '.flag-output'),
+ );
+ my @command_list = (
+ shellwords($P->($NAME)),
+ _props_to_opts($opt_of{o}, $abs2rel_func->($target->get_path())),
+ $opt_of{c},
+ _props_to_opts($opt_of{D}, shellwords($P->($NAME . '.defs'))),
+ _props_to_opts($opt_of{I}, @include_paths),
+ _props_to_opts($opt_of{I}, shellwords($P->($NAME . '.include-paths'))),
+ _props_to_opts($opt_of{M}, @include_paths),
+ shellwords($P->($NAME . '.flag-omp')),
+ shellwords($P->($NAME . '.flags')),
+ $target->get_path_of_source(),
+ );
+ my %value_of = %{$attrib_ref->{util}->shell_simple(\@command_list)};
+ if ($value_of{rc}) {
+ return $E->throw(
+ $E->SHELL, {command_list => \@command_list, %value_of}, $value_of{e},
+ );
+ }
+ $attrib_ref->{util}->event(
+ FCM::Context::Event->MAKE_BUILD_SHELL_OUT, @value_of{qw{o e}},
+ );
+ $target;
+}
+
+sub _prop_of {
+ my ($attrib_ref) = @_;
+ $attrib_ref->{prop_of}->(@_);
+}
+
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::System::Make::Build::Task::Compile
+
+=head1 SYNOPSIS
+
+ use FCM::System::Make::Build::Task::Compile;
+ my $build_task = FCM::System::Make::Build::Task::Compile->new(\%attrib);
+ $build_task->main($target);
+
+=head1 DESCRIPTION
+
+Invokes the compiler command on the source of a target to generate the path of
+the target.
+
+=head1 METHODS
+
+=over 4
+
+=item $class->new(\%attrib)
+
+Creates and returns a new instance. %attrib should contain:
+
+=over 4
+
+=item {name}
+
+The property name of the compiler command.
+
+=item {prop_of}
+
+A CODE to implement the $instance->prop_of($target) method.
+
+=item {util}
+
+An instance of L<FCM::Util|FCM::Util>.
+
+=back
+
+=item $instance->main($target)
+
+Invokes the compiler command in a shell to compile the source path of the
+$target into an object file in the path of the $target.
+
+=item $instance->prop_of($target)
+
+Returns the HASH that maps the property names (used by this task) to their
+default values.
+
+=back
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/System/Make/Build/Task/Compile/C.pm b/lib/FCM/System/Make/Build/Task/Compile/C.pm
new file mode 100644
index 0000000..7fc470c
--- /dev/null
+++ b/lib/FCM/System/Make/Build/Task/Compile/C.pm
@@ -0,0 +1,70 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+# ------------------------------------------------------------------------------
+package FCM::System::Make::Build::Task::Compile::C;
+use base qw{FCM::System::Make::Build::Task::Compile};
+
+our %PROP_OF = (
+ 'cc' => 'gcc',
+ 'cc.defs' => '',
+ 'cc.flags' => '',
+ 'cc.flag-compile' => '-c',
+ 'cc.flag-define' => '-D%s',
+ 'cc.flag-include' => '-I%s',
+ 'cc.flag-omp' => '',
+ 'cc.flag-output' => '-o%s',
+ 'cc.include-paths' => '',
+);
+
+sub new {
+ my ($class, $attrib_ref) = @_;
+ bless(
+ $class->SUPER::new(
+ {name => 'cc', prop_of => sub {return {%PROP_OF}}, %{$attrib_ref}},
+ ),
+ $class,
+ );
+}
+
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::System::Make::Build::Task::Compile::C
+
+=head1 SYNOPSIS
+
+ use FCM::System::Make::Build::Task::Compile::C;
+ my $task = FCM::System::Make::Build::Task::Compile::C->new(\%attrib);
+ $task->main($target);
+
+=head1 DESCRIPTION
+
+Wraps L<FCM::System::Make::Build::Task::Compile> to compile a C source into an
+object.
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/System/Make/Build/Task/Compile/CXX.pm b/lib/FCM/System/Make/Build/Task/Compile/CXX.pm
new file mode 100644
index 0000000..7f7e509
--- /dev/null
+++ b/lib/FCM/System/Make/Build/Task/Compile/CXX.pm
@@ -0,0 +1,70 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+# ------------------------------------------------------------------------------
+package FCM::System::Make::Build::Task::Compile::CXX;
+use base qw{FCM::System::Make::Build::Task::Compile};
+
+our %PROP_OF = (
+ 'cxx' => 'g++',
+ 'cxx.defs' => '',
+ 'cxx.flags' => '',
+ 'cxx.flag-compile' => '-c',
+ 'cxx.flag-define' => '-D%s',
+ 'cxx.flag-include' => '-I%s',
+ 'cxx.flag-omp' => '',
+ 'cxx.flag-output' => '-o%s',
+ 'cxx.include-paths' => '',
+);
+
+sub new {
+ my ($class, $attrib_ref) = @_;
+ bless(
+ $class->SUPER::new(
+ {name => 'cxx', prop_of => sub {return {%PROP_OF}}, %{$attrib_ref}},
+ ),
+ $class,
+ );
+}
+
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::System::Make::Build::Task::Compile::CXX
+
+=head1 SYNOPSIS
+
+ use FCM::System::Make::Build::Task::Compile::CXX;
+ my $task = FCM::System::Make::Build::Task::Compile::CXX->new(\%attrib);
+ $task->main($target);
+
+=head1 DESCRIPTION
+
+Wraps L<FCM::System::Make::Build::Task::Compile> to compile a C++ source into an
+object.
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/System/Make/Build/Task/Compile/Fortran.pm b/lib/FCM/System/Make/Build/Task/Compile/Fortran.pm
new file mode 100644
index 0000000..abee5d4
--- /dev/null
+++ b/lib/FCM/System/Make/Build/Task/Compile/Fortran.pm
@@ -0,0 +1,114 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+# ------------------------------------------------------------------------------
+package FCM::System::Make::Build::Task::Compile::Fortran;
+use base qw{FCM::System::Make::Build::Task::Compile};
+
+our %PROP_NO_PREPROCESS_OF = (
+ 'fc' => 'gfortran',
+ 'fc.flags' => '',
+ 'fc.flag-compile' => '-c',
+ 'fc.flag-include' => '-I%s',
+ 'fc.flag-module' => '',
+ 'fc.flag-omp' => '',
+ 'fc.flag-output' => '-o%s',
+ 'fc.include-paths' => '',
+);
+
+our %PROP_OF = (
+ %PROP_NO_PREPROCESS_OF,
+ 'fc.defs' => '',
+ 'fc.flag-define' => '-D%s',
+);
+
+sub new {
+ my ($class, $attrib_ref) = @_;
+ bless(
+ $class->SUPER::new(
+ {name => 'fc', prop_of => \&_prop_of, %{$attrib_ref}},
+ ),
+ $class,
+ );
+}
+
+sub _prop_of {
+ my ($attrib_ref, $target) = @_;
+ if (!defined($target)) {
+ return {%PROP_OF};
+ }
+ my $file_ext = $attrib_ref->{util}->file_ext($target->get_path_of_source());
+ (lc($file_ext) eq $file_ext) ? {%PROP_NO_PREPROCESS_OF} : {%PROP_OF};
+}
+
+# ------------------------------------------------------------------------------
+package FCM::System::Make::Build::Task::Compile::Fortran::Extra;
+use base qw{FCM::Class::CODE};
+
+use FCM::System::Exception;
+use File::Basename qw{basename};
+
+my $E = 'FCM::System::Exception';
+
+our %PROP_OF = %FCM::System::Make::Build::Task::Compile::Fortran::PROP_OF;
+
+__PACKAGE__->class(
+ {prop_of => {isa => '%', default => {%PROP_OF}}},
+ {action_of => {main => \&_main, prop_of => sub {\%PROP_OF}}},
+);
+
+sub _main {
+ my ($attrib_ref, $target) = @_;
+ my ($source, $dest) = (basename($target->get_key()), $target->get_path());
+ if (!-e $source) {
+ return;
+ }
+ rename($source, $dest) || return $E->throw($E->COPY, [$source, $dest], $!);
+ $target;
+}
+
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::System::Make::Build::Task::Compile::Fortran
+
+=head1 SYNOPSIS
+
+ use FCM::System::Make::Build::Task::Compile::Fortran;
+ my $task = FCM::System::Make::Build::Task::Compile::Fortran->new(\%attrib);
+ $task->main($target);
+
+=head1 DESCRIPTION
+
+Wraps L<FCM::System::Make::Build::Task::Compile> to compile a Fortran source into
+an object.
+
+The module also provides the
+FCM::System::Make::Build::Task::Compile::Fortran::Extra class to deal with the
+module definition file generated by the compiler in the working directory.
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/System/Make/Build/Task/ExtractInterface.pm b/lib/FCM/System/Make/Build/Task/ExtractInterface.pm
new file mode 100644
index 0000000..82e602d
--- /dev/null
+++ b/lib/FCM/System/Make/Build/Task/ExtractInterface.pm
@@ -0,0 +1,536 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+
+# ------------------------------------------------------------------------------
+package FCM::System::Make::Build::Task::ExtractInterface;
+use base qw{FCM::Class::CODE};
+
+use FCM::System::Exception;
+use Text::Balanced qw{extract_bracketed extract_delimited};
+use Text::ParseWords qw{shellwords};
+
+# Alias
+my $E = 'FCM::System::Exception';
+
+# Regular expressions
+my $RE_ATTR = qr{
+ allocatable|dimension|external|intent|optional|parameter|pointer|save|target
+}imsx;
+my $RE_FILE = qr{[\w\-+.]+}imsx;
+my $RE_NAME = qr{[A-Za-z]\w*}imsx;
+my $RE_SPEC = qr{
+ character|class|complex|double\s*complex|double\s*precision|integer|
+ logical|procedure|real|type
+}imsx;
+my $RE_UNIT_BASE = qr{block\s*data|module|program|submodule}imsx;
+my $RE_UNIT_CALL = qr{function|subroutine}imsx;
+my $RE_UNIT = qr{$RE_UNIT_BASE|$RE_UNIT_CALL}msx;
+my %RE = (
+ COMMENT => qr{\A\s*(?:!|\z)}msx,
+ COMMENT_END => qr{\A([^'"]*?)\s*!.*\z}msx,
+ CONT => qr{\A(.*)&\s*\z}msx,
+ CONT_LEAD => qr{\A(\s*&)(.*)\z}msx,
+ INCLUDE => qr{(?:\#|\s*)include\s*}imsx,
+ NAME_COMP => qr{\b($RE_NAME)(?:\s*\%\s*$RE_NAME)*\b}msx,
+ NAME_LEAD => qr{\A\s*$RE_NAME\s*}msx,
+ NAME_LIST => qr{\A(?:.*?)\s*,\s*($RE_NAME)\b(.*)\z}msx,
+ QUOTE => qr{\A[^'"]*(['"])}msx,
+ TYPE_ATTR => qr{\A\s*($RE_ATTR)\b}msx,
+ TYPE_SPEC => qr{\A\s*($RE_SPEC)\b}msx,
+ UNIT_ATTR => qr{\A\s*(?:(?:(?:impure\s+)?elemental|recursive|pure)\s+)+(.*)\z}imsx,
+ UNIT_BASE => qr{\A\s*($RE_UNIT_BASE)\s+($RE_NAME)\s*\z}imsx,
+ UNIT_CALL => qr{\A\s*($RE_UNIT_CALL)\s+($RE_NAME)\b}imsx,
+ UNIT_END => qr{\A\s*(end)(?:\s+($RE_NAME)(?:\s+($RE_NAME))?)?\s*\z}imsx,
+ UNIT_SPEC => qr{\A\s*$RE_SPEC\b(.*)\z}imsx,
+);
+
+# Keywords in type declaration statements
+my %TYPE_DECL_KEYWORD_SET = map { ($_, 1) } qw{
+ allocatable
+ asynchronous
+ contiguous
+ dimension
+ external
+ in
+ inout
+ intent
+ kind
+ len
+ optional
+ out
+ parameter
+ pointer
+ save
+ target
+ value
+ volatile
+};
+
+__PACKAGE__->class({util => '&'}, {action_of => {main => \&_main}});
+
+sub _main {
+ my ($attrib_ref, $target) = @_;
+ my $handle
+ = $attrib_ref->{util}->file_load_handle($target->get_path_of_source());
+ eval {
+ $attrib_ref->{util}->file_save(
+ $target->get_path(),
+ [ map { s{\s+}{ }gmsx; s{\s+\z}{\n}msx; $_ }
+ map { @{$_->{lines}} }
+ @{_reduce_to_interface(_extract_statements($handle))}
+ ],
+ );
+ };
+ if ($@) {
+ my $e = $@;
+ if ($E->caught($e) && $e->get_code() eq $E->BUILD_SOURCE_SYN) {
+ unshift(@{$e->get_ctx()}, $target->get_path_of_source());
+ }
+ die($e);
+ }
+ close($handle);
+ $target;
+}
+
+# Reads $handle for the next Fortran statement, handling continuations.
+sub _extract_statements {
+ my ($handle) = @_;
+ my $context = {signature_token_set_of => {}, statements => []};
+ my $state = {
+ in_contains => undef,
+ in_interface => undef,
+ in_quote => undef,
+ in_type => undef,
+ stack => [],
+ };
+ my $NEW_STATEMENT = sub {
+ { name => q{},
+ lines => [],
+ line_number => 0,
+ symbol => q{},
+ type => q{},
+ value => q{},
+ };
+ };
+ my $statement;
+LINE:
+ while (my $line = readline($handle)) {
+ if (!defined($statement)) {
+ $statement = $NEW_STATEMENT->();
+ }
+ my $value = $line;
+ chomp($value);
+ if (!$statement->{line_number} && index($value, '#') == 0) {
+ $statement->{line_number} = $.;
+ $statement->{name} = 'cpp';
+ }
+ if ($statement->{name} eq 'cpp') {
+ push(@{$statement->{lines}}, $line);
+ $statement->{value} .= $value;
+ if (rindex($value, '\\') != length($value) - 1) {
+ #push(@{$context->{statements}}, $statement);
+ $statement = undef;
+ }
+ next LINE;
+ }
+ if ($value =~ $RE{COMMENT}) {
+ next LINE;
+ }
+ if (!$statement->{line_number}) {
+ $statement->{line_number} = $.;
+ }
+ my ($cont_head, $cont_tail);
+ if ($statement->{line_number} != $.) { # is a continuation
+ ($cont_head, $cont_tail) = $value =~ $RE{CONT_LEAD};
+ if ($cont_head) {
+ $value = $cont_tail;
+ }
+ }
+ my ($head, $tail) = (q{}, $value);
+ if ($state->{in_quote} && index($value, $state->{in_quote}) >= 0) {
+ my $index = index($value, $state->{in_quote});
+ $head = substr($value, 0, $index + 1);
+ $tail
+ = length($value) > $index + 1
+ ? substr($value, $index + 2)
+ : q{};
+ $state->{in_quote} = undef;
+ }
+ if (!$state->{in_quote}) {
+ while ($tail) {
+ if (index($tail, q{!}) >= 0) {
+ if (!($tail =~ s/$RE{COMMENT_END}/$1/)) {
+ ($head, $tail, $state->{in_quote})
+ = _extract_statement_quote($head, $tail);
+ }
+ }
+ else {
+ while (index($tail, q{'}) > 0
+ || index($tail, q{"}) > 0)
+ {
+ ($head, $tail, $state->{in_quote})
+ = _extract_statement_quote($head, $tail);
+ }
+ $head .= $tail;
+ $tail = q{};
+ }
+ }
+ }
+ $cont_head ||= q{};
+ push(@{$statement->{lines}}, $cont_head . $head . $tail . "\n");
+ $statement->{value} .= $head . $tail;
+ if (!($statement->{value} =~ s/$RE{CONT}/$1/)) {
+ $statement->{value} =~ s{\s+\z}{}msx;
+ if (_process($statement, $context, $state)) {
+ push(@{$context->{statements}}, $statement);
+ }
+ $statement = undef;
+ }
+ }
+ return $context;
+}
+
+# Helper, removes a quoted string from $tail.
+sub _extract_statement_quote {
+ my ($head, $tail) = @_;
+ my ($extracted, $remainder, $prefix)
+ = extract_delimited($tail, q{'"}, qr{[^'"]*}msx, q{});
+ if ($extracted) {
+ return ($head . $prefix . $extracted, $remainder);
+ }
+ else {
+ my ($quote) = $tail =~ $RE{QUOTE};
+ return ($head . $tail, q{}, $quote);
+ }
+}
+
+# Read a statement and put attributes into $statement
+sub _process {
+ my ($statement, $context, $state) = @_;
+ my $name;
+
+ # End Interface
+ if ($state->{in_interface}) {
+ if ($statement->{value} =~ qr{\A\s*end\s*interface\b}imsx) {
+ $state->{in_interface} = 0;
+ }
+ return;
+ }
+
+ # End Program Unit
+ if (@{$state->{stack}} && $statement->{value} =~ qr{\A\s*end\b}imsx) {
+ my ($end, $type, $symbol) = lc($statement->{value}) =~ $RE{UNIT_END};
+ if (!$end) {
+ return;
+ }
+ my ($top_type, $top_symbol) = @{$state->{stack}->[-1]};
+ if (!$type
+ || $top_type eq $type && (!$symbol || $top_symbol eq $symbol))
+ {
+ pop(@{$state->{stack}});
+ if ($state->{in_contains} && !@{$state->{stack}}) {
+ $state->{in_contains} = 0;
+ }
+ if (!$state->{in_contains}) {
+ $statement->{name} = $top_type;
+ $statement->{symbol} = $top_symbol;
+ $statement->{type} = 'end';
+ return $statement;
+ }
+ }
+ return;
+ }
+
+ # Interface/Contains
+ if ($statement->{value} =~ qr{\A\s*contains\b}imsx) {
+ $state->{'in_contains'} = 1;
+ return;
+ }
+ if ($statement->{value} =~ qr{\A\s*(?:abstract\s+)?interface\b}imsx) {
+ $state->{'in_interface'} = 1;
+ return;
+ }
+
+ # Program Unit
+ my ($type, $symbol, @tokens) = _process_prog_unit($statement->{value});
+ if ($type) {
+ push(@{$state->{stack}}, [$type, $symbol]);
+ if ($state->{in_contains}) {
+ return;
+ }
+ $statement->{name} = lc($type);
+ $statement->{type} = 'signature';
+ $statement->{symbol} = lc($symbol);
+ $context->{signature_token_set_of}{$symbol}
+ = {map { (lc($_) => 1) } @tokens};
+ return $statement;
+ }
+ if ($state->{in_contains}) {
+ return;
+ }
+
+ # Use
+ if ($statement->{value} =~ qr{\A\s*(use)\b}imsx) {
+ $statement->{name} = 'use';
+ $statement->{type} = 'use';
+ return $statement;
+ }
+
+ # Type Declarations
+ ($name) = $statement->{value} =~ $RE{TYPE_SPEC};
+ if ($name) {
+ $name =~ s{\s}{}gmsx;
+ $statement->{name} = lc($name);
+ $statement->{type} = 'type';
+ return $statement;
+ }
+
+ # Attribute Statements
+ ($name) = $statement->{value} =~ $RE{TYPE_ATTR};
+ if ($name) {
+ $statement->{name} = $name;
+ $statement->{type} = 'attr';
+ }
+}
+
+# Parse a statement for program unit header. Returns a list containing the type,
+# the symbol and the signature tokens of the program unit.
+sub _process_prog_unit {
+ my ($string) = @_;
+ my ($type, $symbol, @args) = (q{}, q{});
+ ($type, $symbol) = $string =~ $RE{UNIT_BASE};
+ if ($type) {
+ $type = lc($type);
+ $type =~ s{\s*}{}gmsx;
+ return ($type, $symbol);
+ }
+ $string =~ s/$RE{UNIT_ATTR}/$1/;
+ my ($match) = $string =~ $RE{UNIT_SPEC};
+ if ($match) {
+ $string = $match;
+ extract_bracketed($string);
+ }
+ ($type, $symbol) = lc($string) =~ $RE{UNIT_CALL};
+ if (!$type) {
+ return;
+ }
+ my $extracted = extract_bracketed($string, q{()}, qr{[^(]*}msx);
+
+ # Get arguments/keywords from SUBROUTINE/FUNCTION
+ if ($extracted) {
+ $extracted =~ s{\s}{}gmsx;
+ @args = split(q{,}, substr($extracted, 1, length($extracted) - 2));
+ if ($type eq 'function') {
+ my $result = extract_bracketed($string, q{()}, qr{[^(]*}msx);
+ if ($result) {
+ $result =~ s{\A\(\s*(.*?)\s*\)\z}{$1}msx; # remove braces
+ push(@args, $result);
+ }
+ else {
+ push(@args, $symbol);
+ }
+ }
+ }
+ return (lc($type), lc($symbol), map { lc($_) } @args);
+}
+
+# Reduces the list of statements to contain only the interface block.
+sub _reduce_to_interface {
+ my ($context) = @_;
+ my (%token_set, @interface_statements);
+STATEMENT:
+ for my $statement (reverse(@{$context->{statements}})) {
+ if ($statement->{type} eq 'end'
+ && grep { $_ eq $statement->{name} } qw{subroutine function})
+ {
+ push(@interface_statements, $statement);
+ %token_set
+ = %{$context->{signature_token_set_of}{$statement->{symbol}}};
+ next STATEMENT;
+ }
+ if ($statement->{type} eq 'signature'
+ && grep { $_ eq $statement->{name} } qw{subroutine function})
+ {
+ push(@interface_statements, $statement);
+ %token_set = ();
+ next STATEMENT;
+ }
+ if ($statement->{type} eq 'use') {
+ my ($head, $tail)
+ = split(qr{,\s*only\s*:\s*}msx, lc($statement->{value}), 2);
+ if ($tail) {
+ my @imports = map { [split(qr{\s*=>\s*}msx, $_, 2)] }
+ split(qr{\s*,\s*}msx, $tail);
+ my @useful_imports
+ = grep { exists($token_set{$_->[0]}) } @imports;
+ if (!@useful_imports) {
+ next STATEMENT;
+ }
+ if (@imports != @useful_imports) {
+ my @token_strings
+ = map { $_->[0] . ($_->[1] ? ' => ' . $_->[1] : q{}) }
+ @useful_imports;
+ my ($last, @rest) = reverse(@token_strings);
+ my @token_lines
+ = (reverse(map { $_ . q{,&} } @rest), $last);
+ push(
+ @interface_statements,
+ { lines => [
+ sprintf("%s, only:&\n", $head),
+ (map { sprintf(" & %s\n", $_) } @token_lines),
+ ]
+ },
+ );
+ next STATEMENT;
+ }
+ }
+ push(@interface_statements, $statement);
+ next STATEMENT;
+ }
+ if ($statement->{type} eq 'attr') {
+ my ($spec, @tokens) = ($statement->{value} =~ /$RE{NAME_COMP}/g);
+ if (grep { exists($token_set{$_}) } @tokens) {
+ for my $token (@tokens) {
+ $token_set{$token} = 1;
+ }
+ push(@interface_statements, $statement);
+ next STATEMENT;
+ }
+ }
+ if ($statement->{type} eq 'type') {
+ my ($variable_string, $spec_string)
+ = reverse(split('::', lc($statement->{value}), 2));
+ if ($spec_string) {
+ $spec_string =~ s{$RE{NAME_LEAD}}{}msx;
+ }
+ else {
+ $variable_string =~ s{$RE{NAME_LEAD}}{}msx;
+ $spec_string = extract_bracketed($variable_string, '()',
+ qr{[\s\*]*}msx);
+ }
+ my $tail = q{,} . lc($variable_string);
+ my @tokens;
+ while ($tail) {
+ if ($tail =~ qr{\A\s*['"]}msx) {
+ my $old_tail = $tail;
+ extract_delimited($tail, q{'"}, qr{\A[^'"]*}msx, q{});
+ if ($old_tail eq $tail) {
+ return $E->throw(
+ $E->BUILD_SOURCE_SYN, [$statement->{line_number}]);
+ }
+ }
+ elsif ($tail =~ qr{\A\s*\(}msx) {
+ my $old_tail = $tail;
+ extract_bracketed($tail, '()', qr{\A[^(]*}msx);
+ if ($old_tail eq $tail) {
+ return $E->throw(
+ $E->BUILD_SOURCE_SYN, [$statement->{line_number}]);
+ }
+ }
+ else {
+ my $token;
+ ($token, $tail) = $tail =~ $RE{NAME_LIST};
+ if ($token && $token_set{$token}) {
+ @tokens = ($variable_string =~ /$RE{NAME_COMP}/g);
+ $tail = q{};
+ }
+ }
+ }
+ if (@tokens && $spec_string) {
+ my @spec_tokens = (lc($spec_string) =~ /$RE{NAME_COMP}/g);
+ push(
+ @tokens,
+ ( grep { !exists($TYPE_DECL_KEYWORD_SET{$_}) }
+ @spec_tokens
+ ),
+ );
+ }
+ if (grep { exists($token_set{$_}) } @tokens) {
+ for my $token (@tokens) {
+ $token_set{$token} = 1;
+ }
+ push(@interface_statements, $statement);
+ next STATEMENT;
+ }
+ }
+ }
+ if (!@interface_statements) {
+ return [];
+ }
+ [ {lines => ["interface\n"]},
+ reverse(@interface_statements),
+ {lines => ["end interface\n"]},
+ ];
+}
+
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::System::Make::Build::Task::ExtractInterface
+
+=head1 SYNOPSIS
+
+ use FCM::System::Make::Build::Task::ExtractInterface;
+ my $task = FCM::System::Make::Build::Task::ExtractInterface->new(\%attrib);
+ $task->main($target);
+
+=head1 DESCRIPTION
+
+Extracts the calling interfaces of top level functions and subroutines in the
+Fortran source file of the target.
+
+=head1 METHODS
+
+=over 4
+
+=item $class->new(\%attrib)
+
+Creates and returns a new instance. %attrib should contain:
+
+=over 4
+
+=item {util}
+
+An instance of L<FCM::Util|FCM::Util>.
+
+=back
+
+=item $instance->main($target)
+
+Extracts the calling interfaces of top level functions and subroutines in the
+Fortran source file of the target, and writes the results to the path of the
+target.
+
+=back
+
+=head1 ACKNOWLEDGEMENT
+
+This module is inspired by the logic developed by the European Centre
+for Medium-Range Weather Forecasts (ECMWF).
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/System/Make/Build/Task/Install.pm b/lib/FCM/System/Make/Build/Task/Install.pm
new file mode 100644
index 0000000..45bd1b0
--- /dev/null
+++ b/lib/FCM/System/Make/Build/Task/Install.pm
@@ -0,0 +1,90 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+# ------------------------------------------------------------------------------
+package FCM::System::Make::Build::Task::Install;
+use base qw{FCM::Class::CODE};
+
+use FCM::System::Exception;
+use File::Copy qw{copy};
+
+my $E = 'FCM::System::Exception';
+
+__PACKAGE__->class({util => '&'}, {action_of => {main => \&_main}});
+
+sub _main {
+ my ($attrib_ref, $target) = @_;
+ my ($source, $dest) = ($target->get_path_of_source(), $target->get_path());
+ if ($source) {
+ copy($source, $dest) || return $E->throw($E->COPY, [$source, $dest], $!);
+ chmod((stat($source))[2] & oct(7777), $dest)
+ || return $E->throw($E->DEST_CREATE, $dest, $!);
+ }
+ else {
+ $attrib_ref->{util}->file_save($dest, q{});
+ }
+ $target;
+}
+
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::System::Make::Build::Task::Install
+
+=head1 SYNOPSIS
+
+ use FCM::System::Make::Build::Task::Install;
+ my $build_task = FCM::System::Make::Build::Task::Install->new(\%attrib);
+ $build_task->main( $target);
+
+=head1 DESCRIPTION
+
+Copies the source of the target to its path.
+
+=head1 METHODS
+
+=over 4
+
+=item $class->new(\%attrib)
+
+Creates and returns a new instance. %attrib should contain:
+
+=over 4
+
+=item {util}
+
+An instance of L<FCM::Util|FCM::Util>.
+
+=back
+
+=item $instance->main($target)
+
+Copies the source of the target to its path.
+
+=back
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/System/Make/Build/Task/Link.pm b/lib/FCM/System/Make/Build/Task/Link.pm
new file mode 100644
index 0000000..5ab63da
--- /dev/null
+++ b/lib/FCM/System/Make/Build/Task/Link.pm
@@ -0,0 +1,195 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+# ------------------------------------------------------------------------------
+package FCM::System::Make::Build::Task::Link;
+use base qw{FCM::Class::CODE};
+
+use FCM::Context::Event;
+use FCM::System::Exception;
+use FCM::System::Make::Build::Task::Archive;
+use FCM::System::Make::Build::Task::Share qw{_props_to_opts};
+use File::Basename qw{basename};
+use File::Path qw{mkpath rmtree};
+use File::Spec::Functions qw{abs2rel catfile};
+use File::Temp qw{tempdir};
+use List::Util qw{first};
+use Text::ParseWords qw{shellwords};
+
+my $E = 'FCM::System::Exception';
+
+our %PROP_OF = (
+ %FCM::System::Make::Build::Task::Archive::PROP_OF,
+ 'ld' => '',
+ 'keep-lib-o' => '',
+);
+
+__PACKAGE__->class(
+ {name => '$', prop_of => '%', util => '&'},
+ {action_of => {main => \&_main, prop_of => sub {$_[0]->{prop_of}}}},
+);
+
+sub _main {
+ my ($attrib_ref, $target) = @_;
+ my $NAME = $attrib_ref->{name};
+ my $P = sub {$target->get_prop_of($_[0])};
+ # Selects the correct dependent objects
+ my @paths = @{$target->get_info_of('paths')};
+ my %dep_keys_of = %{$target->get_info_of('deps')};
+ my %paths_of = (o => [], 'o.special' => []);
+ my $abs2rel_func
+ = sub {index($_[0], $paths[0]) == 0 ? abs2rel($_[0], $paths[0]) : $_[0]};
+ while (my ($type, $key_list_ref) = each(%dep_keys_of)) {
+ for my $key (@{$key_list_ref}) {
+ my $path = first {-e} map {catfile($_, 'o', $key)} @paths;
+ if ($path) {
+ push(@{$paths_of{$type}}, $abs2rel_func->($path));
+ }
+ }
+ }
+ my $path_of_main_o = shift(@{$paths_of{o}});
+ my $keep_lib_o = $P->('keep-lib-o');
+ my $lib_o_dir;
+ if ($keep_lib_o) {
+ $lib_o_dir = $target->CT_LIB;
+ mkpath($lib_o_dir);
+ }
+ else {
+ $lib_o_dir = tempdir(CLEANUP => 1);
+ }
+ my ($extension, $root)
+ = $attrib_ref->{util}->file_ext(basename($target->get_key()));
+ my $lib_o = catfile($lib_o_dir, "lib$root.a");
+ my %opt_of = (
+ o => $P->($NAME . '.flag-output'),
+ L => $P->($NAME . '.flag-lib-path'),
+ l => $P->($NAME . '.flag-lib'),
+ );
+ for my $command_list_ref (
+ # Archive (when linking multiple objects)
+ ( @{$paths_of{o}}
+ ? [ shellwords($P->('ar')),
+ shellwords($P->('ar.flags')),
+ $lib_o,
+ @{$paths_of{o}},
+ ]
+ : ()
+ ),
+ # Link
+ [ ($P->('ld') ? shellwords($P->('ld')) : shellwords($P->($NAME))),
+ _props_to_opts($opt_of{o}, $abs2rel_func->($target->get_path())),
+ $path_of_main_o,
+ @{$paths_of{'o.special'}},
+ ( @{$paths_of{o}}
+ ? ( _props_to_opts($opt_of{L}, $lib_o_dir),
+ _props_to_opts($opt_of{l}, $root),
+ )
+ : ()
+ ),
+ _props_to_opts($opt_of{L}, shellwords($P->($NAME . '.lib-paths'))),
+ _props_to_opts($opt_of{l}, shellwords($P->($NAME . '.libs'))),
+ shellwords($P->($NAME . '.flag-omp')),
+ shellwords($P->($NAME . '.flags-ld')),
+ ],
+ ) {
+ my %value_of = %{$attrib_ref->{util}->shell_simple($command_list_ref)};
+ if ($value_of{rc}) {
+ return $E->throw(
+ $E->SHELL,
+ {command_list => $command_list_ref, %value_of},
+ $value_of{e},
+ );
+ }
+ $attrib_ref->{util}->event(
+ FCM::Context::Event->MAKE_BUILD_SHELL_OUT, @value_of{qw{o e}},
+ );
+ }
+ if (!$keep_lib_o) {
+ unlink($lib_o);
+ rmtree($lib_o_dir);
+ }
+ $target;
+}
+
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::System::Make::Build::Task::Link
+
+=head1 SYNOPSIS
+
+ use FCM::System::Make::Build::Task::Link;
+ my $build_task = FCM::System::Make::Build::Task::Link->new(\%attrib);
+ $build_task->main($target);
+
+=head1 DESCRIPTION
+
+Invokes the linker to create the target executable.
+
+=head1 METHODS
+
+=over 4
+
+=item $class->new(\%attrib)
+
+Creates and returns a new instance. %attrib should contain:
+
+=over 4
+
+=item {name}
+
+The property name of the linker.
+
+=item {prop_of}
+
+A HASH to map the property names (used by this task) to their default values.
+
+=item {util}
+
+An instance of L<FCM::Util|FCM::Util>.
+
+=back
+
+=item $instance->main($target)
+
+Invokes the linker to create the $target executable. It uses the
+$target->get_info_of('deps')->{o} ARRAY and
+$target->get_info_of('deps')->{"o.special"} ARRAY as dependencies. The first
+type "o" dependency item is expected to be the object file containing the main
+program. All other "o" dependency items are placed in a temporary archive
+before invoking the linker command. The main object and "o.special" dependency
+items are entered into the command line of the linker to produce the
+executable.
+
+=item $instance->prop_of()
+
+Returns the HASH that maps the property names (used by this task) to their
+default values.
+
+=back
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/System/Make/Build/Task/Link/C.pm b/lib/FCM/System/Make/Build/Task/Link/C.pm
new file mode 100644
index 0000000..99ebef3
--- /dev/null
+++ b/lib/FCM/System/Make/Build/Task/Link/C.pm
@@ -0,0 +1,72 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+# ------------------------------------------------------------------------------
+package FCM::System::Make::Build::Task::Link::C;
+use base qw{FCM::System::Make::Build::Task::Link};
+
+use FCM::System::Make::Build::Task::Compile::C;
+
+our %PROP_OF = (
+ %FCM::System::Make::Build::Task::Link::PROP_OF,
+ ( map {$_ => $FCM::System::Make::Build::Task::Compile::C::PROP_OF{$_}}
+ qw{cc cc.flag-omp cc.flag-output}
+ ),
+ 'cc.flags-ld' => '',
+ 'cc.flag-lib' => '-l%s',
+ 'cc.flag-lib-path' => '-L%s',
+ 'cc.libs' => '',
+ 'cc.lib-paths' => '',
+);
+
+sub new {
+ my ($class, $attrib_ref) = @_;
+ bless(
+ $class->SUPER::new(
+ {name => 'cc', prop_of => {%PROP_OF}, %{$attrib_ref}},
+ ),
+ $class,
+ );
+}
+
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::System::Make::Build::Task::Link::C
+
+=head1 SYNOPSIS
+
+ use FCM::System::Make::Build::Task::Link::C;
+ my $task = FCM::System::Make::Build::Task::Link::C->new(\%attrib);
+ $task->main($target);
+
+=head1 DESCRIPTION
+
+Wraps L<FCM::System::Make::Build::Task::Link> to link a C object into an
+executable.
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/System/Make/Build/Task/Link/CXX.pm b/lib/FCM/System/Make/Build/Task/Link/CXX.pm
new file mode 100644
index 0000000..1c5848c
--- /dev/null
+++ b/lib/FCM/System/Make/Build/Task/Link/CXX.pm
@@ -0,0 +1,72 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+# ------------------------------------------------------------------------------
+package FCM::System::Make::Build::Task::Link::CXX;
+use base qw{FCM::System::Make::Build::Task::Link};
+
+use FCM::System::Make::Build::Task::Compile::CXX;
+
+our %PROP_OF = (
+ %FCM::System::Make::Build::Task::Link::PROP_OF,
+ ( map {$_ => $FCM::System::Make::Build::Task::Compile::CXX::PROP_OF{$_}}
+ qw{cxx cxx.flag-omp cxx.flag-output}
+ ),
+ 'cxx.flags-ld' => '',
+ 'cxx.flag-lib' => '-l%s',
+ 'cxx.flag-lib-path' => '-L%s',
+ 'cxx.libs' => '',
+ 'cxx.lib-paths' => '',
+);
+
+sub new {
+ my ($class, $attrib_ref) = @_;
+ bless(
+ $class->SUPER::new(
+ {name => 'cxx', prop_of => {%PROP_OF}, %{$attrib_ref}},
+ ),
+ $class,
+ );
+}
+
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::System::Make::Build::Task::Link::CXX
+
+=head1 SYNOPSIS
+
+ use FCM::System::Make::Build::Task::Link::CXX;
+ my $task = FCM::System::Make::Build::Task::Link::CXX->new(\%attrib);
+ $task->main($target);
+
+=head1 DESCRIPTION
+
+Wraps L<FCM::System::Make::Build::Task::Link> to link a C++ object into an
+executable.
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/System/Make/Build/Task/Link/Fortran.pm b/lib/FCM/System/Make/Build/Task/Link/Fortran.pm
new file mode 100644
index 0000000..94e8cb4
--- /dev/null
+++ b/lib/FCM/System/Make/Build/Task/Link/Fortran.pm
@@ -0,0 +1,72 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+# ------------------------------------------------------------------------------
+package FCM::System::Make::Build::Task::Link::Fortran;
+use base qw{FCM::System::Make::Build::Task::Link};
+
+use FCM::System::Make::Build::Task::Compile::Fortran;
+
+our %PROP_OF = (
+ %FCM::System::Make::Build::Task::Link::PROP_OF,
+ ( map {$_ => $FCM::System::Make::Build::Task::Compile::Fortran::PROP_OF{$_}}
+ qw{fc fc.flag-omp fc.flag-output}
+ ),
+ 'fc.flags-ld' => '',
+ 'fc.flag-lib' => '-l%s',
+ 'fc.flag-lib-path' => '-L%s',
+ 'fc.libs' => '',
+ 'fc.lib-paths' => '',
+);
+
+sub new {
+ my ($class, $attrib_ref) = @_;
+ bless(
+ $class->SUPER::new(
+ {name => 'fc', prop_of => {%PROP_OF}, %{$attrib_ref}},
+ ),
+ $class,
+ );
+}
+
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::System::Make::Build::Task::Link::Fortran
+
+=head1 SYNOPSIS
+
+ use FCM::System::Make::Build::Task::Link::Fortran;
+ my $task = FCM::System::Make::Build::Task::Link::Fortran->new(\%attrib);
+ $task->main($target);
+
+=head1 DESCRIPTION
+
+Wraps L<FCM::System::Make::Build::Task::Link> to link a Fortran object into an
+executable.
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/System/Make/Build/Task/Preprocess.pm b/lib/FCM/System/Make/Build/Task/Preprocess.pm
new file mode 100644
index 0000000..2147d7b
--- /dev/null
+++ b/lib/FCM/System/Make/Build/Task/Preprocess.pm
@@ -0,0 +1,131 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+# ------------------------------------------------------------------------------
+package FCM::System::Make::Build::Task::Preprocess;
+use base qw{FCM::Class::CODE};
+
+use FCM::Context::Event;
+use FCM::System::Exception;
+use FCM::System::Make::Build::Task::Share qw{_props_to_opts};
+use File::Spec::Functions qw{catfile};
+use Text::ParseWords qw{shellwords};
+
+use FCM::System::Exception;
+
+my $E = 'FCM::System::Exception';
+
+__PACKAGE__->class(
+ {name => '$', prop_of => '%', util => '&'},
+ {action_of => {main => \&_main, prop_of => sub {$_[0]->{prop_of}}}},
+);
+
+sub _main {
+ my ($attrib_ref, $target) = @_;
+ my $NAME = $attrib_ref->{name};
+ my $P = sub {$target->get_prop_of($_[0])};
+ my @paths = @{$target->get_info_of('paths')};
+ my @include_paths
+ = map {catfile(($_ eq $paths[0] ? q{.} : $_), 'include')} @paths;
+ my %opt_of = (
+ D => $P->($NAME . '.flag-define'),
+ I => $P->($NAME . '.flag-include'),
+ );
+ my @command = (
+ shellwords($P->($NAME)),
+ shellwords($P->($NAME . '.flags')),
+ _props_to_opts($opt_of{D}, shellwords($P->($NAME . '.defs'))),
+ _props_to_opts($opt_of{I}, @include_paths),
+ _props_to_opts($opt_of{I}, shellwords($P->($NAME . '.include-paths'))),
+ $target->get_path_of_source(),
+ );
+ my %value_of = %{$attrib_ref->{util}->shell_simple(\@command)};
+ if ($value_of{rc}) {
+ return $E->throw(
+ $E->SHELL, {command_list => \@command, %value_of}, $value_of{e},
+ );
+ }
+ $attrib_ref->{util}->event(
+ FCM::Context::Event->MAKE_BUILD_SHELL_OUT, undef, $value_of{e},
+ );
+ $value_of{o} ||= q{};
+ $attrib_ref->{util}->file_save($target->get_path(), $value_of{o});
+ $target;
+}
+
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::System::Make::Build::Task::Preprocess
+
+=head1 SYNOPSIS
+
+ use FCM::System::Make::Build::Task::Preprocess;
+ my $build_task = FCM::System::Make::Build::Task::Preprocess->new(\%attrib);
+ $build_task->main($target);
+
+=head1 DESCRIPTION
+
+Invokes the preprocessor on the target source to generate the target.
+
+=head1 METHODS
+
+=over 4
+
+=item $class->new(\%attrib)
+
+Creates and returns a new instance. %attrib should contain:
+
+=over 4
+
+=item {name}
+
+The property name of the preprocessor.
+
+=item {prop_of}
+
+A HASH to map the property names and their default values.
+
+=item {util}
+
+An instance of L<FCM::Util|FCM::Util>.
+
+=back
+
+=item $instance->main($target)
+
+Invokes the preprocessor shell command on the target source to generate the
+target.
+
+=item $instance->prop_of()
+
+Returns the HASH that maps the property names (used by this task) to their
+default values.
+
+=back
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/System/Make/Build/Task/Preprocess/C.pm b/lib/FCM/System/Make/Build/Task/Preprocess/C.pm
new file mode 100644
index 0000000..75961fb
--- /dev/null
+++ b/lib/FCM/System/Make/Build/Task/Preprocess/C.pm
@@ -0,0 +1,66 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+# ------------------------------------------------------------------------------
+package FCM::System::Make::Build::Task::Preprocess::C;
+use base qw{FCM::System::Make::Build::Task::Preprocess};
+
+our %PROP_OF = (
+ 'cpp' => 'cpp',
+ 'cpp.defs' => '',
+ 'cpp.flags' => '',
+ 'cpp.flag-define' => '-D%s',
+ 'cpp.flag-include' => '-I%s',
+ 'cpp.include-paths' => '',
+);
+
+sub new {
+ my ($class, $attrib_ref) = @_;
+ bless(
+ $class->SUPER::new(
+ {name => 'cpp', prop_of => {%PROP_OF}, %{$attrib_ref}},
+ ),
+ $class,
+ );
+}
+
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::System::Make::Build::Task::Preprocess::C
+
+=head1 SYNOPSIS
+
+ use FCM::System::Make::Build::Task::Preprocess::C;
+ my $task = FCM::System::Make::Build::Task::Preprocess::C->new(\%attrib);
+ $task->main($target);
+
+=head1 DESCRIPTION
+
+Wraps L<FCM::System::Make::Build::Task::Preprocess> to preprocess a C source.
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/System/Make/Build/Task/Preprocess/Fortran.pm b/lib/FCM/System/Make/Build/Task/Preprocess/Fortran.pm
new file mode 100644
index 0000000..f093f43
--- /dev/null
+++ b/lib/FCM/System/Make/Build/Task/Preprocess/Fortran.pm
@@ -0,0 +1,67 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+# ------------------------------------------------------------------------------
+package FCM::System::Make::Build::Task::Preprocess::Fortran;
+use base qw{FCM::System::Make::Build::Task::Preprocess};
+
+our %PROP_OF = (
+ 'fpp' => 'cpp',
+ 'fpp.defs' => '',
+ 'fpp.flags' => '-P -traditional',
+ 'fpp.flag-define' => '-D%s',
+ 'fpp.flag-include' => '-I%s',
+ 'fpp.include-paths' => '',
+);
+
+sub new {
+ my ($class, $attrib_ref) = @_;
+ bless(
+ $class->SUPER::new(
+ {name => 'fpp', prop_of => {%PROP_OF}, %{$attrib_ref}},
+ ),
+ $class,
+ );
+}
+
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::System::Make::Build::Task::Preprocess::Fortran
+
+=head1 SYNOPSIS
+
+ use FCM::System::Make::Build::Task::Preprocess::Fortran;
+ my $task = FCM::System::Make::Build::Task::Preprocess::Fortran->new(\%attrib);
+ $task->main($target);
+
+=head1 DESCRIPTION
+
+Wraps L<FCM::System::Make::Build::Task::Preprocess> to preprocess a Fortran
+source.
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/System/Make/Build/Task/Share.pm b/lib/FCM/System/Make/Build/Task/Share.pm
new file mode 100644
index 0000000..4625b37
--- /dev/null
+++ b/lib/FCM/System/Make/Build/Task/Share.pm
@@ -0,0 +1,94 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+
+# ------------------------------------------------------------------------------
+package FCM::System::Make::Build::Task::Share;
+use base qw{Exporter};
+
+use Text::ParseWords qw{shellwords};
+
+our @EXPORT = qw{_props_to_opts};
+
+sub _props_to_opts {
+ # $opt_value should be an sprintf format with one %s.
+ my ($opt_value, @props) = @_;
+ if (!$opt_value) {
+ return;
+ }
+ my @opt_values = shellwords($opt_value);
+ my $index = -1;
+ I:
+ for my $i (0 .. $#opt_values) {
+ if (index($opt_values[$i], '%s') >= 0) {
+ $index = $i;
+ last I;
+ }
+ }
+ if ($index == -1) {
+ return (@opt_values, @props);
+ }
+ my @return;
+ for my $prop (@props) {
+ push(@return, @opt_values[0 .. $index - 1]);
+ push(@return, sprintf($opt_values[$index], $prop));
+ push(@return, @opt_values[$index + 1 .. $#opt_values]);
+ }
+ return @return;
+}
+
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::System::Make::Build::Task::Share
+
+=head1 SYNOPSIS
+
+ use FCM::System::Make::Build::Task::Share
+
+=head1 DESCRIPTION
+
+Provides common "local" functions for a make build task.
+
+=head1 FUNCTIONS
+
+The following functions are automatically exported by this module.
+
+=over 4
+
+=item _props_to_opts($opt_value, @props)
+
+Expect $opt_value to be an sprintf format containing one %s, and @props is a
+list of values. Return a list that can be used in a shell command. E.g.:
+
+ _props_to_opts('-D%s', 'HELLO="greetings"', 'WORLD="mars and venus"')
+ # => ('-DHELLO="greetings"', '-DWORLD="mars and venus"')
+
+ _props_to_opts('-I %s', '/path/1', '/path/2')
+ # => ('-I', '/path/1', '-I', '/path/2')
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/System/Make/Extract.pm b/lib/FCM/System/Make/Extract.pm
new file mode 100644
index 0000000..bcff360
--- /dev/null
+++ b/lib/FCM/System/Make/Extract.pm
@@ -0,0 +1,1212 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+
+# ------------------------------------------------------------------------------
+package FCM::System::Make::Extract;
+use base qw{FCM::Class::CODE};
+
+use FCM::Context::ConfigEntry;
+use FCM::Context::Event;
+use FCM::Context::Make::Extract;
+use FCM::Context::Locator;
+use FCM::Context::Task;
+use FCM::System::Exception;
+use FCM::System::Make::Share::Subsystem;
+use File::Basename qw{dirname};
+use File::Compare qw{compare};
+use File::Copy qw{copy};
+use File::Path qw{mkpath rmtree};
+use File::Spec::Functions qw{catfile tmpdir};
+use File::Temp;
+use List::Util qw{first};
+use Storable qw{dclone};
+
+# Aliases
+our $UTIL;
+my $E = 'FCM::System::Exception';
+
+# Configuration parser map: label to action
+our %CONFIG_PARSER_OF = (
+ 'location' => \&_config_parse_location,
+ 'ns' => \&_config_parse_ns_list,
+ 'path-excl' => _config_parse_path_func(
+ sub {$_->get_path_excl()}, sub {$_->set_path_excl(@_)}, '@',
+ ),
+ 'path-incl' => _config_parse_path_func(
+ sub {$_->get_path_incl()}, sub {$_->set_path_incl(@_)}, '@',
+ ),
+ 'path-root' => _config_parse_path_func(
+ sub {$_->get_path_root()}, sub {$_->set_path_root(@_)},
+ ),
+);
+
+# Properties from FCM::Util
+our @UTIL_PROP_KEYS = qw{diff3 diff3.flags};
+
+# Creates the class.
+__PACKAGE__->class(
+ { config_parser_of => {isa => '%', default => {%CONFIG_PARSER_OF}},
+ prop_of => '%',
+ shared_util_of => '%',
+ util => '&',
+ },
+ { init => \&_init,
+ action_of => {
+ config_parse => \&_config_parse,
+ config_parse_class_prop => \&_config_parse_class_prop,
+ config_parse_inherit_hook => \&_config_parse_inherit_hook,
+ config_unparse => \&_config_unparse,
+ config_unparse_class_prop => \&_config_unparse_class_prop,
+ ctx => \&_ctx,
+ main => \&_main,
+ },
+ },
+);
+
+# Initialises the helpers of the class.
+sub _init {
+ my ($attrib_ref) = @_;
+ for my $util_prop_key (@UTIL_PROP_KEYS) {
+ my $prop = $attrib_ref->{util}->external_cfg_get($util_prop_key);
+ $attrib_ref->{prop_of}{$util_prop_key} = [$prop];
+ }
+}
+
+# Reads the extract.location declaration from a config entry.
+sub _config_parse_location {
+ my ($attrib_ref, $ctx, $entry) = @_;
+ if (!@{$entry->get_ns_list()}) {
+ return $E->throw($E->CONFIG_NS, $entry);
+ }
+ my %PARSER_OF = (
+ 'base' => \&_config_parse_location_base,
+ 'diff' => \&_config_parse_location_diff,
+ 'primary' => \&_config_parse_location_primary,
+ );
+ my %modifier_of = %{$entry->get_modifier_of()};
+ if (!grep {exists($modifier_of{$_})} keys(%PARSER_OF)) {
+ $modifier_of{'base'} = 1;
+ }
+ for my $key (grep {exists($modifier_of{$_})} keys(%PARSER_OF)) {
+ $PARSER_OF{$key}->($attrib_ref, $ctx, $entry);
+ }
+}
+
+# Reads the extract.location{base} declaration from a config entry.
+sub _config_parse_location_base {
+ my ($attrib_ref, $ctx, $entry) = @_;
+ my %option;
+ if (exists($entry->get_modifier_of()->{'type'})) {
+ %option = ('type' => $entry->get_modifier_of()->{'type'});
+ }
+ for my $ns (@{$entry->get_ns_list()}) {
+ if (!exists($ctx->get_project_of()->{$ns})) {
+ $ctx->get_project_of()->{$ns} = $ctx->CTX_PROJECT->new({ns => $ns});
+ }
+ my $project = $ctx->get_project_of()->{$ns};
+ if ($project->get_inherited()) {
+ if (!$entry->get_value()) {
+ return $E->throw($E->CONFIG_VALUE, $entry);
+ }
+ my $locator = FCM::Context::Locator->new(
+ $entry->get_value(), \%option,
+ );
+ if ($project->get_locator()) {
+ $attrib_ref->{util}->loc_rel2abs(
+ $locator,
+ $project->get_locator(),
+ );
+ }
+ $attrib_ref->{util}->loc_as_invariant($locator);
+ my $i_locator = $project->get_trees()->[0]->get_locator();
+ if ($locator->get_value() ne $i_locator->get_value()) {
+ return $E->throw($E->CONFIG_CONFLICT, $entry);
+ }
+ }
+ else {
+ if ( !exists($project->get_trees()->[0])
+ || !defined($project->get_trees()->[0])
+ ) {
+ $project->get_trees()->[0]
+ = $ctx->CTX_TREE->new({key => 0, ns => $ns});
+ }
+ if ($entry->get_value()) {
+ my $locator = FCM::Context::Locator->new(
+ $entry->get_value(), \%option,
+ );
+ $project->get_trees()->[0]->set_locator($locator);
+ }
+ else {
+ $project->get_trees()->[0] = undef;
+ }
+ }
+ }
+}
+
+# Reads the extract.location{diff} declaration from a config entry.
+sub _config_parse_location_diff {
+ my ($attrib_ref, $ctx, $entry) = @_;
+ my %option;
+ if (exists($entry->get_modifier_of()->{'type'})) {
+ %option = ('type' => $entry->get_modifier_of()->{'type'});
+ }
+ for my $ns (@{$entry->get_ns_list()}) {
+ if (!exists($ctx->get_project_of()->{$ns})) {
+ $ctx->get_project_of()->{$ns} = $ctx->CTX_PROJECT->new({ns => $ns});
+ }
+ my $project = $ctx->get_project_of()->{$ns};
+ my ($base, @diffs) = @{$project->get_trees()};
+ @diffs = grep {
+ $_->get_inherited()
+ || $option{type}
+ && $_->get_locator()->get_type()
+ && $option{type} ne $_->get_locator()->get_type()
+ } @diffs;
+ for my $value ($entry->get_values()) {
+ if (!$value) {
+ return $E->throw($E->CONFIG_VALUE, $entry);
+ }
+ push(
+ @diffs,
+ $ctx->CTX_TREE->new({
+ key => scalar(@diffs) + 1,
+ locator => FCM::Context::Locator->new($value, \%option),
+ ns => $ns,
+ }),
+ );
+ }
+ @{$project->get_trees()} = ($base, @diffs);
+ }
+}
+
+# Reads the extract.location{primary} declaration from a config entry.
+sub _config_parse_location_primary {
+ my ($attrib_ref, $ctx, $entry) = @_;
+ my %option;
+ if (exists($entry->get_modifier_of()->{'type'})) {
+ %option = ('type' => $entry->get_modifier_of()->{'type'});
+ }
+ for my $ns (@{$entry->get_ns_list()}) {
+ if (!exists($ctx->get_project_of()->{$ns})) {
+ $ctx->get_project_of()->{$ns} = $ctx->CTX_PROJECT->new({ns => $ns});
+ }
+ my $project = $ctx->get_project_of()->{$ns};
+ if ($project->get_inherited()) {
+ if ($project->get_locator()->get_value() ne $entry->get_value()) {
+ return $E->throw($E->CONFIG_CONFLICT, $entry);
+ }
+ }
+ elsif ($entry->get_value()) {
+ $project->set_locator(
+ FCM::Context::Locator->new($entry->get_value(), \%option),
+ );
+ }
+ else {
+ $project->set_locator(undef);
+ }
+ }
+}
+
+# Reads the extract.ns declaration from a config entry.
+sub _config_parse_ns_list {
+ my ($attrib_ref, $ctx, $entry) = @_;
+ @{$ctx->get_ns_list()} = $entry->get_values();
+}
+
+# Returns a function to parse extract.path-*.
+sub _config_parse_path_func {
+ my ($getter, $setter, $isa) = @_;
+ $isa ||= '$';
+ sub {
+ my ($attrib_ref, $ctx, $entry) = @_;
+ my @ns_list
+ = @{$entry->get_ns_list()} ? @{$entry->get_ns_list()}
+ : @{$ctx->get_ns_list()}
+ ;
+ for my $ns (@ns_list) {
+ if (!exists($ctx->get_project_of()->{$ns})) {
+ $ctx->get_project_of()->{$ns}
+ = $ctx->CTX_PROJECT->new({ns => $ns});
+ }
+ my $project = $ctx->get_project_of()->{$ns};
+ my $value = $entry->get_value();
+ if ($isa eq '@') {
+ $value = [map {$_ eq q{/} ? q{} : $_} $entry->get_values()];
+ }
+ local($_) = $project;
+ if ($_->get_inherited()) {
+ my $old = $getter->();
+ my $new = $value;
+ if ($isa eq '@') {
+ $old = _config_unparse_join(@{$old});
+ $new = _config_unparse_join(@{$new});
+ }
+ if ($old ne $new) {
+ return $E->throw($E->CONFIG_CONFLICT, $entry);
+ }
+ }
+ else {
+ $setter->($value);
+ }
+ }
+ };
+}
+
+# A hook command for the "inherit/use" declaration.
+sub _config_parse_inherit_hook {
+ my ($attrib_ref, $ctx, $i_ctx) = @_;
+ @{$ctx->get_ns_list()} = @{$i_ctx->get_ns_list()};
+ while (my ($ns, $i_project) = each(%{$i_ctx->get_project_of()})) {
+ my $project = dclone($i_project);
+ $project->set_inherited(1);
+ for my $tree (@{$project->get_trees()}) {
+ $tree->set_inherited(1);
+ }
+ $ctx->get_project_of()->{$ns} = $project;
+ }
+ _config_parse_inherit_hook_prop($attrib_ref, $ctx, $i_ctx);
+}
+
+# Turns a context into a list of configuration entries.
+sub _config_unparse {
+ my ($attrib_ref, $ctx) = @_;
+ my %LABEL_OF
+ = map {($_ => $ctx->get_id() . q{.} . $_)} keys(%CONFIG_PARSER_OF);
+ my @entries = (
+ FCM::Context::ConfigEntry->new({
+ label => $LABEL_OF{ns},
+ value => _config_unparse_join(@{$ctx->get_ns_list()}),
+ }),
+ );
+ for my $p_ns (sort keys(%{$ctx->get_project_of()})) {
+ my $project = $ctx->get_project_of($p_ns);
+ my ($base, @diffs) = @{$project->get_trees()};
+ if (!$project->get_inherited()) {
+ if (defined($project->get_locator())) {
+ my $locator = $project->get_locator();
+ my %modifier_of = (primary => 1, type => $locator->get_type());
+ push(
+ @entries,
+ FCM::Context::ConfigEntry->new({
+ label => $LABEL_OF{location},
+ modifier_of => \%modifier_of,
+ ns_list => [$p_ns],
+ value => $locator->get_value(),
+ }),
+ );
+ }
+ if (@{$project->get_path_excl()}) {
+ my @values = map {$_ ? $_ : q{/}} @{$project->get_path_excl()};
+ push(
+ @entries,
+ FCM::Context::ConfigEntry->new({
+ label => $LABEL_OF{'path-excl'},
+ ns_list => [$p_ns],
+ value => _config_unparse_join(@values),
+ }),
+ );
+ }
+ if (@{$project->get_path_incl()}) {
+ my @values = map {$_ ? $_ : q{/}} @{$project->get_path_incl()};
+ push(
+ @entries,
+ FCM::Context::ConfigEntry->new({
+ label => $LABEL_OF{'path-incl'},
+ ns_list => [$p_ns],
+ value => _config_unparse_join(@values),
+ }),
+ );
+ }
+ if ($project->get_path_root()) {
+ push(
+ @entries,
+ FCM::Context::ConfigEntry->new({
+ label => $LABEL_OF{'path-root'},
+ ns_list => [$p_ns],
+ value => $project->get_path_root(),
+ }),
+ );
+ }
+ my $value = $base->get_locator()->get_value();
+ push(
+ @entries,
+ FCM::Context::ConfigEntry->new({
+ label => $LABEL_OF{'location'},
+ modifier_of => {type => $base->get_locator()->get_type()},
+ ns_list => [$p_ns],
+ value => $value,
+ }),
+ );
+ }
+ @diffs = grep {!$_->get_inherited()} @diffs;
+ if (@diffs) {
+ my %type_set = map {($_->get_locator()->get_type() => 1)} @diffs;
+ for my $type (sort(keys(%type_set))) {
+ my $value = _config_unparse_join(
+ map {$_->get_locator()->get_value()}
+ grep {$_->get_locator()->get_type() eq $type}
+ @diffs
+ );
+ push(
+ @entries,
+ FCM::Context::ConfigEntry->new({
+ label => $LABEL_OF{'location'},
+ modifier_of => {diff => 1, type => $type},
+ ns_list => [$p_ns],
+ value => $value,
+ }),
+ );
+ }
+ }
+ }
+ push(@entries, _config_unparse_prop($attrib_ref, $ctx));
+ return @entries;
+}
+
+# Returns a new context.
+sub _ctx {
+ my ($attrib_ref, $id_of_class, $id) = @_;
+ FCM::Context::Make::Extract->new({id => $id, id_of_class => $id_of_class});
+}
+
+# The main function of this class.
+sub _main {
+ my ($attrib_ref, $m_ctx, $ctx) = @_;
+ local($UTIL) = $attrib_ref->{util};
+ for my $function (
+ \&_elaborate_ctx_of_project,
+ \&_elaborate_ctx_of_target,
+ \&_extract_incremental,
+ \&_project_tree_caches_update,
+ \&_symlink_handle,
+ \&_targets_update,
+ ) {
+ $function->($attrib_ref, $m_ctx, $ctx);
+ }
+}
+
+# Elaborates the context: project and tree.
+sub _elaborate_ctx_of_project {
+ my ($attrib_ref, $m_ctx, $ctx) = @_;
+
+ # Reports projects that are not used
+ my @bad_ns_list;
+ while (my ($p_ns, $project) = each(%{$ctx->get_project_of()})) {
+ if ( !$project->get_inherited()
+ && !grep {$_ eq $p_ns} @{$ctx->get_ns_list()}
+ ) {
+ push(@bad_ns_list, $p_ns);
+ }
+ }
+ if (@bad_ns_list) {
+ return $E->throw($E->EXTRACT_NS, \@bad_ns_list);
+ }
+
+ # Determines a list of new trees
+ my $prev_m_ctx = $m_ctx->get_prev_ctx();
+ my $prev_ctx
+ = defined($prev_m_ctx) ? $prev_m_ctx->get_ctx_of($ctx->get_id())
+ : undef
+ ;
+ my @trees; # list of new trees
+ for my $p_ns (@{$ctx->get_ns_list()}) {
+ # Ensures the project settings are defined
+ if (!exists($ctx->get_project_of()->{$p_ns})) {
+ $ctx->get_project_of()->{$p_ns}
+ = $ctx->CTX_PROJECT->new({ns => $p_ns});
+ }
+ my $project = $ctx->get_project_of()->{$p_ns};
+
+ # Determine the root location of the project, if possible
+ if (defined($project->get_locator())) {
+ $UTIL->loc_as_normalised($project->get_locator());
+ }
+ else {
+ my $uri = $UTIL->loc_kw_prefix() . ':' . $p_ns;
+ my $locator = FCM::Context::Locator->new($uri);
+ local($@);
+ eval {$UTIL->loc_as_normalised($locator)};
+ if (!$@) {
+ $project->set_locator($locator);
+ }
+ }
+ # Ensures base tree is defined
+ if (!@{$project->get_trees()} || !defined($project->get_trees()->[0])) {
+ if (!defined($project->get_locator())) {
+ return $E->throw($E->EXTRACT_LOC_BASE, $p_ns);
+ }
+ my $head_locator = $UTIL->loc_trunk_at_head($project->get_locator());
+ my $locator
+ = $head_locator ? $head_locator
+ : dclone($project->get_locator())
+ ;
+ $project->get_trees()->[0] = $ctx->CTX_TREE->new(
+ {key => 0, locator => $locator, ns => $p_ns},
+ );
+ }
+ # Determine whether there is a usable previous extract
+ my %path_excl = map {($_, 1)} @{$project->get_path_excl()};
+ my %path_incl = map {($_, 1)} @{$project->get_path_incl()};
+ my $path_root = $project->get_path_root();
+ my ($can_use_prev, $prev_project);
+ if (defined($prev_ctx) && defined($prev_ctx->get_project_of($p_ns))) {
+ $prev_project = $prev_ctx->get_project_of($p_ns);
+ my %prev_path_excl = map {($_, 1)} @{$prev_project->get_path_excl()};
+ my %prev_path_incl = map {($_, 1)} @{$prev_project->get_path_incl()};
+ my $prev_path_root = $prev_project->get_path_root();
+ $can_use_prev
+ = $prev_ctx->get_status() eq $m_ctx->ST_OK
+ && !$UTIL->hash_cmp(\%path_excl, \%prev_path_excl, 1)
+ && !$UTIL->hash_cmp(\%path_incl, \%prev_path_incl, 1)
+ && $path_root eq $prev_path_root
+ ;
+ }
+ # Tree locators as invariant
+ TREE:
+ for my $tree (grep {!$_->get_inherited()} @{$project->get_trees()}) {
+ my $tree_locator = $tree->get_locator();
+ # Ensures that the tree locator is an absolute path
+ if (defined($project->get_locator())) {
+ $UTIL->loc_rel2abs($tree_locator, $project->get_locator());
+ }
+ # Determines invariant form of the locator of the project tree.
+ $UTIL->loc_as_invariant($tree_locator);
+ }
+ # Remove diff trees that are the same as the base tree
+ my ($base_tree, @old_diff_trees) = @{$project->get_trees()};
+ my $base_value = $base_tree->get_locator()->get_value();
+ my @new_diff_trees;
+ TREE:
+ for my $tree (@old_diff_trees) {
+ if ($base_value ne $tree->get_locator()->get_value()) {
+ push(@new_diff_trees, $tree);
+ $tree->set_key(scalar(@new_diff_trees)); # reset key (index)
+ }
+ }
+ $project->set_trees([$base_tree, @new_diff_trees]);
+ # Determine the new trees
+ TREE:
+ for my $tree (grep {!$_->get_inherited()} @{$project->get_trees()}) {
+ my $tree_locator = $tree->get_locator();
+ if ( $can_use_prev
+ && $tree_locator->get_value_level() >= $tree_locator->L_INVARIANT
+ ) {
+ my $prev_tree = first {
+ $tree_locator->get_value() eq $_->get_locator()->get_value()
+ } @{$prev_project->get_trees()};
+ if ($prev_tree) {
+ my $prev_tree_locator = $prev_tree->get_locator();
+ $tree->set_sources($prev_tree->get_sources());
+ if ($tree->get_key() || !$prev_tree->get_key()) {
+ # Only safe to re-use cache if both are base trees
+ # or for diff tree with an unchanged base tree
+ $tree->set_cache($prev_tree->get_cache());
+ }
+ next TREE;
+ }
+ if (!$tree->get_key()) { # base tree changed
+ $can_use_prev = 0;
+ }
+ }
+ push(@trees, $tree); # new tree
+ }
+ }
+
+ # Obtain source info for each new tree, using the task runner
+ if (@trees) {
+ my $timer = $UTIL->timer();
+ my $n_jobs = $m_ctx->get_option_of('jobs');
+ if ($n_jobs && $n_jobs > scalar(@trees)) {
+ $n_jobs = scalar(@trees);
+ }
+ my $elapse_tasks = 0;
+ my $runner = $UTIL->task_runner(
+ sub {_elaborate_ctx_of_project_tree($attrib_ref, $m_ctx, $ctx, @_)},
+ $n_jobs,
+ );
+ my $n = eval {
+ $runner->main(
+ # get
+ sub {
+ if (!@trees) {
+ return;
+ }
+ my $tree = shift(@trees);
+ my $id = join(':', $tree->get_ns(), $tree->get_key());
+ FCM::Context::Task->new({ctx => $tree, id => $id});
+ },
+ # put
+ sub {
+ my ($task) = @_;
+ if ($task->get_state() eq $task->ST_FAILED) {
+ die($task->get_error());
+ }
+ my $ns = $task->get_ctx()->get_ns();
+ my $key = $task->get_ctx()->get_key();
+ my $project = $ctx->get_project_of()->{$ns};
+ my $tree = $project->get_trees()->[$key];
+ $tree->set_locator($task->get_ctx()->get_locator());
+ $tree->set_sources($task->get_ctx()->get_sources());
+ $elapse_tasks += $task->get_elapse();
+ },
+ );
+ };
+ my $e = $@;
+ $runner->destroy();
+ if ($e) {
+ die($e);
+ }
+ $UTIL->event(
+ FCM::Context::Event->MAKE_EXTRACT_RUNNER_SUMMARY,
+ 'tree-sources-info-get', $n, $timer->(), $elapse_tasks,
+ );
+ }
+ $UTIL->event(
+ FCM::Context::Event->MAKE_EXTRACT_PROJECT_TREE,
+ { map {($_ => [
+ map {$_->get_locator()}
+ @{$ctx->get_project_of()->{$_}->get_trees()}
+ ])}
+ sort keys(%{$ctx->get_project_of()})
+ },
+ );
+}
+
+# Elaborates the context: new tree in a project.
+sub _elaborate_ctx_of_project_tree {
+ my ($attrib_ref, $m_ctx, $ctx, $tree) = @_;
+ my $project = $ctx->get_project_of()->{$tree->get_ns()};
+ my $path_root = $project->get_path_root();
+ # TODO: support regular expression or wildcards?
+ my %path_incl = map {($_ => 1)} @{$project->get_path_incl()};
+ my %path_excl = map {($_ => 1)} @{$project->get_path_excl()};
+ $UTIL->loc_find(
+ $tree->get_locator(),
+ sub {
+ my ($locator, $locator_attrib_ref) = @_;
+ if ($locator_attrib_ref->{is_dir}) {
+ return;
+ }
+ my $ns_in_tree = $locator_attrib_ref->{ns};
+ my $ns = $ns_in_tree;
+ if ($path_root) {
+ if ($path_root ne $UTIL->ns_common($path_root, $ns)) {
+ return;
+ }
+ $ns = $ns eq $path_root ? q{}
+ : substr($ns, length($path_root) + 1)
+ ;
+ }
+ my $ns_iter_ref = $UTIL->ns_iter($ns, $UTIL->NS_ITER_UP);
+ NS:
+ while (defined(my $head = $ns_iter_ref->())) {
+ if (exists($path_incl{$head})) {
+ last NS;
+ }
+ if (exists($path_excl{$head})) {
+ return;
+ }
+ }
+ push(
+ @{$tree->get_sources()},
+ $ctx->CTX_SOURCE->new({
+ key_of_tree => $tree->get_key(),
+ locator => $locator,
+ ns => $UTIL->ns_cat($tree->get_ns(), $ns),
+ ns_in_tree => $ns_in_tree,
+ }),
+ );
+ },
+ );
+ $tree;
+}
+
+# Elaborates the context: target.
+sub _elaborate_ctx_of_target {
+ my ($attrib_ref, $m_ctx, $ctx) = @_;
+ # Works out the extract sources and targets
+ my $DEST = $attrib_ref->{shared_util_of}{dest};
+ my $ns_sep = $UTIL->ns_sep();
+ while (my ($p_ns, $project) = each(%{$ctx->get_project_of()})) {
+ my ($tree_base, @trees) = @{$project->get_trees()};
+ # Sources from the base tree
+ for my $source (@{$tree_base->get_sources()}) {
+ my $ns = $source->get_ns();
+ my @paths = split($ns_sep, $ns);
+ my $dest_list_ref = $DEST->paths(
+ $m_ctx, 'target', $ctx->get_id(), @paths
+ );
+ $ctx->get_target_of()->{$ns} = $ctx->CTX_TARGET->new({
+ dests => $dest_list_ref,
+ ns => $ns,
+ source_of => {$tree_base->get_key() => $source},
+ });
+ }
+ my %sources_in_base
+ = map {($_->get_ns() => $_)} @{$tree_base->get_sources()};
+ # Sources from the diff trees
+ for my $tree (@trees) {
+ my $key = $tree->get_key();
+ my %sources_deleted = %sources_in_base;
+ # Handles new/modified sources
+ for my $source (@{$tree->get_sources()}) {
+ my $ns = $source->get_ns();
+ delete($sources_deleted{$ns});
+ if (exists($ctx->get_target_of()->{$ns})) {
+ my $target = $ctx->get_target_of()->{$ns};
+ my $base_source = $target->get_source_of()->{0};
+ if ( $base_source->get_locator()
+ && _source_eq($base_source, $source)
+ ) {
+ $source->set_status($source->ST_UNCHANGED);
+ }
+ else {
+ # Source modified by diff tree
+ $target->get_source_of()->{$key} = $source;
+ }
+ }
+ else {
+ # Source added by diff tree
+ my @paths = split($ns_sep, $ns);
+ my $dest_list_ref = $DEST->paths(
+ $m_ctx, 'target', $ctx->get_id(), @paths,
+ );
+ $ctx->get_target_of()->{$ns} = $ctx->CTX_TARGET->new({
+ dests => $dest_list_ref,
+ ns => $ns,
+ source_of => {
+ 0 => $ctx->CTX_SOURCE->new({
+ key_of_tree => 0,
+ status => $ctx->CTX_SOURCE->ST_MISSING,
+ }),
+ $key => $source,
+ },
+ });
+ }
+ }
+ # Handle deleted sources
+ while (my ($ns) = each(%sources_deleted)) {
+ my $target = $ctx->get_target_of()->{$ns};
+ $target->get_source_of()->{$key} = $ctx->CTX_SOURCE->new({
+ key_of_tree => $key,
+ ns => $ns,
+ status => $ctx->CTX_SOURCE->ST_MISSING,
+ });
+ }
+ }
+ }
+}
+
+# Extract: compare with previous extract.
+sub _extract_incremental {
+ my ($attrib_ref, $m_ctx, $ctx) = @_;
+ my $prev_m_ctx = $m_ctx->get_prev_ctx();
+ my $prev_ctx
+ = defined($prev_m_ctx) ? $prev_m_ctx->get_ctx_of($ctx->get_id())
+ : undef
+ ;
+ if (!defined($prev_ctx)) {
+ return;
+ }
+ my %deleted = map {($_ => 1)} keys(%{$prev_ctx->get_target_of()});
+ # Compares the sources in each target
+ TARGET:
+ while (my ($ns, $target) = each(%{$ctx->get_target_of()})) {
+ delete($deleted{$ns});
+ if (!exists($prev_ctx->get_target_of()->{$ns})) {
+ next TARGET;
+ }
+ my $prev_target = $prev_ctx->get_target_of()->{$ns};
+ my %prev_source_of = %{$prev_target->get_source_of()};
+ my %source_of = %{$target->get_source_of()};
+ if (keys(%prev_source_of) != keys(%source_of)) {
+ next TARGET;
+ }
+ while (my ($key_of_tree, $source) = each(%source_of)) {
+ if (!exists($prev_source_of{$key_of_tree})) {
+ next TARGET;
+ }
+ my $prev_source = $prev_source_of{$key_of_tree};
+ if ( $prev_source->get_status() ne $source->get_status()
+ || !$source->is_missing() && !_source_eq($prev_source, $source)
+ ) {
+ next TARGET;
+ }
+ }
+ $target->set_status_of_source($prev_target->get_status_of_source());
+ if ($prev_target->is_ok()) {
+ $target->set_path($prev_target->get_path());
+ $target->set_status($target->ST_UNCHANGED);
+ }
+ }
+ # Creates a dummy target for each deleted target
+ my $ns_sep = $UTIL->ns_sep();
+ while (my $ns = each(%deleted)) {
+ my $target = $prev_ctx->get_target_of($ns);
+ if ($target->get_status() ne $target->ST_DELETED) {
+ my @paths = split($ns_sep, $ns);
+ my $dest_list_ref = $attrib_ref->{shared_util_of}{dest}->paths(
+ $m_ctx, 'target', $ctx->get_id(), @paths,
+ );
+ $ctx->get_target_of()->{$ns}
+ = $ctx->CTX_TARGET->new({dests => $dest_list_ref, ns => $ns});
+ }
+ }
+}
+
+# Updates the project tree caches.
+sub _project_tree_caches_update {
+ my ($attrib_ref, $m_ctx, $ctx) = @_;
+ my $timer = $UTIL->timer();
+ my $n_jobs = $m_ctx->get_option_of('jobs');
+ my $n_trees = scalar(
+ grep {!$_->get_cache()}
+ map {@{$_->get_trees()}}
+ values(%{$ctx->get_project_of()})
+ );
+ if ($n_trees == 0) {
+ return;
+ }
+ if ($n_jobs && $n_jobs > $n_trees) {
+ $n_jobs = $n_trees;
+ }
+ my $elapse_tasks = 0;
+ my @args = ($attrib_ref, $m_ctx, $ctx);
+ my $runner = $UTIL->task_runner(
+ sub {_project_tree_cache_update_by_export(@args, @_)},
+ $n_jobs,
+ );
+ my $n = eval {
+ $runner->main(
+ _project_tree_cache_update_get_func(@args),
+ _project_tree_cache_update_put_func(@args, \$elapse_tasks),
+ );
+ };
+ my $e = $@;
+ $runner->destroy();
+ if ($e) {
+ die($e);
+ }
+ $UTIL->event(
+ FCM::Context::Event->MAKE_EXTRACT_RUNNER_SUMMARY,
+ 'tree-cache-export', $n, $timer->(), $elapse_tasks,
+ );
+}
+
+# Updates the source cache for a project tree by exporting it.
+sub _project_tree_cache_update_by_export {
+ my ($attrib_ref, $m_ctx, $ctx, $tree) = @_;
+ my $cache = $tree->get_cache();
+ # Exports the smallest common tree
+ my $root_ns;
+ SOURCE:
+ for my $source (@{$tree->get_sources()}) {
+ if ($source->is_unchanged()) {
+ next SOURCE;
+ }
+ if (!defined($root_ns)) {
+ $root_ns = $source->get_ns_in_tree();
+ next SOURCE;
+ }
+ $root_ns = $UTIL->ns_common(
+ $root_ns, $source->get_ns_in_tree(),
+ );
+ if (!$root_ns) {
+ last SOURCE;
+ }
+ }
+ if (!defined($root_ns)) {
+ return;
+ }
+ my $cache_ns = $root_ns ? catfile($cache, $root_ns) : $cache;
+ my $locator_ns = $UTIL->loc_cat(
+ $tree->get_locator(), split($UTIL->ns_sep(), $root_ns),
+ );
+ eval{
+ mkpath(dirname($cache_ns));
+ $UTIL->loc_export($locator_ns, $cache_ns);
+ };
+ if (my $e = $@ || !-e $cache_ns && !-l $cache_ns) {
+ return $E->throw($E->DEST_CREATE, $cache_ns, $e);
+ }
+}
+
+# Generates an iterator for each tree requiring cache update.
+sub _project_tree_cache_update_get_func {
+ my ($attrib_ref, $m_ctx, $ctx) = @_;
+ my @trees = map {@{$_->get_trees()}} values(%{$ctx->get_project_of()});
+ sub {
+ while (my $tree = shift(@trees)) {
+ if (!$tree->get_cache()) {
+ if ($UTIL->loc_export_ok($tree->get_locator())) {
+ my $cache = $attrib_ref->{shared_util_of}{dest}->path(
+ $m_ctx,
+ 'sys-cache',
+ $ctx->get_id(),
+ $tree->get_ns(),
+ $tree->get_key(),
+ );
+ $tree->set_cache($cache);
+ rmtree($cache);
+ mkpath(dirname($cache));
+ my $id = $tree->get_ns() . '/' . $tree->get_key();
+ return FCM::Context::Task->new({ctx => $tree, id => $id});
+ }
+ else {
+ $tree->set_cache($tree->get_locator()->get_value());
+ _project_tree_cache_update_sources(
+ $attrib_ref, $m_ctx, $ctx, $tree,
+ );
+ }
+ }
+ }
+ return;
+ };
+}
+
+# Generates a callback when a tree has a cache.
+sub _project_tree_cache_update_put_func {
+ my ($attrib_ref, $m_ctx, $ctx, $elapse_tasks_ref) = @_;
+ sub {
+ my ($task) = @_;
+ if ($task->get_state() eq $task->ST_FAILED) {
+ die($task->get_error());
+ }
+ my $ns = $task->get_ctx()->get_ns();
+ my $key = $task->get_ctx()->get_key();
+ my $tree = $ctx->get_project_of()->{$ns}->get_trees()->[$key];
+ _project_tree_cache_update_sources($attrib_ref, $m_ctx, $ctx, $tree);
+ ${$elapse_tasks_ref} += $task->get_elapse();
+ };
+}
+
+# Sets the caches of individual project tree sources.
+sub _project_tree_cache_update_sources {
+ my ($attrib_ref, $m_ctx, $ctx, $tree) = @_;
+ for my $source (@{$tree->get_sources()}) {
+ my $cache = catfile(
+ $tree->get_cache(),
+ split($UTIL->ns_sep(), $source->get_ns_in_tree()),
+ );
+ $source->set_cache($cache);
+ }
+}
+
+# Handles symbolic links.
+sub _symlink_handle {
+ my ($attrib_ref, $m_ctx, $ctx) = @_;
+ TARGET:
+ while (my ($ns, $target) = each(%{$ctx->get_target_of()})) {
+ if ($target->is_unchanged()) {
+ next TARGET;
+ }
+ my $source_hash_ref = $target->get_source_of();
+ # Remove sources that are symbolic links
+ while (my ($key, $source) = each(%{$source_hash_ref})) {
+ if ($source->get_cache() && -l $source->get_cache()) {
+ delete($source_hash_ref->{$key});
+ $UTIL->event(
+ FCM::Context::Event->MAKE_EXTRACT_SYMLINK, $source,
+ );
+ }
+ }
+ # It is OK to have a target with no sources, but a target must have a
+ # base source if it has at least one diff source.
+ if ( keys(%{$source_hash_ref})
+ && !exists($source_hash_ref->{0})
+ ) {
+ $source_hash_ref->{0} = $ctx->CTX_SOURCE->new(
+ {key_of_tree => 0, status => $ctx->CTX_SOURCE->ST_MISSING},
+ );
+ }
+ }
+}
+
+# Updates the targets.
+sub _targets_update {
+ my ($attrib_ref, $m_ctx, $ctx) = @_;
+ my %basket_of = (status => {}, status_of_source => {});
+ while (my ($ns, $target) = each(%{$ctx->get_target_of()})) {
+ if ($target->get_status() eq $target->ST_UNKNOWN) {
+ my %source_of = %{$target->get_source_of()};
+ my $handler
+ = keys(%source_of) ? \&_target_update
+ : \&_target_delete
+ ;
+ $handler->($attrib_ref, $m_ctx, $ctx, $target);
+ my $base = delete($source_of{0});
+ my @diffs = grep {!$_->is_unchanged()} values(%source_of);
+ $target->set_status_of_source(
+ !keys(%{$target->get_source_of()}) ? $target->ST_UNKNOWN
+ : $base->is_missing() ? $target->ST_ADDED
+ : (grep {$_->is_missing()} @diffs) ? $target->ST_DELETED
+ : scalar(@diffs) > 1 ? $target->ST_MERGED
+ : scalar(@diffs) ? $target->ST_MODIFIED
+ : $target->ST_UNCHANGED
+ );
+ $UTIL->event(
+ FCM::Context::Event->MAKE_EXTRACT_TARGET, $target,
+ );
+ }
+ $basket_of{status}{$target->get_status()}++;
+ $basket_of{status_of_source}{$target->get_status_of_source()}++;
+ }
+ $UTIL->event(
+ FCM::Context::Event->MAKE_EXTRACT_TARGET_SUMMARY, \%basket_of,
+ );
+}
+
+# Updates a deleted target.
+sub _target_delete {
+ my ($attrib_ref, $m_ctx, $ctx, $target) = @_;
+ my ($dest, @inherited_dests) = @{$target->get_dests()};
+ if (-f $dest) {
+ unlink($dest) || return $E->throw($E->DEST_CLEAN, $dest, $!);
+ $target->set_status($target->ST_DELETED);
+ }
+ for my $inherited_dest (@inherited_dests) {
+ if (-f $inherited_dest) {
+ $target->set_status($target->ST_O_DELETED);
+ return;
+ }
+ }
+}
+
+# Updates a normal target.
+sub _target_update {
+ my ($attrib_ref, $m_ctx, $ctx, $target) = @_;
+ my %source_of = %{$target->get_source_of()};
+ my $source_of_base = delete($source_of{0});
+ # Either missing source in a diff-tree
+ # Or missing source in base-tree and no diff-trees
+ if ( (grep {$_->is_missing()} values(%source_of))
+ || $source_of_base->is_missing() && !keys(%source_of)
+ ) {
+ return _target_delete($attrib_ref, $m_ctx, $ctx, $target);
+ }
+ $target->set_status($target->ST_UNCHANGED);
+ my $path = _target_update_source($attrib_ref, $m_ctx, $ctx, $target);
+ # Note: $path may be a File::Temp object.
+ my ($is_diff, $is_diff_in_perms, $is_in_prev, $rc) = (1, 1, undef, 1);
+ DEST:
+ for my $i (0 .. @{$target->get_dests()} - 1) {
+ my $dest = $target->get_dests()->[$i];
+ if (-f $dest) {
+ $is_in_prev = $i;
+ ($is_diff_in_perms, $is_diff) = _compare("$path", $dest);
+ last DEST;
+ }
+ }
+ if (!$is_diff && !$is_diff_in_perms) {
+ $target->set_path($target->get_dests()->[$is_in_prev]);
+ return; # up to date
+ }
+ my $dest = $target->get_dests()->[0];
+ if ($is_diff) {
+ my $dest_dir = dirname($dest);
+ if (!-d $dest_dir) {
+ eval {mkpath($dest_dir)};
+ if (my $e = $@) {
+ return $E->throw($E->DEST_CREATE, $dest_dir, $e);
+ }
+ }
+ copy("$path", $dest)
+ || return $E->throw($E->COPY, ["$path", $dest], $!);
+ }
+ chmod((stat("$path"))[2] & oct(7777), $dest)
+ || return $E->throw($E->DEST_CREATE, $dest, $!);
+ $target->set_path($target->get_dests()->[0]);
+ $target->set_status(
+ $is_in_prev ? $target->ST_O_ADDED
+ : defined($is_in_prev) ? $target->ST_MODIFIED
+ : $target->ST_ADDED
+ );
+}
+
+# Returns the source path that is to be used to update a target.
+sub _target_update_source {
+ my ($attrib_ref, $m_ctx, $ctx, $target) = @_;
+ my %source_of = %{$target->get_source_of()};
+ my $path_of_base = delete($source_of{0})->get_cache();
+ my @keys_and_paths;
+ while (my ($key, $source) = each(%source_of)) {
+ my $path = $source->get_cache();
+ if (!$path_of_base || _compare($path_of_base, $path)) {
+ if (!grep {!_compare($_->[1], $path)} @keys_and_paths) {
+ push(@keys_and_paths, [$key, $path]);
+ }
+ }
+ else {
+ $source->set_status($source->ST_UNCHANGED);
+ }
+ }
+ my @args = (
+ $m_ctx, $ctx, $target, $path_of_base,
+ (sort {$a->[0] <=> $b->[0]} @keys_and_paths),
+ );
+ return (
+ @keys_and_paths == 0 ? $path_of_base
+ : @keys_and_paths == 1 ? $keys_and_paths[0][1]
+ : _target_update_source_merge($attrib_ref, @args)
+ );
+}
+
+# Merges changes in contents of paths in @keys_and_paths against content in
+# $path_of_base.
+sub _target_update_source_merge {
+ my ($attrib_ref, $m_ctx, $ctx, $target, $path_of_base, @keys_and_paths) = @_;
+ if (!$path_of_base) {
+ $path_of_base = File::Temp->new();
+ if (!defined($path_of_base) || !close($path_of_base)) {
+ return $E->throw($E->DEST_CREATE, tmpdir(), $!);
+ }
+ }
+ my ($key_of_mine, $path_of_mine) = @{shift(@keys_and_paths)};
+ my @keys_done = ($key_of_mine);
+ while (my $key_and_path = shift(@keys_and_paths)) {
+ my ($key, $path) = @{$key_and_path};
+ my @command = (
+ (map {_props($attrib_ref, $_, $ctx)} qw{diff3 diff3.flags}),
+ "$path_of_mine", "$path_of_base", $path,
+ );
+ my %value_of = %{$UTIL->shell_simple(\@command)};
+ if ($value_of{rc} && $value_of{rc} == 1) {
+ # Write conflict output to .fcm-make/extract/conflict/$NS
+ my $file = $attrib_ref->{shared_util_of}{dest}->path(
+ $m_ctx, 'sys', $ctx->get_id(), 'merge',
+ $target->get_ns() . '.diff',
+ );
+ $UTIL->file_save($file, $value_of{o});
+ return $E->throw($E->EXTRACT_MERGE, {
+ 'target' => $target,
+ 'output' => $file,
+ 'keys_done' => \@keys_done,
+ 'key' => $key,
+ 'keys_left' => [map {$_->[0]} @keys_and_paths],
+ });
+ }
+ elsif ($value_of{rc}) {
+ return $E->throw(
+ $E->SHELL, {command_list => \@command, %value_of}, $value_of{e},
+ );
+ }
+ my $perm = (stat("$path_of_mine"))[2] & 07777 | (stat($path))[2] & 07777;
+ for my $action (
+ sub {$path_of_mine = File::Temp->new()},
+ sub {print({$path_of_mine} $value_of{o})},
+ sub {close($path_of_mine)},
+ sub {chmod($perm, "$path_of_mine")},
+ ) {
+ $action->() || return $E->throw($E->DEST_CREATE, "$path_of_mine", $!);
+ }
+ push(@keys_done, $key);
+ }
+ return $path_of_mine;
+}
+
+# In scalar context, returns true if the contents or permissions of 2 paths
+# differ. In array context, returns ($is_diff_in_perms, $is_diff_in_content).
+sub _compare {
+ my ($path1, $path2) = @_;
+ my $is_diff_in_perms = (stat($path1))[2] != (stat($path2))[2];
+ wantarray()
+ ? ($is_diff_in_perms, compare($path1, $path2))
+ : ($is_diff_in_perms || compare($path1, $path2))
+ ;
+}
+
+# Returns true if two sources are the same or if their latest modified revisions
+# are the same.
+sub _source_eq {
+ my ($source1, $source2) = @_;
+ my ($locator1, $locator2) = map {$_->get_locator()} ($source1, $source2);
+ # Compares their value + mtime or their last modified revision
+ $locator1->get_value() eq $locator2->get_value()
+ && defined($locator1->get_last_mod_time())
+ && defined($locator2->get_last_mod_time())
+ && $locator1->get_last_mod_time() eq $locator2->get_last_mod_time()
+ || defined($locator1->get_last_mod_rev())
+ && defined($locator2->get_last_mod_rev())
+ && $locator1->get_last_mod_rev() eq $locator2->get_last_mod_rev()
+ ;
+}
+
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::System::Make::Extract
+
+=head1 SYNOPSIS
+
+ use FCM::System::Make::Extract;
+ my $extract = FCM::System::Make::Extract->new(\%attrib);
+ $extract->($m_ctx, $ctx);
+
+=head1 DESCRIPTION
+
+Implements the extract sub-system. An instance of this class is expected to be
+initialised and called by L<FCM::System::Make|FCM::System::Make>.
+
+=head1 METHODS
+
+See L<FCM::System::Make|FCM::System::Make> for detail.
+
+=head1 ATTRIBUTES
+
+The $class->new(\%attrib) method of this class supports the following
+attributes:
+
+=over 4
+
+=item config_parser_of
+
+A HASH to map the labels in a configuration file to their parsers. (default =
+%FCM::System::Make::Extract::CONFIG_PARSER_OF)
+
+=item prop_of
+
+A HASH to map the names of the properties to their settings. Each setting
+is a 2-element ARRAY reference, where element [0] is the default setting
+and element [1] is a flag to indicate whether the property accepts a name-space
+or not. (default = %FCM::System::Make::Extract::PROP_OF)
+
+=item shared_util_of
+
+See L<FCM::System::Make|FCM::System::Make> for detail.
+
+=item util
+
+See L<FCM::System::Make|FCM::System::Make> for detail.
+
+=back
+
+=head1 TODO
+
+Handle alternate method of merge (e.g. Algorithm::Merge).
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/System/Make/Mirror.pm b/lib/FCM/System/Make/Mirror.pm
new file mode 100644
index 0000000..b1e117b
--- /dev/null
+++ b/lib/FCM/System/Make/Mirror.pm
@@ -0,0 +1,415 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+
+# ------------------------------------------------------------------------------
+package FCM::System::Make::Mirror;
+use base qw{FCM::Class::CODE};
+
+use FCM::Context::ConfigEntry;
+use FCM::Context::Event;
+use FCM::Context::Make;
+use FCM::Context::Make::Mirror;
+use FCM::Context::Make::Share::Property;
+use FCM::System::Make::Share::Subsystem;
+use File::Basename qw{dirname};
+use File::Path qw{mkpath};
+use File::Spec::Functions qw{abs2rel file_name_is_absolute rel2abs};
+use POSIX qw{strftime};
+use Storable qw{dclone};
+use Sys::Hostname qw{hostname};
+use Text::ParseWords qw{shellwords};
+
+# Alias
+my $E = 'FCM::System::Exception';
+
+# Configuration parser label to action map
+our %CONFIG_PARSER_OF = (
+ 'target' => \&_config_parse_target,
+);
+
+# Default properties
+our %PROP_OF = (
+ 'config-file.steps' => [q{}],
+ 'no-config-file' => [q{}],
+);
+
+# Properties from FCM::Util
+our @UTIL_PROP_KEYS = qw{ssh ssh.flags rsync rsync.flags};
+
+# Creates the class.
+__PACKAGE__->class(
+ { config_parser_of => {isa => '%', default => {%CONFIG_PARSER_OF}},
+ prop_of => {isa => '%', default => {%PROP_OF}},
+ shared_util_of => '%',
+ util => '&',
+ },
+ { init => \&_init,
+ action_of => {
+ config_parse => \&_config_parse,
+ config_parse_class_prop => \&_config_parse_class_prop,
+ config_parse_inherit_hook => \&_config_parse_inherit_hook,
+ config_unparse => \&_config_unparse,
+ config_unparse_class_prop => \&_config_unparse_class_prop,
+ ctx => \&_ctx,
+ main => \&_main,
+ },
+ },
+);
+
+# Initialises the helpers of the class.
+sub _init {
+ my ($attrib_ref) = @_;
+ for my $util_prop_key (@UTIL_PROP_KEYS) {
+ my $prop = $attrib_ref->{util}->external_cfg_get($util_prop_key);
+ $attrib_ref->{prop_of}{$util_prop_key} = [$prop];
+ }
+}
+
+# Reads the mirror.target declaration from a config entry.
+sub _config_parse_target {
+ my ($attrib_ref, $ctx, $entry) = @_;
+ my $value = $entry->get_value();
+ # Note: it is easier to parse the value in reverse because a target may look
+ # like "path", "machine:path" or "logname at machine:path"
+ my ($path, $auth) = reverse(split(':', $value, 2));
+ my ($machine, $logname) = $auth ? reverse(split('@', $auth, 2)) : ();
+ if (!$path || ($logname && !$machine)) {
+ return $E->throw($E->CONFIG_VALUE, $entry);
+ }
+ $ctx->set_target_logname($logname);
+ $ctx->set_target_machine($machine);
+ $ctx->set_target_path($path);
+}
+
+# A hook command for the "inherit/use" declaration (extract).
+sub _config_parse_inherit_hook {
+ my ($attrib_ref, $ctx, $i_ctx) = @_;
+ $ctx->set_target_machine($i_ctx->get_target_machine());
+ _config_parse_inherit_hook_prop($attrib_ref, $ctx, $i_ctx);
+}
+
+# Turns a context into a list of configuration entries.
+sub _config_unparse {
+ my ($attrib_ref, $ctx) = @_;
+ ( ( $ctx->get_target_path()
+ ? FCM::Context::ConfigEntry->new({
+ label => $ctx->get_id() . q{.} . 'target',
+ value => (_target_and_authority($ctx))[0],
+ })
+ : ()
+ ),
+ _config_unparse_prop($attrib_ref, $ctx),
+ );
+}
+
+# Returns a new context.
+sub _ctx {
+ my ($attrib_ref, $id_of_class, $id) = @_;
+ FCM::Context::Make::Mirror->new({id => $id, id_of_class => $id_of_class});
+}
+
+# The main function.
+sub _main {
+ my ($attrib_ref, $m_ctx, $ctx) = @_;
+ if (!$ctx->get_target_path()) {
+ return $E->throw($E->MIRROR_NULL);
+ }
+ my $do_config_file = !_prop($attrib_ref, 'no-config-file', $ctx);
+ my @sources;
+ my @bad_step_list;
+ for my $step (@{$m_ctx->get_steps()}) {
+ my $ctx = $m_ctx->get_ctx_of($step);
+ if ( $ctx->get_status() eq $m_ctx->ST_OK
+ && $ctx->can('get_dest')
+ && -e $ctx->get_dest()
+ ) {
+ if ($do_config_file && !$ctx->can('MIRROR')) {
+ push(@bad_step_list, $step);
+ }
+ else {
+ push(@sources, $ctx->get_dest());
+ }
+ }
+ }
+ if (@bad_step_list) {
+ return $E->throw($E->MIRROR_SOURCE, \@bad_step_list);
+ }
+ for my $action (
+ \&_mirror_mkdir,
+ (@sources ? \&_mirror : ()),
+ ($do_config_file ? \&_mirror_config_file : ()),
+ \&_mirror_orig_config_file,
+ ) {
+ $action->($attrib_ref, $m_ctx, $ctx, \@sources);
+ }
+}
+
+# Creates a configuration file at the destination.
+sub _mirror_config_file {
+ my ($attrib_ref, $m_ctx, $ctx, $sources_ref) = @_;
+ my ($target) = _target_and_authority($ctx);
+ my $mirror_m_ctx = FCM::Context::Make->new({dest => '$HERE'});
+ my %no_inherit_from;
+ if (@{$m_ctx->get_inherit_ctx_list()}) {
+ # Inherited destinations
+ for my $i_m_ctx (@{$m_ctx->get_inherit_ctx_list()}) {
+ my $i_ctx = $i_m_ctx->get_ctx_of($ctx->get_id());
+ if (defined($i_ctx)) {
+ push(
+ @{$mirror_m_ctx->get_inherit_ctx_list()},
+ FCM::Context::Make->new({dest => $i_ctx->get_target_path()}),
+ );
+ }
+ }
+ # Completed steps, from which the targets can be sourced
+ DONE_STEP:
+ for my $step (@{$m_ctx->get_steps()}) {
+ my $step_ctx = $m_ctx->get_ctx_of($step);
+ if ( !defined($step_ctx)
+ || $step_ctx->get_status() ne $m_ctx->ST_OK
+ || !$step_ctx->can('get_target_of')
+ ) {
+ next DONE_STEP;
+ }
+ while (my ($key, $target) = each(%{$step_ctx->get_target_of()})) {
+ if (!$target->is_ok()) {
+ $no_inherit_from{$target->get_ns()} = 1;
+ }
+ }
+ }
+ }
+ # Steps to include in the configuration file
+ for my $step (_props($attrib_ref, 'config-file.steps', $ctx)) {
+ my $step_ctx = $m_ctx->get_ctx_of($step);
+ if ( !defined($step_ctx)
+ || $step_ctx->get_status() ne $m_ctx->ST_UNKNOWN
+ ) {
+ return $E->throw(
+ $E->MAKE_PROP_VALUE,
+ [[$ctx->get_id(), 'config-file.steps', q{}, $step]],
+ );
+ }
+ push(@{$mirror_m_ctx->get_steps()}, $step);
+ $mirror_m_ctx->get_ctx_of()->{$step} = dclone($step_ctx);
+ my $mirror_ctx = $mirror_m_ctx->get_ctx_of()->{$step};
+ if ($mirror_ctx->can('get_input_source_of')) {
+ %{$mirror_ctx->get_input_source_of()} = (
+ q{} => [map {abs2rel($_, $m_ctx->get_dest())} @{$sources_ref}],
+ );
+ }
+ if (keys(%no_inherit_from)) {
+ my @no_inherit_from_ns_list = sort keys(%no_inherit_from);
+ push(
+ @no_inherit_from_ns_list,
+ _props($attrib_ref, 'no-inherit-source', $mirror_ctx),
+ );
+ my $prop_value = FCM::Context::Make::Share::Property::Value->new({
+ value => join(
+ q{ },
+ map {s{['"\s]}{\\$1}gmsx; $_} @no_inherit_from_ns_list,
+ ),
+ });
+ my $prop = FCM::Context::Make::Share::Property->new({
+ id => 'no-inherit-source',
+ ctx_of => {q{} => $prop_value},
+ });
+ $mirror_ctx->get_prop_of()->{'no-inherit-source'} = $prop;
+ }
+ }
+ # Saves the configuration file
+ my @lines = map {$_ . "\n"}
+ $attrib_ref->{shared_util_of}{config}->unparse($mirror_m_ctx);
+ my $path = $attrib_ref->{shared_util_of}{dest}->path($ctx, 'config');
+ $attrib_ref->{util}->file_save($path, \@lines);
+ _mirror(
+ $attrib_ref, $m_ctx, $ctx,
+ [$path], $attrib_ref->{shared_util_of}{dest}->path($target, 'config'),
+ );
+}
+
+# Creates mirror destination.
+sub _mirror_mkdir {
+ my ($attrib_ref, $m_ctx, $ctx) = @_;
+ my ($target, $authority, $path) = _target_and_authority($ctx);
+ eval {
+ if ($authority) {
+ my @ssh
+ = (_shell_cmd_list($attrib_ref, 'ssh', $ctx), $authority);
+ if (!file_name_is_absolute($path)) {
+ my $value_hash_ref = _shell($attrib_ref, [@ssh, 'pwd']);
+ my $path_root = $value_hash_ref->{'o'};
+ chomp($path_root);
+ $ctx->set_target_path(rel2abs($path, $path_root));
+ }
+ _shell($attrib_ref, [@ssh, 'mkdir', '-p', $path]);
+ }
+ else {
+ if (!file_name_is_absolute($path)) {
+ $ctx->set_target_path(rel2abs($path));
+ }
+ if (!-d $path) {
+ mkpath($path);
+ }
+ }
+ };
+ if (my $e = $@) {
+ return $E->throw($E->MIRROR_TARGET, $target, $e);
+ }
+ 1;
+}
+
+# Mirror original configuration (by unparsing $m_ctx).
+sub _mirror_orig_config_file {
+ my ($attrib_ref, $m_ctx, $ctx) = @_;
+ my @lines = (
+ "# Original fcm make configuration.\n",
+ sprintf("# Generated by %s@%s at %s.\n",
+ scalar(getpwuid($<)),
+ hostname(),
+ strftime("%Y-%m-%dT%H:%M:%S%z", localtime()),
+ ),
+ map {$_ . "\n"} $attrib_ref->{shared_util_of}{config}->unparse($m_ctx),
+ );
+ my $path = $attrib_ref->{shared_util_of}{dest}->path($ctx, 'config-orig');
+ $attrib_ref->{util}->file_save($path, \@lines);
+ my ($target) = _target_and_authority($ctx);
+ _mirror(
+ $attrib_ref, $m_ctx, $ctx,
+ [$path],
+ $attrib_ref->{shared_util_of}{dest}->path($target, 'config-orig'),
+ );
+}
+
+# Mirrors.
+sub _mirror {
+ my ($attrib_ref, $m_ctx, $ctx, $sources_ref, $target) = @_;
+ $target ||= (_target_and_authority($ctx))[0];
+ $attrib_ref->{util}->event(
+ FCM::Context::Event->MAKE_MIRROR, $target, @{$sources_ref},
+ );
+ eval {
+ _shell(
+ $attrib_ref,
+ [ _shell_cmd_list($attrib_ref, 'rsync', $ctx),
+ @{$sources_ref},
+ $target,
+ ],
+ );
+ };
+ if (my $e = $@) {
+ return $E->throw($E->MIRROR, [$target, @{$sources_ref}], $e);
+ }
+ 1;
+}
+
+# Invokes a known shell command.
+sub _shell {
+ my ($attrib_ref, $command_list_ref) = @_;
+ my $value_hash_ref = $attrib_ref->{util}->shell_simple($command_list_ref);
+ if ($value_hash_ref->{rc}) {
+ return $E->throw(
+ $E->SHELL,
+ {command_list => $command_list_ref, %{$value_hash_ref}},
+ $value_hash_ref->{e},
+ );
+ }
+ $value_hash_ref;
+}
+
+# Returns a shell command and its flags from a named property.
+sub _shell_cmd_list {
+ my ($attrib_ref, $id, $ctx) = @_;
+ map {_props($attrib_ref, $_, $ctx)} ($id, $id . '.flags');
+}
+
+# Returns the authority and the target.
+sub _target_and_authority {
+ my ($ctx) = @_;
+ my $logname = $ctx->get_target_logname();
+ my $machine = $ctx->get_target_machine();
+ my $path = $ctx->get_target_path();
+ my $authority
+ = $logname && $machine ? $logname . '@' . $machine
+ : $logname ? $logname . '@' . 'localhost'
+ : $machine ? $machine
+ : undef
+ ;
+ my $target = $authority ? $authority . ':' . $path : $path;
+ ($target, $authority, $path);
+}
+
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::System::Make::Mirror
+
+=head1 SYNOPSIS
+
+ use FCM::System::Make::Mirror;
+ my $subsystem = FCM::System::Make::Mirror->new(\%attrib);
+ $subsystem->main($m_ctx, $ctx);
+
+=head1 DESCRIPTION
+
+Implements the mirror sub-system. An instance of this class is expected to be
+initialised and called by L<FCM::System::Make|FCM::System::Make>.
+
+=head1 METHODS
+
+See L<FCM::System::Make|FCM::System::Make> for detail.
+
+=head1 ATTRIBUTES
+
+The $class->new(\%attrib) method of this class supports the following
+attributes:
+
+=over 4
+
+=item config_parser_of
+
+A HASH to map the labels in a configuration file to their parsers. (default =
+%FCM::System::Make::Mirror::CONFIG_PARSER_OF)
+
+=item prop_of
+
+A HASH to map the names of the properties to their settings. Each setting
+is a 2-element ARRAY reference, where element [0] is the default setting
+and element [1] is a flag to indicate whether the property accepts a name-space
+or not. (default = %FCM::System::Make::Mirror::PROP_OF)
+
+=item shared_util_of
+
+See L<FCM::System::Make|FCM::System::Make> for detail.
+
+=item util
+
+See L<FCM::System::Make|FCM::System::Make> for detail.
+
+=back
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/System/Make/Preprocess.pm b/lib/FCM/System/Make/Preprocess.pm
new file mode 100644
index 0000000..890c6b3
--- /dev/null
+++ b/lib/FCM/System/Make/Preprocess.pm
@@ -0,0 +1,87 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+# ------------------------------------------------------------------------------
+package FCM::System::Make::Preprocess;
+use base qw{FCM::System::Make::Build};
+
+use FCM::System::Make::Build::FileType::CPP;
+use FCM::System::Make::Build::FileType::FPP;
+use FCM::System::Make::Build::FileType::H ;
+
+# Default target selection
+our %TARGET_SELECT_BY = (task => {'process' => 1});
+
+# Classes for working with typed source files
+our @FILE_TYPE_UTILS = (
+ 'FCM::System::Make::Build::FileType::FPP',
+ 'FCM::System::Make::Build::FileType::CPP',
+ 'FCM::System::Make::Build::FileType::HPP',
+);
+
+# Default properties
+my %PROP_OF = (
+ 'ignore-missing-dep-ns' => [q{}, undef],
+ 'no-step-source' => [q{}, undef],
+ 'no-inherit-source' => [q{}, undef],
+ 'no-inherit-target-category' => [q{}, undef],
+);
+
+# Returns an instance of FCM::System::Make::Build;
+sub new {
+ my ($class, $attrib_ref) = @_;
+ bless(
+ FCM::System::Make::Build->new({
+ target_select_by => {%TARGET_SELECT_BY},
+ file_type_utils => [@FILE_TYPE_UTILS],
+ prop_of => {%PROP_OF},
+ %{$attrib_ref},
+ }),
+ $class,
+ );
+}
+
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::System::Make::Preprocess
+
+=head1 SYNOPSIS
+
+ use FCM::System::Make::Preprocess;
+ my $system = FCM::System::Make::Preprocess->new(\%attrib);
+ $system->main(\%option_of, @args);
+
+=head1 DESCRIPTION
+
+A wrapper of L<FCM::System::Make::Build|FCM::System::Make::Build> with
+configuration to trigger the preprocessing of Fortran and C source files.
+
+=back
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
+
diff --git a/lib/FCM/System/Make/Share/Config.pm b/lib/FCM/System/Make/Share/Config.pm
new file mode 100644
index 0000000..ec2639a
--- /dev/null
+++ b/lib/FCM/System/Make/Share/Config.pm
@@ -0,0 +1,404 @@
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+use strict;
+use warnings;
+#-------------------------------------------------------------------------------
+
+package FCM::System::Make::Share::Config;
+use base qw{FCM::Class::CODE};
+
+use Cwd qw{cwd};
+use FCM::Context::ConfigEntry;
+use FCM::Context::Locator;
+use FCM::System::Exception;
+use File::Spec::Functions qw{file_name_is_absolute};
+use File::Temp;
+use Scalar::Util qw{blessed};
+
+# Alias to class name
+my $E = 'FCM::System::Exception';
+
+# Configuration parser label to action map
+my %CONFIG_PARSER_OF = (
+ 'dest' => \&_parse_dest,
+ 'use' => \&_parse_use,
+ 'step.class' => \&_parse_step_class,
+ 'steps' => \&_parse_steps,
+);
+
+__PACKAGE__->class(
+ {shared_util_of => '%', subsystem_of => '%', util => '&'},
+ {action_of => {parse => \&_parse, unparse => \&_unparse}},
+);
+
+# Get configuration file entries from an iterator, and use the entries to
+# populate the context of the current make.
+sub _parse {
+ my ($attrib_ref, $entry_callback_ref, $m_ctx, @args) = @_;
+ my $dir = $m_ctx->get_option_of('directory')
+ ? $m_ctx->get_option_of('directory') : cwd();
+ my $dir_locator = FCM::Context::Locator->new($dir);
+ my @config_file_paths = $m_ctx->get_option_of('config-file-path')
+ ? @{$m_ctx->get_option_of('config-file-path')} : ();
+ my @config_file_path_locators
+ = map {FCM::Context::Locator->new($_)} @config_file_paths;
+ my @config_file_names = $m_ctx->get_option_of('config-file')
+ ? @{$m_ctx->get_option_of('config-file')} : (undef);
+ my @config_reader_refs;
+ for my $config_file_name (@config_file_names) {
+ my $is_specified_name = 1;
+ if (!defined($config_file_name)) {
+ $config_file_name
+ = $attrib_ref->{shared_util_of}{dest}->path_of('config');
+ $is_specified_name = 0;
+ }
+ if ( $attrib_ref->{util}->uri_match($config_file_name)
+ || file_name_is_absolute($config_file_name)
+ ) {
+ push(@config_reader_refs, _get_config_reader(
+ $attrib_ref, $config_file_name, [@config_file_path_locators],
+ ));
+ }
+ else { # $config_file_name is relative
+ my $config_reader_ref;
+ HEAD:
+ for my $head_locator ($dir_locator, @config_file_path_locators) {
+ my $locator = $attrib_ref->{util}->loc_cat(
+ $head_locator, $config_file_name,
+ );
+ if ($attrib_ref->{util}->loc_exists($locator)) {
+ $config_reader_ref = _get_config_reader(
+ $attrib_ref, $locator, [@config_file_path_locators],
+ );
+ last HEAD;
+ }
+ }
+ if (defined($config_reader_ref)) {
+ push(@config_reader_refs, $config_reader_ref);
+ }
+ elsif ($is_specified_name) {
+ return $E->throw($E->MAKE_CFG_FILE, $config_file_name);
+ }
+ }
+ }
+ if (!@config_reader_refs) {
+ my $config_file_name = $attrib_ref->{shared_util_of}{dest}->path(
+ $dir_locator->get_value(), 'config',
+ );
+ if (-f $config_file_name) {
+ push(@config_reader_refs, _get_config_reader(
+ $attrib_ref, $config_file_name, [@config_file_path_locators],
+ ));
+ }
+ }
+ my $args_config_handle;
+ if (@args) {
+ $args_config_handle = File::Temp->new(
+ SUFFIX => '-fcm-make-args.cfg',
+ TEMPLATE => 'XXXXXX',
+ TMPDIR => 1,
+ );
+ for my $arg (@args) {
+ print($args_config_handle "$arg\n");
+ }
+ $args_config_handle->seek(0, 0);
+ push(@config_reader_refs, _get_config_reader(
+ $attrib_ref,
+ $args_config_handle->filename(),
+ [@config_file_path_locators],
+ ));
+ }
+ if (!@config_reader_refs) {
+ return $E->throw($E->MAKE_CFG);
+ }
+ my $entry_iter_ref = sub {
+ while (@config_reader_refs) {
+ my $entry = $config_reader_refs[0]->();
+ if (defined($entry)) {
+ return $entry;
+ }
+ shift(@config_reader_refs);
+ }
+ return undef;
+ };
+ my @unknown_entries;
+ while (defined(my $entry = $entry_iter_ref->())) {
+ if (defined($entry_callback_ref)) {
+ $entry_callback_ref->($entry);
+ }
+ if (exists($CONFIG_PARSER_OF{$entry->get_label()})) {
+ $CONFIG_PARSER_OF{$entry->get_label()}->(
+ $attrib_ref, $m_ctx, $entry,
+ );
+ }
+ else {
+ my ($id, $label) = split(qr{\.}msx, $entry->get_label(), 2);
+ if ( $label
+ && $label eq 'prop'
+ && exists($entry->get_modifier_of()->{'class'})
+ && exists($attrib_ref->{subsystem_of}{$id})
+ ) {
+ my $subsystem = $attrib_ref->{subsystem_of}{$id};
+ if (!$subsystem->config_parse_class_prop($entry, $label)) {
+ push(@unknown_entries, $entry);
+ }
+ }
+ else {
+ my $ctx = $m_ctx->get_ctx_of($id);
+ if ( !defined($ctx)
+ && exists($attrib_ref->{subsystem_of}{$id})
+ ) {
+ $ctx = $attrib_ref->{subsystem_of}{$id}->ctx($id, $id);
+ $m_ctx->get_ctx_of()->{$id} = $ctx;
+ }
+ my $rc;
+ if (defined($ctx)) {
+ my $id_of_class = $ctx->get_id_of_class();
+ my $subsystem = $attrib_ref->{subsystem_of}{$id_of_class};
+ $rc = $subsystem->config_parse($ctx, $entry, $label);
+ }
+ if (!$rc) {
+ push(@unknown_entries, $entry);
+ }
+ }
+ }
+ }
+ if (defined($args_config_handle)) {
+ $args_config_handle->close();
+ }
+ if (@unknown_entries) {
+ return $E->throw($E->CONFIG_UNKNOWN, \@unknown_entries);
+ }
+ $m_ctx;
+}
+
+# Returns a config reader.
+sub _get_config_reader {
+ my ($attrib_ref, $locator, $config_file_path_locators_ref) = @_;
+ if (!blessed($locator)) {
+ $locator = FCM::Context::Locator->new($locator);
+ }
+ $attrib_ref->{util}->config_reader(
+ $locator,
+ { event_level => $attrib_ref->{util}->util_of_report()->LOW,
+ include_paths => $config_file_path_locators_ref,
+ },
+ );
+}
+
+# Reads the dest declaration.
+sub _parse_dest {
+ my ($attrib_ref, $m_ctx, $entry) = @_;
+ $m_ctx->set_dest($entry->get_value());
+}
+
+# Reads the step.class declaration from a config entry.
+sub _parse_step_class {
+ my ($attrib_ref, $m_ctx, $entry) = @_;
+ my $id_of_class = $entry->get_value();
+ if (!exists($attrib_ref->{subsystem_of}{$id_of_class})) {
+ return $E->throw($E->CONFIG_VALUE, $entry);
+ }
+ my $subsystem = $attrib_ref->{subsystem_of}{$id_of_class};
+ for my $id (@{$entry->get_ns_list()}) {
+ if (!defined($m_ctx->get_ctx_of($id))) {
+ $m_ctx->get_ctx_of()->{$id} = $subsystem->ctx($id_of_class, $id);
+ }
+ }
+}
+
+# Reads the steps declaration from a config entry.
+sub _parse_steps {
+ my ($attrib_ref, $m_ctx, $entry) = @_;
+ my @steps = $entry->get_values();
+ $m_ctx->set_steps(\@steps);
+ for my $id (@steps) {
+ if (!defined($m_ctx->get_ctx_of($id))) {
+ if (!exists($attrib_ref->{subsystem_of}{$id})) {
+ return $E->throw($E->CONFIG_VALUE, $entry);
+ }
+ my $subsystem = $attrib_ref->{subsystem_of}{$id};
+ $m_ctx->get_ctx_of()->{$id} = $subsystem->ctx($id, $id);
+ }
+ }
+}
+
+# Reads the use declaration.
+sub _parse_use {
+ my ($attrib_ref, $m_ctx, $entry) = @_;
+ my $DEST = $attrib_ref->{shared_util_of}{dest};
+ my $inherit_ctx_list_ref = $m_ctx->get_inherit_ctx_list();
+ for my $value ($entry->get_values()) {
+ $value = $attrib_ref->{util}->file_tilde_expand($value);
+ my $i_m_ctx = eval {
+ $DEST->ctx_load(
+ $DEST->path($value, 'sys-ctx'),
+ blessed($m_ctx),
+ );
+ };
+ if (!defined($i_m_ctx) && (my $e = $@)) {
+ return $E->throw($E->CONFIG_VALUE, $entry, $e);
+ }
+ if ($i_m_ctx->get_status() != $i_m_ctx->ST_OK) {
+ return $E->throw($E->CONFIG_INHERIT, $entry);
+ }
+ push(@{$m_ctx->get_inherit_ctx_list()}, $i_m_ctx);
+ while (my ($id, $i_ctx) = each(%{$i_m_ctx->get_ctx_of()})) {
+ my $id_of_class = $i_ctx->get_id_of_class();
+ if (exists($attrib_ref->{subsystem_of}{$id_of_class})) {
+ my $subsystem = $attrib_ref->{subsystem_of}{$id_of_class};
+ if (!defined($m_ctx->get_ctx_of($id))) {
+ $m_ctx->get_ctx_of()->{$id}
+ = $subsystem->ctx($id_of_class, $id);
+ }
+ if ($subsystem->can('config_parse_inherit_hook')) {
+ $subsystem->config_parse_inherit_hook(
+ $m_ctx->get_ctx_of($id), $i_ctx,
+ );
+ }
+ }
+ }
+ if (!@{$m_ctx->get_steps()}) {
+ $m_ctx->set_steps([@{$i_m_ctx->get_steps()}]);
+ }
+ }
+}
+
+# Turns the context back into a config.
+sub _unparse {
+ my ($attrib_ref, $m_ctx) = @_;
+ my %subsystem_of = map {
+ my $id = $m_ctx->get_ctx_of()->{$_}->get_id_of_class();
+ ($id, $attrib_ref->{subsystem_of}->{$id});
+ } @{$m_ctx->get_steps()};
+ map {$_->as_string()} (
+ ( map { FCM::Context::ConfigEntry->new({
+ label => 'step.class',
+ ns_list => [$_->get_id()],
+ value => $_->get_id_of_class(),
+ });
+ }
+ grep {$_->get_id() ne $_->get_id_of_class()}
+ values(%{$m_ctx->get_ctx_of()})
+ ),
+ ( map { my ($action_ref, $label) = @{$_};
+ my $value = $action_ref->($attrib_ref, $m_ctx);
+ defined($value)
+ ? FCM::Context::ConfigEntry->new(
+ {label => $label, value => $value},
+ )
+ : ()
+ ;
+ }
+ ( [\&_unparse_use , 'use' ],
+ [\&_unparse_steps , 'steps'],
+ [sub {$m_ctx->get_dest()}, 'dest' ],
+ ),
+ ),
+ ( map { my $id = $_;
+ $subsystem_of{$id}->config_unparse_class_prop($id);
+ }
+ sort keys(%subsystem_of)
+ ),
+ ( map { my $ctx = $m_ctx->get_ctx_of()->{$_};
+ my $id_of_class = $ctx->get_id_of_class();
+ $subsystem_of{$id_of_class}->config_unparse($ctx);
+ }
+ @{$m_ctx->get_steps()}
+ ),
+ );
+}
+
+# Serializes a list of words.
+sub _unparse_join {
+ join(q{ }, map {s{(["'\s])}{\\$1}xms; $_} grep {defined()} @_);
+}
+
+# The value of "steps" declaration from the context.
+sub _unparse_steps {
+ my ($attrib_ref, $m_ctx) = @_;
+ if (!@{$m_ctx->get_steps()}) {
+ return;
+ }
+ _unparse_join(@{$m_ctx->get_steps()});
+}
+
+# The value of "use" declaration from the context.
+sub _unparse_use {
+ my ($attrib_ref, $m_ctx) = @_;
+ if (!@{$m_ctx->get_inherit_ctx_list()}) {
+ return;
+ }
+ my @i_ctx_list = @{$m_ctx->get_inherit_ctx_list()};
+ _unparse_join(map {$_->get_dest()} @i_ctx_list);
+}
+
+#-------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::System::Make::Share::Config
+
+=head1 SYNOPSIS
+
+ use FCM::System::Make::Share::Config;
+ my $instance = FCM::System::Make::Share::Config->new(\%attrib);
+ my $ok = $instance->parse($m_ctx, $entry_iter_ref);
+ my @entries = $instance->unparse($m_ctx);
+
+=head1 DESCRIPTION
+
+A helper class for (un)parsing make config entries into the make context.
+
+=head1 METHODS
+
+=over 4
+
+=item $class->new(\%attrib)
+
+Returns a new instance. The allowed elements for %attrib are:
+
+=over 4
+
+=item {shared_util_of}{dest}
+
+A helper object for manipulating the destination in a make context. Expects an
+instance of L<FCM::System::Make::Share::Dest|FCM::System::Make::Share::Dest>.
+
+=back
+
+=item $instance->parse($m_ctx, $entry_iter_ref)
+
+Parses entries returned by the $entry_iter_ref iterator into the $m_ctx.
+Throws a variety of L<FCM::System::Exception|FCM::System::Exception> if some
+data in the configuration file is incorrectly set.
+
+=item $instance->unparse($m_ctx)
+
+Turns $m_ctx back into a list of configuration entries.
+
+=back
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/System/Make/Share/Dest.pm b/lib/FCM/System/Make/Share/Dest.pm
new file mode 100644
index 0000000..c3f4ad6
--- /dev/null
+++ b/lib/FCM/System/Make/Share/Dest.pm
@@ -0,0 +1,438 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+
+# ------------------------------------------------------------------------------
+package FCM::System::Make::Share::Dest;
+use base qw{FCM::Class::CODE};
+
+use Cwd qw{cwd};
+use FCM::Context::Event;
+use FCM::System::Exception;
+use File::Basename qw{dirname};
+use File::Path qw{mkpath rmtree};
+use File::Spec::Functions qw{catfile rel2abs};
+use IO::File;
+use IO::Uncompress::Gunzip qw{gunzip};
+use IO::Compress::Gzip qw{gzip};
+use Scalar::Util qw{blessed};
+use Storable qw{fd_retrieve nstore_fd};
+use Sys::Hostname qw{hostname};
+
+# The relative paths for locating files in a destination
+our %PATH_OF = (
+ 'config' => 'fcm-make.cfg',
+ 'config-orig' => 'fcm-make.cfg.orig',
+ 'sys' => '.fcm-make',
+ 'sys-cache' => '.fcm-make/cache',
+ 'sys-config-as-parsed' => '.fcm-make/config-as-parsed.cfg',
+ 'sys-config-as-parsed-symlink' => 'fcm-make-as-parsed.cfg',
+ 'sys-config-on-success' => '.fcm-make/config-on-success.cfg',
+ 'sys-config-on-success-symlink' => 'fcm-make-on-success.cfg',
+ 'sys-ctx-uncompressed' => '.fcm-make/ctx',
+ 'sys-ctx' => '.fcm-make/ctx.gz',
+ 'sys-log' => '.fcm-make/log',
+ 'sys-log-symlink' => 'fcm-make.log',
+ 'sys-lock' => 'fcm-make.lock',
+ 'sys-lock-info' => 'fcm-make.lock/info.txt',
+ 'target' => '',
+);
+
+# Aliases to exception classes
+my $E = 'FCM::System::Exception';
+# List of actions
+my %ACTION_OF = (
+ ctx_load => \&_ctx_load,
+ dest_done => \&_dest_done,
+ dest_init => \&_dest_init,
+ path => \&_path,
+ paths => \&_paths,
+ path_of => sub {$_[0]->{'path_of'}{$_[1]}},
+ save => \&_save,
+ tidy => \&_tidy,
+);
+
+# Creates the class.
+__PACKAGE__->class(
+ {path_of => {isa => '%', default => {%PATH_OF}}, util => '&'},
+ {action_of => \%ACTION_OF},
+);
+
+# Loads a storable context from a path.
+sub _ctx_load {
+ my ($attrib_ref, $path, $expected_class) = @_;
+ my $ctx = eval {
+ my $handle = IO::File->new_tmpfile();
+ gunzip($path, $handle) || die($!);
+ $handle->seek(0, 0);
+ fd_retrieve($handle);
+ };
+ if (my $e = $@) {
+ return $E->throw($E->CACHE_LOAD, $path, $e);
+ }
+ if (!$ctx || !$ctx->isa($expected_class)) {
+ return $E->throw($E->CACHE_TYPE, $path);
+ }
+ return $ctx;
+}
+
+# Finalises the destination of a make context.
+sub _dest_done {
+ my ($attrib_ref, $m_ctx) = @_;
+ if (!$m_ctx->get_dest()) {
+ return;
+ }
+ my $dest = _path($attrib_ref, $m_ctx, 'sys-ctx-uncompressed');
+ my $dest_parent = dirname($dest);
+ if (-d $dest_parent) {
+ eval {
+ my $handle = IO::File->new_tmpfile();
+ nstore_fd($m_ctx, $handle) || die($!);
+ $handle->seek(0, 0) || die($!);
+ gzip($handle, _path($attrib_ref, $m_ctx, 'sys-ctx')) || die($!);
+ };
+ if (my $e = $@) {
+ return $E->throw($E->DEST_CREATE, $dest, $e);
+ }
+ }
+ my %ctx_of = %{$m_ctx->get_ctx_of()};
+ for my $path (
+ _path($attrib_ref, $m_ctx, 'sys'),
+ (map {_path($attrib_ref, $m_ctx, 'target', $_)} keys(%ctx_of)),
+ ) {
+ _tidy($attrib_ref, $path);
+ }
+ if ($m_ctx->get_dest_lock()) {
+ rmtree($m_ctx->get_dest_lock());
+ }
+}
+
+# Initialises the destination of a make context.
+sub _dest_init {
+ my ($attrib_ref, $m_ctx) = @_;
+ my %OPTION_OF = %{$m_ctx->get_option_of()};
+ # Select destination
+ my $dest
+ = $OPTION_OF{directory} ? $OPTION_OF{directory}
+ : $m_ctx->get_dest() ? $m_ctx->get_dest()
+ : cwd()
+ ;
+ $m_ctx->set_dest(rel2abs($dest));
+ # Check lock
+ my $lock = _path($attrib_ref, $m_ctx, 'sys-lock');
+ if (!$OPTION_OF{'ignore-lock'} && -e $lock) {
+ return $E->throw($E->DEST_LOCKED, $lock);
+ }
+ # Creates the lock (and the destination), if necessary
+ if (!-e $lock) {
+ eval {mkpath($lock)};
+ if (my $e = $@) {
+ return $E->throw($E->DEST_CREATE, $lock, $e);
+ }
+ my $lock_info = scalar(getpwuid($<)) . '@' . hostname() . ':' . $$;
+ _save($attrib_ref, $lock_info, $m_ctx, 'sys-lock-info');
+ }
+ $m_ctx->set_dest_lock($lock);
+ # Cleans items created by previous make, if necessary
+ for my $path (
+ _path($attrib_ref, $m_ctx, 'sys-config-as-parsed-symlink'),
+ _path($attrib_ref, $m_ctx, 'sys-config-on-success-symlink'),
+ _path($attrib_ref, $m_ctx, 'sys-config-on-success'),
+ _path($attrib_ref, $m_ctx, 'sys-log-symlink'),
+ ) {
+ eval {rmtree($path)};
+ if (my $e = $@) {
+ return $E->throw($E->DEST_CLEAN, $path, $e);
+ }
+ }
+ if ($OPTION_OF{new}) {
+ my @steps = @{$m_ctx->get_steps()};
+ for my $path (
+ _path($attrib_ref, $m_ctx, 'sys'),
+ (map {_path($attrib_ref, $m_ctx, 'target', $_)} @steps),
+ ) {
+ eval {rmtree($path)};
+ if (my $e = $@) {
+ return $E->throw($E->DEST_CLEAN, $path, $e);
+ }
+ }
+ }
+ # Loads context of previous make, if possible
+ my $prev_m_ctx = eval {
+ my $path = _path($attrib_ref, $m_ctx, 'sys-ctx');
+ -f $path ? _ctx_load($attrib_ref, $path, blessed($m_ctx)) : undef;
+ };
+ if (my $e = $@) {
+ if (!$E->caught($e) || $e->get_code() ne $E->CACHE_LOAD) {
+ die($e);
+ }
+ $@ = undef;
+ }
+ if (defined($prev_m_ctx)) {
+ $m_ctx->set_prev_ctx($prev_m_ctx);
+ }
+ else {
+ # Creates the system directory
+ my $sys_dir_path = _path($attrib_ref, $m_ctx, 'sys');
+ eval {mkpath($sys_dir_path)};
+ if (my $e = $@) {
+ return $E->throw($E->DEST_CREATE, $sys_dir_path, $e);
+ }
+ }
+ # Diagnostic
+ $attrib_ref->{util}->event(
+ FCM::Context::Event->MAKE_DEST,
+ $m_ctx, join('@', scalar(getpwuid($<)), hostname()),
+ );
+ 1;
+}
+
+# Returns the path of a named item relative to the context destination.
+sub _path {
+ my ($attrib_ref, $m_ctx, $key, @paths) = @_;
+ my $path
+ = blessed($m_ctx) && $m_ctx->can('get_dest') ? $m_ctx->get_dest()
+ : defined($m_ctx) ? $m_ctx
+ : cwd()
+ ;
+ catfile($path, split(q{/}, $attrib_ref->{path_of}{$key}), @paths);
+}
+
+# Returns an ARRAY reference containing the search paths of a named item
+# relative to the destinations of the context and its inherited contexts.
+sub _paths {
+ my ($attrib_ref, $m_ctx, $key, @paths) = @_;
+ my @dests;
+ my @ctx_list = ($m_ctx);
+ # Adds destinations from inherited contexts recursively
+ # Note: if A inherits from B and C, B from B1 and B2, and C from C1 and C2,
+ # the search path will be A, C, C2, C1, B, B2, B1.
+ while (my $current_ctx = pop(@ctx_list)) {
+ push(@ctx_list, @{$current_ctx->get_inherit_ctx_list()});
+ push(@dests, _path($attrib_ref, $current_ctx, $key, @paths));
+ }
+ return \@dests;
+}
+
+# Saves $item in a path given by _path($attrib_ref, $m_ctx, $key, @paths).
+sub _save {
+ my ($attrib_ref, $item, $m_ctx, $key, @paths) = @_;
+ my $path = _path($attrib_ref, $m_ctx, $key, @paths);
+ my @contents
+ = (ref($item) && ref($item) eq 'ARRAY') ? (map {$_ . "\n"} @{$item})
+ : ($item . "\n")
+ ;
+ $attrib_ref->{util}->file_save($path, \@contents);
+}
+
+# Removes empty directories in a tree.
+sub _tidy {
+ my ($attrib_ref, @paths) = @_;
+ # Selects only directories which are not symbolic links
+ my @items = map {[$_, undef, undef]} grep {-d && !-l} @paths;
+ while (my $item = pop(@items)) {
+ my ($path, $n_children_ref, $n_siblings_ref) = @{$item};
+ if (!defined($n_children_ref)) {
+ opendir(my $handle, $path)
+ || return $E->throw($E->DEST_CLEAN, $path, $!);
+ my @children = grep {$_ ne q{.} && $_ ne q{..}} (readdir($handle));
+ closedir($handle);
+ $n_children_ref = \scalar(@children);
+ if (@children) {
+ # Descends into directories
+ my @sub_dirs
+ = grep {-d && !-l} map {catfile($path, $_)} @children;
+ if (@sub_dirs == @children) {
+ # If all children are directories, it may be possible to
+ # remove this directory later if all children are empty
+ push(@items, [$path, $n_children_ref, $n_siblings_ref]);
+ }
+ push(@items, (map {[$_, undef, $n_children_ref]} @sub_dirs));
+ }
+ }
+ if (!${$n_children_ref}) { # i.e. directory is empty
+ rmdir($path) || return $E->throw($E->DEST_CLEAN, $path, $!);
+ if (defined($n_siblings_ref)) {
+ --${$n_siblings_ref};
+ }
+ }
+ }
+}
+
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::System::Make::Share::Dest
+
+=head1 SYNOPSIS
+
+ use FCM::System::Make::Share::Dest;
+ my $helper = FCM::System::Make::Share::Dest->new(\%attrib);
+ my $ctx = $helper->ctx_load($path, $expected_class);
+ my $path = $helper->path($m_ctx, $key);
+ # ...
+
+=head1 DESCRIPTION
+
+A helper class for manipulating the destination of a context in a FCM make
+sub-system, e.g. extract.
+
+=head1 METHODS
+
+=over 4
+
+=item $class->new(\%attrib)
+
+Returns a new instance. The %attrib should contain the following:
+
+=over 4
+
+=item dest_items
+
+An ARRAY containing the names of the items that can be created at the context
+destination.
+
+=item path_of
+
+A HASH to map the (keys) names of the items and (values) their relative paths
+(as ARRAY) in a context destination.
+
+=back
+
+=item $instance->ctx_load($path,$expected_class)
+
+Loads a storable context from $path and returns the context. The $expected_class
+is the expected class of the loaded context. The method die() if it fails to
+load the context or if the loaded context does not belong to the expected class.
+
+=item $instance->dest_done($ctx)
+
+Finalises the destination of $ctx by freezing the $ctx in the system directory,
+removing the lock file, and tidying up any empty directories created by the
+system.
+
+=item $instance->dest_init($ctx)
+
+Initialises the destination of $ctx by checking for a lock directory in the
+destination, creating a lock if possible, cleaning up items created by the
+previous make of the system if necessary, and setting up the system directory.
+
+=item $instance->path($ctx,$key, at paths)
+
+Returns the path of a named item ($key) relative to $ctx, which can either be a
+blessed object with a $ctx->get_dest() method, a scalar path, or undef (in which
+case, cwd() is used). If @paths are specified, they are concatenated at the end
+of the path.
+
+=item $instance->path_of($key)
+
+Returns the value of the named item in a make destination.
+
+=item $instance->paths($ctx,$key, at paths)
+
+Returns an ARRAY reference containing the search paths of a named item ($key)
+relative to the destinations of $ctx and its inherited contexts. If @paths are
+specified, they are concatenated at the end of each returned path.
+
+=item $instance->save($item,$ctx,$key, at paths)
+
+Saves $item in a path given by $instance->path($ctx,$key, at paths). $item can be a
+string or a reference to an ARRAY of strings. A "\n" is added to the end of each
+string.
+
+=item $instance->tidy(@paths)
+
+Recursively removes empty directories in @paths.
+
+=back
+
+=head1 CONSTANTS
+
+=over 4
+
+=item %FCM::System::Make::PATH_OF
+
+A HASH containing the default values of named paths in a make destination. The
+following keys are used by the system:
+
+=over 4
+
+=item config
+
+The standard path to the configuration file.
+
+=item sys
+
+The path to the system directory.
+
+=item sys-cache
+
+The path to the system cache directory.
+
+=item sys-config-as-parsed
+
+The path to the as-parsed configuration file.
+
+=item sys-config-on-success
+
+The path to the on-success configuration file.
+
+=item sys-ctx
+
+The path to the frozen make context (for retrieval by incremental makes).
+
+=item sys-ctx-uncompressed
+
+The path to the uncompressed form of sys-ctx.
+
+=item sys-lock
+
+The path to the lock directory.
+
+=item sys-lock-info
+
+The path to the lock info file.
+
+=item target
+
+The target destination of a make.
+
+=back
+
+=back
+
+=head1 DIAGNOSTICS
+
+=head2 FCM::System::Exception
+
+The methods of this class throws this exception on errors.
+
+=head1 TODO
+
+Time-stamp the as-parsed and the on-success configuration files.
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/System/Make/Share/Subsystem.pm b/lib/FCM/System/Make/Share/Subsystem.pm
new file mode 100644
index 0000000..3cda0c3
--- /dev/null
+++ b/lib/FCM/System/Make/Share/Subsystem.pm
@@ -0,0 +1,322 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+
+# ------------------------------------------------------------------------------
+package FCM::System::Make::Share::Subsystem;
+use base qw{Exporter};
+
+our @EXPORT = qw{
+ _config_parse
+ _config_parse_class_prop
+ _config_parse_prop
+ _config_parse_inherit_hook_prop
+ _config_unparse_class_prop
+ _config_unparse_join
+ _config_unparse_prop
+ _prop
+ _prop0
+ _props
+};
+
+use FCM::Context::ConfigEntry;
+use FCM::Context::Make::Share::Property;
+use FCM::System::Exception;
+use Storable qw{dclone};
+use Text::ParseWords qw{shellwords};
+
+use constant {PROP_DEFAULT => 0, PROP_NS_OK => 1};
+
+# Aliases
+my $E = 'FCM::System::Exception';
+
+# Parses a configuration entry into the context.
+sub _config_parse {
+ my ($attrib_ref, $ctx, $entry, $label) = @_;
+ my %config_parser_of = (
+ 'prop' => \&_config_parse_prop,
+ %{$attrib_ref->{config_parser_of}},
+ );
+ if (!$label || !exists($config_parser_of{$label})) {
+ return;
+ }
+ $config_parser_of{$label}->($attrib_ref, $ctx, $entry);
+ 1;
+}
+
+# Parses a configuration entry into the subsystem property.
+sub _config_parse_class_prop {
+ my ($attrib_ref, $entry, $label) = @_;
+ if ($label ne 'prop') {
+ return;
+ }
+ if (@{$entry->get_ns_list()}) {
+ return $E->throw($E->CONFIG_NS, $entry);
+ }
+ my @keys = grep {$_ ne 'class'} keys(%{$entry->get_modifier_of()});
+ if (grep {!exists($attrib_ref->{prop_of}{$_})} @keys) {
+ return $E->throw($E->CONFIG_MODIFIER, $entry);
+ }
+ for my $key (@keys) {
+ $attrib_ref->{prop_of}{$key}[PROP_DEFAULT] = $entry->get_value();
+ }
+ 1;
+}
+
+# Reads the ?.prop declaration from a config entry.
+sub _config_parse_prop {
+ my ($attrib_ref, $ctx, $entry) = @_;
+ for my $key (keys(%{$entry->get_modifier_of()})) {
+ my $prop = $ctx->get_prop_of($key);
+ if (!defined($prop)) {
+ if (!defined(_prop_default($attrib_ref, $key))) {
+ return $E->throw($E->CONFIG_MODIFIER, $entry);
+ }
+ $prop = FCM::Context::Make::Share::Property->new({id => $key});
+ $ctx->get_prop_of()->{$key} = $prop;
+ }
+ my $prop_ctx;
+ if (defined($entry->get_value())) {
+ $prop_ctx = $prop->CTX_VALUE->new({value => $entry->get_value()});
+ }
+ if (!@{$entry->get_ns_list()}) {
+ @{$entry->get_ns_list()} = (q{});
+ }
+ for my $ns (@{$entry->get_ns_list()}) {
+ if ($ns && !_prop_ns_ok($attrib_ref, $key)) {
+ return $E->throw($E->CONFIG_NS, $entry);
+ }
+ if (defined($prop_ctx)) {
+ $prop->get_ctx_of()->{$ns} = $prop_ctx;
+ }
+ elsif (exists($prop->get_ctx_of()->{$ns})) {
+ delete($prop->get_ctx_of()->{$ns});
+ }
+ }
+ }
+}
+
+# A hook command for the "inherit/use" declaration, inherit properties.
+sub _config_parse_inherit_hook_prop {
+ my ($attrib_ref, $ctx, $i_ctx) = @_;
+ while (my ($key, $i_prop) = each(%{$i_ctx->get_prop_of()})) {
+ if (!defined($ctx->get_prop_of($key))) {
+ $ctx->get_prop_of()->{$key} = dclone($i_prop);
+ }
+ my %prop_ctx_of = %{$ctx->get_prop_of($key)->get_ctx_of()};
+ while (my ($ns, $i_prop_ctx) = each(%{$i_prop->get_ctx_of()})) {
+ if ( !exists($prop_ctx_of{$ns})
+ || $prop_ctx_of{$ns}->get_inherited()
+ ) {
+ my $prop_ctx = dclone($i_prop_ctx);
+ $prop_ctx->set_inherited(1);
+ $ctx->get_prop_of($key)->get_ctx_of()->{$ns} = $prop_ctx;
+ }
+ }
+ }
+}
+
+# Serializes a list of words.
+sub _config_unparse_join {
+ join(
+ q{ },
+ (map {my $s = $_; $s =~ s{(["'\s])}{\\$1}gxms; $s} grep {defined()} @_),
+ );
+}
+
+# Entries of the class prop settings.
+sub _config_unparse_class_prop {
+ my ($attrib_ref, $id) = @_;
+ map {
+ my $key = $_;
+ FCM::Context::ConfigEntry->new({
+ label => join(q{.}, $id, 'prop'),
+ modifier_of => {'class' => 1, $key => 1},
+ value => $attrib_ref->{prop_of}{$key}[PROP_DEFAULT],
+ });
+ } sort keys(%{$attrib_ref->{prop_of}});
+}
+
+# Entries of the prop settings.
+sub _config_unparse_prop {
+ my ($attrib_ref, $ctx) = @_;
+ my $label = join(q{.}, $ctx->get_id(), 'prop');
+ my %prop_of = %{$ctx->get_prop_of()};
+ map {
+ my $key = $_;
+ my $setting = $prop_of{$key};
+ map {
+ my $ns = $_;
+ my $prop_ctx = $setting->get_ctx_of()->{$ns};
+ $prop_ctx->get_inherited()
+ ? ()
+ : FCM::Context::ConfigEntry->new({
+ label => $label,
+ modifier_of => {$key => 1},
+ ns_list => ($ns ? [$ns] : []),
+ value => $prop_ctx->get_value(),
+ });
+ } sort(keys(%{$setting->get_ctx_of()}));
+ } sort(keys(%prop_of));
+}
+
+# Returns the value of a named property (for a given $ns).
+sub _prop {
+ my ($attrib_ref, $id, $ctx, $ns) = @_;
+ my $setting = defined($ctx) ? $ctx->get_prop_of()->{$id} : undef;
+ if (!defined($ctx) || !defined($setting)) {
+ return _prop_default($attrib_ref, $id);
+ }
+ if (!_prop_ns_ok($attrib_ref, $id) || !$ns) {
+ my $prop_ctx = $setting->get_ctx();
+ return (
+ defined($prop_ctx) ? $prop_ctx->get_value()
+ : _prop_default($attrib_ref, $id)
+ );
+ }
+ my %prop_ctx_of = %{$setting->get_ctx_of()};
+ my $iter_ref
+ = $attrib_ref->{util}->ns_iter($ns, $attrib_ref->{util}->NS_ITER_UP);
+ while (defined(my $item = $iter_ref->())) {
+ if (exists($prop_ctx_of{$item}) && defined($prop_ctx_of{$item})) {
+ return $prop_ctx_of{$item}->get_value();
+ }
+ }
+ return _prop_default($attrib_ref, $id);
+}
+
+# Returns the first non-space value of a $setting for a given $ns.
+sub _prop0 {
+ (_props(@_))[0];
+}
+
+# Returns all suitable values of a $setting for a given $ns.
+sub _props {
+ my $prop = _prop(@_);
+ shellwords($prop ? $prop : q{});
+}
+
+# Returns the default value of a named property.
+sub _prop_default {
+ my ($attrib_ref, $id) = @_;
+ if (!exists($attrib_ref->{prop_of}{$id})) {
+ return;
+ }
+ $attrib_ref->{prop_of}{$id}[PROP_DEFAULT];
+}
+
+# Returns true if the given property can accept a name-space.
+sub _prop_ns_ok {
+ my ($attrib_ref, $id) = @_;
+ exists($attrib_ref->{prop_of}{$id})
+ && exists($attrib_ref->{prop_of}{$id}[PROP_NS_OK])
+ && $attrib_ref->{prop_of}{$id}[PROP_NS_OK]
+ ;
+}
+
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::System::Make::Share::Subsystem
+
+=head1 SYNOPSIS
+
+ use FCM::System::Make::Share::Subsystem;
+
+=head1 DESCRIPTION
+
+Provides common "local" functions for a make subsystem.
+
+=head1 FUNCTIONS
+
+The following functions are automatically exported by this module.
+
+=over 4
+
+=item _config_parse(\%attrib,$ctx,$entry,$label)
+
+Reads a configuration $entry into the $ctx context. The $label is the label of
+the $entry, but with the prefix (which should be the same as $ctx->get_id() plus
+a dot) removed.
+
+=item _config_parse_class_prop(\%attrib,$entry,$label)
+
+Reads a configuration $entry into the subsystem default property
+$attrib{prop_of}. The $label is the label of the $entry, but with the prefix
+(the subsystem ID plus a dot) removed.
+
+=item _config_parse_prop(\%attrib,$ctx,$entry)
+
+Reads a property configuration $entry into the $ctx context. This method may
+die() with a FCM::System::Exception on error. If the property modifier is
+invalid for the given subsystem, it returns an exception with the CODE
+FCM::System::Exception->CONFIG_MODIFIER. If the property does not support a
+namespace, it returns an exception with the CODE
+FCM::System::Exception->CONFIG_NS.
+
+=item _config_parse_inherit_hook_prop(\%attrib,$ctx,$i_ctx)
+
+The $ctx context is the current subsystem context and the $i_ctx context is the
+inherited subsystem context. Inherits property settings from $i_ctx into $ctx.
+
+=item _config_unparse_join(@list)
+
+Joins the @list into a string that can be parsed again by shellwords.
+
+=item _config_unparse_class_prop(\%attrib,$id)
+
+Turns the default properties in the current subsystem into a list of
+configuration entries. $id is the ID of the current subsystem.
+
+=item _config_unparse_prop(\%attrib,$ctx)
+
+Turns the properties in $ctx into a list of configuration entries.
+
+=item _prop(\%attrib,$id,$ctx,$ns)
+
+Returns the value of property $id. If the property does not exist, it returns
+undef. If the property is not defined in $ctx, it returns the default value. If
+the property is defined in $ctx, it returns the defined value in $ctx. If $ns is
+set and a name-space is allowed for the property, it walks the name-space to
+attempt to return the nearest value of the property for the given name-space.
+
+=item _prop0(\%attrib,$id,$ctx,$ns)
+
+Shorthand for (_props(\%attrib,$id,$ctx,$ns))[0].
+
+=item _props(\%attrib,$id,$ctx,$ns)
+
+Shorthand for shellwords(_prop(\%attrib,$id,$ctx,$ns)).
+
+=back
+
+=head1 DEPENDENCIES
+
+The %attrib argument to the functions in this module may require the following
+keys to be set correctly: {config_parser_of}, {prop_of}, {util}.
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/System/Misc.pm b/lib/FCM/System/Misc.pm
new file mode 100644
index 0000000..3315f64
--- /dev/null
+++ b/lib/FCM/System/Misc.pm
@@ -0,0 +1,354 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+# ------------------------------------------------------------------------------
+package FCM::System::Misc;
+use base qw{FCM::Class::CODE};
+
+use Cwd qw{cwd};
+use FCM::Context::Event;
+use FCM::Context::Locator;
+use FCM::System::Exception;
+use FCM::Util::ConfigReader;
+use File::Path qw{mkpath rmtree};
+use File::Spec::Functions qw{catfile};
+use List::Util qw{max};
+use Text::ParseWords qw{shellwords};
+
+# The (keys) named actions of this class and (values) their implementations.
+our %ACTION_OF = (
+ browse => \&_browse,
+ config_parse => \&_config_parse,
+ export_items => \&_export_items,
+ keyword_find => \&_keyword_find,
+ version => \&_version,
+);
+# Alias to exception class
+my $E = 'FCM::System::Exception';
+
+# Creates the class.
+__PACKAGE__->class({util => '&'}, {action_of => \%ACTION_OF});
+
+# Launches a web browser to display some version controlled resources.
+sub _browse {
+ my ($attrib_ref, $option_ref, @args) = @_;
+ my $UTIL = $attrib_ref->{util};
+ my @command = shellwords(
+ exists($option_ref->{browser}) ? $option_ref->{browser}
+ : $UTIL->external_cfg_get('browser')
+ );
+ if (!@args) {
+ @args = (cwd());
+ }
+ for my $value (@args) {
+ my $locator = FCM::Context::Locator->new($value);
+ my $url = $UTIL->loc_browser_url($locator);
+ my %value_of = %{$UTIL->shell_simple([@command, $url])};
+ if ($value_of{rc}) {
+ return $E->throw(
+ $E->SHELL,
+ {command_list => [@command, $url], %value_of},
+ $value_of{e},
+ );
+ }
+ $attrib_ref->{util}->event(FCM::Context::Event->OUT, $value_of{o});
+ }
+ return;
+}
+
+# Parses and displays the content of a FCM configuration file.
+sub _config_parse {
+ my ($attrib_ref, $option_ref, @args) = @_;
+ my $reader_attrib_ref;
+ if (exists($option_ref->{'fcm1'})) {
+ $reader_attrib_ref = \%FCM::Util::ConfigReader::FCM1_ATTRIB;
+ }
+ for my $value (@args) {
+ my $locator = FCM::Context::Locator->new($value);
+ my $iter = $attrib_ref->{util}->config_reader(
+ $locator, $reader_attrib_ref,
+ );
+ while (my $entry = $iter->()) {
+ $attrib_ref->{util}->event(
+ FCM::Context::Event->CONFIG_ENTRY,
+ $entry,
+ exists($option_ref->{'fcm1'}),
+ );
+ }
+ }
+ return;
+}
+
+# Exports directories in a project as sequential versioned items.
+sub _export_items {
+ my ($attrib_ref, $option_ref, $location) = @_;
+ if (!$location) {
+ return $E->throw($E->EXPORT_ITEMS_SRC);
+ }
+ $location ||= q{.};
+ my $UTIL = $attrib_ref->{util};
+ # Options and arguments
+ $option_ref->{directory} ||= cwd();
+ $option_ref->{'config-file'} ||= ['fcm-export-items.cfg'];
+ my $locator = FCM::Context::Locator->new($location);
+ $UTIL->loc_as_invariant($locator);
+ # Timer
+ my $time_start = time();
+ my $timer = $UTIL->timer();
+ my %EVENT = (
+ 'create' => sub {
+ $UTIL->event(FCM::Context::Event->EXPORT_ITEM_CREATE, @_);
+ },
+ 'delete' => sub {
+ $UTIL->event(FCM::Context::Event->EXPORT_ITEM_DELETE, @_);
+ },
+ 'timer' => sub {
+ $UTIL->event(
+ FCM::Context::Event->TIMER, 'export-items', $time_start, @_,
+ );
+ },
+ );
+ $EVENT{'timer'}->();
+ # Reads configuration file
+ my $config_reader = $attrib_ref->{util}->config_reader(
+ FCM::Context::Locator->new($option_ref->{'config-file'}->[0]),
+ { %FCM::Util::ConfigReader::FCM1_ATTRIB,
+ event_level => $attrib_ref->{util}->util_of_report()->LOW,
+ },
+ );
+ my %conditions_of;
+ while (defined(my $entry = $config_reader->())) {
+ # Value: conditions
+ my @conditions;
+ for my $word (shellwords($entry->get_value())) {
+ my ($operator, $rev) = $word =~ qr{\A ([<>]=?|[!=]=) (.+) \z}imsx;
+ if (!$operator || !$rev) {
+ return $E->throw($E->CONFIG_VALUE, $entry);
+ }
+ push(@conditions, $operator . $rev); # FIXME: keyword?
+ }
+ # Label: targets and namespaces
+ my ($target) = $entry->get_label() =~ qr{\A (.+) / \*\z}msx;
+ if ($target) {
+ my $l_target = $UTIL->loc_cat($locator, $target);
+ $UTIL->loc_find(
+ $l_target,
+ sub {
+ my ($l_child, $attrib_of_child_ref) = @_;
+ if (!$attrib_of_child_ref->{is_dir}) {
+ my $ns_of_child = $attrib_of_child_ref->{ns};
+ my $iter
+ = $UTIL->ns_iter($ns_of_child, $UTIL->NS_ITER_UP);
+ $iter->(); # discard
+ my $ns = $UTIL->ns_cat($target, $iter->());
+ if (!exists($conditions_of{$ns})) {
+ $conditions_of{$ns} = \@conditions;
+ }
+ }
+ },
+ );
+ }
+ else {
+ $conditions_of{$entry->get_label()} = \@conditions;
+ }
+ }
+ # Export
+ NS:
+ while (my ($ns, $conditions_ref) = each(%conditions_of)) {
+ # FIXME: this should be encapsulated by the locator util.
+ my @command_list = (
+ qw{svn log -q},
+ $UTIL->loc_cat($locator, $ns)->get_value(),
+ );
+ my %value_of = %{$UTIL->shell_simple(\@command_list)};
+ if ($value_of{rc}) {
+ return $E->throw(
+ $E->SHELL,
+ {command_list => \@command_list, %value_of},
+ $value_of{e},
+ );
+ }
+ my @revs = map {($_ =~ qr{\Ar(\d+)})} split("\n", $value_of{o});
+ my %v_of;
+ my $v = 0;
+ for my $rev (reverse(@revs)) {
+ $v_of{$rev} = 'v' . ++$v;
+ }
+ my %cur_v_of = %v_of;
+ # Exports only revisions matching the conditions
+ for my $condition (@{$conditions_ref}) {
+ for my $rev (keys(%cur_v_of)) {
+ if (!eval($rev . $condition)) {
+ delete($cur_v_of{$rev});
+ }
+ }
+ }
+ # Destination directory
+ my $path = catfile($option_ref->{directory}, $ns);
+ if (-d $path) {
+ if ($option_ref->{new} || !keys(%cur_v_of)) {
+ rmtree($path);
+ }
+ else {
+ # Delete excluded revisions if they exist in incremental mode
+ if (opendir(my $handle, $path)) {
+ while (my $item = readdir($handle)) {
+ if (exists($v_of{$item}) && !exists($cur_v_of{$item})) {
+ for (($item, $v_of{$item})) {
+ my $p = catfile($path, $_);
+ rmtree($p);
+ $EVENT{'delete'}->($ns, $item, $p);
+ }
+ }
+ }
+ closedir($handle);
+ }
+ }
+ }
+ if (!keys(%cur_v_of)) {
+ next NS;
+ }
+ if (!-d $path) {
+ mkpath($path);
+ }
+
+ # Exports each revision, and creates symlink for each v
+ while (my ($rev, $v) = each(%cur_v_of)) {
+ my $target = catfile($option_ref->{directory}, $ns, $v);
+ if (-l $target || -f $target) {
+ unlink($target);
+ $EVENT{'delete'}->($ns, $v, $target);
+ }
+ if (!-d $target) {
+ my $url_peg_rev = $UTIL->loc_cat($locator, $ns)->get_value();
+ my ($url) = $url_peg_rev =~ qr{\A(.*?)(?:@[^@/]+)?\z}msx;
+ my @command_list = (qw{svn export -q -r}, $rev, $url, $target);
+ my %value_of = %{$UTIL->shell_simple(\@command_list)};
+ if ($value_of{rc} || !-d $target) {
+ return $E->throw(
+ $E->SHELL,
+ {command_list => \@command_list, %value_of},
+ $value_of{e},
+ );
+ }
+ $EVENT{'create'}->($ns, $v, $target);
+ }
+ my $link = catfile($option_ref->{directory}, $ns, $rev);
+ if (-e $link && !-l $link) {
+ rmtree($link);
+ $EVENT{'delete'}->($ns, $rev, $link);
+ }
+ elsif (-l $link && readlink($link) ne $v) {
+ unlink($link);
+ $EVENT{'delete'}->($ns, $rev, $link);
+ }
+ if (!-e $link) {
+ symlink($v, $link);
+ $EVENT{'create'}->($ns, $rev, $link);
+ }
+ }
+
+ # Symbolic link to the "latest" version directory
+ my $link_of_latest = catfile($option_ref->{directory}, $ns, 'latest');
+ my $v_of_latest = $cur_v_of{max(keys(%cur_v_of))};
+ if (-e $link_of_latest && !-l $link_of_latest) {
+ rmtree($link_of_latest);
+ $EVENT{'delete'}->($ns, 'latest', $link_of_latest);
+ }
+ elsif (-l $link_of_latest && readlink($link_of_latest) ne $v_of_latest) {
+ unlink($link_of_latest);
+ $EVENT{'delete'}->($ns, 'latest', $link_of_latest);
+ }
+ if (!-l $link_of_latest) {
+ symlink($v_of_latest, $link_of_latest);
+ $EVENT{'create'}->($ns, 'latest', $link_of_latest);
+ }
+ }
+ $EVENT{'timer'}->($timer->());
+}
+
+# Searches FCM keywords.
+sub _keyword_find {
+ my ($attrib_ref, $option_ref, @args) = @_;
+ my $UTIL = $attrib_ref->{util};
+ my @entries;
+ if (@args) {
+ for my $key (@args) {
+ my $iter = $UTIL->loc_kw_iter(FCM::Context::Locator->new($key));
+ while (my $entry = $iter->()) {
+ if (!$entry->get_implied()) {
+ $UTIL->loc_kw_load_rev_prop($entry);
+ push(@entries, $entry);
+ }
+ }
+ }
+ }
+ else {
+ @entries = values(%{$UTIL->loc_kw_ctx()->get_entry_by_key()});
+ }
+ for my $entry (sort {$a->get_key() cmp $b->get_key()} @entries) {
+ $UTIL->event(FCM::Context::Event->KEYWORD_ENTRY, $entry);
+ }
+ return;
+}
+
+# Emit an FCM::Context::Event->OUT event to print FCM's version.
+sub _version {
+ my ($attrib_ref, $option_ref, @args) = @_;
+ my $UTIL = $attrib_ref->{util};
+ $UTIL->event(FCM::Context::Event->OUT, $UTIL->version(@_) . "\n");
+ return;
+}
+
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::System::Misc
+
+=head1 SYNOPSIS
+
+ use FCM::System::Misc;
+ my $system = FCM::System::Misc->new(\%attrib);
+ $system->keyword_find(@args);
+
+=head1 DESCRIPTION
+
+The rest of the FCM system.
+
+=head1 METHODS
+
+Implements the browse(), config_parse(), export_items(), keyword_find() and
+version() methods for L<FCM::System|FCM::System>. See L<FCM::System|FCM::System>
+for a description of the calling interfaces of these functions.
+
+=head1 DIAGNOSTICS
+
+=head2 FCM::System::Exception
+
+Methods of this class may throw a FCM::System::Exception.
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/System/Old.pm b/lib/FCM/System/Old.pm
new file mode 100644
index 0000000..f62b846
--- /dev/null
+++ b/lib/FCM/System/Old.pm
@@ -0,0 +1,142 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+# ------------------------------------------------------------------------------
+package FCM::System::Old;
+use base qw{FCM::Class::CODE};
+
+use Cwd qw{cwd};
+use FCM1::Build;
+use FCM1::Config;
+use FCM1::Extract;
+#use FCM1::ExtractConfigComparator;
+use FCM1::Keyword;
+
+my %CLASS_OF = (build => 'FCM1::Build', extract => 'FCM1::Extract');
+
+my %KEY_OF = (
+ 'archive' => 'ARCHIVE',
+ 'clean' => 'CLEAN',
+ 'full' => 'FULL',
+ 'ignore-lock' => 'IGNORE_LOCK',
+ 'jobs' => 'JOBS',
+ 'stage' => 'STAGE',
+ 'targets' => 'TARGETS',
+);
+
+__PACKAGE__->class(
+ {util => '&'},
+ { init => \&_init,
+ action_of => {
+ build => sub {_run('build', @_)},
+ config_compare => \&_config_compare,
+ extract => sub {_run('extract', @_)},
+ },
+ },
+);
+
+sub _init {
+ my ($attrib_ref) = @_;
+ if (!defined(FCM1::Keyword::get_util())) {
+ FCM1::Keyword::set_util($attrib_ref->{util});
+ }
+}
+
+sub _config_compare {
+ my ($attrib_ref, $option_hash_ref, @args) = @_;
+ $attrib_ref->{util}->class_load('FCM1::CmUrl');
+ $attrib_ref->{util}->class_load('FCM1::ExtractConfigComparator');
+ if (exists($option_hash_ref->{verbosity})) {
+ FCM1::Config->instance()->verbose($option_hash_ref->{verbosity});
+ }
+ my %option = %{$option_hash_ref};
+ if (exists($option{'wiki-format'})) {
+ $option{'wiki'} = delete($option{'wiki-format'});
+ }
+ my $system = FCM1::ExtractConfigComparator->new({files => \@args, %option});
+ $system->invoke();
+}
+
+sub _run {
+ my ($key, $attrib_ref, $option_hash_ref, @args) = @_;
+ if (exists($option_hash_ref->{targets})) {
+ @{$option_hash_ref->{targets}}
+ = split(qr{:}msx, join(':', @{$option_hash_ref->{targets}}));
+ }
+ if (exists($option_hash_ref->{verbosity})) {
+ FCM1::Config->instance()->verbose($option_hash_ref->{verbosity});
+ }
+ my $system = $CLASS_OF{$key}->new();
+ my $path_to_cfg = @args ? $args[0] : cwd();
+ $system->cfg()->src($path_to_cfg);
+ my %option_of;
+ while (my ($key, $value) = each(%{$option_hash_ref})) {
+ if (exists($KEY_OF{$key})) {
+ $option_of{$KEY_OF{$key}} = $value;
+ }
+ }
+ $system->invoke(%option_of);
+}
+
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::System::Old
+
+=head1 SYNOPSIS
+
+ use FCM::System::Old;
+ my $system = FCM::System::Old->new();
+ $system->('extract', \%option, \@args);
+
+=head1 DESCRIPTION
+
+Provides a compatibility layer for obsolete FCM 1 functionalities.
+
+=head1 METHODS
+
+=over 4
+
+=item $class->new()
+
+Creates and returns an instance of this class.
+
+=item $instance->build(\%option, at args)
+
+Invokes the FCM 1 build system.
+
+=item $instance->config_compare(\%option, at args)
+
+Invokes the FCM 1 cmp-ext-cfg application.
+
+=item $instance->extract(\%option, at args)
+
+Invokes the FCM 1 extract system.
+
+=back
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/Util.pm b/lib/FCM/Util.pm
new file mode 100644
index 0000000..2019eaa
--- /dev/null
+++ b/lib/FCM/Util.pm
@@ -0,0 +1,1023 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+# ------------------------------------------------------------------------------
+
+package FCM::Util;
+use base qw{FCM::Class::CODE};
+
+use Digest::MD5;
+use FCM::Context::Event;
+use FCM::Context::Locator;
+use FCM::Util::ConfigReader;
+use FCM::Util::ConfigUpgrade;
+use FCM::Util::Event;
+use FCM::Util::Exception;
+use FCM::Util::Locator;
+use FCM::Util::Reporter;
+use FCM::Util::Shell;
+use FCM::Util::TaskRunner;
+use File::Basename qw{basename dirname};
+use File::Path qw{mkpath};
+use File::Spec::Functions qw{catfile};
+use FindBin;
+use Scalar::Util qw{blessed reftype};
+use Text::ParseWords qw{shellwords};
+use Time::HiRes qw{gettimeofday tv_interval};
+
+use constant {NS_ITER_UP => 1};
+
+# The (keys) named actions of this class and (values) their implementations.
+our %ACTION_OF = (
+ cfg_init => \&_cfg_init,
+ class_load => \&_class_load,
+ config_reader => _util_of_func('config_reader', 'main'),
+ external_cfg_get => \&_external_cfg_get,
+ event => \&_event,
+ file_ext => \&_file_ext,
+ file_head => \&_file_head,
+ file_load => \&_file_load,
+ file_load_handle => \&_file_load_handle,
+ file_md5 => \&_file_md5,
+ file_save => \&_file_save,
+ file_tilde_expand => \&_file_tilde_expand,
+ hash_cmp => \&_hash_cmp,
+ loc_as_invariant => _util_of_loc_func('as_invariant'),
+ loc_as_keyword => _util_of_loc_func('as_keyword'),
+ loc_as_normalised => _util_of_loc_func('as_normalised'),
+ loc_as_parsed => _util_of_loc_func('as_parsed'),
+ loc_browser_url => _util_of_loc_func('browser_url'),
+ loc_cat => _util_of_loc_func('cat'),
+ loc_dir => _util_of_loc_func('dir'),
+ loc_export => _util_of_loc_func('export'),
+ loc_export_ok => _util_of_loc_func('export_ok'),
+ loc_exists => _util_of_loc_func('test_exists'),
+ loc_find => _util_of_loc_func('find'),
+ loc_kw_ctx => _util_of_loc_func('kw_ctx'),
+ loc_kw_ctx_load => _util_of_loc_func('kw_ctx_load'),
+ loc_kw_iter => _util_of_loc_func('kw_iter'),
+ loc_kw_load_rev_prop => _util_of_loc_func('kw_load_rev_prop'),
+ loc_kw_prefix => _util_of_func('locator', 'kw_prefix'),
+ loc_origin => _util_of_loc_func('origin'),
+ loc_reader => _util_of_loc_func('reader'),
+ loc_rel2abs => _util_of_loc_func('rel2abs'),
+ loc_trunk_at_head => _util_of_loc_func('trunk_at_head'),
+ loc_what_type => _util_of_loc_func('what_type'),
+ loc_up_iter => _util_of_loc_func('up_iter'),
+ ns_cat => \&_ns_cat,
+ ns_common => \&_ns_common,
+ ns_in_set => \&_ns_in_set,
+ ns_iter => \&_ns_iter,
+ ns_sep => sub {$_[0]->{ns_sep}},
+ report => _util_of_func('reporter', 'report'),
+ shell => _util_of_func('shell', 'invoke'),
+ shell_simple => _util_of_func('shell', 'invoke_simple'),
+ shell_which => _util_of_func('shell', 'which'),
+ task_runner => _util_of_func('task_runner', 'main'),
+ timer => \&_timer,
+ uri_match => \&_uri_match,
+ util_of_event => _util_impl_func('event'),
+ util_of_report => _util_impl_func('reporter'),
+ version => \&_version,
+);
+# The default paths to the configuration files.
+our @FCM1_KEYWORD_FILES = (
+ catfile((getpwuid($<))[7], qw{.fcm}),
+);
+our @CONF_PATHS = (
+ catfile($FindBin::Bin, qw{.. etc fcm}),
+ catfile((getpwuid($<))[7], qw{.met-um fcm}),
+ catfile((getpwuid($<))[7], qw{.metomi fcm}),
+);
+our %CFG_BASENAME_OF = (
+ external => 'external.cfg',
+ keyword => 'keyword.cfg',
+);
+# Values of external commands
+our %EXTERNAL_VALUE_OF = (
+ 'browser' => 'firefox',
+ 'diff3' => 'diff3',
+ 'diff3.flags' => '-E -m',
+ 'graphic-diff' => 'xxdiff',
+ 'graphic-merge' => 'xxdiff',
+ 'ssh' => 'ssh',
+ 'ssh.flags' => '-n -oBatchMode=yes',
+ 'rsync' => 'rsync',
+ 'rsync.flags' => '-a --exclude=.* --delete-excluded --timeout=900'
+ . ' --rsh="ssh -oBatchMode=yes"',
+);
+# The name-space separator
+our $NS_SEP = '/';
+# The (keys) named utilities and their implementation classes.
+our %UTIL_CLASS_OF = (
+ config_reader => 'FCM::Util::ConfigReader',
+ event => 'FCM::Util::Event',
+ locator => 'FCM::Util::Locator',
+ reporter => 'FCM::Util::Reporter',
+ shell => 'FCM::Util::Shell',
+ task_runner => 'FCM::Util::TaskRunner',
+);
+
+# Alias
+my $E = 'FCM::Util::Exception';
+
+# Regular expression: match a URI
+my $RE_URI = qr/
+ \A (?# start)
+ ( (?# capture 1, scheme, start)
+ [A-Za-z] (?# alpha)
+ [\w\+\-\.]* (?# optional alpha, numeric, plus, minus and dot)
+ ) (?# capture 1, scheme, end)
+ : (?# colon)
+ (.*) (?# capture 2, opaque, rest of string)
+ \z (?# end)
+/xms;
+
+# Creates the class.
+__PACKAGE__->class(
+ { cfg_basename_of => {isa => '%', default => {%CFG_BASENAME_OF}},
+ conf_paths => {isa => '@', default => [@CONF_PATHS]},
+ event => '&',
+ external_value_of => {isa => '%', default => {%EXTERNAL_VALUE_OF}},
+ ns_sep => {isa => '$', default => $NS_SEP},
+ util_class_of => {isa => '%', default => {%UTIL_CLASS_OF}},
+ util_of => '%',
+ },
+ {init => \&_init, action_of => \%ACTION_OF},
+);
+
+# Initialises attributes.
+sub _init {
+ my ($attrib_ref, $self) = @_;
+ # Initialise the utilities
+ while (my ($key, $util_class) = each(%{$attrib_ref->{util_class_of}})) {
+ if (!defined($attrib_ref->{util_of}{$key})) {
+ _class_load($attrib_ref, $util_class);
+ $attrib_ref->{util_of}{$key} = $util_class->new({util => $self});
+ }
+ }
+ if (exists($ENV{FCM_CONF_PATH})) {
+ $attrib_ref->{conf_paths} = [shellwords($ENV{FCM_CONF_PATH})];
+ }
+}
+
+# Loads the named configuration from its configuration files.
+sub _cfg_init {
+ my ($attrib_ref, $basename, $action_ref) = @_;
+ if (exists($ENV{FCM_CONF_PATH})) {
+ $attrib_ref->{conf_paths} = [shellwords($ENV{FCM_CONF_PATH})];
+ }
+ for my $path (
+ grep {-f} map {catfile($_, $basename)} @{$attrib_ref->{conf_paths}}
+ ) {
+ my $config_reader = $ACTION_OF{config_reader}->(
+ $attrib_ref, FCM::Context::Locator->new($path),
+ );
+ $action_ref->($config_reader);
+ }
+}
+
+# Loads a class/package.
+sub _class_load {
+ my ($attrib_ref, $name, $test_method) = @_;
+ $test_method ||= 'new';
+ if (!UNIVERSAL::can($name, $test_method)) {
+ eval('require ' . $name);
+ if (my $e = $@) {
+ return $E->throw($E->CLASS_LOADER, $name, $e);
+ }
+ }
+ return $name;
+}
+
+# Invokes an event.
+sub _event {
+ my ($attrib_ref, $event, @args) = @_;
+ if (!blessed($event)) {
+ $event = FCM::Context::Event->new({code => $event, args => \@args}),
+ }
+ $attrib_ref->{'util_of'}{'event'}->main($event);
+}
+
+# Returns the value of an external tool.
+{ my $EXTERNAL_CFG_INIT;
+ sub _external_cfg_get {
+ my ($attrib_ref, $key) = @_;
+ my $value_hash_ref = $attrib_ref->{external_value_of};
+ if (!$EXTERNAL_CFG_INIT) {
+ $EXTERNAL_CFG_INIT = 1;
+ _cfg_init(
+ $attrib_ref,
+ $attrib_ref->{cfg_basename_of}{external},
+ sub {
+ my $config_reader = shift();
+ while (defined(my $entry = $config_reader->())) {
+ my $k = $entry->get_label();
+ if ($k && exists($value_hash_ref->{$k})) {
+ $value_hash_ref->{$k} = $entry->get_value();
+ }
+ }
+ }
+ );
+ }
+ if (!$key || !exists($value_hash_ref->{$key})) {
+ return;
+ }
+ return $value_hash_ref->{$key};
+ }
+}
+
+# Returns the file extension of a file system path.
+sub _file_ext {
+ my ($attrib_ref, $path) = @_;
+ my $pos_of_dot = rindex($path, q{.});
+ if ($pos_of_dot == -1) {
+ return (wantarray() ? (undef, $path) : undef);
+ }
+ my $ext = substr($path, $pos_of_dot + 1);
+ wantarray() ? ($ext, substr($path, 0, $pos_of_dot)) : $ext;
+}
+
+# Loads the first $n lines from a file system path.
+sub _file_head {
+ my ($attrib_ref, $path, $n) = @_;
+ $n ||= 1;
+ my $handle = _file_load_handle(@_);
+ my $content = q{};
+ for (1 .. $n) {
+ $content .= readline($handle);
+ }
+ close($handle);
+ (wantarray() ? (map {$_ . "\n"} split("\n", $content)) : $content);
+}
+
+# Loads the contents from a file system path.
+sub _file_load {
+ my ($attrib_ref, $path) = @_;
+ my $handle = _file_load_handle(@_);
+ my $content = do {local($/); readline($handle)};
+ close($handle);
+ (wantarray() ? (map {$_ . "\n"} split("\n", $content)) : $content);
+}
+
+# Opens a file handle to read from a file system path.
+sub _file_load_handle {
+ my ($attrib_ref, $path) = @_;
+ open(my($handle), '<', $path) || return $E->throw($E->IO, $path, $!);
+ $handle;
+}
+
+# Returns the MD5 checksum of the content in a file system path.
+sub _file_md5 {
+ my ($attrib_ref, $path) = @_;
+ my $handle = _file_load_handle($attrib_ref, $path);
+ binmode($handle);
+ my $digest = Digest::MD5->new();
+ $digest->addfile($handle);
+ my $checksum = $digest->hexdigest();
+ close($handle);
+ return $checksum;
+}
+
+# Saves content to a file system path.
+sub _file_save {
+ my ($attrib_ref, $path, $content) = @_;
+ if (!-e dirname($path)) {
+ eval {mkpath(dirname($path))};
+ if (my $e = $@) {
+ return $E->throw($E->IO, $path, $e);
+ }
+ }
+ open(my($handle), '>', $path) || return $E->throw($E->IO, $path, $!);
+ if (ref($content) && ref($content) eq 'ARRAY') {
+ print($handle @{$content}) || return $E->throw($E->IO, $path, $!);
+ }
+ else {
+ print($handle $content) || return $E->throw($E->IO, $path, $!);
+ }
+ close($handle) || return $E->throw($E->IO, $path, $!);
+}
+
+# Expand leading ~ and ~USER syntax in $path and return the resulting string.
+sub _file_tilde_expand {
+ my ($attrib_ref, $path) = @_;
+ $path =~ s{\A~([^/]*)}{$1 ? (getpwnam($1))[7] : (getpwuid($<))[7]}exms;
+ return $path;
+}
+
+# Compares contents of 2 HASH references.
+sub _hash_cmp {
+ my ($attrib_ref, $hash_1_ref, $hash_2_ref, $keys_only) = @_;
+ my %hash_2 = %{$hash_2_ref};
+ my %modified;
+ while (my ($key, $v1) = each(%{$hash_1_ref})) {
+ if (exists($hash_2{$key})) {
+ my $v2 = $hash_2{$key};
+ if ( !$keys_only
+ && (
+ defined($v1) && defined($v2) && $v1 ne $v2
+ || defined($v1) && !defined($v2)
+ || !defined($v1) && defined($v2)
+ )
+ ) {
+ $modified{$key} = 0;
+ }
+ delete($hash_2{$key});
+ }
+ else {
+ $modified{$key} = -1;
+ }
+ }
+ while (my $key = each(%hash_2)) {
+ if (!exists($hash_1_ref->{$key})) {
+ $modified{$key} = 1;
+ }
+ }
+ return %modified;
+}
+
+# Concatenates 2 name-spaces.
+sub _ns_cat {
+ my ($attrib_ref, @ns_list) = @_;
+ join(
+ $attrib_ref->{ns_sep},
+ grep {$_ && $_ ne $attrib_ref->{ns_sep}} @ns_list,
+ );
+}
+
+# Returns the common parts of 2 name-spaces.
+sub _ns_common {
+ my ($attrib_ref, $ns1, $ns2) = @_;
+ my $iter1 = _ns_iter($attrib_ref, $ns1);
+ my $iter2 = _ns_iter($attrib_ref, $ns2);
+ my $common_ns = q{};
+ while (defined(my $s1 = $iter1->()) && defined(my $s2 = $iter2->())) {
+ if ($s1 ne $s2) {
+ return $common_ns;
+ }
+ $common_ns = $s1;
+ }
+ return $common_ns;
+}
+
+# Returns true if $ns is in one of the name-spaces given by keys(%set).
+sub _ns_in_set {
+ my ($attrib_ref, $ns, $ns_set_ref) = @_;
+ if (!keys(%{$ns_set_ref})) {
+ return;
+ }
+ my @ns_list;
+ my $ns_iter = _ns_iter($attrib_ref, $ns);
+ while (defined(my $n = $ns_iter->())) {
+ push(@ns_list, $n);
+ }
+ grep {exists($ns_set_ref->{$_})} @ns_list;
+}
+
+# Returns an iterator to walk up/down a name-space.
+sub _ns_iter {
+ my ($attrib_ref, $ns, $up) = @_;
+ if ($ns eq $attrib_ref->{ns_sep}) {
+ $ns = q{};
+ }
+ my @give = split($attrib_ref->{ns_sep}, $ns);
+ my @take = ();
+ my $next = q{};
+ if ($up) {
+ @give = reverse(@give);
+ $next = $ns;
+ }
+ sub {
+ my $ret = $next;
+ $next = undef;
+ if (@give) {
+ push(@take, shift(@give));
+ $next = join($attrib_ref->{ns_sep}, ($up ? reverse(@give) : @take));
+ }
+ return $ret;
+ };
+}
+
+# Returns a timer.
+sub _timer {
+ my ($attrib_ref, $start_ref) = @_;
+ $start_ref ||= [gettimeofday()];
+ sub {tv_interval($start_ref)};
+}
+
+# Matches a URI.
+sub _uri_match {
+ my ($attrib_ref, $string) = @_;
+ $string =~ $RE_URI;
+}
+
+# Returns a function to return/set the object in the "util_of" basket.
+sub _util_impl_func {
+ my ($id) = @_;
+ sub {
+ my ($attrib_ref, $value) = @_;
+ if (defined($value) && ref($value) && reftype($value) eq 'CODE') {
+ $attrib_ref->{'util_of'}{$id} = $value;
+ }
+ $attrib_ref->{'util_of'}{$id};
+ };
+}
+
+# Returns a function to delegate a method to a utility in the "util_of" basket.
+sub _util_of_func {
+ my ($id, $method) = @_;
+ sub {
+ my $attrib_ref = shift();
+ $attrib_ref->{util_of}{$id}->(($method ? ($method) : ()), @_);
+ };
+}
+
+# Returns a function to delegate a method to the locator utility.
+{ my $KEYWORD_CFG_INIT;
+ sub _util_of_loc_func {
+ my ($method) = @_;
+ sub {
+ my $attrib_ref = shift();
+ if (!$KEYWORD_CFG_INIT) {
+ $KEYWORD_CFG_INIT = 1;
+ my $config_upgrade = FCM::Util::ConfigUpgrade->new();
+ for my $path (grep {-f} @FCM1_KEYWORD_FILES) {
+ my $config_reader = $ACTION_OF{config_reader}->(
+ $attrib_ref,
+ FCM::Context::Locator->new($path),
+ \%FCM::Util::ConfigReader::FCM1_ATTRIB,
+ );
+ $ACTION_OF{loc_kw_ctx_load}->(
+ $attrib_ref,
+ sub {$config_upgrade->upgrade($config_reader->())},
+ );
+ }
+ _cfg_init(
+ $attrib_ref,
+ $attrib_ref->{cfg_basename_of}{keyword},
+ sub {$ACTION_OF{loc_kw_ctx_load}->($attrib_ref, @_)},
+ );
+ }
+ $attrib_ref->{util_of}{locator}->($method, @_);
+ };
+ }
+}
+
+# Returns the FCM version string.
+sub _version {
+ my ($attrib_ref) = @_;
+ # Try "git describe"
+ my $value_hash_ref = eval {
+ $ACTION_OF{shell_simple}->(
+ $attrib_ref,
+ ['git', "--git-dir=$FindBin::Bin/../.git", 'describe'],
+ );
+ };
+ if (my $e = $@) {
+ if (!$E->caught($e)) {
+ die($e);
+ }
+ $@ = undef;
+ }
+ if ($value_hash_ref->{o} && !$value_hash_ref->{rc}) {
+ chomp($value_hash_ref->{o});
+ return "FCM " . $value_hash_ref->{o};
+ }
+ # Read fcm-version.js file
+ my $path = catfile($FindBin::Bin, qw{.. doc etc fcm-version.js});
+ open(my($handle), '<', $path) || die("$path: $!");
+ my $content = do {local($/); readline($handle)};
+ close($handle);
+ my ($version) = $content =~ qr{\AFCM\.VERSION="(.*)";}msx;
+ return "FCM " . $version;
+}
+
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::Util
+
+=head1 SYNOPSIS
+
+ use FCM::Util;
+ $u = FCM::Util->new();
+ $u->class_load('Foo');
+
+=head1 DESCRIPTION
+
+Utilities used by the FCM system.
+
+=head1 METHODS
+
+=over 4
+
+=item $class->new(\%attrib)
+
+Returns a new instance. The %attrib hash can be used configure the behaviour of
+the instance:
+
+=over 4
+
+=item conf_paths
+
+The search paths to the configuration files. The default is the value in
+ at FCM::Util::CONF_PATHS.
+
+=item cfg_basename_of
+
+A HASH to map the named configuration with the base names of their paths.
+(default=%CFG_BASENAME_OF)
+
+=item external_value_of
+
+A HASH to map the named external tools with their default values.
+(default=%EXTERNAL_VALUE_OF)
+
+=item event
+
+A CODE to handle event.
+
+=item ns_sep
+
+The name space separator. (default=/)
+
+=item util_class_of
+
+A HASH to map (keys) utility names to (values) their implementation classes. See
+%FCM::System::UTIL_CLASS_OF.
+
+=item util_of
+
+A HASH to map (keys) utility names to (values) their implementation instances.
+
+=back
+
+=item $u->cfg_init($basename,\&action)
+
+Search site/user configuration given by $basename. Invoke the callback
+&action($config_reader) for each configuration file found.
+
+=item $u->class_load($name,$test_method)
+
+If $name can call $test_method, returns $name. (If $test_method is not defined,
+the default is "new".) Otherwise, calls require($name). Returns $name.
+
+=item $u->config_reader($locator,\%reader_attrib)
+
+Returns an iterator for getting the configuration entries from $locator (which
+should be an instance of L<FCM::Context::Locator|FCM::Context::Locator>.
+
+The iterator returns the next useful entry of the configuration file as an
+object of L<FCM::Context::ConfigEntry|FCM::Context::ConfigEntry>. It returns
+under if there is no more useful entry to return.
+
+The %reader_attrib may be used to override the default attributes. The HASH
+should contain a {parser} and a {processor}. The {parser} is a CODE reference to
+parse a declaration in the configuration file into an entry. The {processor} is
+a CODE reference to process the entry. If the {processor} returns true, the
+entry is considered a special entry (e.g. a variable declaration or an
+C<include> declaration) that is processed, and will not be returned by the
+iterator.
+
+The %reader_attrib can be defined using the following pre-defined sets:
+
+=over 4
+
+=item %FCM::Util::ConfigReader::FCM1_ATTRIB
+
+Using this will generate a reader for configuration files written in the FCM 1
+format.
+
+=item %FCM::Util::ConfigReader::FCM2_ATTRIB
+
+Using this will generate a reader for configuration files written in the FCM 2
+format. (default)
+
+=back
+
+In addition, $reader_attrib{event_level} can be used to adjust the event
+verbosity level.
+
+The parser and the processor are called with a %state, which contains the
+current state of the reader, and has the following elements:
+
+=over 4
+
+=item cont
+
+This is set to true if there is a continue marker at the end of the current
+line. The next line should be parsed as part of the current context.
+
+=item ctx
+
+The context of the current entry, which should be an instance of
+L<FCM::Context::ConfigEntry|FCM::Context::ConfigEntry>.
+
+=item line
+
+The content of the current line.
+
+=item stack
+
+An ARRAY reference that represents an include stack. The top of the stack
+(the final element) represents the most current file being read. An include file
+will be put on top of the stack, and removed when EOF is reached. When the stack
+is empty, the iterator is exhausted.
+
+Each element of the stack is an 4-element ARRAY reference. Element 1 is the
+L<FCM::Context::Locator|FCM::Context::Locator> object that represents the
+current file. Element 2 is the line number of the current file. Element 3 is the
+file handle for reading the current file. Element 4 is a CODE reference with an
+interface $f->($path), for turning $path from a relative location under the
+container of the current file into an absolute location.
+
+=item var
+
+A HASH reference containing the variables (from the environment and local to the
+configuration file) that can be used for substitution.
+
+=back
+
+=item $u->external_cfg_get($key)
+
+Returns the value of a named tool.
+
+=item $u->event($event, at args)
+
+Raises an event. The 1st argument $event can either be a blessed reference of
+L<FCM::Context::Event|FCM::Context::Event> or a valid event code. If the former
+is true, @args is not used, otherwise, @args should be the event arguments for
+the specified event code.
+
+=item $u->file_ext($path)
+
+Returns file extension of $path. E.g.:
+
+ my $path = '/foo/bar.baz';
+ my $extension = $u->file_ext($path); # 'baz'
+ my ($extension, $root) = $u->file_ext($path); # ('baz', '/foo/bar')
+
+=item $u->file_head($path, $n)
+
+Loads $n lines (or 1 line if $n not specified) from a $path in the file system.
+In scalar context, returns the content in a scalar. In list context, separate
+the content by the new line character "\n", and returns the resulting list.
+
+=item $u->file_load($path)
+
+Loads contents from a $path in the file system. In scalar context, returns the
+content in a scalar. In list context, separate the content by the new line
+character "\n", and returns the resulting list.
+
+=item $u->file_load_handle($path)
+
+Returns a file handle for loading contents from $path.
+
+=item $u->file_md5($path)
+
+Returns the MD5 checksum of $path.
+
+=item $u->file_save($path, $content)
+
+Saves $content to a $path in the file system.
+
+=item $u->file_tilde_expand($path)
+
+Expand any leading "~" or "~USER" syntax to the HOME directory of the current
+user or the HOME directory of USER. Return the modified string.
+
+=item $u->hash_cmp(\%hash_1,\%hash_2,$keys_only)
+
+Compares the contents of 2 HASH references. If $keys_only is specified, only
+compares the keys. Returns a HASH where each element represents a difference
+between %hash_1 and %hash_2 - if the value is positive, the key exists in
+%hash_2 but not %hash_1, if the value is negative, the key exists in %hash_1 but
+not %hash_2, and if the value is zero, the key exists in both, but the values
+are different.
+
+=item $u->loc_as_invariant($locator)
+
+If the $locator->get_value_level() is below FCM::Context::Locator->L_INVARIANT,
+determines the invariant value of $locator, and sets its value to the result.
+Returns $locator->get_value().
+
+See L<FCM::Context::Locator|FCM::Context::Locator> for information on locator
+value level.
+
+=item $u->loc_as_keyword($locator)
+
+Calls $u->loc_as_normalised($locator) if $locator->get_value_level() is below
+FCM::Context::Locator->L_NORMALISED. Returns the value of the locator as an FCM
+keyword, where possible.
+
+=item $u->loc_as_normalised($locator)
+
+If the $locator->get_value_level() is below FCM::Context::Locator->L_NORMALISED,
+determines the normalised value of $locator, and sets its value to the result.
+Returns $locator->get_value().
+
+See L<FCM::Context::Locator|FCM::Context::Locator> for information on locator
+value level.
+
+=item $u->loc_as_parsed($locator)
+
+If the $locator->get_value_level() is below FCM::Context::Locator->L_PARSED,
+determines the parsed value of $locator, and sets its value to the result.
+Returns $locator->get_value().
+
+See L<FCM::Context::Locator|FCM::Context::Locator> for information on locator
+value level.
+
+=item $u->loc_browser_url($locator)
+
+Calls $u->loc_as_normalised($locator) if $locator->get_value_level() is below
+FCM::Context::Locator->L_NORMALISED. Returns the value of the locator as a
+browser URL, where possible.
+
+=item $u->loc_cat($locator, at paths)
+
+Calls $u->loc_as_parsed($locator) if $locator->get_value_level() is below
+FCM::Context::Locator->L_PARSED. Concatenates the value of the $locator with the
+given @paths according to the $locator type. Returns a new FCM::Context::Locator
+that represents the concatenated value.
+
+=item $u->loc_dir($locator)
+
+Calls $u->loc_as_parsed($locator) if $locator->get_value_level() is below
+FCM::Context::Locator->L_PARSED. Determines the "directory" name of the value of
+the $locator according to the $locator type. Returns a new FCM::Context::Locator
+that represents the resulting value.
+
+=item $u->loc_exists($locator)
+
+Calls $u->loc_as_normalised($locator) if $locator->get_value_level() is below
+FCM::Context::Locator->L_NORMALISED. Return a true value if the location
+represented by $locator exists.
+
+=item $u->loc_export($locator,$dest)
+
+Calls $u->loc_as_normalised($locator) if $locator->get_value_level() is below
+FCM::Context::Locator->L_NORMALISED. Exports the file or directory tree
+represented by $locator to a file system $dest.
+
+=item $u->loc_export_ok($locator)
+
+Calls $u->loc_as_parsed($locator) if $locator->get_value_level() is below
+FCM::Context::Locator->L_PARSED. Returns true if it is possible and safe to
+call $u->loc_export($locator).
+
+=item $u->loc_find($locator,\&callback)
+
+Searches the directory tree of $locator. Invokes &callback for each node with
+the following interface:
+
+ $callback_ref->($locator_of_child_node, \%target_attrib);
+
+where %target_attrib contains the keys:
+
+=over 4
+
+=item {is_dir}
+
+This is set to true if the child node is a directory.
+
+=item {last_modified_rev}
+
+This is set to the last modified revision of the child node, if relevant.
+
+=item {last_modified_time}
+
+This is set to the last modified time of the child node.
+
+=item {ns}
+
+This is set to the relative name-space (i.e. the relative path) of the child
+node.
+
+=back
+
+=item $u->loc_kw_ctx()
+
+Returns the keyword context (an instance of FCM::Context::Keyword).
+
+=item $u->loc_kw_ctx_load(@config_entry_iterators)
+
+Loads configuration entries into the keyword context. The
+ at config_entry_iterators should be a list of CODE references, with the following
+calling interfaces:
+
+ while (my $config_entry = $config_entry_iterator->()) {
+ # ... $config_entry should be an instance of FCM::Context::ConfigEntry
+ }
+
+=item $u->loc_kw_iter($locator)
+
+Returns an iterator. When called, the iterator returns location keyword entry
+context (as an instance of
+L<FCM::Context::Keyword::Entry::Location|FCM::Context::Keyword>) for $locator
+until exhausted.
+
+ my $iterator = $u->loc_kw_iter($locator)
+ while (my $kw_ctx_entry = $iterator->()) {
+ # ... do something with $kw_ctx_entry
+ }
+
+=item $u->loc_kw_load_rev_prop($entry)
+
+Loads the revision keywords to $entry
+(L<FCM::Context::Keyword::Entry::Location|FCM::Context::Keyword>), assuming that
+$entry is not an implied location keyword, and that the keyword locator points
+to a VCS location that supports setting up revision keywords in properties.
+
+=item $u->loc_kw_prefix()
+
+Returns the prefix of a FCM keyword. This should be "fcm".
+
+=item $u->loc_origin($locator)
+
+Calls $u->loc_as_parsed($locator) if $locator->get_value_level() is below
+FCM::Context::Locator->L_PARSED. Determines the origin of $locator, and returns
+a new FCM::Context::Locator that represents the result. E.g. if $locator points
+to a Subversion working copy, it returns a new locator that represents the URL
+of the working copy.
+
+=item $u->loc_reader($locator)
+
+Calls $u->loc_as_normalised($locator) if $locator->get_value_level() is below
+FCM::Context::Locator->L_NORMALISED. Returns a file handle for reading the
+content from $locator.
+
+=item $u->loc_rel2abs($locator,$locator_base)
+
+If the value of $locator is a relative path, sets it to an absolute path base on
+the $locator_base, provided that $locator and $locator_base is the same type.
+
+=item $u->loc_trunk_at_head($locator)
+
+Returns a string to represent the relative path to the latest main tree, if it
+is relevant for $locator.
+
+=item $u->loc_what_type($locator)
+
+Sets $locator->get_type() and returns its value. Currently, this can either be
+"svn" for a locator pointing to a Subversion resource or "fs" for a locator
+pointing to a file system resource.
+
+=item $u->loc_up_iter($locator)
+
+Returns an iterator that walks up the hierarchy of the $locator, according to
+its type.
+
+=item $u->ns_cat(@name_spaces)
+
+Concatenates name-spaces and returns the result.
+
+=item $u->ns_common($ns1,$ns2)
+
+Returns the common parts of 2 name-spaces. For example, if $ns1 is
+"egg/ham/bacon" and $ns2 is "egg/ham/sausage", it should return "egg/ham".
+
+=item $u->ns_in_set($ns,\%set)
+
+Returns true if $ns is in a name-space given by the keys of %set.
+
+=item $u->ns_iter($ns,$up)
+
+Returns an iterator that walks up or down a name-space. E.g.:
+
+ $iter_ref = $u->ns_iter('a/bee/cee', $u->NS_ITER_UP);
+ while (defined(my $item = $iter_ref->())) {
+ print("[$item]");
+ }
+ # should print: [a/bee/cee][a/bee][a][]
+
+ $iter_ref = $u->ns_iter('a/bee/cee');
+ while (defined(my $item = $iter_ref->())) {
+ print("[$item]");
+ }
+ # should print: [][a][a/bee][a/bee/cee]
+
+=item $u->ns_sep()
+
+Returns the name-space separator, (i.e. normally "/").
+
+=item $u->report(\%option,$message)
+
+Reports messages using $u->util_of_report(). The default is an instance of
+L<FCM::Util::Reporter|FCM::Util::Reporter>. See
+L<FCM::Util::Reporter|FCM::Util::Reporter> for detail.
+
+=item $u->shell($command,\%action_of)
+
+Invokes the $command, which can be scalar or a reference to an ARRAY. If a
+scalar is specified, it will be separated into an array using the shellwords()
+function in L<Text::ParseWords|Text::ParseWords>. If it is a reference to an
+ARRAY, the ARRAY will be passed to open3() as is.
+
+The %action_of should contain the actions for i: standard input, e: standard
+error output and o: standard output. The default for each of these is an
+anonymous subroutinue that does nothing.
+
+Each time the pipe to the child standard input is available for writing, it will
+call $action_of{i}->(). If it returns a defined value, the value will be written
+to the pipe. If it returns undef, the pipe will be closed.
+
+Each time the pipe from the child standard (error) output is available for
+reading, it will read some values to a buffer, and invoke the callback
+$action_of{o}->($buffer) (or $action_of{e}->($buffer)). The return value of the
+callback will be ignored.
+
+On normal completion, it returns the status code of the command and raises an
+FCM::Context::Event->SHELL event:
+
+Any abnormal failure will cause an instance of FCM::Util::Exception to be
+thrown. (The return of a non-zero status code by the child is considered a
+normal completion.)
+
+=item $u->shell_simple($command)
+
+Wraps $u->shell(), and returns a HASH reference containing {e} (the
+standard error), {o} (the standard output) and {rc} (the return code).
+
+=item $u->shell_which($name)
+
+Returns the full path of an executable command $name if it can be found in the
+system PATH.
+
+=item $u->task_runner($action_code_ref,$n_workers)
+
+Returns a runner of tasks. It can be configured to work in serial (default) or
+parallel. The runner has the following methods:
+
+ $n_done = $runner->main($get_code_ref,$put_code_ref);
+ $runner->destroy();
+
+For each $task (L<FCM::Context::Task|FCM::Context::Task>) returned by the
+$get_code_ref->() iterator, invokes $action_ref->($task->get_ctx()). When
+$action_ref returns, send the $task back to the caller by calling
+$put_code_ref->($task). When it is done, the runner returns the number of tasks
+it has done.
+
+The $runner->destroy() method should be called to destroy the $runner when it is
+not longer used.
+
+=item $u->timer(\@start)
+
+Returns a CODE reference, which can be called to return the elapsed time. The
+ at start argument is optional. If specified, it should be in a format as returned
+by Time::HiRes::gettimeofday(). If not specified, the current gettimeofday() is
+used.
+
+=item $u->uri_match($string)
+
+Returns true if $string is a URI. In array context, returns the scheme and the
+opague part of the URI if $string is a URI, or an empty list otherwise.
+
+=item $u->util_of_event($value)
+
+Returns and/or sets the L<FCM::Util::Event|FCM::Util::Event> object that is used
+to handle the $u->report() method.
+
+=item $u->util_of_report($value)
+
+Returns and/or sets the L<FCM::Util::Reporter|FCM::Util::Reporter> object that
+is used to handle the $u->report() method.
+
+=item $u->version()
+
+Returns the FCM version string.
+
+=back
+
+=head1 DIAGNOSTICS
+
+=head2 FCM::Util::Exception
+
+This exception is a sub-class of L<FCM::Exception|FCM::Exception> and is thrown
+by methods of this class on error.
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/Util/ConfigReader.pm b/lib/FCM/Util/ConfigReader.pm
new file mode 100644
index 0000000..624f3ef
--- /dev/null
+++ b/lib/FCM/Util/ConfigReader.pm
@@ -0,0 +1,610 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+
+# ------------------------------------------------------------------------------
+package FCM::Util::ConfigReader;
+use base qw{FCM::Class::CODE};
+
+use FCM::Context::ConfigEntry;
+use FCM::Context::Event;
+use FCM::Context::Locator;
+use FCM::Util::Exception;
+use File::Spec::Functions qw{file_name_is_absolute};
+use Text::Balanced qw{extract_bracketed};
+use Text::ParseWords qw{parse_line shellwords};
+
+# Alias
+our $UTIL;
+# Alias to exception class
+my $E = 'FCM::Util::Exception';
+# The variable name, which means the container of the current configuration file
+my $HERE = 'HERE';
+# Element indices in a stack item
+my ($I_LOCATOR, $I_LINE_NUM, $I_HANDLE, $I_HERE_LOCATOR) = (0 .. 3);
+# Patterns for extracting/matching strings
+my %PATTERN_OF = (
+ # Config: comment delimiter, e.g. "... #comment"
+ comment => qr/\s+ \#/xms,
+ # Config: continue, start of next line
+ cont_next => qr/
+ \A (.*?) (?# start and capture 1, shortest of anything)
+ (\\*) (?# capture 2, a number of backslashes)
+ \s* \z (?# optional space until the end)
+ /xms,
+ # Config: continue, end of previous line
+ cont_prev => qr/\A \s* \\? (.*) \z/xms,
+ # Config: removal of the assignment operator at start of string
+ fcm2_equal => qr/
+ \A \s* (?# start and optional spaces)
+ (=) (?# capture 1, equal sign)
+ (.*) \z (?# capture 2, rest of string)
+ /xms,
+ # Config: label of an inc statement
+ fcm1_include => qr/\Ainc\z/ixms,
+ # Config: label of an include statement
+ fcm2_include => qr/\Ainclude\z/ixms,
+ # Config: label of an include-path statement
+ fcm2_include_path => qr/\Ainclude-path\z/ixms,
+ # Config: label
+ fcm2_label => qr/
+ \A \s* (?# start and optional spaces)
+ (\$?[\w\-\.]+) (?# capture 1, optional dollar, then valid label)
+ (.*) \z (?# capture 2, rest of string)
+ /xms,
+ # Config: a variable identifier in a value, e.g. "... ${var}", "$var"
+ fcm1_var => qr/
+ \A (?# start)
+ (.*?) (?# capture 1, shortest of anything)
+ ([\$\%]) (?# capture 2, variable sigil, dollar or percent)
+ (\{)? (?# capture 3, curly brace start, optional)
+ ([A-z]\w+(?:::[A-z]\w+)*) (?# capture 4, variable name)
+ ((?(3)\})) (?# capture 5, curly brace end, if started in capture 4)
+ (.*) (?# capture 6, rest of string)
+ \z (?# end)
+ /xms,
+ # Config: a variable identifier in a value, e.g. "... ${var}", "$var"
+ fcm2_var => qr/
+ \A (?# start)
+ (.*?) (?# capture 1, shortest of anything)
+ (\\*) (?# capture 2, escapes)
+ (\$) (?# capture 3, variable sigil, dollar)
+ (\{)? (?# capture 4, curly brace start, optional)
+ ([A-z]\w+) (?# capture 5, variable name)
+ ((?(4)\})) (?# capture 6, curly brace end, if started in capture 4)
+ (.*) (?# capture 7, rest of string)
+ \z (?# end)
+ /xms,
+ # Config: a $HERE, ${HERE} in the beginning of a string
+ here => qr/
+ \A (?# start)
+ (\$HERE|\$\{HERE\}) (?# capture 1, \$HERE)
+ (\/.*)? (?# capture 2, rest of string)
+ \z (?# end)
+ /xms,
+ # Config: an empty or comment line
+ ignore => qr/\A \s* (?:\#|\z)/xms,
+ # Config: comma separator
+ delim_csv => qr/\s*,\s*/xms,
+ # Config: modifier key:value separator
+ delim_mod => qr/\s*:\s*/xms,
+ # A variable name
+ var_name => qr/\A [A-Za-z_]\w* \z/xms,
+ # Config: trim value
+ trim => qr/\A \s* (.*?) \s* \z/xms,
+ # Config: trim value within braces
+ trim_brace => qr/\A [\[\{] \s* (.*?) \s* [\]\}] \z/xms,
+);
+# Default (post-)processors for a configuration entry
+our %FCM1_ATTRIB = (
+ parser => _parse_func(\&_parse_fcm1_label, \&_parse_fcm1_var),
+ processor => sub {
+ _process_assign_func('%')->(@_)
+ ||
+ _process_include_func('fcm1_include')->(@_)
+ ||
+ _process_fcm1_label(@_)
+ ;
+ },
+);
+# Default (post-)processors for a configuration entry
+our %FCM2_ATTRIB = (
+ parser => _parse_func(\&_parse_fcm2_label, \&_parse_fcm2_var),
+ processor => sub {
+ _process_assign_func('$', '?')->(@_)
+ ||
+ _process_include_path_func('fcm2_include_path')->(@_)
+ ||
+ _process_include_func('fcm2_include')->(@_)
+ ;
+ },
+);
+
+# Creates the class.
+__PACKAGE__->class(
+ { event_level => '$',
+ parser => {isa => '&', default => sub {$FCM2_ATTRIB{parser}} },
+ processor => {isa => '&', default => sub {$FCM2_ATTRIB{processor}}},
+ util => '&',
+ },
+ {action_of => {main => \&_main}},
+);
+
+# Returns a configuration reader.
+sub _main {
+ my ($attrib_ref, $locator, $reader_attrib_ref) = @_;
+ if (!defined($locator)) {
+ return;
+ }
+ my %reader_attrib
+ = defined($reader_attrib_ref) ? %{$reader_attrib_ref} : ();
+ my @include_paths = exists($reader_attrib{include_paths})
+ ? @{$reader_attrib{include_paths}} : ();
+ my %state = (
+ cont => undef,
+ ctx => undef,
+ line => undef,
+ include_paths => \@include_paths,
+ stack => [[$locator, 0]],
+ var => {},
+ );
+ my %attrib = (%{$attrib_ref}, %reader_attrib);
+ sub {_read(\%attrib, \%state)};
+}
+
+# Returns a parser for a configuration line (FCM 1 or FCM 2 format).
+sub _parse_func {
+ my ($parse_label_func, $parse_var_func) = @_;
+ sub {
+ my ($state_ref) = @_;
+ my $line
+ = $state_ref->{cont} ? $state_ref->{line}
+ : $parse_label_func->($state_ref)
+ ;
+ my $value
+ = $parse_var_func->($state_ref, _parse_value($state_ref, $line));
+ if ($state_ref->{ctx}->get_value()) {
+ $value = $state_ref->{ctx}->get_value() . $value;
+ }
+ $state_ref->{ctx}->set_value($value);
+ if (!$state_ref->{cont}) {
+ _parse_var_here($state_ref);
+ }
+ };
+}
+
+# Parses a configuration line label (FCM 1 format).
+sub _parse_fcm1_label {
+ my ($state_ref) = @_;
+ my ($label, $line) = split(qr{\s+}xms, $state_ref->{line}, 2);
+ $state_ref->{ctx}->set_label($label);
+ return $line;
+}
+
+# Parses a configuration line label (FCM 2 format).
+sub _parse_fcm2_label {
+ my ($state_ref) = @_;
+ my %EXTRACTOR_OF = (
+ equal => sub {($_[0] =~ $PATTERN_OF{fcm2_equal})},
+ label => sub {($_[0] =~ $PATTERN_OF{fcm2_label})},
+ modifier => sub {extract_bracketed($_[0], '{}')} ,
+ ns => sub {extract_bracketed($_[0], '["]')},
+ );
+ my %ACTION_OF = (
+ equal => sub {$_[1] || $E->throw($E->CONFIG_SYNTAX, $_[0])},
+ label => sub {$_[0]->set_label($_[1])},
+ modifier => \&_parse_fcm2_label_modifier,
+ ns => \&_parse_fcm2_label_ns,
+ );
+ my %EXPAND_VAR_IN = (modifier => 1, ns => 1);
+ my $line = $state_ref->{line};
+ for my $key (qw{label modifier ns equal}) {
+ $line ||= q{};
+ (my $content, $line) = $EXTRACTOR_OF{$key}->($line);
+ if ($EXPAND_VAR_IN{$key}) {
+ $content = _parse_fcm2_var($state_ref, $content);
+ }
+ $ACTION_OF{$key}->($state_ref->{ctx}, $content);
+ }
+ return $line;
+}
+
+# Parses the modifier part in a configuration line label (FCM 2 format).
+sub _parse_fcm2_label_modifier {
+ my ($ctx, $content) = @_;
+ if ($content) {
+ my ($str) = $content =~ $PATTERN_OF{trim_brace};
+ my %hash;
+ for my $item (parse_line($PATTERN_OF{delim_csv}, 0, $str)) {
+ my ($key, $value) = split($PATTERN_OF{delim_mod}, $item, 2);
+ # Note: "key1, key2: value2, ..." == "key1: 1, key2: value2, ..."
+ $hash{$key} = ($value ? $value : 1);
+ }
+ $ctx->set_modifier_of(\%hash);
+ }
+}
+
+# Parses the ns part in a configuration line label (FCM 2 format).
+sub _parse_fcm2_label_ns {
+ my ($ctx, $content) = @_;
+ if ($content) {
+ my ($str) = $content =~ $PATTERN_OF{trim_brace};
+ my @ns = map {$_ eq q{/} ? q{} : $_} parse_line(q{ }, 0, $str);
+ $ctx->set_ns_list(\@ns);
+ }
+}
+
+# Expands variables in a string in a FCM 1 configuration file.
+sub _parse_fcm1_var {
+ my ($state_ref, $value) = @_;
+ my %V = %{$state_ref->{var}};
+ my $lead = q{};
+ my $tail = $value;
+ MATCH:
+ while (defined($tail) && length($tail) > 0) {
+ my ($pre, $sigil, $br_open, $name, $br_close, $post)
+ = map {defined($_) ? $_ : q{}} ($tail =~ $PATTERN_OF{fcm1_var});
+ if (!$name) {
+ return $lead . $tail;
+ }
+ $tail = $post;
+ my $symbol = $sigil . $br_open . $name . $br_close;
+ my $substitute
+ = $name eq $HERE ? $symbol
+ : $sigil eq '$' && exists($ENV{$name}) ? $ENV{$name}
+ : $sigil eq '%' && exists($V{$name}) ? $V{$name}
+ : undef
+ ;
+ if (!defined($substitute)) {
+ $UTIL->event(
+ FCM::Context::Event->CONFIG_VAR_UNDEF,
+ $state_ref->{ctx},
+ $symbol,
+ );
+ }
+ $substitute ||= $symbol;
+ $lead .= $pre . $substitute;
+ }
+ return $lead;
+}
+
+# Expands variables in a string in a FCM 2 configuration file.
+sub _parse_fcm2_var {
+ my ($state_ref, $value) = @_;
+ my %V = (%ENV, %{$state_ref->{var}});
+ my $lead = q{};
+ my $tail = $value;
+ while (defined($tail) && length($tail) > 0) {
+ my ($pre, $esc, $sigil, $br_open, $name, $br_close, $post)
+ = map {defined($_) ? $_ : q{}} ($tail =~ $PATTERN_OF{fcm2_var});
+ if (!$name) {
+ return $lead . $tail;
+ }
+ $tail = $post;
+ my $symbol = $sigil . $br_open . $name . $br_close;
+ my $substitute
+ = $name eq $HERE ? $symbol
+ : $esc && length($esc) % 2 ? $symbol
+ : exists($V{$name}) ? $V{$name}
+ : undef
+ ;
+ if (!defined($substitute)) {
+ return $E->throw(
+ $E->CONFIG_VAR_UNDEF, $state_ref->{ctx}, "undef($symbol)",
+ );
+ }
+ $substitute ||= q{};
+ $lead .= $pre . substr($esc, 0, length($esc) / 2) . $substitute;
+ }
+ return $lead;
+}
+
+# Parses the value part of a configuration line.
+sub _parse_value {
+ my ($state_ref, $line) = @_;
+ $line ||= q{};
+ my ($value) = parse_line($PATTERN_OF{comment}, 1, $line);
+ $value ||= q{};
+ chomp($value);
+ ($value) = $value =~ $PATTERN_OF{$state_ref->{cont} ? 'cont_prev' : 'trim'};
+ $state_ref->{cont} = q{};
+ if ($value) {
+ my ($lead, $tail) = $value =~ $PATTERN_OF{cont_next};
+ if ($tail && length($tail) % 2) {
+ $value = $lead;
+ $state_ref->{cont} = $tail;
+ }
+ }
+ return $value;
+}
+
+# Expands the leading $HERE variable in the value of a configuration entry.
+sub _parse_var_here {
+ my ($state_ref) = @_;
+ my @values = shellwords($state_ref->{ctx}->get_value());
+ if (!grep {$_ =~ $PATTERN_OF{here}} @values) {
+ return;
+ }
+ VALUE:
+ for my $value (@values) {
+ my ($head, $tail)
+ = map {defined($_) ? $_ : q{}} $value =~ $PATTERN_OF{here};
+ if (!$head) {
+ next VALUE;
+ }
+ $tail = index($tail, '/') == 0 ? substr($tail, 1) : q{}; # FIXME
+ my $here = $state_ref->{stack}->[-1]->[$I_HERE_LOCATOR];
+ $value = $UTIL->loc_cat($here, $tail)->get_value();
+ }
+ $state_ref->{ctx}->set_value(join(
+ q{ },
+ map {my $s = $_; $s =~ s{(['"\s])}{\\$1}gmsx; $s} @values,
+ ));
+}
+
+# Returns a function to process a variable assignment. If
+# $assign_if_undef_modifier is specified and is present in the declaration, only
+# assign a variable if it is not yet defined.
+sub _process_assign_func {
+ my ($sigil, $assign_if_undef_modifier) = @_;
+ sub {
+ my ($state_ref) = @_;
+ my $ctx = $state_ref->{ctx};
+ if (index($ctx->get_label(), $sigil) != 0) { # not a variable assignment
+ return;
+ }
+ my $name = substr($ctx->get_label(), length($sigil));
+ if ($name !~ $PATTERN_OF{var_name}) {
+ return $E->throw($E->CONFIG_SYNTAX, $state_ref->{ctx});
+ }
+ if ($name eq $HERE) {
+ return $E->throw($E->CONFIG_USAGE, $state_ref->{ctx});
+ }
+ if ( !$assign_if_undef_modifier
+ || !exists($ctx->get_modifier_of()->{$assign_if_undef_modifier})
+ || !exists($ENV{$name}) && !exists($state_ref->{var}{$name})
+ ) {
+ $state_ref->{var}{$name} = $ctx->get_value();
+ }
+ return 1;
+ }
+}
+
+# Processes a FCM 1 label.
+sub _process_fcm1_label {
+ my ($state_ref) = @_;
+ $state_ref->{var}{$state_ref->{ctx}->get_label()}
+ = $state_ref->{ctx}->get_value();
+ return;
+}
+
+# Processes an include-path declaration.
+sub _process_include_path_func {
+ my ($key) = @_;
+ my $PATTERN = $PATTERN_OF{$key};
+ sub {
+ my ($state_ref) = @_;
+ if ($state_ref->{ctx}->get_label() !~ $PATTERN) {
+ return;
+ }
+ my $M = $state_ref->{ctx}->get_modifier_of();
+ my $type = exists($M->{type}) ? $M->{type} : undef;
+ if (exists($M->{'+'})) {
+ push(@{$state_ref->{include_paths}}, (
+ map {
+ FCM::Context::Locator->new($_, {type => $type});
+ } shellwords($state_ref->{ctx}->get_value()),
+ )),
+ }
+ else {
+ $state_ref->{include_paths} = [
+ map {
+ FCM::Context::Locator->new($_, {type => $type});
+ } shellwords($state_ref->{ctx}->get_value())
+ ],
+ }
+ return 1;
+ };
+}
+
+# Processes an include declaration.
+sub _process_include_func {
+ my ($key) = @_;
+ my $PATTERN = $PATTERN_OF{$key};
+ sub {
+ my ($state_ref) = @_;
+ if ($state_ref->{ctx}->get_label() !~ $PATTERN) {
+ return;
+ }
+ my $M = $state_ref->{ctx}->get_modifier_of();
+ my $type = exists($M->{type}) ? $M->{type} : undef;
+ push(@{$state_ref->{stack}}, (map {
+ my $name = $_;
+ my $locator;
+ if ( $UTIL->uri_match($name)
+ || file_name_is_absolute($name)
+ ) {
+ $locator = FCM::Context::Locator->new($name, {type => $type});
+ }
+ if (!defined($locator)) {
+ HEAD:
+ for my $head (
+ $state_ref->{stack}->[-1]->[$I_HERE_LOCATOR],
+ @{$state_ref->{include_paths}},
+ ) {
+ my $locator_at_head = $UTIL->loc_cat($head, $name);
+ if ($UTIL->loc_exists($locator_at_head)) {
+ $locator = $locator_at_head;
+ last HEAD;
+ }
+ }
+ }
+ if (!defined($locator)) {
+ return $E->throw(
+ $E->CONFIG_LOAD, $state_ref->{stack}, "include=$name",
+ );
+ }
+ [$locator, 0, undef, undef];
+ } shellwords($state_ref->{ctx}->get_value())));
+ return 1;
+ };
+}
+
+# Reads the next entry of a configuration file.
+sub _read {
+ my ($attrib_ref, $state_ref) = @_;
+ local($UTIL) = $attrib_ref->{util};
+ STACK:
+ while (@{$state_ref->{stack}}) {
+ my $S = $state_ref->{stack}->[-1];
+ # Open a file handle for the top of the stack, if necessary
+ if (!defined($S->[$I_HANDLE])) {
+ eval {
+ # Check for cyclic dependency
+ for my $i (-scalar(@{$state_ref->{stack}}) .. -2) {
+ my $value = $UTIL->loc_as_invariant(
+ $state_ref->{stack}->[$i]->[$I_LOCATOR],
+ );
+ if ($value eq $UTIL->loc_as_invariant($S->[$I_LOCATOR])) {
+ return $E->throw($E->CONFIG_CYCLIC, $state_ref->{stack});
+ }
+ }
+ $S->[$I_HANDLE] = $UTIL->loc_reader($S->[$I_LOCATOR]);
+ $S->[$I_HERE_LOCATOR] = $UTIL->loc_dir($S->[$I_LOCATOR]);
+ };
+ if (my $e = $@) {
+ if ($E->caught($e) && $e->get_code() eq $E->CONFIG_CYCLIC) {
+ die($e);
+ }
+ return $E->throw($E->CONFIG_LOAD, $state_ref->{stack}, $e);
+ }
+ $UTIL->event(
+ FCM::Context::Event->CONFIG_OPEN,
+ _stack_cp($state_ref->{stack}),
+ $attrib_ref->{event_level},
+ );
+ }
+ # Read a line and parse it
+ LINE:
+ while ($state_ref->{line} = readline($S->[$I_HANDLE])) {
+ if ($state_ref->{line} =~ $PATTERN_OF{ignore}) {
+ next LINE;
+ }
+ $S->[$I_LINE_NUM] = $.;
+ if (!$state_ref->{cont}) {
+ $state_ref->{ctx} = FCM::Context::ConfigEntry->new({
+ stack => _stack_cp($state_ref->{stack}),
+ });
+ }
+ $attrib_ref->{parser}->($state_ref);
+ if (!$state_ref->{cont}) {
+ if ($attrib_ref->{processor}->($state_ref)) {
+ next STACK;
+ }
+ return $state_ref->{ctx};
+ }
+ }
+ # At end of file
+ if ($state_ref->{cont}) {
+ return $E->throw($E->CONFIG_CONT_EOF, $state_ref->{ctx});
+ }
+ close($state_ref->{stack}->[-1]->[$I_HANDLE]);
+ $state_ref->{stack}->[-1]->[$I_HANDLE] = undef; # free the memory
+ pop(@{$state_ref->{stack}});
+ }
+ return;
+}
+
+# Copies a stack, selecting only the and the line number.
+sub _stack_cp {
+ [map {[@{$_}[$I_LOCATOR, $I_LINE_NUM]]} @{$_[0]}];
+}
+
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::Util::Config
+
+=head1 SYNOPSIS
+
+ use FCM::Util;
+ my $util = FCM::Util->new(\%attrib);
+ # ... time passes, and now we want to read a FCM 1 config
+ my ($locator, $reader);
+ $locator = FCM::Context::Locator->new($path_to_an_fcm1_config);
+ $reader
+ = $util->config_reader($locator, \%FCM::Util::ConfigReader::FCM1_ATTRIB);
+ while (my $entry = $reader->()) {
+ # ...
+ }
+ # ... time passes, and now we want to read a FCM 2 config
+ $locator = FCM::Context::Locator->new($path_to_an_fcm2_config);
+ $reader = $util->config_reader($locator);
+ while (my $entry = $reader->()) {
+ # ...
+ }
+
+=head1 DESCRIPTION
+
+This module is part of L<FCM::Util|FCM::Util>. Provides a function to generate
+configuration file readers.
+
+=head1 METHODS
+
+=over 4
+
+=item $class->new(\%attrib)
+
+Returns a new new instance. The %attrib must contain the following:
+
+=over 4
+
+=item {parser}
+
+A CODE reference to parse the lines in a configuration file into entry contexts.
+It should have a calling interface $f->(\%state). (See L</STATE> for a
+description of %state.) The return value is ignored.
+
+=item {processor}
+
+A CODE reference to post-process each entry context. It should have a calling
+interface $f->(\%state). (See L</STATE> for a description of %state.) The
+processor should return true if the current entry has been processed and is no
+longer considered useful for the user.
+
+=item {util}
+
+The L<FCM::Util|FCM::Util> object, which initialises this class.
+
+=back
+
+=back
+
+See the description of the config_reader() method in L<FCM::Util|FCM::Util> for
+detail.
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/Util/ConfigUpgrade.pm b/lib/FCM/Util/ConfigUpgrade.pm
new file mode 100644
index 0000000..fa4f86f
--- /dev/null
+++ b/lib/FCM/Util/ConfigUpgrade.pm
@@ -0,0 +1,136 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+# ------------------------------------------------------------------------------
+package FCM::Util::ConfigUpgrade;
+use base qw{FCM::Class::CODE};
+
+use FCM::Context::ConfigEntry;
+
+my %DECL_PATTERN_OF = (
+ browser_mapping => qr{\A set::browser_mapping(?:_default|:: ([^:]+)):: (.+) \z}ixms,
+ keyword_loc => qr{\A set::(?:repos|url):: (.+) \z}ixms,
+ keyword_rev => qr{\A set::revision:: ([^:]+) :: (.+) \z}ixms,
+);
+my @UPGRADE_FUNCS = (
+ \&_upgrade_browser_mapping_decl,
+ \&_upgrade_keyword_loc_decl,
+ \&_upgrade_keyword_rev_decl,
+);
+
+# Creates the class.
+__PACKAGE__->class({}, {action_of => {upgrade => \&_upgrade}});
+
+sub _upgrade {
+ my ($attrib_ref, $config_entry) = @_;
+ if (!defined($config_entry)) {
+ return;
+ }
+ for my $func (@UPGRADE_FUNCS) {
+ $func->($config_entry);
+ if ($func->($config_entry)) {
+ return $config_entry;
+ }
+ }
+ return $config_entry;
+}
+
+# Upgrades a browser mapping declaration.
+sub _upgrade_browser_mapping_decl {
+ my ($config_entry) = @_;
+ my ($ns, $key)
+ = $config_entry->get_label() =~ $DECL_PATTERN_OF{browser_mapping};
+ if (!$key) {
+ return;
+ }
+ $config_entry->set_label(
+ $key eq 'browser_url_template' ? 'browser.loc-tmpl'
+ : $key eq 'browser_rev_template' ? 'browser.rev-tmpl'
+ : 'browser.comp-pat'
+ );
+ if ($ns) {
+ $config_entry->set_ns_list([$ns]);
+ }
+}
+
+# Upgrades a location keyword declaration.
+sub _upgrade_keyword_loc_decl {
+ my ($config_entry) = @_;
+ my ($ns) = $config_entry->get_label() =~ $DECL_PATTERN_OF{keyword_loc};
+ if (!$ns) {
+ return;
+ }
+ $config_entry->set_label('location');
+ $config_entry->get_modifier_of()->{primary} = 1;
+ $config_entry->set_ns_list([$ns]);
+}
+
+# Upgrades a revision keyword declaration.
+sub _upgrade_keyword_rev_decl {
+ my ($config_entry) = @_;
+ my ($ns, $key) = $config_entry->get_label() =~ $DECL_PATTERN_OF{keyword_rev};
+ if (!$ns || !$key) {
+ return;
+ }
+ $config_entry->set_label('revision');
+ $config_entry->set_ns_list([$ns, $key]);
+}
+
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::Util::ConfigUpgrade
+
+=head1 SYNOPSIS
+
+ use FCM::Util::ConfigUpgrade;
+ $upgrade = FCM::Util::ConfigUpgrade->new();
+ if (!$upgrade->($entry)) {
+ die($entry->get_label(), ": cannot upgrade.\n");
+ }
+ # ... do something with $entry
+
+=head1 DESCRIPTION
+
+Provides a utility to upgrade FCM 1 configuration to FCM 2 configuration.
+
+=head1 METHODS
+
+=over 4
+
+=item $class->new()
+
+Creates and returns a new instance of this utility.
+
+=item $util->($entry)
+
+Upgrades the content of $entry, where possible. Only keyword related
+declarations in the FCM 1 common configuration files are currently supported.
+
+=back
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/Util/Event.pm b/lib/FCM/Util/Event.pm
new file mode 100644
index 0000000..7a16f54
--- /dev/null
+++ b/lib/FCM/Util/Event.pm
@@ -0,0 +1,1244 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+
+# ------------------------------------------------------------------------------
+package FCM::Util::Event;
+use base qw{FCM::Class::CODE};
+
+use Data::Dumper qw{Dumper};
+use FCM::Context::Event;
+use File::Basename qw{basename};
+use List::Util qw{first};
+use POSIX qw{strftime};
+use Scalar::Util qw{blessed};
+
+my $CTX = 'FCM::Context::Event';
+my $IS_MULTI_LINE = 1;
+
+# Event keys and their actions.
+my %ACTION_OF = (
+ $CTX->CM_ABORT => \&_event_cm_abort,
+ $CTX->CM_BRANCH_CREATE_SOURCE => _func('cm_branch_create_source'),
+ $CTX->CM_BRANCH_LIST => \&_event_cm_branch_list,
+ $CTX->CM_COMMIT_MESSAGE => \&_event_cm_commit_message,
+ $CTX->CM_CONFLICT_TEXT => _func('cm_conflict_text'),
+ $CTX->CM_CONFLICT_TEXT_SKIP => \&_event_cm_conflict_text_skip,
+ $CTX->CM_CONFLICT_TREE => _func('cm_conflict_tree'),
+ $CTX->CM_CONFLICT_TREE_SKIP => \&_event_cm_conflict_tree_skip,
+ $CTX->CM_CONFLICT_TREE_TIME_WARN => \&_event_cm_conflict_tree_time_warn,
+ $CTX->CM_CREATE_TARGET => _func('cm_create_target'),
+ $CTX->CM_LOG_EDIT => _func('cm_log_edit'),
+ $CTX->CONFIG_OPEN => \&_event_config_open,
+ $CTX->CONFIG_ENTRY => \&_event_config_entry,
+ $CTX->CONFIG_VAR_UNDEF => \&_event_config_var_undef,
+ $CTX->E => \&_event_e,
+ $CTX->EXPORT_ITEM_CREATE => _func('export_item_create'),
+ $CTX->EXPORT_ITEM_DELETE => _func('export_item_delete'),
+ $CTX->FCM_VERSION => _func('fcm_version'),
+ $CTX->KEYWORD_ENTRY => \&_event_keyword_entry,
+ $CTX->MAKE_BUILD_SHELL_OUT => \&_event_make_build_shell_out,
+ $CTX->MAKE_BUILD_SOURCE_ANALYSE => \&_event_make_build_source_analyse,
+ $CTX->MAKE_BUILD_SOURCE_SUMMARY => _func('make_build_source_summary'),
+ $CTX->MAKE_BUILD_TARGET_DONE => \&_event_make_build_target_done,
+ $CTX->MAKE_BUILD_TARGET_FAIL => \&_event_make_build_target_fail,
+ $CTX->MAKE_BUILD_TARGET_FROM_NS => \&_event_make_build_target_from_ns,
+ $CTX->MAKE_BUILD_TARGET_SELECT => \&_event_make_build_target_select,
+ $CTX->MAKE_BUILD_TARGET_SELECT_TIMER=> _func('make_build_target_select_t'),
+ $CTX->MAKE_BUILD_TARGET_MISSING_DEP => \&_event_make_build_target_missing_dep,
+ $CTX->MAKE_BUILD_TARGET_STACK => \&_event_make_build_target_stack,
+ $CTX->MAKE_BUILD_TARGET_SUMMARY => _func('make_build_target_sum'),
+ $CTX->MAKE_BUILD_TARGET_TASK_SUMMARY=> _func('make_build_target_task_sum'),
+ $CTX->MAKE_BUILD_TARGETS_FAIL => \&_event_make_build_targets_fail,
+ $CTX->MAKE_DEST => \&_event_make_dest,
+ $CTX->MAKE_EXTRACT_PROJECT_TREE => \&_event_make_extract_project_tree,
+ $CTX->MAKE_EXTRACT_RUNNER_SUMMARY => \&_event_make_extract_runner_summary,
+ $CTX->MAKE_EXTRACT_SYMLINK => \&_event_make_extract_symlink,
+ $CTX->MAKE_EXTRACT_TARGET => \&_event_make_extract_target,
+ $CTX->MAKE_EXTRACT_TARGET_SUMMARY => \&_event_make_extract_target_summary,
+ $CTX->MAKE_MIRROR => \&_event_make_mirror,
+ $CTX->OUT => \&_event_out,
+ $CTX->SHELL => \&_event_shell,
+ $CTX->TASK_WORKERS => \&_event_task_workers,
+ $CTX->TIMER => \&_event_timer,
+);
+# Helper for "_event_e", list of exception classes and their formatters.
+our @E_FORMATTERS = (
+ ['FCM1::Cm::Exception' , \&_format_e_cm ],
+ ['FCM1::CLI::Exception' , sub {$_[0]->get_message()}],
+ ['FCM::Class::Exception' , \&_format_e_class ],
+ ['FCM::CLI::Exception' , \&_format_e_cli ],
+ ['FCM::System::Exception', \&_format_e_sys ],
+ ['FCM::Util::Exception' , \&_format_e_util ],
+);
+# Error format strings for FCM1::Cm::Exception.
+our %E_CM_FORMAT_FOR = (
+ DIFF_PROJECTS => "%s (target) and %s (source) are not related.\n",
+ INVALID_BRANCH => "%s: not a valid URL of a standard FCM branch.\n",
+ INVALID_PROJECT => "%s: not a valid URL of a standard FCM project.\n",
+ INVALID_TARGET => "%s: not a valid working copy or URL.\n",
+ INVALID_URL => "%s: not a valid URL.\n",
+ INVALID_WC => "%s: not a valid working copy.\n",
+ MERGE_REV_INVALID => "%s: not a revision in the available merge list.\n",
+ MERGE_SELF => "%s: cannot be merged to its own working copy: %s.\n",
+ MERGE_UNRELATED => "%s: target and %s: source not directly related.\n",
+ MERGE_UNSAFE => "%s: source contains changes outside the target"
+ . " sub-directory. Please merge with a full tree.\n",
+ MKPATH => "%s: cannot create directory.\n",
+ NOT_EXIST => "%s: does not exist.\n",
+ PARENT_NOT_EXIST => "%s: parent %s no longer exists.\n",
+ RMTREE => "%s: cannot remove.\n",
+ ST_CONFLICT => "File(s) in conflicts:\n%s",
+ ST_MISSING => "File(s) missing:\n%s",
+ ST_OUT_OF_DATE => "File(s) out of date:\n%s",
+ SWITCH_UNSAFE => "%s: merge template exists."
+ . " Please remove before retrying.\n",
+ WC_EXIST => "%s: working copy already exists.\n",
+ WC_INVALID_BRANCH => "%s: not a working copy of a standard FCM branch.\n",
+ WC_URL_NOT_EXIST => "%s: working copy URL does not exists at HEAD.\n",
+);
+# Helper for "_format_e_sys", formatters based on exception code.
+our %E_SYS_FORMATTER_FOR = (
+ BUILD_SOURCE => _format_e_func('e_sys_build_source'),
+ BUILD_SOURCE_SYN => _format_e_func('e_sys_build_source_syn'),
+ BUILD_TARGET => \&_format_e_sys_build_target,
+ BUILD_TARGET_BAD => _format_e_func('e_sys_build_target_bad', $IS_MULTI_LINE),
+ BUILD_TARGET_CYC => \&_format_e_sys_build_target_cyc,
+ BUILD_TARGET_DEP => \&_format_e_sys_build_target_dep,
+ BUILD_TARGET_DUP => \&_format_e_sys_build_target_dup,
+ CACHE_LOAD => _format_e_func('e_sys_cache_load'),
+ CACHE_TYPE => _format_e_func('e_sys_cache_type'),
+ CM_ALREADY_EXIST => _format_e_func('e_sys_cm_already_exist'),
+ CM_ARG => _format_e_func('e_sys_cm_arg'),
+ CM_BRANCH_NAME => _format_e_func('e_sys_cm_branch_name'),
+ CM_BRANCH_SOURCE => _format_e_func('e_sys_cm_branch_source'),
+ CM_CHECKOUT => _format_e_func('e_sys_cm_checkout'),
+ CM_LOG_EDIT_NULL => _format_e_func('e_sys_cm_log_edit_null'),
+ CM_LOG_EDIT_DELIMITER => _format_e_func('e_sys_cm_log_edit_delimiter'),
+ CM_OPT_ARG => _format_e_func('e_sys_cm_opt_arg'),
+ CM_PROJECT_NAME => _format_e_func('e_sys_cm_project_name'),
+ CM_REPOSITORY => _format_e_func('e_sys_cm_repository'),
+ CONFIG_CONFLICT => _format_e_sys_config_func('conflict'),
+ CONFIG_INHERIT => _format_e_sys_config_func('inherit'),
+ CONFIG_MODIFIER => _format_e_sys_config_func('modifier'),
+ CONFIG_NS => _format_e_sys_config_func('ns'),
+ CONFIG_NS_VALUE => _format_e_sys_config_func('ns_value'),
+ CONFIG_UNKNOWN => _format_e_sys_config_func('unknown'),
+ CONFIG_VALUE => _format_e_sys_config_func('value'),
+ COPY => _format_e_func('e_sys_copy'),
+ DEST_CLEAN => _format_e_func('e_sys_dest_clean'),
+ DEST_CREATE => _format_e_func('e_sys_dest_create'),
+ DEST_LOCK => _format_e_func('e_sys_dest_lock'),
+ DEST_LOCKED => _format_e_func('e_sys_dest_locked'),
+ EXPORT_ITEMS_SRC => _format_e_func('e_sys_export_items_src'),
+ EXTRACT_LOC_BASE => _format_e_func('e_sys_extract_loc_base'),
+ EXTRACT_MERGE => \&_format_e_sys_extract_merge,
+ EXTRACT_NS => _format_e_func('e_sys_extract_ns', $IS_MULTI_LINE),
+ MIRROR => \&_format_e_sys_mirror,
+ MIRROR_NULL => _format_e_func('e_sys_mirror_null'),
+ MIRROR_SOURCE => _format_e_func('e_sys_mirror_source', $IS_MULTI_LINE),
+ MIRROR_TARGET => _format_e_func('e_sys_mirror_target'),
+ MAKE => _format_e_func('e_sys_make'),
+ MAKE_ARG => \&_format_e_sys_make_arg,
+ MAKE_CFG => _format_e_func('e_sys_make_cfg'),
+ MAKE_PROP_NS => \&_format_e_sys_make_prop_ns,
+ MAKE_PROP_VALUE => \&_format_e_sys_make_prop_value,
+ SHELL => \&_format_e_sys_shell,
+);
+# Helper for "_format_e_util", formatters based on exception code.
+our %E_UTIL_FORMATTER_FOR = (
+ CLASS_LOADER => _format_e_func('e_util_class_loader'),
+ CONFIG_CONT_EOF => _format_e_util_config_func('eof'),
+ CONFIG_CYCLIC => _format_e_util_config_stack_func('cyclic'),
+ CONFIG_LOAD => _format_e_util_config_stack_func('load'),
+ CONFIG_SYNTAX => _format_e_util_config_func('syntax'),
+ CONFIG_USAGE => _format_e_util_config_func('usage'),
+ CONFIG_VAR_UNDEF => _format_e_util_config_func('var_undef'),
+ IO => _format_e_func('e_util_io'),
+ LOCATOR_AS_INVARIANT => _format_e_util_locator_func(''),
+ LOCATOR_BROWSER_URL => _format_e_util_locator_func('_browser_url'),
+ LOCATOR_FIND => _format_e_util_locator_func(''),
+ LOCATOR_KEYWORD_LOC => _format_e_util_locator_func('_keyword_loc'),
+ LOCATOR_KEYWORD_REV => _format_e_util_locator_func('_keyword_rev'),
+ LOCATOR_READER => _format_e_util_locator_func('_reader'),
+ LOCATOR_TYPE => _format_e_util_locator_func('_type'),
+ SHELL_OPEN3 => _format_e_util_shell_func('_open3'),
+ SHELL_OS => _format_e_util_shell_func('_os'),
+ SHELL_SIGNAL => _format_e_util_shell_func('_signal'),
+ SHELL_WHICH => _format_e_util_shell_func('_which'),
+);
+# Alias
+our $R;
+# Named diagnostic strings
+our %S = (
+ # ERROR DIAGNOSTICS
+ e_class => '%s: %s => %s: internal error at %s:%d',
+ e_cli_app => '%s: unknown command,'
+ . ' type \'%s help\' for help',
+ e_cli_opt => '%s: incorrect usage,'
+ . ' type \'%s help %1$s\' for help',
+ e_sys_build_source => '%s: source does not exist',
+ e_sys_build_source_syn => '%s(%d): syntax error',
+ e_sys_build_target => '%s: target not found after an update:',
+ e_sys_build_target_1 => '%s: expect target file',
+ e_sys_build_target_bad => '%s: don\'t know how to build specified'
+ . ' target',
+ e_sys_build_target_cyclic => '%s: target depends on itself',
+ e_sys_build_target_dep => '%s: bad or missing dependency (type=%s)',
+ e_sys_build_target_dup => '%s: same target from [%s]',
+ e_sys_build_target_stack => ' required by: %s',
+ e_sys_cache_load => '%s: cannot retrieve cache',
+ e_sys_cache_type => '%s: unexpected cache type',
+ e_sys_cm_already_exist => '%s: already exists',
+ e_sys_cm_arg => '%s: bad argument',
+ e_sys_cm_branch_name => '%s: invalid branch name',
+ e_sys_cm_branch_source => '%s: invalid branch source',
+ e_sys_cm_checkout => '%s: is already a working copy of %s',
+ e_sys_cm_log_edit_delimiter => '%sthe above log delimiter is altered',
+ e_sys_cm_log_edit_null => 'log message is empty',
+ e_sys_cm_opt_arg => '%s=%s: bad option argument',
+ e_sys_cm_project_name => '%s: invalid project name',
+ e_sys_cm_repository => '%s: invalid repository',
+ e_sys_config_conflict => '%s: cannot modify, value is inherited',
+ e_sys_config_inherit => '%s: cannot inherit from an incomplete make',
+ e_sys_config_modifier => '%s: incorrect modifier in declaration',
+ e_sys_config_ns => '%s: incorrect name-space declaration',
+ e_sys_config_ns_value => '%s: mismatch between name-space and value',
+ e_sys_config_unknown => '%s: unknown declaration',
+ e_sys_config_value => '%s: incorrect value in declaration',
+ e_sys_copy => '%s -> %s: copy failed',
+ e_sys_dest_clean => '%s: cannot remove',
+ e_sys_dest_create => '%s: cannot create',
+ e_sys_dest_locked => '%s: lock exists at the destination',
+ e_sys_export_items_src => 'source location not specified',
+ e_sys_extract_loc_base => '%s: cannot determine base location',
+ e_sys_extract_merge => '%s: merge results in conflict',
+ e_sys_extract_merge_output => ' merge output: %s',
+ e_sys_extract_merge_source => ' source from location %2d: %s',
+ e_sys_extract_merge_source_0 => '(none)',
+ e_sys_extract_merge_source_x => '!!! source from location %2d: %s',
+ e_sys_extract_ns => '%s: name-spaces declared but not used',
+ e_sys_mirror => '%s <- %s: mirror failed',
+ e_sys_mirror_null => 'mirror target not specified',
+ e_sys_mirror_source => '%s: cannot mirror this step',
+ e_sys_mirror_target => '%s: cannot create mirror target',
+ e_sys_make => '%s: step is not implemented',
+ e_sys_make_arg => 'arg %d (%s): invalid config declaration',
+ e_sys_make_cfg => 'no configuration specified or found',
+ e_sys_make_arg_more => 'did you mean "%s"?',
+ e_sys_make_prop_ns => '%s.prop{%s}[%s] = %s: bad name-space',
+ e_sys_make_prop_value => '%s.prop{%s}[%s] = %s: bad value',
+ e_sys_shell => '%s # rc=%d',
+ e_unknown => 'command failed',
+ e_util_class_loader => '%s: required package cannot be loaded',
+ e_util_config => '%s:%d: %s',
+ e_util_config_eof => 'continuation at eof',
+ e_util_config_syntax => 'syntax error',
+ e_util_config_usage => 'incorrect usage',
+ e_util_config_var_undef => 'reference to undefined variable',
+ e_util_config_stack_cyclic => '%s: cannot load config file,'
+ . ' cyclic dependency',
+ e_util_config_stack_load => '%s: cannot load config file',
+ e_util_io => '%s: I/O error',
+ e_util_locator => '%s: not found',
+ e_util_locator_browser_url => '%s: cannot determine browser URL',
+ e_util_locator_keyword_loc => '%s: location keyword not defined',
+ e_util_locator_keyword_rev => '%s: revision keyword not defined',
+ e_util_locator_reader => '%s: cannot be read',
+ e_util_locator_type => '%s: unsupported type of location',
+ e_util_shell_open3 => '%s: command failed to invoke',
+ e_util_shell_os => '%s: command failed due to OS error',
+ e_util_shell_signal => '%s: command received a signal',
+ e_util_shell_which => '%s: command not found',
+
+ # NORMAL DIAGNOSTICS
+ cm_abort_null => 'command will result in no change',
+ cm_abort_user => 'by user',
+ cm_branch_create_source => 'Source: %s (%d)',
+ cm_branch_list => '%s: %d match(es)',
+ cm_commit_message => 'Change summary:' . "\n"
+ . '-' x 80 . "\n" . '%s'
+ . '-' x 80 . "\n"
+ . 'Commit message is as follows:' . "\n"
+ . '-' x 80 . "\n" . '%s%s'
+ . '-' x 80,
+ cm_conflict_text => '%s: in text conflict.',
+ cm_conflict_text_skip => '%s: skipped binary file in text conflict.',
+ cm_conflict_tree => '%s: in tree conflict.',
+ cm_conflict_tree_skip => '%s: skipped unhandled tree conflict.',
+ cm_conflict_tree_time_warn => '%s: looking for a rename operation,'
+ . ' please wait...',
+ cm_create_target => 'Created: %s',
+ cm_log_edit => '%s: starting commit message editor...',
+ config_open => 'config-file=%s%s',
+ config_var_undef => '%s:%d: %s: variable not defined',
+ event => '%s: event raised',
+ export_item_create => 'A %s@%s -> %s',
+ export_item_delete => 'D %s@%s -> %s',
+ fcm_version => '%s',
+ keyword_loc => 'location[%s] = %s',
+ keyword_loc_primary => 'location{primary}[%s] = %s',
+ keyword_rev => 'revision[%s:%s] = %s',
+ make_build_shell_out_1 => '[>>&1] ',
+ make_build_shell_out_2 => '[>>&2] ',
+ make_build_source_analyse => 'analyse %4.1f %s',
+ make_build_source_analyse_1 => ' -> (%9s) %s',
+ make_build_source_summary => 'sources: total=%d, analysed=%d,'
+ . ' elapsed-time=%.1fs, total-time=%.1fs',
+ make_build_target_done_0 => '%-9s ---- %s %-20s <- %s',
+ make_build_target_done_1 => '%-9s %4.1f %s %-20s <- %s',
+ make_build_target_from_ns => 'source->target %s -> (%s) %s/ %s',
+ make_build_target_select => 'required-target: %-9s %-7s %s',
+ make_build_target_select_t => 'target-tree-analysis: elapsed-time=%.1fs',
+ make_build_target_stack => 'target %s%s%s',
+ make_build_target_stack_more => ' (n-deps=%d)',
+ make_build_target_missing_dep=> '%-30s: ignore-missing-dep: (%3$9s) %2$s',
+ make_build_target_sum => 'TOTAL targets:'
+ . ' modified=%d, unchanged=%d, failed=%d,'
+ . ' elapsed-time=%.1fs',
+ make_build_target_task_sum => '%-9s targets:'
+ . ' modified=%d, unchanged=%d, failed=%d,'
+ . ' total-time=%.1fs',
+ make_build_targets_fail_0 => '! %-20s: depends on failed target: %s',
+ make_build_targets_fail_1 => '! %-20s: update task failed',
+ make_dest => 'dest=%s',
+ make_dest_use => 'use=%s',
+ make_extract_project_tree => 'location %5s:%2d: %s%s',
+ make_extract_project_tree_1 => ' (%s)',
+ make_extract_runner_summary => '%s: n-tasks=%d,'
+ . ' elapsed-time=%.1fs, total-time=%.1fs',
+ make_extract_target => '%s%s %5s:%-6s %s',
+ make_extract_target_base_yes => '0',
+ make_extract_target_base_no => '-',
+ make_extract_symlink => 'symlink ignored: %s',
+ make_extract_target_summary_d=> ' dest: %4d [%1s %s]',
+ make_extract_target_summary_s=> 'source: %4d [%1s %s]',
+ make_mirror => '%s <- %s',
+ make_mode => 'mode=%s',
+ make_mode_new => 'new',
+ make_mode_incr => 'incremental',
+ shell => 'shell(%d %4.1f) %s',
+ task_workers_destroy => '%s worker processes destroyed',
+ task_workers_init => '%s worker processes started',
+ timer_done => '%-20s# %.1fs',
+ timer_init => '%-20s# %s',
+);
+# Symbols/Descriptions for a make extract target status.
+my %MAKE_EXTRACT_TARGET_SYM_OF = (
+ ST_ADDED => ['A', 'added' ],
+ ST_DELETED => ['D', 'deleted' ],
+ ST_MODIFIED => ['M', 'modified' ],
+ ST_O_ADDED => ['a', 'added, overriding inherited' ],
+ ST_O_DELETED => ['d', 'deleted, overriding inherited'],
+ ST_UNCHANGED => ['U', 'unchanged' ],
+ ST_UNKNOWN => ['?', 'unknown' ],
+);
+# Symbols/Descriptions for a make source status.
+my %MAKE_EXTRACT_SOURCE_SYM_OF = (
+ ST_ADDED => ['A', 'added by a diff source tree' ],
+ ST_DELETED => ['D', 'deleted by a diff source tree' ],
+ ST_MERGED => ['G', 'merged from 2+ diff source trees'],
+ ST_MODIFIED => ['M', 'modified by a diff source tree' ],
+ ST_UNCHANGED => ['U', 'from base' ],
+ ST_UNKNOWN => ['?', 'unknown' ],
+);
+
+# Creates the class.
+__PACKAGE__->class({util => '&'}, {action_of => {main => \&_main}});
+
+sub _main {
+ my ($attrib_ref, $event) = @_;
+ local($R) = $attrib_ref->{util}->util_of_report();
+ if (!exists($ACTION_OF{$event->get_code()})) {
+ return $R->report(
+ {level => $R->HIGH}, sprintf($S{event}, $event->get_code()),
+ );
+ }
+ $ACTION_OF{$event->get_code()}->(@{$event->get_args()});
+}
+
+# Formats a stack of configuration files.
+sub _format_config_stack {
+ my ($config_stack_ref) = @_;
+ my @config_stack = @{$config_stack_ref};
+ my $indent_char = q{};
+ my $return = q{};
+ my $i = 0;
+ for my $item (@config_stack) {
+ my ($locator, $line) = @{$item};
+ my $indent = q{ - } x $i++;
+ $return .= sprintf(
+ $S{'config_open'} . "\n",
+ $indent, ($locator->get_value() . ($line ? ':' . $line : q{})),
+ );
+ }
+ return $return;
+}
+
+# Formats a CM exception.
+sub _format_e_cm {
+ my ($e) = @_;
+ sprintf($E_CM_FORMAT_FOR{$e->get_code()}, $e->get_targets());
+}
+
+# Formats a class exception.
+sub _format_e_class {
+ my ($e) = @_;
+ sprintf(
+ $S{e_class},
+ $e->get_package(),
+ $e->get_key(),
+ (defined($e->get_value()) ? $e->get_value() : 'undef'),
+ @{$e->get_caller()}[1, 2],
+ );
+}
+
+# Formats a CLI exception.
+sub _format_e_cli {
+ my ($e) = @_;
+ my $format
+ = $e->get_code() eq $e->APP ? $S{e_cli_app}
+ : $S{e_cli_opt}
+ ;
+ sprintf($format, $e->get_ctx()->[0], basename($0));
+}
+
+# Formats a system exception.
+sub _format_e_sys {
+ my ($e) = @_;
+ if (exists($E_SYS_FORMATTER_FOR{$e->get_code()})) {
+ return $E_SYS_FORMATTER_FOR{$e->get_code()}->($e);
+ }
+ $e;
+}
+
+# Formats a system exception - CONFIG_*.
+sub _format_e_sys_config_func {
+ my ($suffix) = @_;
+ my $key = 'e_sys_config_' . $suffix;
+ sub {
+ my ($e) = @_;
+ my @ctx_list
+ = ref($e->get_ctx()) eq 'ARRAY' ? @{$e->get_ctx()}
+ : ($e->get_ctx())
+ ;
+ map {(
+ sprintf($S{$key}, $_->as_string()),
+ _format_config_stack($_->get_stack()),
+ )} @ctx_list;
+ }
+}
+
+# Formats a system exception - BUILD_TARGET.
+sub _format_e_sys_build_target {
+ my ($e) = @_;
+ my $ctx = $e->get_ctx();
+ ( sprintf($S{e_sys_build_target}, $ctx->get_key()),
+ sprintf($S{e_sys_build_target_1}, $ctx->get_path()),
+ );
+}
+
+# Formats a system exception - BUILD_TARGET_CYC.
+sub _format_e_sys_build_target_cyc {
+ my ($e) = @_;
+ my @messages;
+ while (my ($key, $hash_ref) = each(%{$e->get_ctx()})) {
+ my ($head, @stack) = reverse(@{$hash_ref->{'keys'}});
+ push(@messages, sprintf($S{e_sys_build_target_cyclic}, $head));
+ push(@messages, map {sprintf($S{e_sys_build_target_stack}, $_)} @stack);
+ }
+ @messages;
+}
+
+# Formats a system exception - BUILD_TARGET_DEP.
+sub _format_e_sys_build_target_dep {
+ my ($e) = @_;
+ my @messages;
+ while (my ($key, $hash_ref) = each(%{$e->get_ctx()})) {
+ my ($head, @stack) = reverse(@{$hash_ref->{'keys'}});
+ for (@{$hash_ref->{'values'}}) { # [$dep_key, $dep_type]
+ my ($dep_name, $dep_type, $dep_remark) = @{$_};
+ if ($dep_remark) {
+ $dep_type = $dep_remark . '.' . $dep_type;
+ }
+ push(
+ @messages,
+ sprintf($S{e_sys_build_target_dep}, $dep_name, $dep_type),
+ );
+ }
+ push(@messages, map {sprintf($S{e_sys_build_target_stack}, $_)} @stack);
+ }
+ @messages;
+}
+
+# Formats a system exception - BUILD_TARGET_DUP.
+sub _format_e_sys_build_target_dup {
+ my ($e) = @_;
+ my @messages;
+ while (my ($key, $hash_ref) = each(%{$e->get_ctx()})) {
+ my ($head, @stack) = reverse(@{$hash_ref->{'keys'}});
+ my @ns_list = @{$hash_ref->{'values'}};
+ my $ns = _format_shell_words({'delimiter' => q{, }}, sort(@ns_list));
+ push(@messages, sprintf($S{e_sys_build_target_dup}, $key, $ns));
+ push(@messages, map {sprintf($S{e_sys_build_target_stack}, $_)} @stack);
+ }
+ @messages;
+}
+
+# Formats a system exception - EXTRACT_MERGE.
+sub _format_e_sys_extract_merge {
+ my ($e) = @_;
+ my $target = $e->get_ctx()->{'target'};
+ my $source0 = $target->get_source_of()->{0};
+ my $location_of_0 = $S{e_sys_extract_merge_source_0};
+ if ($source0->get_locator()) {
+ $location_of_0 = $source0->get_locator()->get_value();
+ }
+ my $key = $e->get_ctx()->{'key'};
+ my $location_of_key
+ = $target->get_source_of()->{$key}->get_locator()->get_value();
+ ( sprintf($S{e_sys_extract_merge}, $target->get_ns()),
+ sprintf($S{e_sys_extract_merge_output}, $e->get_ctx()->{'output'}),
+ sprintf($S{e_sys_extract_merge_source}, 0, $location_of_0),
+ ( map {sprintf(
+ $S{e_sys_extract_merge_source},
+ $_,
+ $target->get_source_of()->{$_}->get_locator()->get_value(),
+ )} @{$e->get_ctx()->{'keys_done'}}
+ ),
+ sprintf($S{e_sys_extract_merge_source_x}, $key, $location_of_key),
+ ( map {sprintf(
+ $S{e_sys_extract_merge_source},
+ $_,
+ $target->get_source_of()->{$_}->get_locator()->get_value(),
+ )} @{$e->get_ctx()->{'keys_left'}}
+ ),
+ );
+}
+
+# Formats a system exception - MIRROR.
+sub _format_e_sys_mirror {
+ my ($e) = @_;
+ my ($target, @sources) = @{$e->get_ctx()};
+ sprintf($S{e_sys_mirror}, $target, _format_shell_words(@sources));
+}
+
+# Formats a system exception - MAKE_ARG
+sub _format_e_sys_make_arg {
+ my ($e) = @_;
+ my @return;
+ for (@{$e->get_ctx()}) {
+ my ($arg_index, $arg_value) = @{$_};
+ push(@return, sprintf($S{e_sys_make_arg}, $arg_index, $arg_value));
+ my $advice
+ = $arg_value =~ qr{\.cfg\z}msx ? '-f ' . $arg_value
+ : $arg_value eq '0' ? '-q'
+ : $arg_value eq '2' ? '-v'
+ : $arg_value eq '3' ? '-v -v'
+ : undef;
+ if (defined($advice)) {
+ push(@return, sprintf($S{e_sys_make_arg_more}, $advice));
+ }
+ }
+ return @return;
+}
+
+# Formats a system exception - MAKE_PROP_NS
+sub _format_e_sys_make_prop_ns {
+ my ($e) = @_;
+ map {sprintf($S{e_sys_make_prop_ns}, @{$_})} @{$e->get_ctx()};
+}
+
+# Formats a system exception - MAKE_PROP_VALUE
+sub _format_e_sys_make_prop_value {
+ my ($e) = @_;
+ map {sprintf($S{e_sys_make_prop_value}, @{$_})} @{$e->get_ctx()};
+}
+
+# Formats a system exception - SHELL.
+sub _format_e_sys_shell {
+ my ($e) = @_;
+ my $command = _format_shell_words(@{$e->get_ctx()->{command_list}});
+ my %value_of = (out => q{}, rc => '?', %{$e->get_ctx()});
+ return (
+ #(map {sprintf($S{e_sys_shell_err}, $_)} split("\n", $value_of{err})),
+ #(map {sprintf($S{e_sys_shell_out}, $_)} split("\n", $value_of{out})),
+ sprintf($S{e_sys_shell}, $command, $value_of{rc}),
+ );
+}
+
+# Formats a util exception.
+sub _format_e_util {
+ my ($e) = @_;
+ if (exists($E_UTIL_FORMATTER_FOR{$e->get_code()})) {
+ return $E_UTIL_FORMATTER_FOR{$e->get_code()}->($e);
+ }
+ $e;
+}
+
+# Returns a CODE to format a util config-reader exception.
+sub _format_e_util_config_func {
+ my ($id) = @_;
+ sub {
+ my ($e) = @_;
+ ( sprintf(
+ $S{'e_util_config'},
+ $e->get_ctx()->get_stack()->[-1][0]->get_value(),
+ $e->get_ctx()->get_stack()->[-1][1],
+ $S{'e_util_config_' . $id},
+ ),
+ $e->get_ctx()->as_string(),
+ );
+ };
+}
+
+# Returns a CODE to format a util config-reader exception where the ctx is the
+# locator stack.
+sub _format_e_util_config_stack_func {
+ my ($id) = @_;
+ sub {
+ my ($e) = @_;
+ my @return = (
+ _format_config_stack($e->get_ctx()),
+ sprintf(
+ $S{'e_util_config_stack_' . $id},
+ $e->get_ctx()->[-1][0]->get_value(),
+ ),
+ );
+ @return;
+ };
+}
+
+# Formats a locator exception.
+sub _format_e_util_locator_func {
+ my ($id) = @_;
+ sub {sprintf($S{'e_util_locator' . $id}, $_[0]->get_ctx()->get_value())};
+}
+
+# Formats a shell exception.
+sub _format_e_util_shell_func {
+ my ($id) = @_;
+ sub {
+ sprintf(
+ $S{'e_util_shell' . $id},
+ _format_shell_words(@{$_[0]->get_ctx()})
+ );
+ };
+}
+
+# Returns a CODE to format a exception context in a single/multi line.
+sub _format_e_func {
+ my ($id, $is_multi_line) = @_;
+ sub {
+ my ($e) = @_;
+ my @args;
+ if (defined($e->get_ctx())) {
+ @args = (ref($e->get_ctx()) || ref($e->get_ctx()) eq 'ARRAY')
+ ? @{$e->get_ctx()} : $e->get_ctx();
+ }
+ $is_multi_line
+ ? (map {sprintf($S{$id}, $_)} @args) : (sprintf($S{$id}, @args));
+ };
+}
+
+# Formats a simple reference.
+sub _format_ref {
+ my ($hash_ref) = @_;
+ local($Data::Dumper::Terse) = 1;
+ local($Data::Dumper::Indent) = 0;
+ Dumper($hash_ref);
+}
+
+# Formats words into a string suitable for used in a shell command.
+sub _format_shell_words {
+ my %option = ('delimiter' => q{ });
+ if (@_ && ref($_[0]) && ref($_[0]) eq 'HASH') {
+ %option = (%option, %{$_[0]});
+ shift();
+ }
+ my (@words) = @_;
+ join(
+ $option{'delimiter'},
+ map {my $s = $_; $s =~ s{(['"\s])}{\\$1}gmsx; $s} @words,
+ );
+}
+
+# Notification on abort of a CM command.
+sub _event_cm_abort {
+ my ($id) = @_;
+ $R->report(
+ {level => $R->QUIET, prefix => $R->PREFIX_QUIT, type => $R->TYPE_ERR},
+ $S{'cm_abort_' . $id},
+ );
+}
+
+# Notification on a project branch listing.
+sub _event_cm_branch_list {
+ my ($project, @branches) = @_;
+ $R->report(sprintf($S{'cm_branch_list'}, $project, scalar(@branches)));
+ for my $branch (@branches) {
+ $R->report({level => $R->QUIET, prefix => $R->PREFIX_NULL}, $branch);
+ }
+}
+
+# Notification on a log message to be used by a commit.
+sub _event_cm_commit_message {
+ my ($ctx) = @_;
+ $R->report(
+ {prefix => $R->PREFIX_NULL},
+ sprintf(
+ $S{'cm_commit_message'},
+ $ctx->get_info_part(), $ctx->get_user_part(), $ctx->get_auto_part(),
+ ),
+ );
+}
+
+# Notification on a skipped file in text conflict.
+sub _event_cm_conflict_text_skip {
+ my ($ctx) = @_;
+ $R->report({type => $R->TYPE_ERR}, sprintf($S{'cm_conflict_text_skip'}, $ctx));
+}
+
+# Notification for an unhandled type of tree conflict.
+sub _event_cm_conflict_tree_skip {
+ my ($ctx) = @_;
+ $R->report({type => $R->TYPE_ERR}, sprintf($S{'cm_conflict_tree_skip'}, $ctx));
+}
+
+# Warning that the tree conflict operation search may take some time.
+sub _event_cm_conflict_tree_time_warn {
+ my ($ctx) = @_;
+ $R->report({type => $R->TYPE_ERR}, sprintf($S{'cm_conflict_tree_time_warn'}, $ctx));
+}
+
+# Notification when a config entry is found.
+sub _event_config_entry {
+ my ($entry, $in_fcm1) = @_;
+ $R->report(
+ {level => $R->QUIET, prefix => $R->PREFIX_NULL},
+ $entry->as_string($in_fcm1),
+ );
+}
+
+# Notification for a configuration file open.
+sub _event_config_open {
+ my ($config_stack_ref, $level) = @_;
+ $R->report(
+ {level => (defined($level) ? $level : $R->DEBUG)},
+ sub {
+ my $value = $config_stack_ref->[-1][0]->get_value();
+ my $indent = q{ - } x (scalar(@{$config_stack_ref}) - 1);
+ sprintf($S{config_open}, $indent, $value);
+ },
+ );
+}
+
+# Notification when a config variable is undefined.
+sub _event_config_var_undef {
+ my ($entry, $symbol) = @_;
+ $R->report(
+ {type => $R->TYPE_ERR},
+ sprintf(
+ $S{'config_var_undef'},
+ $entry->get_stack()->[-1][0]->get_value(),
+ $entry->get_stack()->[-1][1],
+ $symbol,
+ ),
+ );
+}
+
+# Notification for an exception.
+sub _event_e {
+ my ($exception) = @_;
+ my @e_stack = ($exception);
+ while ( blessed($e_stack[-1])
+ && $e_stack[-1]->can('get_exception')
+ && (my $e = $e_stack[-1]->get_exception())
+ ) {
+ push(@e_stack, $e);
+ }
+ while (my $e = shift(@e_stack)) {
+ my $formatter;
+ if (blessed($e)) {
+ my $item = first {$e->isa($_->[0])} @E_FORMATTERS;
+ if ($item) {
+ $formatter = $item->[1];
+ }
+ if (!$formatter && $e->can('as_string')) {
+ $formatter = sub {$e->as_string()};
+ }
+ }
+ elsif (ref($e)) {
+ $formatter = \&_format_ref;
+ }
+ elsif ($e eq "\n") {
+ chomp($e);
+ }
+ $R->report(
+ {level => $R->FAIL, type => $R->TYPE_ERR},
+ (defined($formatter) ? $formatter->($e) : $e),
+ );
+ }
+ 1;
+}
+
+# Notification when a keyword entry is found.
+sub _event_keyword_entry {
+ my ($entry) = @_;
+ if ($entry->is_implied()) {
+ return;
+ }
+ my @implied_entry_list
+ = values(%{$entry->get_ctx_of_implied()->get_entry_by_key()});
+ if (@implied_entry_list) {
+ $R->report(
+ {level => $R->QUIET, prefix => $R->PREFIX_NULL},
+ sprintf(
+ $S{keyword_loc_primary},
+ $entry->get_key(),
+ $entry->get_value(),
+ ),
+ );
+ for my $implied_entry (
+ sort {$a->get_key() cmp $b->get_key()} @implied_entry_list
+ ) {
+ $R->report(
+ {level => $R->MEDIUM, prefix => $R->PREFIX_NULL},
+ sprintf(
+ $S{keyword_loc},
+ $implied_entry->get_key(),
+ $implied_entry->get_value(),
+ ),
+ );
+ }
+ }
+ else {
+ $R->report(
+ {level => $R->QUIET, prefix => $R->PREFIX_NULL},
+ sprintf($S{keyword_loc}, $entry->get_key(), $entry->get_value()),
+ );
+ }
+ my @revision_entry_list
+ = values(%{$entry->get_ctx_of_rev()->get_entry_by_key()});
+ for my $revision_entry (
+ sort {$a->get_key() cmp $b->get_key()} @revision_entry_list
+ ) {
+ $R->report(
+ {level => $R->QUIET, prefix => $R->PREFIX_NULL},
+ sprintf(
+ $S{keyword_rev},
+ $entry->get_key(),
+ $revision_entry->get_key(),
+ $revision_entry->get_value(),
+ ),
+ );
+ }
+ 1;
+}
+
+# Notification of the output from a command.
+sub _event_out {
+ my ($out, $err) = @_;
+ my %option = (delimiter => q{}, prefix => $R->PREFIX_NULL);
+ if ($err) {
+ $R->report({level => $R->WARN, type => $R->TYPE_ERR, %option}, $err);
+ }
+ if ($out) {
+ $R->report({level => $R->QUIET, %option}, $out);
+ }
+}
+
+# Notification of the output from a command invoked by make/build.
+sub _event_make_build_shell_out {
+ my ($out, $err) = @_;
+ if ($err) {
+ $R->report(
+ { level => $R->HIGH,
+ prefix => $S{'make_build_shell_out_2'},
+ type => $R->TYPE_ERR,
+ },
+ $err,
+ );
+ }
+ if ($out) {
+ $R->report(
+ {level => $R->HIGH, prefix => $S{'make_build_shell_out_1'}},
+ $out,
+ );
+ }
+}
+
+# Notification when a make destination is being set up.
+sub _event_make_dest {
+ my ($m_ctx, $authority) = @_;
+ $R->report(sprintf($S{make_dest}, $authority . ':' . $m_ctx->get_dest()));
+ $R->report(sprintf(
+ $S{make_mode},
+ $S{'make_mode_' . ($m_ctx->get_prev_ctx() ? 'incr' : 'new')},
+ ));
+ for my $i_ctx (@{$m_ctx->get_inherit_ctx_list()}) {
+ $R->report(sprintf($S{make_dest_use}, $i_ctx->get_dest()));
+ }
+}
+
+# Notification when performing a mirroring.
+sub _event_make_mirror {
+ my ($target, @sources) = @_;
+ $R->report(sprintf($S{make_mirror}, $target, _format_shell_words(@sources)));
+}
+
+# Notification when the multi-thread task runner initiates its workers.
+sub _event_task_workers {
+ my ($id, $n_workers) = @_;
+ my $key = 'task_workers_' . $id;
+ if (exists($S{$key})) {
+ $R->report({level => $R->HIGH}, sprintf($S{$key}, $n_workers));
+ }
+}
+
+# Notification when invoking a shell command.
+sub _event_shell {
+ my ($names_ref, $rc, $elapsed) = @_;
+ my $name = _format_shell_words(@{$names_ref});
+ my $message = sprintf($S{shell}, $rc, $elapsed, $name);
+ $R->report({level => $R->HIGH}, $message);
+}
+
+# Notification when a timer starts/ends.
+sub _event_timer {
+ my ($name, $start, $elapsed, $failed) = @_;
+ my $message;
+ if (defined($elapsed)) {
+ $message = sprintf($S{timer_done}, $name, $elapsed);
+ }
+ else {
+ my $format = '%Y-%m-%dT%H:%M:%SZ';
+ $message = sprintf(
+ $S{timer_init}, $name, strftime($format, gmtime($start)));
+ }
+ my $prefix
+ = $failed ? $R->PREFIX_FAIL
+ : defined($elapsed) ? $R->PREFIX_DONE
+ : $R->PREFIX_INIT
+ ;
+ $R->report({prefix => $prefix}, $message);
+}
+
+# Notification when make-build analyse a source.
+sub _event_make_build_source_analyse {
+ my ($source, $elapse) = @_;
+ $R->report(
+ {level => $R->MEDIUM},
+ sprintf($S{make_build_source_analyse}, $elapse, $source->get_ns()),
+ );
+ for my $dep (@{$source->get_deps()}) {
+ $R->report(
+ {level => $R->HIGH},
+ sprintf($S{make_build_source_analyse_1}, reverse(@{$dep})),
+ );
+ }
+}
+
+# Notification when make-build has updated or does not need to update a target.
+sub _event_make_build_target_done {
+ my ($target, $elapsed_time) = @_;
+ my $tmpl = defined($elapsed_time)
+ ? $S{make_build_target_done_1} : $S{make_build_target_done_0};
+ $R->report(
+ {level => $R->MEDIUM},
+ sprintf(
+ $tmpl,
+ $target->get_task(),
+ (defined($elapsed_time) ? ($elapsed_time) : ()),
+ $MAKE_EXTRACT_TARGET_SYM_OF{$target->get_status()}[0],
+ $target->get_key(),
+ $target->get_ns(),
+ ),
+ );
+}
+
+# Notification when make-build a target fails to update or is failed by
+# dependencies.
+sub _event_make_build_target_fail {
+ my ($target, $elapsed_time) = @_;
+ my $tmpl = defined($elapsed_time)
+ ? $S{make_build_target_done_1} : $S{make_build_target_done_0};
+ $R->report(
+ {level => $R->FAIL, type => $R->TYPE_ERR},
+ sprintf(
+ $tmpl,
+ $target->get_task(),
+ (defined($elapsed_time) ? ($elapsed_time) : ()),
+ '!',
+ $target->get_key(),
+ $target->get_ns(),
+ ),
+ );
+}
+
+# Notification when make-build ignores a missing dependency from a target.
+sub _event_make_build_target_missing_dep {
+ $R->report(
+ {level => $R->WARN, type => $R->TYPE_ERR},
+ sprintf($S{make_build_target_missing_dep}, @_),
+ );
+}
+
+# Notification when make-build generates a target from source.
+sub _event_make_build_target_from_ns {
+ $R->report(
+ {level => $R->HIGH},
+ sprintf($S{make_build_target_from_ns}, @_),
+ );
+}
+
+# Notification when make-build chooses a list of targets to build.
+sub _event_make_build_target_select {
+ my ($target_set_ref) = @_;
+ $R->report(
+ {level => $R->HIGH},
+ sub {
+ map {
+ my $key = $_;
+ my $target = $target_set_ref->{$key};
+ sprintf(
+ $S{make_build_target_select},
+ $target->get_task(), $target->get_category(), $key,
+ );
+ }
+ sort keys(%{$target_set_ref});
+ },
+ );
+}
+
+# Notification when make-build checks a target for cyclic dependency.
+sub _event_make_build_target_stack {
+ my ($key, $rank, $n_deps) = @_;
+ $R->report(
+ {level => $R->HIGH},
+ sub {
+ my $indent = q{ - } x $rank;
+ my $more
+ = $n_deps ? sprintf($S{make_build_target_stack_more}, $n_deps)
+ : q{}
+ ;
+ sprintf($S{make_build_target_stack}, $indent, $key, $more),
+ },
+ );
+}
+
+# Notification when make-build fails to update some targets.
+sub _event_make_build_targets_fail {
+ my ($targets_ref) = @_;
+ $R->report(
+ {type => $R->TYPE_ERR, level => $R->FAIL},
+ (map {
+ my $target = $_;
+ my @failed_by = @{$target->get_failed_by()};
+ my @lines;
+ if (grep {$_ eq $target->get_key()} @failed_by) {
+ push(
+ @lines,
+ sprintf($S{make_build_targets_fail_1}, $target->get_key()),
+ );
+ }
+ for my $failed_by_key (grep {$_ ne $target->get_key()} @failed_by) {
+ push(
+ @lines,
+ sprintf(
+ $S{make_build_targets_fail_0},
+ $target->get_key(),
+ $failed_by_key,
+ ),
+ );
+ }
+ @lines;
+ } sort {$a->get_key() cmp $b->get_key()} @{$targets_ref}),
+ );
+}
+
+# Notification when make-extract finished gathering information for its project
+# source trees.
+sub _event_make_extract_project_tree {
+ my %locators_of = %{$_[0]};
+ for my $ns (sort(keys(%locators_of))) {
+ my $i = 0;
+ for my $locator (@{$locators_of{$ns}}) {
+ my $format_last_mod_rev = q{};
+ if ($locator->get_last_mod_rev()) {
+ $format_last_mod_rev = sprintf(
+ $S{'make_extract_project_tree_1'},
+ $locator->get_last_mod_rev(),
+ );
+ }
+ $R->report(
+ sprintf(
+ $S{'make_extract_project_tree'},
+ $ns, $i++, $locator->get_value(), $format_last_mod_rev
+ ),
+ );
+ }
+ }
+}
+
+# Notification when make-extract used the task runner to perform tasks.
+sub _event_make_extract_runner_summary {
+ $R->report(
+ {level => $R->HIGH},
+ sprintf($S{'make_extract_runner_summary'}, @_),
+ );
+}
+
+# Notification when make-extract completes updating its targets.
+sub _event_make_extract_target_summary {
+ my ($basket) = @_;
+ for (
+ [ 'status',
+ 'make_extract_target_summary_d',
+ \%MAKE_EXTRACT_TARGET_SYM_OF,
+ ],
+ [ 'status_of_source',
+ 'make_extract_target_summary_s',
+ \%MAKE_EXTRACT_SOURCE_SYM_OF,
+ ],
+ ) {
+ my ($name, $format_name, $sym_hash_ref) = @{$_};
+ for my $key (sort keys(%{$basket->{$name}})) {
+ $R->report(sprintf(
+ $S{$format_name},
+ $basket->{$name}{$key},
+ $sym_hash_ref->{$key}[0],
+ $sym_hash_ref->{$key}[1],
+ ));
+ }
+ }
+}
+
+# Notification when make-extract ignores a symlink.
+sub _event_make_extract_symlink {
+ my ($source) = @_;
+ $R->report(
+ {type => $R->TYPE_ERR},
+ sprintf($S{make_extract_symlink}, $source->get_locator()->get_value()),
+ );
+}
+
+# Notification when make-extract updates a target.
+sub _event_make_extract_target {
+ my ($target) = @_;
+ if (!exists($MAKE_EXTRACT_TARGET_SYM_OF{$target->get_status()})) {
+ return;
+ }
+ $R->report(
+ {level => $R->MEDIUM},
+ sub {
+ my ($verbosity) = @_;
+ if ($verbosity < $R->DEBUG && $target->is_unchanged()) {
+ return;
+ }
+ my ($ns, $path) = split(qr{/}msx, $target->get_ns(), 2);
+ my %source_of = %{$target->get_source_of()};
+ my $base = delete($source_of{0});
+ my @diff_keys
+ = grep {!$source_of{$_}->is_unchanged()} keys(%source_of);
+ my @st_missing_diff_keys
+ = grep {$source_of{$_}->is_missing()} @diff_keys;
+ if (@st_missing_diff_keys) {
+ @diff_keys = @st_missing_diff_keys;
+ }
+ sprintf(
+ $S{make_extract_target},
+ $MAKE_EXTRACT_TARGET_SYM_OF{$target->get_status()}[0],
+ $MAKE_EXTRACT_SOURCE_SYM_OF{$target->get_status_of_source()}[0],
+ $ns,
+ join(
+ q{,},
+ ( defined($base) && defined($base->get_locator())
+ ? ($S{make_extract_target_base_yes})
+ : ($S{make_extract_target_base_no})
+ ),
+ sort({$a <=> $b} @diff_keys),
+ ),
+ $path,
+ );
+ },
+ );
+}
+
+# Returns a CODE to perform a simple notification with sprintf format.
+sub _func {
+ my ($id) = @_;
+ sub {$R->report(sprintf($S{$id}, @_))};
+}
+
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::Util::Event
+
+=head1 SYNOPSIS
+
+ use FCM::Util::Event;
+ $event_handler = FCM::Util::Event->new(\%attrib);
+ $event_handler->($event);
+
+=head1 DESCRIPTION
+
+Handles events wrapped as L<FCM::Context::Event|FCM::Context::Event> objects by
+stringifying and reporting them.
+
+This module is part of L<FCM::Util|FCM::Util>. See also the description of the
+$u->report() method in L<FCM::Util|FCM::Util>.
+
+=head1 METHODS
+
+=over 4
+
+=item $class->new(\%attrib)
+
+Returns a new instance. The %attrib HASH can have the following elements:
+
+=over 4
+
+=item util
+
+The parent L<FCM::Util|FCM::Util> object.
+
+=back
+
+=item $util->event($event_ctx)
+
+Notification of an $event_ctx, which should be a blessed reference of
+L<FCM::Context::Event|FCM::Context::Event>.
+
+=back
+
+=head1 TODO
+
+Modularise?
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/Util/Exception.pm b/lib/FCM/Util/Exception.pm
new file mode 100644
index 0000000..92fe05b
--- /dev/null
+++ b/lib/FCM/Util/Exception.pm
@@ -0,0 +1,219 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+# ------------------------------------------------------------------------------
+
+package FCM::Util::Exception;
+use base qw{FCM::Exception};
+
+use constant {
+ CLASS_LOADER => 'CLASS_LOADER',
+ CONFIG_CONT_EOF => 'CONFIG_CONT_EOF',
+ CONFIG_CYCLIC => 'CONFIG_CYCLIC',
+ CONFIG_LOAD => 'CONFIG_LOAD',
+ CONFIG_SYNTAX => 'CONFIG_SYNTAX',
+ CONFIG_USAGE => 'CONFIG_USAGE',
+ CONFIG_VAR_UNDEF => 'CONFIG_VAR_UNDEF',
+ IO => 'IO',
+ LOCATOR_AS_INVARIANT => 'LOCATOR_AS_INVARIANT',
+ LOCATOR_BROWSER_URL => 'LOCATOR_BROWSER_URL',
+ LOCATOR_FIND => 'LOCATOR_FIND',
+ LOCATOR_KEYWORD_LOC => 'LOCATOR_KEYWORD_LOC',
+ LOCATOR_KEYWORD_REV => 'LOCATOR_KEYWORD_REV',
+ LOCATOR_READER => 'LOCATOR_READER',
+ LOCATOR_TYPE => 'LOCATOR_TYPE',
+ SHELL_OPEN3 => 'SHELL_OPEN3',
+ SHELL_OS => 'SHELL_OS',
+ SHELL_SIGNAL => 'SHELL_SIGNAL',
+ SHELL_WHICH => 'SHELL_WHICH',
+};
+
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::Util::Exception
+
+=head1 SYNOPSIS
+
+ use FCM::Util::Exception;
+ eval {
+ # something does not work ...
+ FCM::Util::Exception->throw($code, $ctx, $exception);
+ };
+ if (my $e = $@) {
+ if (FCM::Util::Exception->caught($e)) {
+ # do something ...
+ }
+ else {
+ # do something else ...
+ }
+ }
+
+=head1 DESCRIPTION
+
+This exception represents an error condition in an FCM utility. It is a
+sub-class of L<FCM::Exception|FCM::Exception>.
+
+=head1 CONSTANTS
+
+The following are known error code:
+
+=over 4
+
+=item CLASS_LOADER
+
+L<FCM::Util::ClassLoader|FCM::Util::ClassLoader>: The utility fails to load the
+specified class. The $e->get_ctx() method returns the name of the class it fails
+to load.
+
+=item CONFIG_CONT_EOF
+
+L<FCM::Util::ConfigReader|FCM::Util::ConfigReader>: The last line of the
+configuration file has a continuation marker. Expects the $e->get_ctx() method
+to return the L<FCM::Context::ConfigEntry|FCM::Context::ConfigEntry> object that
+represents the problem entry.
+
+=item CONFIG_CYCLIC
+
+L<FCM::Util::ConfigReader|FCM::Util::ConfigReader>: There is a cyclic dependency
+in the include hierarchy. Expects the $e->get_ctx() method to return an ARRAY
+reference of the locator stack. (The last element of the ARRAY is the top of the
+stack, and each element is a 2-element ARRAY reference, where the first element
+is a L<FCM::Context::Locator|FCM::Context::Locator> object and the second
+element is the line number.)
+
+=item CONFIG_LOAD
+
+L<FCM::Util::ConfigReader|FCM::Util::ConfigReader>: An error occurs when loading
+a configuration file. Expects the $e->get_ctx() method to return an ARRAY
+reference of the locator stack. (The last element of the ARRAY is the top of the
+stack, and each element is a 2-element ARRAY reference, where the first element
+is a L<FCM::Context::Locator|FCM::Context::Locator> object and the second
+element is the line number.)
+
+=item CONFIG_SYNTAX
+
+L<FCM::Util::ConfigReader|FCM::Util::ConfigReader>: A syntax error in the
+declaration. Expects the $e->get_ctx() method to return the
+L<FCM::Context::ConfigEntry|FCM::Context::ConfigEntry> object that represents
+the problem entry.
+
+=item CONFIG_USAGE
+
+L<FCM::Util::ConfigReader|FCM::Util::ConfigReader>: An attempt to assign a value
+to a reserved variable. Expects the $e->get_ctx() method to return the
+L<FCM::Context::ConfigEntry|FCM::Context::ConfigEntry> object that represents
+the problem entry.
+
+=item CONFIG_VAR_UNDEF
+
+L<FCM::Util::ConfigReader|FCM::Util::ConfigReader>: References to an undefined
+variable. Expects the $e->get_ctx() method to return the
+L<FCM::Context::ConfigEntry|FCM::Context::ConfigEntry> object that represents
+the problem entry, and $e->get_exception() to return a string that looks like
+"undef($symbol)" where $symbol is the symbol that references the variable.
+
+=item IO
+
+L<FCM::Util::IO|FCM::Util::IO>: I/O exception. Expects $e->get_ctx() to return
+the path that triggers the exception, and the $e->get_exception() to return the
+$! string.
+
+=item LOCATOR_AS_INVARIANT
+
+L<FCM::Util::Locator|FCM::Util::Locator>: The invariant value of the locator
+cannot be determined. Expects $e->get_ctx() method to return the associated
+L<FCM::Context::Locator|FCM::Context::Locator> object.
+
+=item LOCATOR_BROWSER_URL
+
+L<FCM::Util::Locator|FCM::Util::Locator>: The locator cannot be mapped to a
+browser URL. Expects $e->get_ctx() method to return the associated
+L<FCM::Context::Locator|FCM::Context::Locator> object.
+
+=item LOCATOR_FIND
+
+L<FCM::Util::Locator|FCM::Util::Locator>: The locator does not exist. Expects
+$e->get_ctx() method to return the associated
+L<FCM::Context::Locator|FCM::Context::Locator> object.
+
+=item LOCATOR_KEYWORD_LOC
+
+L<FCM::Util::Locator|FCM::Util::Locator>: The location keyword as specified in
+the locator is not defined. Expects $e->get_ctx() method to return the
+associated L<FCM::Context::Locator|FCM::Context::Locator> object.
+
+=item LOCATOR_KEYWORD_REV
+
+L<FCM::Util::Locator|FCM::Util::Locator>: The revision keyword as specified in
+the locator is not defined. Expects $e->get_ctx() method to return the
+associated L<FCM::Context::Locator|FCM::Context::Locator> object.
+
+=item LOCATOR_READER
+
+L<FCM::Util::Locator|FCM::Util::Locator>: The locator cannot be read. Expects
+$e->get_ctx() method to return the associated
+L<FCM::Context::Locator|FCM::Context::Locator> object.
+
+=item LOCATOR_TYPE
+
+L<FCM::Util::Locator|FCM::Util::Locator>: The locator type cannot be determined
+or cannot be supported. Expects $e->get_ctx() method to return the associated
+L<FCM::Context::Locator|FCM::Context::Locator> object.
+
+=item LOCATOR_WHEN
+
+L<FCM::Util::Locator|FCM::Util::Locator>: The last modified date and revision of
+the locator cannot be determined. Expects $e->get_ctx() method to return the
+associated L<FCM::Context::Locator|FCM::Context::Locator> object.
+
+=item SHELL_OPEN3
+
+L<FCM::Util::Shell|FCM::Util::Shell>: The utility fails to invoke
+IPC::Open3::open3(). Expects $e->get_ctx() to return an ARRAY reference of the
+command line, and $e->get_exception() to return the error from open3().
+
+=item SHELL_OS
+
+L<FCM::Util::Shell|FCM::Util::Shell>: An OS error occurs when invoking a shell
+command. Expects $e->get_ctx() to return an ARRAY reference of the
+command line, and $e->get_exception() to return $!.
+
+=item SHELL_SIGNAL
+
+L<FCM::Util::Shell|FCM::Util::Shell>: The system receives a signal when invoking
+a shell command. Expects $e->get_ctx() to return an ARRAY reference of the
+command line, and $e->get_exception() to return the signal number.
+
+=item SHELL_WHICH
+
+L<FCM::Util::Shell|FCM::Util::Shell>: The shell command does not exist in the
+PATH. Expects $e->get_ctx() to return an ARRAY reference of the command line.
+
+=back
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/Util/Locator.pm b/lib/FCM/Util/Locator.pm
new file mode 100644
index 0000000..74153a6
--- /dev/null
+++ b/lib/FCM/Util/Locator.pm
@@ -0,0 +1,763 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+
+# ------------------------------------------------------------------------------
+package FCM::Util::Locator;
+use base qw{FCM::Class::CODE};
+
+use FCM::Context::Keyword;
+use FCM::Context::Locator;
+use FCM::Util::Exception;
+use FCM::Util::Locator::FS;
+use FCM::Util::Locator::SSH;
+use FCM::Util::Locator::SVN;
+
+# URI prefix for FCM scheme
+use constant PREFIX => 'fcm';
+
+# Methods of an instance of this class
+my %ACTION_OF = (
+ as_invariant => \&_as_invariant,
+ as_keyword => \&_as_keyword,
+ as_normalised => \&_as_normalised,
+ as_parsed => \&_as_parsed,
+ browser_url => \&_browser_url,
+ cat => _locator_func(sub {$_[0]->cat(@_[1 .. $#_])}),
+ dir => _locator_func(sub {$_[0]->dir($_[1])}),
+ export => \&_export,
+ export_ok => \&_export_ok,
+ find => \&_find,
+ kw_ctx => sub {$_[0]->{kw_ctx}},
+ kw_ctx_load => \&_kw_ctx_load,
+ kw_iter => \&_kw_iter,
+ kw_load_rev_prop => \&_kw_load_rev_prop,
+ kw_prefix => sub {PREFIX},
+ origin => _locator_func(sub {$_[0]->origin($_[1])}),
+ reader => \&_reader,
+ rel2abs => \&_rel2abs,
+ test_exists => \&_test_exists,
+ trunk_at_head => \&_trunk_at_head,
+ what_type => \&_what_type,
+ up_iter => \&_up_iter,
+);
+# Default browser config
+our %BROWSER_CONFIG = (
+ comp_pat => qr{\A // ([^/]+) /+ ([^/]+)_svn /*(.*) \z}xms,
+ rev_tmpl => '@{1}',
+ loc_tmpl => 'http://{1}/projects/{2}/intertrac/source:/{3}{4}',
+);
+# Alias to the exception class
+my $E = 'FCM::Util::Exception';
+# Loaders for keyword context from configuration entries
+my %KEYWORD_CFG_LOADER_FOR = (
+ 'location'
+ => \&_kw_ctx_load_loc,
+ 'revision'
+ => \&_kw_ctx_load_rev,
+ 'browser.comp-pat'
+ => _kw_ctx_load_browser_func(sub {$_[0]->set_comp_pat($_[1])}),
+ 'browser.loc-tmpl'
+ => _kw_ctx_load_browser_func(sub {$_[0]->set_loc_tmpl($_[1])}),
+ 'browser.rev-tmpl'
+ => _kw_ctx_load_browser_func(sub {$_[0]->set_rev_tmpl($_[1])}),
+);
+my @KEYWORD_IMPLIED_SUFFICES = (
+ [branches => [qw{-br _br}]],
+ [tags => [qw{-tg _tg}]],
+ [trunk => [qw{-tr _tr}]],
+);
+# Patterns for parsing keyword configurations, etc
+my %PATTERN_OF = (
+ # Assignment delimiter, e.g. "label = value"
+ delim_of_assign => qr/\s* = \s*/xms,
+ # Key of a FCM location keyword, e.g. "um" in "fcm:um"
+ parse => qr/
+ \A (?# start)
+ ([\w\+\-\.]+) (?# capture 1, 1 or more word, plus, minus or dot)
+ (.*) \z (?# capture 2, rest of string)
+ /xms,
+);
+# The name of the property where revision keywords are set in primary locations
+our $REV_PROP_NAME = 'fcm:revision';
+# The known types
+our @TYPES = qw{svn ssh fs};
+# The classes for the known types
+our %TYPE_UTIL_CLASS_OF = (
+ fs => 'FCM::Util::Locator::FS',
+ ssh => 'FCM::Util::Locator::SSH',
+ svn => 'FCM::Util::Locator::SVN',
+);
+
+# Creates the class.
+__PACKAGE__->class(
+ { types => {isa => '@', default => [@TYPES]},
+ type_util_class_of => {isa => '%', default => {%TYPE_UTIL_CLASS_OF}},
+ type_util_of => '%',
+ util => '&',
+ },
+ { init => sub {
+ my ($attrib_ref) = @_;
+ my $K = 'FCM::Context::Keyword';
+ $attrib_ref->{browser_config}
+ = $K->BROWSER_CONFIG->new(\%BROWSER_CONFIG);
+ $attrib_ref->{kw_ctx} = $K->new();
+ for my $type (@{$attrib_ref->{types}}) {
+ if (!exists($attrib_ref->{type_util_of}{$type})) {
+ my $class = $attrib_ref->{type_util_class_of}{$type};
+ $attrib_ref->{type_util_of}{$type} = $class->new({
+ type_util_of => $attrib_ref->{type_util_of},
+ util => $attrib_ref->{util},
+ });
+ }
+ }
+ },
+ action_of => \%ACTION_OF,
+ },
+);
+
+# Determines the invariant value of the $locator.
+sub _as_invariant {
+ my ($attrib_ref, $locator) = @_;
+ if ($locator->get_value_level() < $locator->L_INVARIANT) {
+ _as_normalised($attrib_ref, $locator);
+ my $util_of_type = _util_of_type($attrib_ref, $locator);
+ if ($util_of_type->can('as_invariant')) {
+ my $value = eval {
+ $util_of_type->as_invariant($locator->get_value());
+ };
+ if (my $e = $@) {
+ return $E->throw($E->LOCATOR_AS_INVARIANT, $locator, $e);
+ }
+ if ($value) {
+ $locator->set_value($value);
+ $locator->set_value_level($locator->L_INVARIANT);
+ }
+ }
+ }
+ $locator->get_value();
+}
+
+# Determines the keyword value of the $locator.
+sub _as_keyword {
+ my ($attrib_ref, $locator) = @_;
+ _as_normalised($attrib_ref, $locator);
+ my $util_of_type = _util_of_type($attrib_ref, $locator);
+ my ($target, $rev) = $util_of_type->parse($locator->get_value());
+ my $kw_iter = _kw_iter($attrib_ref, $locator);
+ my $entry;
+ while (!defined($entry) && defined($entry = $kw_iter->())) {
+ if ($entry->is_implied()) {
+ $entry = undef;
+ }
+ }
+ if (defined($entry)) {
+ $target
+ = PREFIX . ':' . $entry->get_key()
+ . substr($target, length($entry->get_value()));
+ }
+ if (defined($rev) && $util_of_type->can_work_with_rev($rev)) {
+ my $transformed_rev = _transform_rev_keyword(
+ $attrib_ref, $locator, $rev,
+ sub {$_[0]->get_entry_by_value($_[1])},
+ sub {$_[0]->get_key()},
+ );
+ if ($transformed_rev) {
+ $rev = $transformed_rev;
+ }
+ }
+ scalar($util_of_type->parse($target, $rev));
+}
+
+# Determines the normalised value of the $locator.
+sub _as_normalised {
+ my ($attrib_ref, $locator) = @_;
+ if ($locator->get_value_level() < $locator->L_NORMALISED) {
+ _as_parsed($attrib_ref, $locator);
+ my $util_of_type = _util_of_type($attrib_ref, $locator);
+ my ($target, $rev) = $util_of_type->parse($locator->get_value());
+ if (defined($rev) && !$util_of_type->can_work_with_rev($rev)) {
+ my $origin = $ACTION_OF{origin}->(
+ $attrib_ref, FCM::Context::Locator->new($target),
+ );
+ $rev = _transform_rev_keyword(
+ $attrib_ref, $origin, lc($rev),
+ sub {$_[0]->get_entry_by_key($_[1])},
+ sub {$_[0]->get_value()},
+ );
+ if (!$rev) {
+ return $E->throw($E->LOCATOR_KEYWORD_REV, $locator);
+ }
+ }
+ $locator->set_value(scalar($util_of_type->parse($target, $rev)));
+ $locator->set_value_level($locator->L_NORMALISED);
+ }
+ $locator->get_value();
+}
+
+# Determines the parsed value of the $locator.
+sub _as_parsed {
+ my ($attrib_ref, $locator) = @_;
+ if ($locator->get_value_level() < $locator->L_PARSED) {
+ my $value = $locator->get_value();
+ my ($scheme, $sps) = $attrib_ref->{util}->uri_match($value);
+ if ($scheme && $scheme eq PREFIX) {
+ my ($key, $trail) = $sps =~ $PATTERN_OF{parse};
+ my $entry = $attrib_ref->{kw_ctx}->get_entry_by_key(lc($key));
+ if (!defined($entry)) {
+ return $E->throw($E->LOCATOR_KEYWORD_LOC, $locator);
+ }
+ $value = $entry->get_value() . $trail;
+ }
+ $locator->set_value($value);
+ $locator->set_value_level($locator->L_PARSED);
+ }
+ $locator->get_value();
+}
+
+# Determines the browser URL of the $locator.
+sub _browser_url {
+ my ($attrib_ref, $locator) = @_;
+ _as_normalised($attrib_ref, $locator);
+ my %GET = (
+ comp_pat => sub {$_[0]->get_comp_pat()},
+ loc_tmpl => sub {$_[0]->get_loc_tmpl()},
+ rev_tmpl => sub {$_[0]->get_rev_tmpl()},
+ );
+ my %value_of = map {($_, undef)} keys(%GET);
+ my $iter = _kw_iter($attrib_ref, $locator);
+ while (my $entry = $iter->()) {
+ if (defined($entry->get_browser_config())) {
+ for my $key (keys(%value_of)) {
+ if (!defined($value_of{$key})) {
+ my $value = $GET{$key}->($entry->get_browser_config());
+ if (defined($value)) {
+ $value_of{$key} = $value;
+ }
+ }
+ }
+ }
+ }
+ for my $key (keys(%value_of)) {
+ if (!$value_of{$key}) {
+ $value_of{$key} = $GET{$key}->($attrib_ref->{browser_config});
+ }
+ }
+ # Extracts components from the locator
+ my $origin = $ACTION_OF{origin}->($attrib_ref, $locator);
+ my ($target, $rev)
+ = _util_of_type($attrib_ref, $origin)->parse($origin->get_value());
+ my ($scheme, $sps) = $attrib_ref->{util}->uri_match($target);
+ if (!$sps) {
+ return $E->throw($E->LOCATOR_BROWSER_URL, $locator);
+ }
+ my @matches = $sps =~ $value_of{comp_pat};
+ if (!@matches) {
+ return $E->throw($E->LOCATOR_BROWSER_URL, $locator);
+ }
+ # Places the components into the template
+ my $result = $value_of{loc_tmpl};
+ for my $field_number (1 .. @matches) {
+ my $match = $matches[$field_number - 1];
+ $result =~ s/\{ $field_number \}/$match/xms;
+ }
+ my $rev_field_number = scalar(@matches) + 1;
+ my $rev_string = q{};
+ if ($rev) {
+ $rev_string = $value_of{rev_tmpl};
+ $rev_string =~ s/\{1\}/$rev/xms;
+ }
+ $result =~ s/\{ $rev_field_number \}/$rev_string/xms;
+ return $result;
+}
+
+# Exports $locator to a $dest.
+sub _export {
+ my ($attrib_ref, $locator, $dest) = @_;
+ if (_util_of_type($attrib_ref, $locator)->can('export')) {
+ _as_normalised($attrib_ref, $locator);
+ my $util_of_type = _util_of_type($attrib_ref, $locator);
+ $util_of_type->export($locator->get_value(), $dest);
+ }
+}
+
+# Returns true if it is possible to safely export $locator.
+sub _export_ok {
+ my ($attrib_ref, $locator) = @_;
+ my $util_of_type = _util_of_type($attrib_ref, $locator);
+ _as_parsed($attrib_ref, $locator);
+ $util_of_type->can('export_ok')
+ && $util_of_type->export_ok($locator->get_value());
+}
+
+# Searches the directory tree of $locator. Calls a function for each node.
+sub _find {
+ my ($attrib_ref, $locator, $callback) = @_;
+ _as_invariant($attrib_ref, $locator);
+ my $type = $locator->get_type();
+ my $util_of_type = _util_of_type($attrib_ref, $locator);
+ my $found = $util_of_type->find(
+ $locator->get_value(),
+ sub {
+ my ($value, $target_attrib_ref) = @_;
+ my $new_locator;
+ if ($value eq $locator->get_value()) {
+ $locator->set_last_mod_rev($target_attrib_ref->{last_mod_rev});
+ $locator->set_last_mod_time($target_attrib_ref->{last_mod_time});
+ $new_locator = $locator;
+ }
+ else {
+ $new_locator = FCM::Context::Locator->new($value, {
+ last_mod_rev => $target_attrib_ref->{last_mod_rev},
+ last_mod_time => $target_attrib_ref->{last_mod_time},
+ type => $type,
+ value_level => FCM::Context::Locator->L_INVARIANT,
+ });
+ }
+ $callback->($new_locator, $target_attrib_ref);
+ },
+ );
+ return ($found ? $found : $E->throw($E->LOCATOR_FIND, $locator));
+}
+
+# Loads the keyword context from configuration entries.
+sub _kw_ctx_load {
+ my ($attrib_ref, @config_entry_iterators) = @_;
+ for my $config_entry_iterator (@config_entry_iterators) {
+ while (my $config_entry = $config_entry_iterator->()) {
+ my $handler = $KEYWORD_CFG_LOADER_FOR{$config_entry->get_label()};
+ if (defined($handler)) {
+ $handler->($attrib_ref, $config_entry);
+ }
+ }
+ }
+}
+
+# Loads a location keyword browser config from a configuration entry.
+sub _kw_ctx_load_browser_func {
+ my ($setter_ref) = @_;
+ sub {
+ my ($attrib_ref, $c_entry) = @_;
+ my %entry_by_key = %{$attrib_ref->{kw_ctx}->get_entry_by_key()};
+ if (@{$c_entry->get_ns_list()}) {
+ for my $key (@{$c_entry->get_ns_list()}) {
+ if (exists($entry_by_key{$key})) {
+ $setter_ref->(
+ $entry_by_key{$key}->get_browser_config(),
+ $c_entry->get_value(),
+ );
+ }
+ }
+ }
+ else {
+ $setter_ref->($attrib_ref->{browser_config}, $c_entry->get_value());
+ }
+ }
+}
+
+# Loads the location keyword context from a configuration entry.
+sub _kw_ctx_load_loc {
+ my ($attrib_ref, $c_entry) = @_;
+ my $key = lc($c_entry->get_ns_list()->[0]);
+ my $value = $c_entry->get_value();
+ my $M = $c_entry->get_modifier_of();
+ my $type = (exists($M->{type}) ? $M->{type} : undef);
+ my $entry
+ = $attrib_ref->{kw_ctx}->add_entry($key, $value, {type => $type});
+ if (exists($M->{primary}) && $M->{primary}) {
+ my $locator = FCM::Context::Locator->new($value, {type => $type});
+ my $util_of_type = _util_of_type($attrib_ref, $locator);
+ for (@KEYWORD_IMPLIED_SUFFICES) {
+ my ($value_suffix, $key_suffix_ref) = @{$_};
+ my $locator = $ACTION_OF{cat}->($attrib_ref, $locator, $value_suffix);
+ my $value = $locator->get_value();
+ for my $key_suffix (@{$key_suffix_ref}) {
+ my $implied_entry = $entry->get_ctx_of_implied()->add_entry(
+ $key . $key_suffix, $value, {implied => 1, type => $type},
+ );
+ $attrib_ref->{kw_ctx}->add_entry($implied_entry);
+ }
+ }
+ }
+}
+
+# Loads the revision keyword context from a configuration entry.
+sub _kw_ctx_load_rev {
+ my ($attrib_ref, $c_entry) = @_;
+ for my $ns (map {lc($_)} @{$c_entry->get_ns_list()}) {
+ my ($key, $r_key) = split(qr{:}msx, $ns);
+ my $entry = $attrib_ref->{kw_ctx}->get_entry_by_key($key);
+ if (defined($entry)) {
+ $entry->get_ctx_of_rev()->add_entry($r_key, $c_entry->get_value());
+ }
+ }
+}
+
+# Returns an iterator that returns location keyword entry context for $locator.
+sub _kw_iter {
+ my ($attrib_ref, $locator, $callback_ref) = @_;
+ my $origin = $ACTION_OF{origin}->($attrib_ref, $locator);
+ my $iter = _up_iter($attrib_ref, $origin);
+ sub {
+ while (my ($leader) = $iter->()) {
+ my $entry = $attrib_ref->{kw_ctx}->get_entry_by_value($leader);
+ if (defined($entry)) {
+ if (defined($callback_ref)) {
+ $callback_ref->($entry);
+ }
+ return $entry;
+ }
+ }
+ return;
+ }
+}
+
+# Loads revision keywords from the "fcm:revision" property of the locator value
+# of a location keyword entry.
+sub _kw_load_rev_prop {
+ my ($attrib_ref, $entry) = @_;
+ if ($entry->get_loaded_rev_prop() || $entry->get_implied()) {
+ return;
+ }
+ $entry->set_loaded_rev_prop(1);
+ my $locator = FCM::Context::Locator->new(
+ $entry->get_value(), {type => $entry->get_type()},
+ );
+ my $property = _read_property($attrib_ref, $locator, $REV_PROP_NAME);
+ if (!$property) {
+ return;
+ }
+ for my $line (split(qr{\s*\n\s*}xms, $property)) {
+ my ($key, $value) = split($PATTERN_OF{delim_of_assign}, $line, 2);
+ $entry->get_ctx_of_rev()->add_entry($key, $value);
+ }
+}
+
+# Returns a function to "transform" a $locator to another locator.
+sub _locator_func {
+ my ($impl_ref) = @_;
+ sub {
+ my ($attrib_ref, $locator, @args) = @_;
+ _as_parsed($attrib_ref, $locator);
+ my $util_of_type = _util_of_type($attrib_ref, $locator);
+ FCM::Context::Locator->new(
+ scalar($impl_ref->($util_of_type, $locator->get_value(), @args)),
+ {type => $locator->get_type()},
+ );
+ }
+}
+
+# Returns a file handle to read the content of $locator.
+sub _reader {
+ my ($attrib_ref, $locator) = @_;
+ _as_normalised($attrib_ref, $locator);
+ my $reader = eval {
+ _util_of_type($attrib_ref, $locator)->reader($locator->get_value());
+ };
+ if (my $e = $@) {
+ return $E->throw($E->LOCATOR_READER, $locator, $e);
+ }
+ if (!defined($reader)) {
+ return $E->throw($E->LOCATOR_READER, $locator);
+ }
+ return $reader;
+}
+
+# Returns the contents in a named property of $locator
+sub _read_property {
+ my ($attrib_ref, $locator, $prop_name) = @_;
+ _as_normalised($attrib_ref, $locator);
+ my $util_of_type = _util_of_type($attrib_ref, $locator);
+ eval {$util_of_type->read_property($locator->get_value(), $prop_name)};
+}
+
+# If $locator->get_value() is a relative path, set it to a absolute path
+# base on $locator_base->get_value(), if $locator->get_type() does not differ
+# from $locator_base->get_type().
+sub _rel2abs {
+ my ($attrib_ref, $locator, $locator_base) = @_;
+ _as_normalised($attrib_ref, $locator_base);
+ if ( $locator->get_type()
+ && $locator->get_type() ne $locator_base->get_type()
+ ) {
+ return $locator;
+ }
+ my $value = $locator->get_value();
+ if ( $attrib_ref->{util}->uri_match($value)
+ || index($value, '/') == 0
+ || index($value, '~') == 0
+ ) {
+ return $locator;
+ }
+ my $new_locator = $ACTION_OF{cat}->($attrib_ref, $locator_base, $value);
+ $locator->set_value($new_locator->get_value());
+ $locator->set_value_level($new_locator->get_value_level());
+ $locator;
+}
+
+# Test if $locator location exists.
+sub _test_exists {
+ my ($attrib_ref, $locator) = @_;
+ _as_normalised($attrib_ref, $locator);
+ _util_of_type($attrib_ref, $locator)->test_exists($locator->get_value());
+}
+
+# Transforms a revision from/to a keyword, and returns the result.
+sub _transform_rev_keyword {
+ my ($attrib_ref, $locator, $rev, $rev_entry_func, $result_func) = @_;
+ my $iter = _kw_iter($attrib_ref, $locator);
+ while (my $entry = $iter->()) {
+ # $entry->get_ctx_of_rev()->get_entry_by_key($rev)
+ # $entry->get_ctx_of_rev()->get_entry_by_value($rev)
+ if (defined($entry->get_ctx_of_rev())) {
+ if (!$rev_entry_func->($entry->get_ctx_of_rev(), $rev)) {
+ _kw_load_rev_prop($attrib_ref, $entry);
+ }
+ }
+ if (defined($entry->get_ctx_of_rev())) {
+ my $rev_entry = $rev_entry_func->($entry->get_ctx_of_rev(), $rev);
+ if (defined($rev_entry)) {
+ # $rev_entry->get_value()
+ # $rev_entry->get_key()
+ return $result_func->($rev_entry);
+ }
+ }
+ }
+ return;
+}
+
+# Returns a string to represent the relative path to the latest main tree.
+sub _trunk_at_head {
+ my ($attrib_ref, $locator) = @_;
+ my $orig_value = $locator->get_value();
+ my $util_of_type = _util_of_type($attrib_ref, $locator);
+ my $head_value = $util_of_type->trunk_at_head($orig_value);
+ if (!$head_value || $head_value eq $orig_value) {
+ return;
+ }
+ return FCM::Context::Locator->new($head_value, {
+ type => $locator->get_type(),
+ });
+}
+
+# Determines the type of the $locator.
+sub _what_type {
+ my ($attrib_ref, $locator) = @_;
+ if (!defined($locator->get_type())) {
+ _as_parsed($attrib_ref, $locator);
+ TYPE:
+ for my $key (@{$attrib_ref->{types}}) {
+ if (!exists($attrib_ref->{type_util_of}{$key})) {
+ next TYPE;
+ }
+ my $util_of_type = $attrib_ref->{type_util_of}{$key};
+ if ($util_of_type->can_work_with($locator->get_value())) {
+ $locator->set_type($key);
+ last TYPE;
+ }
+ }
+ }
+ return $locator->get_type();
+}
+
+# Returns an iterator that walks up the hierarchy of the $locator.
+sub _up_iter {
+ my ($attrib_ref, $locator) = @_;
+ my $util_of_type = _util_of_type($attrib_ref, $locator);
+ my ($target, $revision) = $util_of_type->parse($locator->get_value());
+ my $leader = $target;
+ sub {
+ if (!defined($leader)) {
+ $leader = $target;
+ return;
+ }
+ my $return = $leader;
+ $leader = $util_of_type->dir($return);
+ if ($return eq $leader) {
+ $leader = undef;
+ }
+ return $util_of_type->parse($return, $revision);
+ };
+}
+
+# Returns the utility that implements the functionality for the $locator's type.
+sub _util_of_type {
+ my ($attrib_ref, $locator) = @_;
+ my $type = _what_type($attrib_ref, $locator);
+ if (exists($attrib_ref->{type_util_of}{$type})) {
+ return $attrib_ref->{type_util_of}{$type};
+ }
+ return $E->throw($E->LOCATOR_TYPE, $locator);
+}
+
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::Util::Locator
+
+=head1 SYNOPSIS
+
+ use FCM::Util;
+ my $util = FCM::Util->new(\%attrib);
+
+ # Usage
+ $ctx = $util->loc_kw_ctx();
+ @location_keyword_ctx_list = $util->loc_kw_ctx($locator);
+
+ $type = $util->loc_what_type($locator);
+ ($time, $rev) = $util->loc_when_modified($locator);
+
+ $locator_value = $util->loc_as_normalised($locator);
+ $locator_value = $util->loc_as_invariant( $locator);
+ $locator_value = $util->loc_as_keyword( $locator);
+
+ $url = $util->loc_browser_url($locator);
+
+ $locator_of_parent = $util->loc_dir($locator);
+ $locator_of_child = $util->loc_cat($locator, @paths);
+ $locator_of_origin = $util->loc_origin($locator);
+
+ $iter = $util->loc_up_iter($locator);
+ while (my $value = $iter->()) {
+ # ...
+ }
+
+ $reader = $util->loc_reader($locator);
+
+=head1 DESCRIPTION
+
+This module is part of L<FCM::Util|FCM::Util>. It implements the loc_* methods.
+
+=head1 IMPLEMENTATION
+
+The manipulations of locator values rely on objects with the following
+interface:
+
+=over 4
+
+=item $util_of_type->as_invariant($locator_value)
+
+Should return the invariant form of $locator_value.
+
+=item $util_of_type->can_work_with($locator_value)
+
+Should return true if it can work with $locator_value, i.e. $locator_value is a
+valid type of locator for the utility.
+
+=item $util_of_type->can_work_with_rev($revision_value)
+
+Should return true if it can work with $revision_value, i.e. $revision_value is
+a valid revision for the utility.
+
+=item $util_of_type->cat($locator_value, @paths)
+
+Should concatenate $locator_value and @paths with appropriate separators and
+returns the result.
+
+=item $util_of_type->dir($locator_value)
+
+Should return the parent (directory) of $locator_value.
+
+=item $util_of_type->export($locator_value,$dest)
+
+Optional. Exports a clean directory tree from $locator_value to $dest.
+
+=item $util_of_type->export_ok($locator_value)
+
+Optional. Returns true if it is safe to export $locator_value. E.g. it is not
+safe to export a SVN working copy, because it may contain unversioned items.
+
+=item $util_of_type->find($locator_value,$callback)
+
+Should search the directory tree in $locator_value and for each node (directory
+or file, inclusive of $locator_value), invoke
+$callback->($locator_value_of_child,\%attrib_of_child). %attrib_of_child should
+contain the elements as described by $util->find($locator,$callback).
+
+=item $util_of_type->origin($locator_value)
+
+Should return the origin of $locator_value. E.g. the URL of a Subversion working
+copy.
+
+=item $util_of_type->parse($locator_value)
+
+Should return an absolute and tidied version of $locator_value. In list context,
+should return a 2-element list, separate the scalar context return value into
+the components (PATH,REV).
+
+=item $util_of_type->reader($locator_value)
+
+Should return a file handle for reading the content of $locator_value.
+
+=item $util_of_type->read_property($locator_value,$property_name)
+
+Should return the value of the named property in $locator_value, or undef if
+not relevant for the $locator_value.
+
+=item $util_of_type->test_exists($locator_value)
+
+Should return a true value if the location $locator_value exists.
+
+=item $util_of_type->trunk_at_head($locator_value)
+
+If relevant, should append a string to $locator_value that represents the
+recommended relative path to the latest version of the main tree of a project of
+this type. E.g. for "svn", this should be "$locator_value/trunk at HEAD".
+
+=back
+
+=head1 CONSTANTS
+
+These global variables are for reference only. Their values should not be
+modified. Instead, use the appropriate attributes of the $class->new(\%attrib)
+method to modify the behaviour.
+
+=over 4
+
+=item %FCM::Util::Locator::BROWSER_CONFIG
+
+The default browser configuration.
+
+=item FCM::Util::Locator::PREFIX
+
+The URI prefix for a FCM location keyword.
+
+=item $FCM::Util::Locator::REV_PROP_NAME
+
+The name of the property where revision keywords are set in primary locations.
+
+=item @FCM::Util::Locator::TYPES
+
+The known locator types.
+
+=item %FCM::Util::Locator::TYPE_UTIL_CLASS_OF
+
+Maps the known locator types with their utility classes.
+
+=back
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/Util/Locator/FS.pm b/lib/FCM/Util/Locator/FS.pm
new file mode 100644
index 0000000..719edce
--- /dev/null
+++ b/lib/FCM/Util/Locator/FS.pm
@@ -0,0 +1,202 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+
+# ------------------------------------------------------------------------------
+package FCM::Util::Locator::FS;
+use base qw{FCM::Class::CODE};
+
+use File::Basename qw{dirname};
+use File::Find qw{};
+use File::Spec;
+
+our %ACTION_OF = (
+ can_work_with => sub {1},
+ can_work_with_rev => sub {},
+ cat => \&_cat,
+ dir => \&_dir,
+ find => \&_find,
+ origin => \&_parse,
+ parse => \&_parse,
+ reader => \&_reader,
+ read_property => sub {},
+ test_exists => \&_test_exists,
+ trunk_at_head => sub {},
+);
+
+# Creates the class.
+__PACKAGE__->class({util => '&'}, {action_of => \%ACTION_OF});
+
+# Joins @paths to the end of $value.
+sub _cat {
+ my ($attrib_ref, $value, @paths) = @_;
+ _parse(
+ $attrib_ref,
+ File::Spec->catfile(_parse($attrib_ref, $value), @paths),
+ );
+}
+
+# Returns the directory name of $value.
+sub _dir {
+ my ($attrib_ref, $value) = @_;
+ dirname(_parse($attrib_ref, $value));
+}
+
+# Searches directory tree.
+sub _find {
+ my ($attrib_ref, $value, $callback) = @_;
+ my $found;
+ File::Find::find(
+ sub {
+ $found ||= 1;
+ my $path = $File::Find::name;
+ my ($vol, $dir_name, $base) = File::Spec->splitpath($path);
+ for my $name (File::Spec->splitdir($dir_name), $base) {
+ if (index($name, q{.}) == 0) {
+ return; # ignore Unix hidden/system files
+ }
+ }
+ my $ns = File::Spec->abs2rel($path, $value);
+ if ($ns eq q{.}) {
+ $ns = q{};
+ }
+ my $last_mod_time = (-l $path ? lstat($path) : stat($path))[9];
+ $callback->(
+ $path,
+ { is_dir => scalar(-d $path),
+ last_mod_rev => undef,
+ last_mod_time => $last_mod_time,
+ ns => $ns,
+ },
+ );
+ },
+ $value,
+ );
+ return $found;
+}
+
+# Returns $value in scalar context, or ($value,undef) in list context.
+sub _parse {
+ my ($attrib_ref, $value) = @_;
+ $value = $attrib_ref->{util}->file_tilde_expand($value);
+ $value = File::Spec->rel2abs($value);
+ my ($vol, $dir_name, $base) = File::Spec->splitpath($value);
+ my @dir_names;
+ my %HANDLER_OF = (
+ q{} => sub {push(@dir_names, $_[0])},
+ q{.} => sub {},
+ q{..} => sub {if (@dir_names > 1) {pop(@dir_names)}},
+ );
+ for my $name (File::Spec->splitdir($dir_name)) {
+ my $handler
+ = exists($HANDLER_OF{$name}) ? $HANDLER_OF{$name} : $HANDLER_OF{q{}};
+ $handler->($name);
+ }
+ $value = File::Spec->catpath($vol, File::Spec->catdir(@dir_names), $base);
+ return (wantarray() ? ($value, undef) : $value);
+}
+
+# Returns a reader (file handle) for a given file system value.
+sub _reader {
+ my ($attrib_ref, $value) = @_;
+ $value = _parse($attrib_ref, $value);
+ open(my $handle, '<', $value) || die("$!\n");
+ return $handle;
+}
+
+# Return a true value if the location $value exists.
+sub _test_exists {
+ my ($attrib_ref, $value) = @_;
+ -e $value;
+}
+
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::Util::Locator::FS
+
+=head1 SYNOPSIS
+
+ use FCM::Util::Locator::FS;
+ $util = FCM::Util::Locator::FS->new(\%option);
+ $handle = $util->reader($value);
+
+=head1 DESCRIPTION
+
+Provides utilities to manipulate the values of file system locators.
+
+=head1 METHODS
+
+=over 4
+
+=item $util->can_work_with($value)
+
+Dummy. Always returns true.
+
+=item $util->can_work_with_rev($revision)
+
+Dummy. Always returns false.
+
+=item $util->cat($value, at paths)
+
+Joins @paths to the end of $value.
+
+=item $util->dir($value)
+
+Returns the parent directory of $value.
+
+=item $util->find($value,$callback)
+
+Searches directory tree of $value.
+
+=item $util->origin($value)
+
+Alias of $util->parse($value).
+
+=item $util->parse($value)
+
+In scalar context, returns $value. In list context, returns ($value,undef).
+
+=item $util->reader($value)
+
+Returns a file handle for $value, if it is a readable regular file.
+
+=item $util->read_property($value,$property_name)
+
+Dummy. Always returns undef.
+
+=item $util->test_exists($value)
+
+Return a true value if the location $value exists.
+
+=item $util->trunk_at_head($value)
+
+Dummy. Always returns undef.
+
+=back
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/Util/Locator/SSH.pm b/lib/FCM/Util/Locator/SSH.pm
new file mode 100644
index 0000000..33f4fd0
--- /dev/null
+++ b/lib/FCM/Util/Locator/SSH.pm
@@ -0,0 +1,251 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+
+# ------------------------------------------------------------------------------
+package FCM::Util::Locator::SSH;
+use base qw{FCM::Class::CODE};
+
+use FCM::Util::Exception;
+use File::Temp;
+use Text::ParseWords qw{shellwords};
+
+our %ACTION_OF = (
+ can_work_with => \&_can_work_with,
+ can_work_with_rev => sub {},
+ cat => \&_cat,
+ dir => \&_dir,
+ export => \&_export,
+ export_ok => sub {1},
+ find => \&_find,
+ origin => \&_parse,
+ parse => \&_parse,
+ reader => \&_reader,
+ read_property => sub {},
+ test_exists => \&_test_exists,
+ trunk_at_head => sub {},
+);
+# Alias to the exception class
+my $E = 'FCM::Util::Exception';
+
+# Creates the class.
+__PACKAGE__->class(
+ {type_util_of => '%', util => '&'},
+ {action_of => \%ACTION_OF},
+);
+
+# Returns true if $value looks like a legitimate HOST:PATH.
+sub _can_work_with {
+ my ($attrib_ref, $value) = @_;
+ if (!$value) {
+ return;
+ }
+ my ($auth) = split(':', $value, 2);
+ if (!$auth) {
+ return;
+ }
+ my $host = index($auth, '@') >= 0 ? (split('@', $auth, 2))[1] : $auth;
+ $host ? gethostbyname($host) : undef;
+}
+
+# Joins @paths to the end of $value.
+sub _cat {
+ my ($attrib_ref, $value, @paths) = @_;
+ my ($auth, $path) = split(':', $value, 2);
+ $auth . ':' . $attrib_ref->{type_util_of}{fs}->cat($path, @paths);
+}
+
+# Returns the directory name of $value.
+sub _dir {
+ my ($attrib_ref, $value) = @_;
+ my ($auth, $path) = split(':', $value, 2);
+ $auth . ':' . $attrib_ref->{type_util_of}{fs}->dir($path);
+}
+
+# Rsync $value to $dest.
+sub _export {
+ my ($attrib_ref, $value, $dest) = @_;
+ my ($auth, $path) = _dir($attrib_ref, $value);
+ my $value_hash_ref = $attrib_ref->{util}->shell_simple([
+ _shell_cmd_list($attrib_ref, 'rsync'),
+ $value . '/',
+ $dest,
+ ]);
+ if ($value_hash_ref->{rc}) {
+ die($value_hash_ref);
+ }
+}
+
+# Searches directory tree.
+sub _find {
+ my ($attrib_ref, $value, $callback) = @_;
+ my ($auth, $path) = split(':', $value, 2);
+ my $value_hash_ref = $attrib_ref->{util}->shell_simple([
+ _shell_cmd_list($attrib_ref, 'ssh'),
+ $auth,
+ "find $path -type f -not -path \"*/.*\" -printf \"%T@ %p\\\\n\"",
+ ]);
+ if ($value_hash_ref->{rc}) {
+ die($value_hash_ref);
+ }
+ my $found;
+ LINE:
+ for my $line (grep {$_} split("\n", $value_hash_ref->{o})) {
+ $found ||= 1;
+ my ($mtime, $name) = split(q{ }, $line, 2);
+ my $ns = substr($name, length($path) + 1);
+ $callback->(
+ $auth . ':' . $name,
+ { is_dir => undef,
+ last_mod_rev => undef,
+ last_mod_time => $mtime,
+ ns => $ns,
+ },
+ );
+ }
+ $found;
+}
+
+# Returns a reader (file handle) for a given file system value.
+sub _reader {
+ my ($attrib_ref, $value) = @_;
+ my ($auth, $path) = split(':', $value, 2);
+ my $handle = File::Temp->new();
+ my $e;
+ my $rc = $attrib_ref->{util}->shell(
+ [_shell_cmd_list($attrib_ref, 'ssh'), $auth, 'cat', $path],
+ {'e' => \$e, 'o' => sub {print($handle $_[0])}},
+ );
+ if ($rc) {
+ die($e);
+ }
+ seek($handle, 0, 0);
+ return $handle;
+}
+
+# Returns $value in scalar context, or ($value,undef) in list context.
+sub _parse {
+ my ($attrib_ref, $value) = @_;
+ my ($auth, $path) = split(':', $value, 2);
+ $value = $auth . ':' . $attrib_ref->{type_util_of}{fs}->parse($path);
+ return (wantarray() ? ($value, undef) : $value);
+}
+
+# Return a true value if the location $value exists.
+sub _test_exists {
+ my ($attrib_ref, $value) = @_;
+ my ($auth, $path) = split(':', $value, 2);
+ my $value_hash_ref = $attrib_ref->{util}->shell_simple([
+ _shell_cmd_list($attrib_ref, 'ssh'), $auth, "test -e '$path'",
+ ]);
+ return !$value_hash_ref->{rc};
+}
+
+# Get a named command and its flags, return a list.
+sub _shell_cmd_list {
+ my ($attrib_ref, $key) = @_;
+ map {shellwords($_)} (
+ $attrib_ref->{util}->external_cfg_get($key),
+ $attrib_ref->{util}->external_cfg_get($key . '.flags'),
+ );
+}
+
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::Util::Locator::SSH
+
+=head1 SYNOPSIS
+
+ use FCM::Util::Locator::SSH;
+ $util = FCM::Util::Locator::SSH->new(\%option);
+ $handle = $util->reader($value);
+
+=head1 DESCRIPTION
+
+Provides utilities to manipulate the values of locators on file systems on
+remote hosts accessible via SSH and RSYNC.
+
+=head1 METHODS
+
+=over 4
+
+=item $util->can_work_with($value)
+
+Returns true if $value is in the form AUTH:PATH and AUTH is a valid user at host.
+
+=item $util->can_work_with_rev($revision)
+
+Dummy. Always returns false.
+
+=item $util->cat($value, at paths)
+
+Joins @paths to the end of $value.
+
+=item $util->dir($value)
+
+Returns the auth:parent-directory of $value.
+
+=item $util->export($value,$dest)
+
+Rsync a clean directory tree of $value to $dest.
+
+=item $util->export_ok($value)
+
+Returns true if $util->can_work_with($value).
+
+=item $util->find($value,$callback)
+
+Searches directory tree of $value.
+
+=item $util->origin($value)
+
+Alias of $util->parse($value).
+
+=item $util->parse($value)
+
+In scalar context, returns $value. In list context, returns ($value,undef).
+
+=item $util->reader($value)
+
+Returns a file handle for $value, if it is a readable regular file.
+
+=item $util->read_property($value,$property_name)
+
+Dummy. Always returns undef.
+
+=item $util->test_exists($value)
+
+Return a true value if the location $value exists.
+
+=item $util->trunk_at_head($value)
+
+Dummy. Always returns undef.
+
+=back
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/Util/Locator/SVN.pm b/lib/FCM/Util/Locator/SVN.pm
new file mode 100644
index 0000000..3ea6477
--- /dev/null
+++ b/lib/FCM/Util/Locator/SVN.pm
@@ -0,0 +1,458 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+
+# ------------------------------------------------------------------------------
+package FCM::Util::Locator::SVN;
+use base qw{FCM::Class::CODE};
+
+use File::Temp;
+use POSIX qw{setlocale LC_ALL};
+use Time::Piece;
+
+our %ACTION_OF = (
+ as_invariant => \&_as_invariant,
+ can_work_with => \&_can_work_with,
+ can_work_with_rev => \&_can_work_with_rev,
+ cat => \&_cat,
+ dir => \&_dir,
+ export => \&_export,
+ export_ok => \&_export_ok,
+ find => \&_find,
+ origin => \&_origin,
+ parse => \&_parse,
+ reader => \&_reader,
+ read_property => \&_read_property,
+ test_exists => \&_test_exists,
+ trunk_at_head => \&_trunk_at_head,
+);
+
+my %PATTERN_OF = (
+ REVISION => qr{\A(?:\d+|HEAD|BASE|COMMITTED|PREV|\{[^\}]+\})\z}ixms,
+ TARGET_PEG => qr{\A(.+?)(?:@([^/@]+))?\z}xms,
+ URL_COMPONENTS => qr{\A([A-Za-z][\w\.\+\-]*://)([^/]*)(/?.*)\z}xms,
+);
+
+my %INFO_OF = (
+ 'Path' => 'path',
+ 'URL' => 'URL',
+ 'Repository Root' => 'repos_root_URL',
+ 'Repository UUID' => 'repos_UUID',
+ 'Revision' => 'rev',
+ 'Node Kind' => 'kind',
+ 'Last Changed Author' => 'last_changed_author',
+ 'Last Changed Rev' => 'last_changed_rev',
+ 'Last Changed Date' => 'last_changed_date',
+ # FIXME: currently omitting lock info and other WC info.
+);
+
+my %INFO_KIND_OF = (directory => 'dir', file => 'file');
+
+my %INFO_MOD_OF = (
+ last_changed_date => \&_svn_info_last_changed_date,
+ kind => sub {exists($INFO_KIND_OF{$_[0]}) ? $INFO_KIND_OF{$_[0]} : $_[0]},
+);
+
+# Creates the class.
+__PACKAGE__->class(
+ {type_util_of => '%', util => '&'},
+ {action_of => \%ACTION_OF},
+);
+
+# Returns the invariant version of $value.
+sub _as_invariant {
+ my ($attrib_ref, $value) = @_;
+ my ($target, $revision) = _parse($attrib_ref, $value);
+ if (!$attrib_ref->{util}->uri_match($target) && !$revision) {
+ return;
+ }
+ $revision ||= $attrib_ref->{util}->uri_match($target) ? 'HEAD' : 'BASE';
+ my %info_of;
+ _svn_info(
+ $attrib_ref,
+ sub {%info_of = %{$_[0]}},
+ [$value],
+ );
+ return _parse_simple($attrib_ref, $info_of{URL}, $info_of{rev});
+}
+
+# Returns true if $value looks like a legitimate SVN target.
+sub _can_work_with {
+ my ($attrib_ref, $value) = @_;
+ my ($scheme) = $attrib_ref->{util}->uri_match($value);
+ if ($scheme && grep {$_ eq $scheme} qw{svn file svn+ssh http https}) {
+ return $value;
+ }
+ my ($target, $revision) = _parse($attrib_ref, $value);
+ my $url;
+ local($@);
+ eval {_svn_info($attrib_ref, sub {$url = $_[0]->{URL}}, [$target])};
+ return $url;
+}
+
+# Returns true if $revision looks like a legitimate SVN revision specifier.
+sub _can_work_with_rev {
+ my ($attrib_ref, $revision) = @_;
+ if (!defined($revision)) {
+ return;
+ }
+ return $revision =~ $PATTERN_OF{REVISION};
+}
+
+# Joins @paths to the end of $value.
+sub _cat {
+ my ($attrib_ref, $value, @paths) = @_;
+ my ($target, $rev) = _parse($attrib_ref, $value);
+ my $is_uri = $attrib_ref->{util}->uri_match($target);
+ $target
+ = $is_uri ? join('/', $target, @paths)
+ : $attrib_ref->{type_util_of}{fs}->cat($target, @paths)
+ ;
+ _parse_simple($attrib_ref, _tidy($target), $rev);
+}
+
+# Returns the directory containing $value.
+sub _dir {
+ my ($attrib_ref, $value) = @_;
+ my ($target, $revision) = _parse($attrib_ref, $value);
+ if ($attrib_ref->{util}->uri_match($target)) {
+ my ($leader, $auth, $trailer) = $target =~ $PATTERN_OF{URL_COMPONENTS};
+ if (!$trailer) {
+ return _parse($attrib_ref, $target, $revision);
+ }
+ $trailer =~ s{/+ [^/]* \z}{}xms;
+ $target = $leader . ($auth ? $auth : q{}) . $trailer;
+ }
+ else {
+ $target = $attrib_ref->{type_util_of}{fs}->dir($target);
+ }
+ _parse_simple($attrib_ref, $target, $revision);
+}
+
+# Export $value to $dest.
+sub _export {
+ my ($attrib_ref, $value, $dest) = @_;
+ _run_svn_simple($attrib_ref, 'export', [$value, $dest], {quiet => undef});
+}
+
+# Returns true if $value is a URL.
+sub _export_ok {
+ my ($attrib_ref, $value) = @_;
+ $attrib_ref->{util}->uri_match($value);
+}
+
+# Searches directory tree of $value.
+sub _find {
+ my ($attrib_ref, $value, $callback) = @_;
+ if (!$attrib_ref->{util}->uri_match($value)) {
+ return $attrib_ref->{type_util_of}{fs}->find($value, $callback);
+ }
+ _svn_info(
+ $attrib_ref,
+ sub {
+ my ($info_ref) = @_;
+ $callback->(
+ $info_ref->{URL} . '@' . $info_ref->{rev},
+ { is_dir => $info_ref->{kind} eq 'dir',
+ last_mod_rev => $info_ref->{last_changed_rev},
+ last_mod_time => $info_ref->{last_changed_date},
+ ns => $info_ref->{path},
+ },
+ );
+ },
+ [$value],
+ {recursive => undef},
+ );
+ return 1;
+}
+
+# Returns the URL version of $value.
+sub _origin {
+ my ($attrib_ref, $value) = @_;
+ my ($target, $revision) = _parse($attrib_ref, $value);
+ if ($attrib_ref->{util}->uri_match($target)) {
+ return _parse_simple($attrib_ref, $value);
+ }
+ $revision ||= 'BASE';
+ _as_invariant(
+ $attrib_ref,
+ scalar(_parse_simple($attrib_ref, $target, $revision)),
+ );
+}
+
+# In list context, returns ($target, $revision). In scalar context, returns
+# "$target@$revision".
+sub _parse {
+ my ($attrib_ref, $value, $revision) = @_;
+ my ($target, $peg_revision) = $value =~ $PATTERN_OF{TARGET_PEG};
+ if ($peg_revision) {
+ $revision = $peg_revision;
+ }
+ $target
+ = $attrib_ref->{util}->uri_match($value)
+ ? _tidy($target)
+ : $attrib_ref->{type_util_of}{fs}->parse($target)
+ ;
+ _parse_simple($attrib_ref, $target, $revision);
+}
+
+# Same as _parse, but without _tidy.
+sub _parse_simple {
+ my ($attrib_ref, $value, $revision) = @_;
+ (
+ wantarray() ? ($value, $revision)
+ : $value . ($revision ? q{@} . $revision : q{})
+ );
+}
+
+# Returns a named property of a Subversion target.
+sub _read_property {
+ my ($attrib_ref, $value, $name) = @_;
+ _run_svn_simple($attrib_ref, 'pg', [$name, $value]);
+}
+
+# Returns a reader (file handle) for a given Subversion target.
+sub _reader {
+ my ($attrib_ref, $value) = @_;
+ my ($target, $revision) = _parse($attrib_ref, $value);
+ if ($attrib_ref->{util}->uri_match($target) || $revision) {
+ return _run_svn_handle($attrib_ref, 'cat', [$value]);
+ }
+ else {
+ return $attrib_ref->{type_util_of}{fs}->reader($target);
+ }
+}
+
+# Helper for _run_svn_*, generates the command.
+sub _run_svn_command {
+ my ($attrib_ref, $key, $args_ref, $option_ref) = @_;
+ $args_ref ||= [];
+ $option_ref ||= {};
+ my @options;
+ while (my ($key, $value) = each(%{$option_ref})) {
+ push(@options, '--' . $key . (defined($value) ? '=' . $value : q{}));
+ }
+ ['svn', $key, @options, @{$args_ref}];
+}
+
+# Runs "svn", sending standard output to a file handle.
+sub _run_svn_handle {
+ my ($attrib_ref, $key, $args_ref, $option_ref) = @_;
+ local($ENV{LANG}) = $ENV{LANG};
+ if (setlocale(LC_ALL, 'en_GB')) {
+ $ENV{LANG} = 'en_GB';
+ }
+ my $handle = File::Temp->new();
+ my $rc = $attrib_ref->{util}->shell(
+ _run_svn_command(@_),
+ {e => \my($err), o => sub {print($handle $_[0])}},
+ );
+ if ($rc || (!tell($handle) && $err)) { # cat, info, etc may return 0 on err
+ chomp($err);
+ die("$err\n");
+ }
+ seek($handle, 0, 0);
+ return $handle;
+}
+
+# Runs a simple "svn" command.
+sub _run_svn_simple {
+ my ($attrib_ref, $key, $args_ref, $option_ref) = @_;
+ local($ENV{LANG}) = $ENV{LANG};
+ if (setlocale(LC_ALL, 'en_GB')) {
+ $ENV{LANG} = 'en_GB';
+ }
+ my $value_hash_ref
+ = $attrib_ref->{util}->shell_simple(_run_svn_command(@_));
+ if ($value_hash_ref->{rc}) {
+ die($value_hash_ref);
+ }
+ $value_hash_ref->{o};
+}
+
+# Runs "svn info".
+sub _svn_info {
+ my ($attrib_ref, $callback_ref, $args_ref, $option_ref) = @_;
+ my $handle = _run_svn_handle($attrib_ref, 'info', $args_ref, $option_ref);
+ my %hash;
+ while (my $line = readline($handle)) {
+ chomp($line);
+ if ($line) {
+ my ($key, $value) = split(qr{:\s*}msx, $line, 2);
+ if (exists($INFO_OF{$key})) {
+ my $id = $INFO_OF{$key};
+ $hash{$id}
+ = exists($INFO_MOD_OF{$id}) ? $INFO_MOD_OF{$id}->($value)
+ : $value
+ ;
+ }
+ }
+ else {
+ $callback_ref->(\%hash);
+ }
+ }
+}
+
+# Parse last changed date from "svn info".
+sub _svn_info_last_changed_date {
+ my $text = (split(qr{\s+\(}msx, $_[0], 2))[0];
+ my $head = Time::Piece->strptime(substr($text, 0, -6), '%Y-%m-%d %H:%M:%S');
+ my $tail = substr($text, -5);
+ my ($tz_sign, $tz_h, $tz_m) = $tail =~ qr{([\-\+])(\d\d)(\d\d)}msx;
+ $head->epoch() - int($tz_sign . 1) * ($tz_h * 3600 + $tz_m * 60);
+}
+
+# Return a true value if the location $value exists.
+sub _test_exists {
+ my ($attrib_ref, $value) = @_;
+ my $url;
+ eval {_svn_info($attrib_ref, sub {$url = $_[0]->{URL}}, [$value])};
+ return $url;
+}
+
+# Returns a tidied version of a Subversion URL.
+sub _tidy {
+ my ($url) = @_;
+ my ($leader, $auth, $trailer) = $url =~ $PATTERN_OF{URL_COMPONENTS};
+ if (!$trailer) {
+ return $url;
+ }
+ my @tidied_names;
+ my %handler_of = (
+ q{} => sub {push(@tidied_names, $_[0])},
+ q{.} => sub {},
+ q{..} => sub {if (@tidied_names > 1) {pop(@tidied_names)}},
+ );
+ for my $name (split(qr{/+}xms, $trailer)) {
+ my $handler
+ = exists($handler_of{$name}) ? $handler_of{$name} : $handler_of{q{}};
+ $handler->($name);
+ }
+ return $leader . ($auth ? $auth : q{}) . join(q{/}, @tidied_names);
+}
+
+# Returns trunk at HEAD for a URL.
+sub _trunk_at_head {
+ my ($attrib_ref, $target) = @_;
+ if (!$attrib_ref->{util}->uri_match($target)) {
+ return;
+ }
+ _cat($attrib_ref, $target, 'trunk at HEAD');
+}
+
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::Util::Locator::SVN
+
+=head1 SYNOPSIS
+
+ use FCM::Util;
+ $util = FCM::Util->new(\%attrib);
+ $reader = $util->loc_reader($locator);
+
+
+=head1 DESCRIPTION
+
+This is part of L<FCM::Util|FCM::Util>. Provides utilities for Subversion
+targets.
+
+=head1 ATTRIBUTES
+
+=over 4
+
+=item util
+
+The L<FCM::Util|FCM::Util> object that initialised this object.
+
+=head1 METHODS
+
+=over 4
+
+=item $util->as_invariant($value)
+
+Returns the invariant version of $value. For example, if the current HEAD
+revision is 1234, and $value is C<svn://foo/bar/baz> or
+C<svn://foo/bar/baz@HEAD>, it will return C<svn://foo/bar/baz@1234>.
+
+=item $util->can_work_with($value)
+
+Returns the URL form of $value (true) if $value is a valid SVN target.
+
+=item $util->can_work_with_rev($revision)
+
+Returns true if $revision looks like a legitimate SVN revision specifier.
+
+=item $util->cat($value, at paths)
+
+Join @paths to the end of $value.
+
+=item $util->dir($value)
+
+Returns the directory name of $value.
+
+=item $util->export($value,$dest)
+
+Exports a clean directory tree of $value to $dest.
+
+=item $util->export_ok($value)
+
+Returns true if $value is a URL. (It is not safe to export a working copy.)
+
+=item $util->find($value,$callback)
+
+Searches directory tree of $value.
+
+=item $util->origin($value)
+
+Returns the URL version of $value.
+
+=item $util->parse($value,$revision)
+
+In scalar context, returns a string in C<TARGET at REV> for $value. In list
+context, given C<TARGET at REV> returns (C<TARGET>, C<REV>). If $value has a peg
+revision, it overrides the specified $revision.
+
+=item $util->reader($value)
+
+Returns a file handle for reading the content in $value, if possible.
+
+=item $util->read_property($value,$name)
+
+Returns the value of a property $name of $value.
+
+=item $util->test_exists($value)
+
+Return a true value if the location $value exists.
+
+=item $util->trunk_at_head($value)
+
+Returns "$value/trunk at HEAD' if $value is a URI or undef otherwise.
+
+=back
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/Util/Reporter.pm b/lib/FCM/Util/Reporter.pm
new file mode 100644
index 0000000..492187c
--- /dev/null
+++ b/lib/FCM/Util/Reporter.pm
@@ -0,0 +1,382 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+
+# ------------------------------------------------------------------------------
+package FCM::Util::Reporter;
+use base qw{FCM::Class::CODE};
+
+use Scalar::Util qw{reftype};
+
+use constant {TYPE_OUT => 1, TYPE_ERR => 2};
+
+use constant { DEFAULT => 1,
+ FAIL => 0, WARN => 1,
+ QUIET => 0, LOW => 1, MEDIUM => 2, HIGH => 3, DEBUG => 4,
+};
+
+use constant {
+ PREFIX_DONE => q{[done] },
+ PREFIX_FAIL => q{[FAIL] },
+ PREFIX_INFO => q{[info] },
+ PREFIX_INIT => q{[init] },
+ PREFIX_NULL => q{},
+ PREFIX_QUIT => q{[quit] },
+ PREFIX_WARN => q{[WARN] },
+};
+
+# Creates the class.
+__PACKAGE__->class(
+ {ctx_of => '%'},
+ { init => sub {
+ my ($attrib_ref) = @_;
+ %{$attrib_ref->{ctx_of}} = (
+ stderr => FCM::Util::Reporter::Context->new_err(),
+ stdout => FCM::Util::Reporter::Context->new(),
+ );
+ },
+ action_of => {
+ add_ctx => \&_add_ctx,
+ del_ctx => \&_del_ctx,
+ get_ctx => \&_get_ctx,
+ get_ctx_of_stderr => sub {$_[0]->{ctx_of}->{stderr}},
+ get_ctx_of_stdout => sub {$_[0]->{ctx_of}->{stdout}},
+ report => \&_report,
+ }
+ },
+);
+
+# Adds a named reporter context.
+sub _add_ctx {
+ my ($attrib_ref, $key, @args) = @_;
+ if (exists($attrib_ref->{ctx_of}->{$key})) {
+ return;
+ }
+ $attrib_ref->{ctx_of}->{$key} = FCM::Util::Reporter::Context->new(@args);
+}
+
+# Deletes a named reporter context.
+sub _del_ctx {
+ my ($attrib_ref, $key) = @_;
+ if (!exists($attrib_ref->{ctx_of}->{$key})) {
+ return;
+ }
+ delete($attrib_ref->{ctx_of}->{$key});
+}
+
+# Returns a named reporter context.
+sub _get_ctx {
+ my ($attrib_ref, $key) = @_;
+ if (!exists($attrib_ref->{ctx_of}->{$key})) {
+ return;
+ }
+ $attrib_ref->{ctx_of}->{$key};
+}
+
+# Reports message.
+sub _report {
+ my ($attrib_ref, @args) = @_;
+ if (!@args) {
+ return;
+ }
+ my %option = (
+ delimiter => "\n",
+ level => DEFAULT,
+ prefix => undef,
+ type => TYPE_OUT,
+ );
+ if (ref($args[0]) && reftype($args[0]) eq 'HASH') {
+ %option = (%option, %{shift(@args)});
+ }
+ # Auto remove ctx with closed file handle
+ while (my ($key, $ctx) = each(%{$attrib_ref->{ctx_of}})) {
+ if (!defined(fileno($ctx->get_handle()))) {
+ delete($attrib_ref->{ctx_of}->{$key});
+ }
+ }
+ # Selects handles
+ my @ctx_and_prefix_list
+ = map {
+ my $prefix = defined($option{prefix})
+ ? $option{prefix} : $_->get_prefix();
+ if (ref($prefix) && reftype($prefix) eq 'CODE') {
+ $prefix = $prefix->($option{level}, $option{type});
+ }
+ [$_, $prefix],
+ }
+ grep { (!$_->get_type() || $_->get_type() eq $option{type})
+ && $_->get_verbosity() >= $option{level}
+ }
+ values(%{$attrib_ref->{ctx_of}});
+ if (!@ctx_and_prefix_list) {
+ return;
+ }
+ for my $arg (@args) {
+ for (@ctx_and_prefix_list) {
+ my ($ctx, $prefix) = @{$_};
+ my $handle = $ctx->get_handle();
+ if ($option{delimiter}) {
+ for my $item (
+ map {grep {$_ ne "\n"} split(qr{(\n)}msx)} (
+ !ref($arg) ? ($arg)
+ : reftype($arg) eq 'ARRAY' ? @{$arg}
+ : reftype($arg) eq 'CODE' ? $arg->($ctx->get_verbosity())
+ : ($arg)
+ )
+ ) {
+ print({$handle} $prefix . $item . $option{delimiter});
+ }
+ }
+ else {
+ print({$handle} $arg);
+ }
+ }
+ }
+ 1;
+}
+
+# ------------------------------------------------------------------------------
+package FCM::Util::Reporter::Context;
+use base qw{FCM::Class::HASH};
+
+# Creates the class.
+__PACKAGE__->class(
+ { handle => {isa => '*', default => \*STDOUT },
+ prefix => { default => sub {\&_prefix} },
+ type => {isa => '$', default => FCM::Util::Reporter->TYPE_OUT},
+ verbosity => {isa => '$', default => FCM::Util::Reporter->DEFAULT },
+ },
+);
+
+# Returns a new reporter context to STDERR.
+sub new_err {
+ my ($class, $attrib_ref) = @_;
+ $class->new({
+ handle => \*STDERR,
+ type => FCM::Util::Reporter->TYPE_ERR,
+ (defined($attrib_ref) ? %{$attrib_ref} : ()),
+ });
+}
+
+# The default prefix function.
+sub _prefix {
+ my ($level, $type) = @_;
+ $type eq FCM::Util::Reporter->TYPE_OUT ? FCM::Util::Reporter->PREFIX_INFO
+ : $level > FCM::Util::Reporter->FAIL ? FCM::Util::Reporter->PREFIX_WARN
+ : FCM::Util::Reporter->PREFIX_FAIL
+ ;
+}
+
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::Reporter
+
+=head1 SYNOPSIS
+
+ use FCM::Util::Reporter;
+ $reporter = FCM::Util::Reporter->new({verbosity => $verbosity});
+ $reporter->($message);
+ $reporter->(\@messages);
+ $reporter->(sub {return @some_strings});
+ $reporter->({level => $reporter->MEDIUM}, $message);
+
+=head1 DESCRIPTION
+
+A simple message reporter.
+
+This module is part of L<FCM::Util|FCM::Util>. See also the description of the
+$u->report() method in L<FCM::Util|FCM::Util>.
+
+=head1 METHODS
+
+=over 4
+
+=item $class->new(\%attrib)
+
+Returns a new instance of this class, which is a CODE reference. %attrib can
+contain the following:
+
+=over 4
+
+=item ctx_of
+
+A HASH containing a map to the named reporter contexts. At initialisation, a new
+ctx for "stdout" and a new ctx for "stderr" is created automatically.
+
+=back
+
+=item $reporter->add_ctx($key,%option)
+
+Creates a new reporter context, and adds it to the ctx_of HASH, if a context
+with the same $key does not already exist. The %option is given to the
+constructir of L</FCM::Util::Reporter::Context>. Return the context on success.
+
+=item $reporter->del_ctx($key)
+
+Removes a new reporter context named $key. Return the context on success.
+
+=item $reporter->get_ctx($key)
+
+Returns a named reporter context L</FCM::Util::Reporter::Context>.
+
+=item $reporter->get_ctx_of_stderr()
+
+Shorthand for $reporter->get_ctx('stderr').
+
+=item $reporter->get_ctx_of_stdout()
+
+Shorthand for $reporter->get_ctx('stdout').
+
+=item $reporter->report(\%option,$message)
+
+Reports the message. If %option is not given, reports using the default options.
+In the form, the following %options can be specified:
+
+=over 4
+
+=item delimiter
+
+The delimiter of each message in the list. The default is "\n". If the delimiter
+is set to the empty string, the items in $message will be treated as raw
+strings, i.e. it will also ignore any "prefix" options.
+
+=item level
+
+The level of the current message. The default is DEFAULT.
+
+=item prefix
+
+The message prefix. It can be a string or a CODE reference. If it is a string,
+it is simply preprended to the message. If it is a code reference, it is calls
+as $prefix_ref->($option{level}, $option{type}), and its result (if defined) is
+prepended to the message.
+
+=item type
+
+The message type. It can be REPORT_ERR or REPORT_OUT (default).
+
+=back
+
+=back
+
+=head1 CONSTANTS
+
+=over 4
+
+=item $reporter->FAIL, $reporter->QUIET
+
+The verbosity level 0.
+
+=item $reporter->DEFAULT, $reporter->LOW, $reporter->WARN
+
+The verbosity level 1.
+
+=item $reporter->MEDIUM
+
+The verbosity level 2.
+
+=item $reporter->HIGH
+
+The verbosity level 3.
+
+=item $reporter->DEBUG
+
+The verbosity level 4.
+
+=item $reporter->PREFIX_DONE
+
+The prefix for a task "done" message.
+
+=item $reporter->PREFIX_FAIL
+
+The prefix for a fatal error message.
+
+=item $reporter->PREFIX_INFO
+
+The prefix for an "info" message.
+
+=item $reporter->PREFIX_INIT
+
+The prefix for a task "init" message.
+
+=item $reporter->PREFIX_NULL
+
+An empty string.
+
+=item $reporter->PREFIX_QUIT
+
+The prefix for a quit/abort message.
+
+=item $reporter->PREFIX_WARN
+
+The prefix for a warning message.
+
+=item $reporter->REPORT_ERR
+
+The message type for exception message.
+
+=item $reporter->REPORT_OUT
+
+The message type for info message.
+
+=back
+
+=head1 FCM::Util::Reporter::Context
+
+An instance of this class represents the context for a reporter for the
+L<FCM::Util->report()|FCM::Util>. This class is a sub-class of
+L<FCM::Class::HASH|FCM::Class::HASH>. It has the following attributes:
+
+=over 4
+
+=item handle
+
+The file handle for info messages. (Default=\*STDOUT)
+
+=item prefix
+
+The message prefix. It can be a string or a CODE reference. If it is a string,
+it is simply preprended to the message. If it is a code reference, it is calls
+as $prefix_ref->($option{level}, $option{type}), and its result (if defined) is
+prepended to the message. The default is a CODE that returns PREFIX_INFO for
+TYPE_OUT messages, PREFIX_WARN for TYPE_ERR messages at WARN level or above or
+PREFIX_FAIL for TYPE_ERR messages at FAIL level.
+
+=item type
+
+Reporter type. (Default=TYPE_OUT)
+
+=item verbosity
+
+The verbosity of the reporter. Only messages at a level above or equal to the
+verbosity will be reported. The default is DEFAULT.
+
+=back
+
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/Util/Shell.pm b/lib/FCM/Util/Shell.pm
new file mode 100644
index 0000000..1b50c22
--- /dev/null
+++ b/lib/FCM/Util/Shell.pm
@@ -0,0 +1,306 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+
+# ------------------------------------------------------------------------------
+package FCM::Util::Shell;
+use base qw{FCM::Class::CODE};
+
+use FCM::Context::Event;
+use FCM::Util::Exception;
+use File::Spec::Functions qw{catfile file_name_is_absolute path};
+use IPC::Open3 qw{open3};
+use List::Util qw{first};
+use Scalar::Util qw{reftype};
+use Text::ParseWords qw{shellwords};
+
+our $BUFFER_SIZE = 4096; # default buffer size
+our $TIME_OUT = 0.005; # default time out for selecting a file handle
+
+my $E = 'FCM::Util::Exception';
+my %FUNCTION_OF = (e => \&_do_r, i => \&_do_w, o => \&_do_r);
+my @IOE = qw{i o e};
+my %ACTION_FUNC_FOR
+ = (e => \&_action_func_r, i => \&_action_func_w, o => \&_action_func_r);
+
+# Creates the class.
+__PACKAGE__->class(
+ { buffer_size => {isa => '$', default => $BUFFER_SIZE},
+ time_out => {isa => '$', default => $TIME_OUT},
+ util => '&',
+ },
+ { action_of => {
+ invoke => \&_invoke,
+ invoke_simple => \&_invoke_simple,
+ which => \&_which,
+ },
+ },
+);
+
+# Returns a CODE to deal with non-CODE read action.
+sub _action_func_r {
+ my ($arg_ref) = @_;
+ ${$arg_ref} ||= q{};
+ sub {${$arg_ref} .= $_[0]};
+}
+
+# Returns a CODE to deal with non-CODE write action.
+sub _action_func_w {
+ my ($arg_ref) = @_;
+ my @inputs
+ = ref($arg_ref) && reftype($arg_ref) eq 'ARRAY' ? @{$arg_ref}
+ : ref($arg_ref) && reftype($arg_ref) eq 'SCALAR' ? (${$arg_ref})
+ : ()
+ ;
+ sub {shift(@inputs)};
+}
+
+# Gets output $value from a selected handle, and invokes $action->($value).
+sub _do_r {
+ my ($attrib_ref, $ctx) = @_;
+ my $n_bytes;
+ while (
+ my @handles = $ctx->get_select()->can_read($attrib_ref->{time_out})
+ ) {
+ my ($handle) = @handles;
+ my $buffer = q{};
+ my $n = sysread($handle, $buffer, $attrib_ref->{buffer_size});
+ if (!defined($n)) {
+ return;
+ }
+ $n_bytes += $n;
+ if ($n == 0) {
+ close($handle) || return;
+ return 0;
+ }
+ $ctx->get_action()->($buffer);
+ }
+ defined($n_bytes) ? $n_bytes : -1;
+}
+
+# Gets input from $action->() and writes to a selected handle if possible.
+# Handles buffering of STDIN to the command.
+sub _do_w {
+ my ($attrib_ref, $ctx) = @_;
+ my $n_bytes;
+ while (
+ my @handles = $ctx->get_select()->can_write($attrib_ref->{time_out})
+ ) {
+ my ($handle) = @handles;
+ if (!$ctx->get_buf()) {
+ $ctx->set_buf($ctx->get_action()->());
+ if (!defined($ctx->get_buf())) {
+ close($handle) || return;
+ return 0;
+ };
+ $ctx->set_buf_length(length($ctx->get_buf()));
+ $ctx->set_buf_offset(0);
+ }
+ my $n = syswrite(
+ $handle,
+ $ctx->get_buf(),
+ $attrib_ref->{buffer_size},
+ $ctx->get_buf_offset(),
+ );
+ if (!defined($n)) {
+ return;
+ }
+ $n_bytes += $n;
+ $ctx->set_buf_offset($ctx->get_buf_offset() + $n);
+ if ($ctx->get_buf_offset() >= $ctx->get_buf_length()) {
+ $ctx->set_buf(undef);
+ $ctx->set_buf_length(0);
+ $ctx->set_buf_offset(0);
+ }
+ }
+ defined($n_bytes) ? $n_bytes : -1;
+}
+
+# Invokes a command.
+sub _invoke {
+ my ($attrib_ref, $command_ref, $action_ref) = @_;
+ # Ensure that the command is an ARRAY
+ if (!ref($command_ref)) {
+ $command_ref = [shellwords($command_ref)];
+ }
+ # Check that the command exists in the PATH
+ if (!_which($attrib_ref, $command_ref->[0])) {
+ return $E->throw($E->SHELL_WHICH, $command_ref);
+ }
+ # Sets up the STDIN, STDOUT and STDERR to the command
+ my %ctx_of = map {($_, FCM::Util::Shell::Context->new())} @IOE;
+ $action_ref ||= {};
+ while (my ($key, $action) = each(%{$action_ref})) {
+ if (exists($ctx_of{$key})) {
+ if (reftype($action) eq 'CODE') {
+ $ctx_of{$key}->set_action($action);
+ }
+ else {
+ $ctx_of{$key}->set_action($ACTION_FUNC_FOR{$key}->($action));
+ }
+ }
+ }
+ # Calls the command with open3
+ my $timer = $attrib_ref->{util}->timer();
+ my $pid = eval {
+ open3((map {$ctx_of{$_}->get_handle()} @IOE), @{$command_ref});
+ };
+ if (my $e = $@) {
+ return $E->throw($E->SHELL_OPEN3, $command_ref, $e);
+ }
+ # Handles input/output of the command
+ for my $ctx (values(%ctx_of)) {
+ $ctx->get_select()->add($ctx->get_handle());
+ }
+ while (keys(%ctx_of)) {
+ while (my ($key, $ctx) = each(%ctx_of)) {
+ my $status = $FUNCTION_OF{$key}->($attrib_ref, $ctx);
+ if (!defined($status)) {
+ return $E->throw($E->SHELL_OS, $command_ref, $!);
+ }
+ if (!$status) {
+ delete($ctx_of{$key});
+ }
+ }
+ }
+ # Wait for command to finish
+ waitpid($pid, 0);
+ my $rc = $?;
+ $attrib_ref->{util}->event(
+ FCM::Context::Event->SHELL, $command_ref, $rc, $timer->(),
+ );
+ # Handles exceptions and signals
+ if ($rc) {
+ if ($rc == -1) {
+ return $E->throw($E->SHELL_OS, $command_ref, $!);
+ }
+ if ($rc & 127) {
+ return $E->throw($E->SHELL_SIGNAL, $command_ref, $rc & 127);
+ }
+ }
+ return $rc >> 8;
+}
+
+# Wraps _invoke.
+sub _invoke_simple {
+ my ($attrib_ref, $command_ref) = @_;
+ my ($e, $o);
+ my $rc = _invoke($attrib_ref, $command_ref, {e => \$e, o => \$o});
+ return {e => $e, o => $o, rc => $rc};
+}
+
+# Returns the full path to the command $name, if it exists in the PATH.
+sub _which {
+ my ($attrib_ref, $name) = @_;
+ if (file_name_is_absolute($name)) {
+ return $name;
+ }
+ use filetest 'access';
+ first {-f $_ && -x _} map {catfile($_, $name)} path();
+ no filetest 'access';
+}
+
+# ------------------------------------------------------------------------------
+package FCM::Util::Shell::Context;
+use base qw{FCM::Class::HASH};
+
+use IO::Select;
+use Symbol qw{gensym};
+
+# A context to hold the information for the command's STDIN, STDOUT or STDERR.
+# action => CODE to call to get more STDIN for the command or to send
+# STDOUT/STDERR to when possible.
+# buf* => A buffer (and its length and the current offset) to hold the STDIN
+# that is yet to be written to the command.
+# handle => The command STDIN, STDOUT or STDERR.
+# select => The IO::Select object that tells us whether the handle is ready for
+# I/O or not.
+__PACKAGE__->class(
+ { action => {isa => '&'},
+ buf => {isa => '$'},
+ buf_length => {isa => '$'},
+ buf_offset => {isa => '$'},
+ handle => {isa => '*', default => \&gensym},
+ 'select' => {isa => 'IO::Select', default => sub {IO::Select->new()}},
+ },
+);
+
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::Util::Shell
+
+=head1 SYNOPSIS
+
+ use FCM::Util;
+ $util = FCM::Util->new(\%attrib);
+ %action_of = {e => \&e_handler, i => \&i_handler, o => \&o_handler};
+ $rc = $util->shell(\@command, \%action_of);
+ %value_of = %{$util->shell_simple(\@command)};
+
+=head1 DESCRIPTION
+
+Wraps L<IPC::Open3|IPC::Open3> to provide an interface driven by callbacks.
+
+=head1 METHODS
+
+=over 4
+
+=item $class->new(\%attrib)
+
+Returns a new instance. The attributes that can be specified in %attrib are:
+
+=over 4
+
+=item {buffer_size}
+
+The size of the read buffer for reading from the standard output and standard
+error output of the command. The default is 4096.
+
+=item {time_out}
+
+The time to wait when selecting a file handle. The default is 0.001.
+
+=item {util}
+
+A CODE reference. The L<FCM::Util|FCM::Util> object that initialised this
+instance.
+
+=back
+
+=back
+
+See the description of the shell(), shell_simpl() and shell_which() methods in
+L<FCM::Util|FCM::Util> for detail.
+
+=head1 SEE ALSO
+
+L<IPC::Open3|IPC::Open3>
+
+Inspired by the CPAN module L<IPC::Cmd|IPC::Cmd> and friends.
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM/Util/TaskRunner.pm b/lib/FCM/Util/TaskRunner.pm
new file mode 100644
index 0000000..09cc7cb
--- /dev/null
+++ b/lib/FCM/Util/TaskRunner.pm
@@ -0,0 +1,380 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+# ------------------------------------------------------------------------------
+package FCM::Util::TaskRunner;
+use base qw{FCM::Class::CODE};
+
+my $P = 'FCM::Util::TaskRunner::Parallel';
+my $S = 'FCM::Util::TaskRunner::Serial';
+
+__PACKAGE__->class({util => '&'}, {action_of => {main => \&_main}});
+
+sub _main {
+ my ($attrib_ref, $action_ref, $n_workers) = @_;
+ $n_workers ||= 1;
+ my $class = $n_workers > 1 ? $P : $S;
+ $attrib_ref->{runner} = $class->new({
+ action => $action_ref,
+ n_workers => $n_workers,
+ util => $attrib_ref->{util},
+ });
+}
+
+# ------------------------------------------------------------------------------
+package FCM::Util::TaskRunner::Serial;
+use base qw{FCM::Class::CODE};
+
+__PACKAGE__->class(
+ {action => '&', util => '&'},
+ {action_of => {destroy => sub {}, main => \&_main}},
+);
+
+sub _main {
+ my ($attrib_ref, $get_ref, $put_ref) = @_;
+ my $n_done = 0;
+ while (my $task = $get_ref->()) {
+ my $timer = $attrib_ref->{util}->timer();
+ eval {
+ $task->set_state($task->ST_WORKING);
+ $attrib_ref->{action}->($task->get_ctx());
+ $task->set_state($task->ST_OK);
+ };
+ if ($@) {
+ $task->set_error($@);
+ $task->set_state($task->ST_FAILED);
+ }
+ $task->set_elapse($timer->());
+ $put_ref->($task);
+ ++$n_done;
+ }
+ $n_done;
+}
+
+# ------------------------------------------------------------------------------
+package FCM::Util::TaskRunner::Parallel;
+use base qw{FCM::Class::CODE};
+
+use FCM::Context::Event;
+use IO::Select;
+use IO::Socket;
+use List::Util qw{first};
+use POSIX qw{WNOHANG};
+use Socket qw{AF_UNIX SOCK_STREAM PF_UNSPEC};
+use Storable qw{freeze thaw};
+
+# Package name of worker event and state
+my $CTX_EVENT = 'FCM::Context::Event';
+my $CTX_STATE = 'FCM::Util::TaskRunner::WorkerState';
+
+# Length of a packed long integer
+my $LEN_OF_LONG = length(pack('N', 0));
+
+# Time out for polling sockets to child processes
+my $TIME_OUT = 0.05;
+
+# Creates the class.
+__PACKAGE__->class(
+ { action => '&',
+ n_workers => '$',
+ worker_states => '@',
+ util => '&',
+ },
+ {init => \&_init, action_of => {destroy => \&_destroy, main => \&_main}},
+);
+
+# Destroys the child processes.
+sub _destroy {
+ my $attrib_ref = shift();
+ local($SIG{CHLD}) = 'IGNORE';
+ my $select = IO::Select->new();
+ my @worker_states = @{$attrib_ref->{worker_states}};
+ for my $worker_state (@worker_states) {
+ $select->add($worker_state->get_socket());
+ }
+ # TBD: reads $socket for any left over event etc?
+ for my $socket ($select->can_write(0)) {
+ my $worker_state = first {$_->get_socket() eq $socket} @worker_states;
+ _item_send($socket);
+ close($socket);
+ waitpid($worker_state->get_pid(), 0);
+ }
+ while (waitpid(-1, WNOHANG) > 0) {
+ }
+ $attrib_ref->{util}->event(
+ FCM::Context::Event->TASK_WORKERS, 'destroy', $attrib_ref->{n_workers},
+ );
+ 1;
+}
+
+# On initialisation.
+sub _init {
+ my $attrib_ref = shift();
+ for my $i (1 .. $attrib_ref->{n_workers}) {
+ my ($from_boss, $from_worker)
+ = IO::Socket->socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC);
+ if (!defined($from_boss) || !defined($from_worker)) {
+ die("socketpair: $!");
+ }
+ $from_worker->autoflush(1);
+ $from_boss->autoflush(1);
+ if (my $pid = fork()) {
+ # I am the boss
+ if ($pid < 0) {
+ die("fork: $!");
+ }
+ local($SIG{CHLD}, $SIG{INT}, $SIG{KILL}, $SIG{TERM}, $SIG{XCPU});
+ for my $key (qw{CHLD INT KILL TERM XCPU}) {
+ local($SIG{$key}) = sub {_destroy($attrib_ref, @_); die($!)};
+ }
+ close($from_worker);
+ push(
+ @{$attrib_ref->{worker_states}},
+ $CTX_STATE->new($pid, $from_boss),
+ );
+ }
+ elsif (defined($pid)) {
+ # I am a worker
+ close($from_boss);
+ $attrib_ref->{worker_states} = [];
+ open(STDIN, '/dev/null');
+ # Ensures that events are sent back to the boss process
+ my $util_of_event = bless(
+ sub {_item_send($from_worker, @_)},
+ __PACKAGE__ . '::WorkerEvent',
+ );
+ no strict 'refs';
+ *{__PACKAGE__ . '::WorkerEvent::main'}
+ = sub {my $self = shift(); $self->(@_)};
+ use strict 'refs';
+ $attrib_ref->{util}->util_of_event($util_of_event);
+ _worker(
+ $from_worker,
+ $attrib_ref->{action},
+ $attrib_ref->{util},
+ );
+ close($from_worker);
+ exit();
+ }
+ else {
+ die("fork: $!");
+ }
+ }
+ $attrib_ref->{util}->event(
+ FCM::Context::Event->TASK_WORKERS, 'init', $attrib_ref->{n_workers},
+ );
+}
+
+# Main function of the class.
+sub _main {
+ my ($attrib_ref, $get_ref, $put_ref) = @_;
+ my $n_done = 0;
+ my $n_wait = 0;
+ my $done_something = 1;
+ my $get_task_ref = _get_task_func($get_ref, $attrib_ref->{n_workers});
+ my $select = IO::Select->new();
+ my @worker_states = @{$attrib_ref->{worker_states}};
+ for my $worker_state (@worker_states) {
+ $select->add($worker_state->get_socket());
+ }
+ while ($n_wait || $done_something) {
+ $done_something = 0;
+ # Handles tasks back from workers
+ while (my @sockets = $select->can_read($TIME_OUT)) {
+ for my $socket (@sockets) {
+ my $worker_state
+ = first {$socket eq $_->get_socket()} @worker_states;
+ my $item = _item_receive($socket);
+ if (defined($item)) {
+ $done_something = 1;
+ if ($item->isa('FCM::Context::Event')) {
+ # Item is only an event, handles it
+ $attrib_ref->{util}->event($item);
+ }
+ else {
+ # Sends something back to the worker immediately
+ if (defined(my $task = $get_task_ref->())) {
+ _item_send($socket, $task);
+ }
+ else {
+ --$n_wait;
+ $worker_state->set_idle(1);
+ }
+ $put_ref->($item);
+ ++$n_done;
+ }
+ }
+ }
+ }
+ # Sends something to the idle workers
+ my @idle_worker_states = grep {$_->get_idle()} @worker_states;
+ if (@idle_worker_states) {
+ for my $worker_state (@idle_worker_states) {
+ if (defined(my $task = $get_task_ref->())) {
+ _item_send($worker_state->get_socket(), $task);
+ ++$n_wait;
+ $done_something = 1;
+ $worker_state->set_idle(0);
+ }
+ }
+ }
+ else {
+ $get_task_ref->(); # only adds more tasks to queue
+ }
+ }
+ $n_done;
+}
+
+# Returns a function to fetch more tasks into a queue.
+sub _get_task_func {
+ my ($get_ref, $n_workers) = @_;
+ my $max_n_in_queue = $n_workers * 2;
+ my @queue;
+ sub {
+ while (@queue < $max_n_in_queue && defined(my $task = $get_ref->())) {
+ push(@queue, $task);
+ }
+ if (!defined(wantarray())) {
+ return;
+ }
+ shift(@queue);
+ };
+}
+
+# Receives an item from a socket.
+sub _item_receive {
+ my ($socket) = @_;
+ my $len_of_data = unpack('N', _item_travel($socket, $LEN_OF_LONG));
+ $len_of_data ? thaw(_item_travel($socket, $len_of_data)) : undef;
+}
+
+# Sends an item to a socket.
+sub _item_send {
+ my ($socket, $item) = @_;
+ my $item_as_data = $item ? freeze($item) : q{};
+ my $message = pack('N', length($item_as_data)) . $item_as_data;
+ _item_travel($socket, length($message), $message);
+}
+
+# Helper for _item_receive/_item_send.
+sub _item_travel {
+ my ($socket, $len_to_travel, $data) = @_;
+ my $action
+ = defined($data) ? sub {syswrite($socket, $data, $_[0], $_[1])}
+ : sub {sysread( $socket, $data, $_[0], $_[1])}
+ ;
+ $data ||= q{};
+ my $n_bytes = 0;
+ while ($n_bytes < $len_to_travel) {
+ my $len_remain = $len_to_travel - $n_bytes;
+ my $n = $action->($len_remain, $n_bytes);
+ if (!defined($n)) {
+ die($!);
+ }
+ $n_bytes += $n;
+ }
+ $data;
+}
+
+# Performs the function of a worker. Receives a task. Actions it. Sends it back.
+sub _worker {
+ my ($socket, $action, $util) = @_;
+ while (defined(my $task = _item_receive($socket))) {
+ my $timer = $util->timer();
+ eval {
+ $task->set_state($task->ST_WORKING);
+ $action->($task->get_ctx());
+ $task->set_state($task->ST_OK);
+ };
+ if ($@) {
+ $task->set_state($task->ST_FAILED);
+ $task->set_error($@);
+ }
+ $task->set_elapse($timer->());
+ _item_send($socket, $task);
+ }
+ 1;
+}
+
+# ------------------------------------------------------------------------------
+# The state of a worker.
+package FCM::Util::TaskRunner::WorkerState;
+use base qw{FCM::Class::HASH};
+
+__PACKAGE__->class(
+ { 'idle' => {isa => '$', default => 1}, # worker is idle?
+ 'pid' => '$', # worker's PID
+ 'socket' => '*', # socket to worker
+ },
+ { init_attrib => sub {
+ my ($pid, $socket) = @_;
+ {'pid' => $pid, 'socket' => $socket};
+ },
+ },
+);
+
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM::Util::TaskRunner
+
+=head1 SYNOPSIS
+
+ use FCM::Context::Task;
+ use FCM::Util;
+ my $util = FCM::Util->new(\%attrib);
+ # ... time passes
+ my $runner = $util->task_runner(\&do_task, 4); # run with 4 workers
+ # ... time passes
+ my $get_ref = sub {
+ # ... an iterator to return an FCM::Context::Task object
+ # one at a time, returns undef if there is no currently available task
+ };
+ my $put_ref = sub {
+ my ($task) = @_;
+ # ... callback at end of each task
+ };
+ my $n_done = $runner->main($get_ref, $put_ref);
+
+=head1 DESCRIPTION
+
+This module is part of L<FCM::Util|FCM::Util>. See the description of the
+task_runner() method for details.
+
+An instance of this class is a runner of tasks. It can be configured to work in
+serial (default) or parallel. The class is a sub-class of
+L<FCM::Class::CODE|FCM::Class::CODE>.
+
+=head1 SEE ALSO
+
+This module is inspired by the CPAN modules Parallel::Fork::BossWorker and
+Parallel::Fork::BossWorkerAsync.
+
+L<FCM::Context::Task|FCM::Context::Task>,
+L<FCM::Util::TaskManager|FCM::Util::TaskManager>
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM1/Base.pm b/lib/FCM1/Base.pm
new file mode 100644
index 0000000..eb60c97
--- /dev/null
+++ b/lib/FCM1/Base.pm
@@ -0,0 +1,125 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# NAME
+# FCM1::Base
+#
+# DESCRIPTION
+# This is base class for all FCM OO packages.
+#
+# ------------------------------------------------------------------------------
+
+package FCM1::Base;
+
+# Standard pragma
+use strict;
+use warnings;
+
+use FCM1::Config;
+
+my @scalar_properties = (
+ 'config', # instance of FCM1::Config, configuration setting
+);
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $obj = FCM1::Base->new;
+#
+# DESCRIPTION
+# This method constructs a new instance of the FCM1::Base class.
+# ------------------------------------------------------------------------------
+
+sub new {
+ my $this = shift;
+ my %args = @_;
+ my $class = ref $this || $this;
+
+ my $self = {};
+ for (@scalar_properties) {
+ $self->{$_} = exists $args{uc ($_)} ? $args{uc ($_)} : undef;
+ }
+
+ bless $self, $class;
+ return $self;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $value = $obj->X;
+# $obj->X ($value);
+#
+# DESCRIPTION
+# Details of these properties are explained in @scalar_properties.
+# ------------------------------------------------------------------------------
+
+for my $name (@scalar_properties) {
+ no strict 'refs';
+
+ *$name = sub {
+ my $self = shift;
+
+ # Argument specified, set property to specified argument
+ if (@_) {
+ $self->{$name} = $_[0];
+ }
+
+ # Default value for property
+ if (not defined $self->{$name}) {
+ if ($name eq 'config') {
+ # Configuration setting of the main program
+ $self->{$name} = FCM1::Config->instance();
+ }
+ }
+
+ return $self->{$name};
+ }
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $value = $self->setting (@args); # $self->config->setting
+# $value = $self->verbose (@args); # $self->config->verbose
+# ------------------------------------------------------------------------------
+
+for my $name (qw/setting verbose/) {
+ no strict 'refs';
+
+ *$name = sub {
+ my $self = shift;
+ return $self->config->$name (@_);
+ }
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $value = $self->cfglabel (@args);
+#
+# DESCRIPTION
+# This is an alias to $self->config->setting ('CFG_LABEL', @args);
+# ------------------------------------------------------------------------------
+
+sub cfglabel {
+ my $self = shift;
+ return $self->setting ('CFG_LABEL', @_);
+}
+
+# ------------------------------------------------------------------------------
+
+1;
+
+__END__
diff --git a/lib/FCM1/Build.pm b/lib/FCM1/Build.pm
new file mode 100644
index 0000000..adb465b
--- /dev/null
+++ b/lib/FCM1/Build.pm
@@ -0,0 +1,1630 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# NAME
+# FCM1::Build
+#
+# DESCRIPTION
+# This is the top level class for the FCM build system.
+#
+# ------------------------------------------------------------------------------
+
+use strict;
+use warnings;
+
+package FCM1::Build;
+use base qw(FCM1::ConfigSystem);
+
+use Carp qw{croak} ;
+use Cwd qw{cwd} ;
+use FCM1::BuildSrc ;
+use FCM1::BuildTask ;
+use FCM1::Config ;
+use FCM1::Dest ;
+use FCM1::CfgLine ;
+use FCM1::Timer qw{timestamp_command} ;
+use FCM1::Util qw{expand_tilde run_command touch_file w_report};
+use File::Basename qw{dirname} ;
+use File::Spec ;
+use List::Util qw{first} ;
+use Text::ParseWords qw{shellwords} ;
+
+# List of scalar property methods for this class
+my @scalar_properties = (
+ 'name', # name of this build
+ 'target', # targets of this build
+);
+
+# List of hash property methods for this class
+my @hash_properties = (
+ 'srcpkg', # source packages of this build
+ 'dummysrcpkg', # dummy for handling package inheritance with file extension
+);
+
+# List of compare_setting_X methods
+my @compare_setting_methods = (
+ 'compare_setting_bld_blockdata', # program executable blockdata dependency
+ 'compare_setting_bld_dep', # custom dependency setting
+ 'compare_setting_bld_dep_excl', # exclude dependency setting
+ 'compare_setting_bld_dep_n', # no dependency check
+ 'compare_setting_bld_dep_pp', # custom PP dependency setting
+ 'compare_setting_bld_dep_exe', # program executable extra dependency
+ 'compare_setting_bld_exe_name', # program executable rename
+ 'compare_setting_bld_pp', # PP flags
+ 'compare_setting_infile_ext', # input file extension
+ 'compare_setting_outfile_ext', # output file extension
+ 'compare_setting_tool', # build tool settings
+);
+
+my $DELIMITER_LIST = $FCM1::Config::DELIMITER_LIST;
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $obj = FCM1::Build->new;
+#
+# DESCRIPTION
+# This method constructs a new instance of the FCM1::Build class.
+# ------------------------------------------------------------------------------
+
+sub new {
+ my $this = shift;
+ my %args = @_;
+ my $class = ref $this || $this;
+
+ my $self = FCM1::ConfigSystem->new (%args);
+
+ $self->{$_} = undef for (@scalar_properties);
+
+ $self->{$_} = {} for (@hash_properties);
+
+ bless $self, $class;
+
+ # List of sub-methods for parse_cfg
+ push @{ $self->cfg_methods }, (qw/target source tool dep misc/);
+
+ # Optional prefix in configuration declaration
+ $self->cfg_prefix ($self->setting (qw/CFG_LABEL BDECLARE/));
+
+ # System type
+ $self->type ('bld');
+
+ return $self;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $value = $obj->X;
+# $obj->X ($value);
+#
+# DESCRIPTION
+# Details of these properties are explained in @scalar_properties.
+# ------------------------------------------------------------------------------
+
+for my $name (@scalar_properties) {
+ no strict 'refs';
+
+ *$name = sub {
+ my $self = shift;
+
+ # Argument specified, set property to specified argument
+ if (@_) {
+ $self->{$name} = $_[0];
+ }
+
+ # Default value for property
+ if (not defined $self->{$name}) {
+ if ($name eq 'target') {
+ # Reference to an array
+ $self->{$name} = [];
+
+ } elsif ($name eq 'name') {
+ # Empty string
+ $self->{$name} = '';
+ }
+ }
+
+ return $self->{$name};
+ }
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# %hash = %{ $obj->X () };
+# $obj->X (\%hash);
+#
+# $value = $obj->X ($index);
+# $obj->X ($index, $value);
+#
+# DESCRIPTION
+# Details of these properties are explained in @hash_properties.
+#
+# If no argument is set, this method returns a hash containing a list of
+# objects. If an argument is set and it is a reference to a hash, the objects
+# are replaced by the specified hash.
+#
+# If a scalar argument is specified, this method returns a reference to an
+# object, if the indexed object exists or undef if the indexed object does
+# not exist. If a second argument is set, the $index element of the hash will
+# be set to the value of the argument.
+# ------------------------------------------------------------------------------
+
+for my $name (@hash_properties) {
+ no strict 'refs';
+
+ *$name = sub {
+ my ($self, $arg1, $arg2) = @_;
+
+ # Ensure property is defined as a reference to a hash
+ $self->{$name} = {} if not defined ($self->{$name});
+
+ # Argument 1 can be a reference to a hash or a scalar index
+ my ($index, %hash);
+
+ if (defined $arg1) {
+ if (ref ($arg1) eq 'HASH') {
+ %hash = %$arg1;
+
+ } else {
+ $index = $arg1;
+ }
+ }
+
+ if (defined $index) {
+ # A scalar index is defined, set and/or return the value of an element
+ $self->{$name}{$index} = $arg2 if defined $arg2;
+
+ return (
+ exists $self->{$name}{$index} ? $self->{$name}{$index} : undef
+ );
+
+ } else {
+ # A scalar index is not defined, set and/or return the hash
+ $self->{$name} = \%hash if defined $arg1;
+ return $self->{$name};
+ }
+ }
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# ($rc, $new_lines) = $self->X ($old_lines);
+#
+# DESCRIPTION
+# This method compares current settings with those in the cache, where X is
+# one of @compare_setting_methods.
+#
+# If setting has changed:
+# * For bld_blockdata, bld_dep_ext and bld_exe_name, it sets the re-generate
+# make-rule flag to true.
+# * For bld_dep_excl, in a standalone build, the method will remove the
+# dependency cache files for affected sub-packages. It returns an error if
+# the current build inherits from previous builds.
+# * For bld_pp, it updates the PP setting for affected sub-packages.
+# * For infile_ext, in a standalone build, the method will remove all the
+# sub-package cache files and trigger a re-build by removing most
+# sub-directories created by the previous build. It returns an error if the
+# current build inherits from previous builds.
+# * For outfile_ext, in a standalone build, the method will remove all the
+# sub-package dependency cache files. It returns an error if the current
+# build inherits from previous builds.
+# * For tool, it updates the "flags" files for any changed tools.
+# ------------------------------------------------------------------------------
+
+for my $name (@compare_setting_methods) {
+ no strict 'refs';
+
+ *$name = sub {
+ my ($self, $old_lines) = @_;
+
+ (my $prefix = uc ($name)) =~ s/^COMPARE_SETTING_//;
+
+ my ($changed, $new_lines) =
+ $self->compare_setting_in_config ($prefix, $old_lines);
+
+ my $rc = scalar (keys %$changed);
+
+ if ($rc and $old_lines) {
+ $self->srcpkg ('')->is_updated (1);
+
+ if ($name =~ /^compare_setting_bld_dep(?:_excl|_n|_pp)?$/) {
+ # Mark affected packages as being updated
+ for my $key (keys %$changed) {
+ for my $pkg (values %{ $self->srcpkg }) {
+ next unless $pkg->is_in_package ($key);
+ $pkg->is_updated (1);
+ }
+ }
+
+ } elsif ($name eq 'compare_setting_bld_pp') {
+ # Mark affected packages as being updated
+ for my $key (keys %$changed) {
+ for my $pkg (values %{ $self->srcpkg }) {
+ next unless $pkg->is_in_package ($key);
+ next unless $self->srcpkg ($key)->is_type_any (
+ keys %{ $self->setting ('BLD_TYPE_DEP_PP') }
+ ); # Is a type requiring pre-processing
+
+ $pkg->is_updated (1);
+ }
+ }
+
+ } elsif ($name eq 'compare_setting_infile_ext') {
+ # Re-set input file type if necessary
+ for my $key (keys %$changed) {
+ for my $pkg (values %{ $self->srcpkg }) {
+ next unless $pkg->src and $pkg->ext and $key eq $pkg->ext;
+
+ $pkg->type (undef);
+ }
+ }
+
+ # Mark affected packages as being updated
+ for my $pkg (values %{ $self->srcpkg }) {
+ $pkg->is_updated (1);
+ }
+
+ } elsif ($name eq 'compare_setting_outfile_ext') {
+ # Mark affected packages as being updated
+ for my $pkg (values %{ $self->srcpkg }) {
+ $pkg->is_updated (1);
+ }
+
+ } elsif ($name eq 'compare_setting_tool') {
+ # Update the "flags" files for changed tools
+ for my $name (sort keys %$changed) {
+ my ($tool, @names) = split /__/, $name;
+ my $pkg = join ('__', @names);
+ my @srcpkgs
+ = $self->srcpkg($pkg) ? ($self->srcpkg($pkg))
+ : $self->dummysrcpkg($pkg) ? @{$self->dummysrcpkg($pkg)->children()}
+ : ()
+ ;
+ for my $srcpkg (@srcpkgs) {
+ my $file = File::Spec->catfile (
+ $self->dest->flagsdir, $srcpkg->flagsbase ($tool)
+ );
+ &touch_file ($file) or croak $file, ': cannot update, abort';
+
+ print $file, ': updated', "\n" if $self->verbose > 2;
+ }
+ }
+ }
+ }
+
+ return ($rc, $new_lines);
+ }
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# ($rc, $new_lines) = $self->compare_setting_dependency ($old_lines, $flag);
+#
+# DESCRIPTION
+# This method uses the previous settings to determine the dependencies of
+# current source files.
+# ------------------------------------------------------------------------------
+
+sub compare_setting_dependency {
+ my ($self, $old_lines, $flag) = @_;
+
+ my $prefix = $flag ? 'DEP_PP' : 'DEP';
+ my $method = $flag ? 'ppdep' : 'dep';
+
+ my $rc = 0;
+ my $new_lines = [];
+
+ # Separate old lines
+ my %old;
+ if ($old_lines) {
+ for my $line (@$old_lines) {
+ next unless $line->label_starts_with ($prefix);
+ $old{$line->label_from_field (1)} = $line;
+ }
+ }
+
+ # Go through each source to see if the cache is up to date
+ my $count = 0;
+ my %mtime;
+ for my $srcpkg (values %{ $self->srcpkg }) {
+ next unless $srcpkg->cursrc and $srcpkg->type;
+
+ my $key = $srcpkg->pkgname;
+ my $out_of_date = $srcpkg->is_updated;
+
+ # Check modification time of cache and source file if not out of date
+ if (exists $old{$key}) {
+ if (not $out_of_date) {
+ $mtime{$old{$key}->src} = (stat ($old{$key}->src))[9]
+ if not exists ($mtime{$old{$key}->src});
+
+ $out_of_date = 1 if $mtime{$old{$key}->src} < $srcpkg->curmtime;
+ }
+ }
+ else {
+ $out_of_date = 1;
+ }
+
+ if ($out_of_date) {
+ # Re-scan dependency
+ $srcpkg->is_updated(1);
+ my ($source_is_read, $dep_hash_ref) = $srcpkg->get_dep($flag);
+ if ($source_is_read) {
+ $count++;
+ }
+ $srcpkg->$method($dep_hash_ref);
+ $rc = 1;
+ }
+ else {
+ # Use cached dependency
+ my ($progname, %hash) = split (
+ /$FCM1::Config::DELIMITER_PATTERN/, $old{$key}->value
+ );
+ $srcpkg->progname ($progname) if $progname and not $flag;
+ $srcpkg->$method (\%hash);
+ }
+
+ # New lines values: progname[::dependency-name::type][...]
+ my @value = ((defined $srcpkg->progname ? $srcpkg->progname : ''));
+ for my $name (sort keys %{ $srcpkg->$method }) {
+ push @value, $name, $srcpkg->$method ($name);
+ }
+
+ push @$new_lines, FCM1::CfgLine->new (
+ LABEL => $prefix . $FCM1::Config::DELIMITER . $key,
+ VALUE => join ($FCM1::Config::DELIMITER, @value),
+ );
+ }
+
+ print 'No. of file', ($count > 1 ? 's' : ''), ' scanned for',
+ ($flag ? ' PP': ''), ' dependency: ', $count, "\n"
+ if $self->verbose and $count;
+
+ return ($rc, $new_lines);
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# ($rc, $new_lines) = $self->compare_setting_srcpkg ($old_lines);
+#
+# DESCRIPTION
+# This method uses the previous settings to determine the type of current
+# source files.
+# ------------------------------------------------------------------------------
+
+sub compare_setting_srcpkg {
+ my ($self, $old_lines) = @_;
+
+ my $prefix = 'SRCPKG';
+
+ # Get relevant items from old lines, stripping out $prefix
+ my %old;
+ if ($old_lines) {
+ for my $line (@$old_lines) {
+ next unless $line->label_starts_with ($prefix);
+ $old{$line->label_from_field (1)} = $line;
+ }
+ }
+
+ # Check for change, use previous setting if exist
+ my $out_of_date = 0;
+ my %mtime;
+ for my $key (keys %{ $self->srcpkg }) {
+ if (exists $old{$key}) {
+ next unless $self->srcpkg ($key)->cursrc;
+
+ my $type = defined $self->setting ('BLD_TYPE', $key)
+ ? $self->setting ('BLD_TYPE', $key) : $old{$key}->value;
+
+ $self->srcpkg ($key)->type ($type);
+
+ if ($type ne $old{$key}->value) {
+ $self->srcpkg ($key)->is_updated (1);
+ $out_of_date = 1;
+ }
+
+ if (not $self->srcpkg ($key)->is_updated) {
+ $mtime{$old{$key}->src} = (stat ($old{$key}->src))[9]
+ if not exists ($mtime{$old{$key}->src});
+
+ $self->srcpkg ($key)->is_updated (1)
+ if $mtime{$old{$key}->src} < $self->srcpkg ($key)->curmtime;
+ }
+
+ } else {
+ $self->srcpkg ($key)->is_updated (1);
+ $out_of_date = 1;
+ }
+ }
+
+ # Check for deleted keys
+ for my $key (keys %old) {
+ next if $self->srcpkg ($key);
+
+ $out_of_date = 1;
+ }
+
+ # Return reference to an array of new lines
+ my $new_lines = [];
+ for my $key (keys %{ $self->srcpkg }) {
+ push @$new_lines, FCM1::CfgLine->new (
+ LABEL => $prefix . $FCM1::Config::DELIMITER . $key,
+ VALUE => $self->srcpkg ($key)->type,
+ );
+ }
+
+ return ($out_of_date, $new_lines);
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# ($rc, $new_lines) = $self->compare_setting_target ($old_lines);
+#
+# DESCRIPTION
+# This method compare the previous target settings with current ones.
+# ------------------------------------------------------------------------------
+
+sub compare_setting_target {
+ my ($self, $old_lines) = @_;
+
+ my $prefix = 'TARGET';
+ my $old;
+ if ($old_lines) {
+ for my $line (@$old_lines) {
+ next unless $line->label_starts_with ($prefix);
+ $old = $line->value;
+ last;
+ }
+ }
+
+ my $new = join (' ', sort @{ $self->target });
+
+ return (
+ (defined ($old) ? $old ne $new : 1),
+ [FCM1::CfgLine->new (LABEL => $prefix, VALUE => $new)],
+ );
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = $self->invoke_fortran_interface_generator ();
+#
+# DESCRIPTION
+# This method invokes the Fortran interface generator for all Fortran free
+# format source files. It returns true on success.
+# ------------------------------------------------------------------------------
+
+sub invoke_fortran_interface_generator {
+ my $self = shift;
+
+ my $pdoneext = $self->setting (qw/OUTFILE_EXT PDONE/);
+
+ # Set up build task to generate interface files for all selected Fortran 9x
+ # sources
+ my %task = ();
+ SRC_FILE:
+ for my $srcfile (values %{ $self->srcpkg }) {
+ if (!defined($srcfile->interfacebase())) {
+ next SRC_FILE;
+ }
+ my $target = $srcfile->interfacebase . $pdoneext;
+
+ $task{$target} = FCM1::BuildTask->new (
+ TARGET => $target,
+ TARGETPATH => $self->dest->donepath,
+ SRCFILE => $srcfile,
+ DEPENDENCY => [$srcfile->flagsbase ('GENINTERFACE')],
+ ACTIONTYPE => 'GENINTERFACE',
+ );
+
+ # Set up build tasks for each source file/package flags file for interface
+ # generator tool
+ for my $i (1 .. @{ $srcfile->pkgnames }) {
+ my $target = $srcfile->flagsbase ('GENINTERFACE', -$i);
+ my $depend = $i < @{ $srcfile->pkgnames }
+ ? $srcfile->flagsbase ('GENINTERFACE', -$i - 1)
+ : undef;
+
+ $task{$target} = FCM1::BuildTask->new (
+ TARGET => $target,
+ TARGETPATH => $self->dest->flagspath,
+ DEPENDENCY => [defined ($depend) ? $depend : ()],
+ ACTIONTYPE => 'UPDATE',
+ ) if not exists $task{$target};
+ }
+ }
+
+ # Set up build task to update the flags file for interface generator tool
+ $task{$self->srcpkg ('')->flagsbase ('GENINTERFACE')} = FCM1::BuildTask->new (
+ TARGET => $self->srcpkg ('')->flagsbase ('GENINTERFACE'),
+ TARGETPATH => $self->dest->flagspath,
+ ACTIONTYPE => 'UPDATE',
+ );
+
+ my $count = 0;
+
+ # Performs task
+ for my $task (values %task) {
+ next unless $task->actiontype eq 'GENINTERFACE';
+
+ my $rc = $task->action (TASKLIST => \%task);
+ $count++ if $rc;
+ }
+
+ print 'No. of generated Fortran interface', ($count > 1 ? 's' : ''), ': ',
+ $count, "\n"
+ if $self->verbose and $count;
+
+ return 1;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = $self->invoke_make (%args);
+#
+# DESCRIPTION
+# This method invokes the make stage of the build system. It returns true on
+# success.
+#
+# ARGUMENTS
+# ARCHIVE - If set to "true", invoke the "archive" mode. Most build files and
+# directories created by this build will be archived using the
+# "tar" command. If not set, the default is not to invoke the
+# "archive" mode.
+# JOBS - Specify number of jobs that can be handled by "make". If set, the
+# value must be a natural integer. If not set, the default value is
+# 1 (i.e. run "make" in serial mode).
+# TARGETS - Specify targets to be built. If set, these targets will be built
+# instead of the ones specified in the build configuration file.
+# ------------------------------------------------------------------------------
+
+sub invoke_make {
+ my ($self, %args) = @_;
+ $args{TARGETS} ||= ['all'];
+ $args{JOBS} ||= 1;
+ my @command = (
+ $self->setting(qw/TOOL MAKE/),
+ shellwords($self->setting(qw/TOOL MAKEFLAGS/)),
+ # -f Makefile
+ ($self->setting(qw/TOOL MAKE_FILE/), $self->dest()->bldmakefile()),
+ # -j N
+ ($args{JOBS} ? ($self->setting(qw/TOOL MAKE_JOB/), $args{JOBS}) : ()),
+ # -s
+ ($self->verbose() < 3 ? $self->setting(qw/TOOL MAKE_SILENT/) : ()),
+ @{$args{TARGETS}}
+ );
+ my $old_cwd = $self->_chdir($self->dest()->rootdir());
+ run_command(
+ \@command, ERROR => 'warn', RC => \my($code), TIME => $self->verbose() >= 3,
+ );
+ $self->_chdir($old_cwd);
+
+ my $rc = !$code;
+ if ($rc && $args{ARCHIVE}) {
+ $rc = $self->dest()->archive();
+ }
+ $rc &&= $self->dest()->create_bldrunenvsh();
+ while (my ($key, $source) = each(%{$self->srcpkg()})) {
+ $rc &&= defined($source->write_lib_dep_excl());
+ }
+ return $rc;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = $self->invoke_pre_process ();
+#
+# DESCRIPTION
+# This method invokes the pre-process stage of the build system. It
+# returns true on success.
+# ------------------------------------------------------------------------------
+
+sub invoke_pre_process {
+ my $self = shift;
+
+ # Check whether pre-processing is necessary
+ my $invoke = 0;
+ for (values %{ $self->srcpkg }) {
+ next unless $_->get_setting ('BLD_PP');
+ $invoke = 1;
+ last;
+ }
+ return 1 unless $invoke;
+
+ # Scan header dependency
+ my $rc = $self->compare_setting (
+ METHOD_LIST => ['compare_setting_dependency'],
+ METHOD_ARGS => ['BLD_TYPE_DEP_PP'],
+ CACHEBASE => $self->setting ('CACHE_DEP_PP'),
+ );
+
+ return $rc if not $rc;
+
+ my %task = ();
+ my $pdoneext = $self->setting (qw/OUTFILE_EXT PDONE/);
+
+ # Set up tasks for each source file
+ for my $srcfile (values %{ $self->srcpkg }) {
+ if ($srcfile->is_type_all (qw/CPP INCLUDE/)) {
+ # Set up a copy build task for each include file
+ $task{$srcfile->base} = FCM1::BuildTask->new (
+ TARGET => $srcfile->base,
+ TARGETPATH => $self->dest->incpath,
+ SRCFILE => $srcfile,
+ DEPENDENCY => [keys %{ $srcfile->ppdep }],
+ ACTIONTYPE => 'COPY',
+ );
+
+ } elsif ($srcfile->lang ('TOOL_SRC_PP')) {
+ next unless $srcfile->get_setting ('BLD_PP');
+
+ # Set up a PP build task for each source file
+ my $target = $srcfile->base . $pdoneext;
+
+ # Issue warning for duplicated tasks
+ if (exists $task{$target}) {
+ w_report 'WARNING: ', $target, ': unable to create task for: ',
+ $srcfile->src, ': task already exists for: ',
+ $task{$target}->srcfile->src;
+ next;
+ }
+
+ $task{$target} = FCM1::BuildTask->new (
+ TARGET => $target,
+ TARGETPATH => $self->dest->donepath,
+ SRCFILE => $srcfile,
+ DEPENDENCY => [$srcfile->flagsbase ('PPKEYS'), keys %{ $srcfile->ppdep }],
+ ACTIONTYPE => 'PP',
+ );
+
+ # Set up update ppkeys/flags build tasks for each source file/package
+ my $ppkeys = $self->setting (
+ 'TOOL_SRC_PP', $srcfile->lang ('TOOL_SRC_PP'), 'PPKEYS'
+ );
+
+ for my $i (1 .. @{ $srcfile->pkgnames }) {
+ my $target = $srcfile->flagsbase ($ppkeys, -$i);
+ my $depend = $i < @{ $srcfile->pkgnames }
+ ? $srcfile->flagsbase ($ppkeys, -$i - 1)
+ : undef;
+
+ $task{$target} = FCM1::BuildTask->new (
+ TARGET => $target,
+ TARGETPATH => $self->dest->flagspath,
+ DEPENDENCY => [defined ($depend) ? $depend : ()],
+ ACTIONTYPE => 'UPDATE',
+ ) if not exists $task{$target};
+ }
+ }
+ }
+
+ # Set up update global ppkeys build tasks
+ for my $lang (keys %{ $self->setting ('TOOL_SRC_PP') }) {
+ my $target = $self->srcpkg ('')->flagsbase (
+ $self->setting ('TOOL_SRC_PP', $lang, 'PPKEYS')
+ );
+
+ $task{$target} = FCM1::BuildTask->new (
+ TARGET => $target,
+ TARGETPATH => $self->dest->flagspath,
+ ACTIONTYPE => 'UPDATE',
+ );
+ }
+
+ # Build all PP tasks
+ my $count = 0;
+ for my $task (values %task) {
+ next unless $task->actiontype eq 'PP';
+
+ my $rc = $task->action (TASKLIST => \%task);
+ $task->srcfile->is_updated ($rc);
+ $count++ if $rc;
+ }
+
+ print 'No. of pre-processed file', ($count > 1 ? 's' : ''), ': ', $count, "\n"
+ if $self->verbose and $count;
+
+ return 1;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = $self->invoke_scan_dependency ();
+#
+# DESCRIPTION
+# This method invokes the scan dependency stage of the build system. It
+# returns true on success.
+# ------------------------------------------------------------------------------
+
+sub invoke_scan_dependency {
+ my $self = shift;
+
+ # Scan/retrieve dependency
+ # ----------------------------------------------------------------------------
+ my $rc = $self->compare_setting (
+ METHOD_LIST => ['compare_setting_dependency'],
+ CACHEBASE => $self->setting ('CACHE_DEP'),
+ );
+
+ # Check whether make file is out of date
+ # ----------------------------------------------------------------------------
+ my $out_of_date = ! -f $self->dest->bldmakefile;
+
+ if ($rc and not $out_of_date) {
+ for (qw/CACHE CACHE_DEP/) {
+ my $cache_mtime = (stat (File::Spec->catfile (
+ $self->dest->cachedir, $self->setting ($_),
+ )))[9];
+ my $mfile_mtime = (stat ($self->dest->bldmakefile))[9];
+
+ next if not defined $cache_mtime;
+ next if $cache_mtime < $mfile_mtime;
+ $out_of_date = 1;
+ last;
+ }
+ }
+
+ if ($rc and not $out_of_date) {
+ for (values %{ $self->srcpkg }) {
+ next unless $_->is_updated;
+ $out_of_date = 1;
+ last;
+ }
+ }
+
+ if ($rc and $out_of_date) {
+ # Write Makefile
+ # --------------------------------------------------------------------------
+ # Register non-word package name
+ my $unusual = 0;
+ for my $key (sort keys %{ $self->srcpkg }) {
+ next if $self->srcpkg ($key)->src;
+ next if $key =~ /^\w*$/;
+
+ $self->setting (
+ ['FCM_PCK_OBJECTS', $key], 'FCM_PCK_OBJECTS' . $unusual++,
+ );
+ }
+
+ # Write different parts in the Makefile
+ my $makefile = '# Automatic Makefile' . "\n\n";
+ $makefile .= 'FCM_BLD_NAME = ' . $self->name . "\n" if $self->name;
+ $makefile .= 'FCM_BLD_CFG = ' . $self->cfg->actual_src . "\n";
+ $makefile .= 'export FCM_VERBOSE ?= ' . $self->verbose . "\n\n";
+ $makefile .= "export OBJECTS\n";
+ $makefile .= $self->dest->write_rules;
+ $makefile .= $self->_write_makefile_perl5lib;
+ $makefile .= $self->_write_makefile_tool;
+ $makefile .= $self->_write_makefile_vpath;
+ $makefile .= $self->_write_makefile_target;
+
+ # Write rules for each source package
+ # Ensure that container packages come before files - this allows $(OBJECTS)
+ # and its dependent variables to expand correctly
+ my @srcpkg = sort {
+ if ($self->srcpkg ($a)->libbase and $self->srcpkg ($b)->libbase) {
+ $b cmp $a;
+
+ } elsif ($self->srcpkg ($a)->libbase) {
+ -1;
+
+ } elsif ($self->srcpkg ($b)->libbase) {
+ 1;
+
+ } else {
+ $a cmp $b;
+ }
+ } keys %{ $self->srcpkg };
+
+ for (@srcpkg) {
+ $makefile .= $self->srcpkg ($_)->write_rules if $self->srcpkg ($_)->rules;
+ }
+ $makefile .= '# EOF' . "\n";
+
+ # Update Makefile
+ open OUT, '>', $self->dest->bldmakefile
+ or croak $self->dest->bldmakefile, ': cannot open (', $!, '), abort';
+ print OUT $makefile;
+ close OUT
+ or croak $self->dest->bldmakefile, ': cannot close (', $!, '), abort';
+
+ print $self->dest->bldmakefile, ': updated', "\n" if $self->verbose;
+
+ # Check for duplicated targets
+ # --------------------------------------------------------------------------
+ # Get list of types that cannot have duplicated targets
+ my @no_duplicated_target_types = split (
+ /$DELIMITER_LIST/,
+ $self->setting ('BLD_TYPE_NO_DUPLICATED_TARGET'),
+ );
+
+ my %targets;
+ for my $name (sort keys %{ $self->srcpkg }) {
+ next unless $self->srcpkg ($name)->rules;
+
+ for my $key (sort keys %{ $self->srcpkg ($name)->rules }) {
+ if (exists $targets{$key}) {
+ # Duplicated target: warning for most file types
+ my $status = 'WARNING';
+
+ # Duplicated target: error for the following file types
+ if (@no_duplicated_target_types and
+ $self->srcpkg ($name)->is_type_any (@no_duplicated_target_types) and
+ $targets{$key}->is_type_any (@no_duplicated_target_types)) {
+ $status = 'ERROR';
+ $rc = 0;
+ }
+
+ # Report the warning/error
+ w_report $status, ': ', $key, ': duplicated targets for building:';
+ w_report ' ', $targets{$key}->src;
+ w_report ' ', $self->srcpkg ($name)->src;
+
+ } else {
+ $targets{$key} = $self->srcpkg ($name);
+ }
+ }
+ }
+ }
+
+ return $rc;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = $self->invoke_setup_build ();
+#
+# DESCRIPTION
+# This method invokes the setup_build stage of the build system. It returns
+# true on success.
+# ------------------------------------------------------------------------------
+
+sub invoke_setup_build {
+ my $self = shift;
+
+ my $rc = 1;
+
+ # Extract archived sub-directories if necessary
+ $rc = $self->dest->dearchive if $rc;
+
+ # Compare cache
+ $rc = $self->compare_setting (METHOD_LIST => [
+ 'compare_setting_target', # targets
+ 'compare_setting_srcpkg', # source package type
+ @compare_setting_methods,
+ ]) if $rc;
+
+ # Set up runtime dependency scan patterns
+ my %dep_pattern = %{ $self->setting ('BLD_DEP_PATTERN') };
+ for my $key (keys %dep_pattern) {
+ my $pattern = $dep_pattern{$key};
+
+ while ($pattern =~ /##([\w:]+)##/g) {
+ my $match = $1;
+ my $val = $self->setting (split (/$FCM1::Config::DELIMITER/, $match));
+
+ last unless defined $val;
+ $val =~ s/\./\\./;
+
+ $pattern =~ s/##$match##/$val/;
+ }
+
+ $self->setting (['BLD_DEP_PATTERN', $key], $pattern)
+ unless $pattern eq $dep_pattern{$key};
+ }
+
+ return $rc;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = $self->invoke_system (%args);
+#
+# DESCRIPTION
+# This method invokes the build system. It returns true on success. See also
+# the header for invoke_make for further information on arguments.
+#
+# ARGUMENTS
+# STAGE - If set, it should be an integer number or a recognised keyword or
+# abbreviation. If set, the build is performed up to the named stage.
+# If not set, the default is to perform all stages of the build.
+# Allowed values are:
+# 1, setup or s
+# 2, pre_process or pp
+# 3, generate_dependency or gd
+# 4, generate_interface or gi
+# 5, all, a, make or m
+# ------------------------------------------------------------------------------
+
+sub invoke_system {
+ my $self = shift;
+ my %args = @_;
+
+ # Parse arguments
+ # ----------------------------------------------------------------------------
+ # Default: run all 5 stages
+ my $stage = (exists $args{STAGE} and $args{STAGE}) ? $args{STAGE} : 5;
+
+ # Resolve named stages
+ if ($stage !~ /^\d$/) {
+ my %stagenames = (
+ 'S(?:ETUP)?' => 1,
+ 'P(?:RE)?_?P(?:ROCESS)?' => 2,
+ 'G(?:ENERATE)?_?D(?:ENPENDENCY)?' => 3,
+ 'G(?:ENERATE)?_?I(?:NTERFACE)?' => 4,
+ '(?:A(?:LL)|M(?:AKE)?)' => 5,
+ );
+
+ # Does it match a recognised stage?
+ for my $name (keys %stagenames) {
+ next unless $stage =~ /$name/i;
+
+ $stage = $stagenames{$name};
+ last;
+ }
+
+ # Specified stage name not recognised, default to 5
+ if ($stage !~ /^\d$/) {
+ w_report 'WARNING: ', $stage, ': invalid build stage, default to 5.';
+ $stage = 5;
+ }
+ }
+
+ # Run the method associated with each stage
+ # ----------------------------------------------------------------------------
+ my $rc = 1;
+
+ my @stages = (
+ ['Setup build' , 'invoke_setup_build'],
+ ['Pre-process' , 'invoke_pre_process'],
+ ['Scan dependency' , 'invoke_scan_dependency'],
+ ['Generate Fortran interface', 'invoke_fortran_interface_generator'],
+ ['Make' , 'invoke_make'],
+ );
+
+ for my $i (1 .. 5) {
+ last if (not $rc) or $i > $stage;
+
+ my ($name, $method) = @{ $stages[$i - 1] };
+ $rc = $self->invoke_stage ($name, $method, %args) if $rc and $stage >= $i;
+ }
+
+ return $rc;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = $self->parse_cfg_dep (\@cfg_lines);
+#
+# DESCRIPTION
+# This method parses the dependency settings in the @cfg_lines.
+# ------------------------------------------------------------------------------
+
+sub parse_cfg_dep {
+ my ($self, $cfg_lines) = @_;
+
+ my $rc = 1;
+
+ # EXCL_DEP, EXE_DEP and BLOCKDATA declarations
+ # ----------------------------------------------------------------------------
+ for my $name (qw/BLD_BLOCKDATA BLD_DEP BLD_DEP_EXCL BLD_DEP_EXE/) {
+ for my $line (grep {$_->slabel_starts_with_cfg ($name)} @$cfg_lines) {
+ # Separate label into a list, delimited by double-colon, remove 1st field
+ my @flds = $line->slabel_fields;
+ shift @flds;
+
+ if ($name =~ /^(?:BLD_DEP|BLD_DEP_EXCL|BLD_DEP_PP)$/) {
+ # BLD_DEP_*: label fields may contain sub-package
+ my $pk = @flds ? join ('__', @flds) : '';
+
+ # Check whether sub-package is valid
+ if ($pk and not ($self->srcpkg ($pk) or $self->dummysrcpkg ($pk))) {
+ $line->error ($line->label . ': invalid sub-package in declaration.');
+ $rc = 0;
+ next;
+ }
+
+ # Setting is stored in an array reference
+ $self->setting ([$name, $pk], [])
+ if not defined $self->setting ($name, $pk);
+
+ # Add current declaration to the array if necessary
+ my $list = $self->setting ($name, $pk);
+ my $value = $name eq 'BLD_DEP_EXCL' ? uc ($line->value) : $line->value;
+ push @$list, $value if not grep {$_ eq $value} @$list;
+
+ } else {
+ # EXE_DEP and BLOCKDATA: label field may be an executable target
+ my $target = @flds ? $flds[0] : '';
+
+ # The value contains a list of objects and/or sub-package names
+ my @deps = split /\s+/, $line->value;
+
+ if (not @deps) {
+ if ($name eq 'BLD_BLOCKDATA') {
+ # The objects containing a BLOCKDATA program unit must be declared
+ $line->error ($line->label . ': value not set.');
+ $rc = 0;
+ next;
+
+ } else {
+ # If $value is a null string, target(s) depends on all objects
+ push @deps, '';
+ }
+ }
+
+ for my $dep (@deps) {
+ $dep =~ s/$FCM1::Config::DELIMITER_PATTERN/__/g;
+ }
+
+ $self->setting ([$name, $target], join (' ', sort @deps));
+ }
+
+ $line->parsed (1);
+ }
+ }
+
+ return $rc;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = $self->parse_cfg_dest (\@cfg_lines);
+#
+# DESCRIPTION
+# This method parses the build destination settings in the @cfg_lines.
+# ------------------------------------------------------------------------------
+
+sub parse_cfg_dest {
+ my ($self, $cfg_lines) = @_;
+
+ my $rc = $self->SUPER::parse_cfg_dest ($cfg_lines);
+
+ # Set up search paths
+ for my $name (@FCM1::Dest::paths) {
+ (my $label = uc ($name)) =~ s/PATH//;
+
+ $self->setting (['PATH', $label], $self->dest->$name);
+ }
+
+ return $rc;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = $self->parse_cfg_misc (\@cfg_lines);
+#
+# DESCRIPTION
+# This method parses misc build settings in the @cfg_lines.
+# ------------------------------------------------------------------------------
+
+sub parse_cfg_misc {
+ my ($self, $cfg_lines_ref) = @_;
+ my $rc = 1;
+ my %item_of = (
+ BLD_DEP_N => [\&_parse_cfg_misc_dep_n , 1 ], # boolean
+ BLD_EXE_NAME => [\&_parse_cfg_misc_exe_name ],
+ BLD_LIB => [\&_parse_cfg_misc_dep_n ],
+ BLD_PP => [\&_parse_cfg_misc_dep_n , 1 ], # boolean
+ BLD_TYPE => [\&_parse_cfg_misc_dep_n ],
+ INFILE_EXT => [\&_parse_cfg_misc_file_ext, 0, 1], # uc($value)
+ OUTFILE_EXT => [\&_parse_cfg_misc_file_ext, 1, 0], # uc($ns)
+ );
+ while (my ($key, $item) = each(%item_of)) {
+ my ($handler, @extra_arguments) = @{$item};
+ for my $line (@{$cfg_lines_ref}) {
+ if ($line->slabel_starts_with_cfg($key)) {
+ if ($handler->($self, $key, $line, @extra_arguments)) {
+ $line->parsed(1);
+ }
+ else {
+ $rc = 0;
+ }
+ }
+ }
+ }
+ return $rc;
+}
+
+# ------------------------------------------------------------------------------
+# parse_cfg_misc: handler of BLD_EXE_NAME or similar.
+sub _parse_cfg_misc_exe_name {
+ my ($self, $key, $line) = @_;
+ my ($prefix, $name, @fields) = $line->slabel_fields();
+ if (!$name || @fields) {
+ $line->error(sprintf('%s: expects a single label name field.', $key));
+ return 0;
+ }
+ $self->setting([$key, $name], $line->value());
+ return 1;
+}
+
+# ------------------------------------------------------------------------------
+# parse_cfg_misc: handler of BLD_DEP_N or similar.
+sub _parse_cfg_misc_dep_n {
+ my ($self, $key, $line, $value_is_boolean) = @_;
+ my ($prefix, @fields) = $line->slabel_fields();
+ my $ns = @fields ? join(q{__}, @fields) : q{};
+ if ($ns && !$self->srcpkg($ns) && !$self->dummysrcpkg($ns)) {
+ $line->error($line->label() . ': invalid sub-package in declaration.');
+ return 0;
+ }
+ my @srcpkgs
+ = $self->dummysrcpkg($ns) ? @{$self->dummysrcpkg($ns)->children()}
+ : $self->srcpkg($ns)
+ ;
+ my $value = $value_is_boolean ? $line->bvalue() : $line->value();
+ for my $srcpkg (@srcpkgs) {
+ $self->setting([$key, $srcpkg->pkgname()], $value);
+ }
+ return 1;
+}
+
+# ------------------------------------------------------------------------------
+# parse_cfg_misc: handler of INFILE_EXT/OUTFILE_EXT or similar.
+sub _parse_cfg_misc_file_ext {
+ my ($self, $key, $line, $ns_in_uc, $value_in_uc) = @_;
+ my ($prefix, $ns) = $line->slabel_fields();
+ my $value = $value_in_uc ? uc($line->value()) : $line->value();
+ $self->setting([$key, ($ns_in_uc ? uc($ns) : $ns)], $value);
+ return 1;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = $self->parse_cfg_source (\@cfg_lines);
+#
+# DESCRIPTION
+# This method parses the source package settings in the @cfg_lines.
+# ------------------------------------------------------------------------------
+
+sub parse_cfg_source {
+ my ($self, $cfg_lines) = @_;
+
+ my $rc = 1;
+ my %src = ();
+
+ # Automatic source directory search?
+ # ----------------------------------------------------------------------------
+ my $search = 1;
+
+ for my $line (grep {$_->slabel_starts_with_cfg ('SEARCH_SRC')} @$cfg_lines) {
+ $search = $line->bvalue;
+ $line->parsed (1);
+ }
+
+ # Search src/ sub-directory if necessary
+ %src = %{ $self->dest->get_source_files } if $search;
+
+ # SRC declarations
+ # ----------------------------------------------------------------------------
+ for my $line (grep {$_->slabel_starts_with_cfg ('FILE')} @$cfg_lines) {
+ # Expand ~ notation and path relative to srcdir of destination
+ my $value = $line->value;
+ $value = File::Spec->rel2abs (&expand_tilde ($value), $self->dest->srcdir);
+
+ if (! -e $value) {
+ $line->error ($value . ': source does not exist or is not readable.');
+ next;
+ }
+
+ # Package name
+ my @names = $line->slabel_fields;
+ shift @names;
+
+ # If package name not set, determine using the path if possible
+ if (not @names) {
+ my $package = $self->dest->get_pkgname_of_path ($value);
+ @names = @$package if defined $package;
+ }
+
+ if (not @names) {
+ $line->error ($self->cfglabel ('FILE') .
+ ': package not specified/cannot be determined.');
+ next;
+ }
+
+ $src{join ('__', @names)} = $value;
+
+ $line->parsed (1);
+ }
+
+ # For directories, get non-recursive file listing, and add to %src
+ # ----------------------------------------------------------------------------
+ for my $key (keys %src) {
+ next unless -d $src{$key};
+
+ opendir DIR, $src{$key} or die $src{$key}, ': cannot read directory';
+ while (my $base = readdir 'DIR') {
+ next if $base =~ /^\./;
+
+ my $file = File::Spec->catfile ($src{$key}, $base);
+ next if ! -f $file;
+
+ my $name = join ('__', ($key, $base));
+ $src{$name} = $file unless exists $src{$name};
+ }
+ closedir DIR;
+
+ delete $src{$key};
+ }
+
+ # Set up source packages
+ # ----------------------------------------------------------------------------
+ my %pkg = ();
+ for my $name (keys %src) {
+ $pkg{$name} = FCM1::BuildSrc->new (PKGNAME => $name, SRC => $src{$name});
+ }
+
+ # INHERIT::SRC declarations
+ # ----------------------------------------------------------------------------
+ my %can_inherit = ();
+ for my $line (
+ grep {$_->slabel_starts_with_cfg(qw/INHERIT FILE/)} @{$cfg_lines}
+ ) {
+ my ($key1, $key2, @ns) = $line->slabel_fields();
+ $can_inherit{join('__', @ns)} = $line->bvalue();
+ $line->parsed(1);
+ }
+
+ # Inherit packages, if it is OK to do so
+ for my $inherited_build (reverse(@{$self->inherit()})) {
+ SRCPKG:
+ while (my ($key, $srcpkg) = each(%{$inherited_build->srcpkg()})) {
+ if (exists($pkg{$key}) || !$srcpkg->src()) {
+ next SRCPKG;
+ }
+ my $known_key = first {exists($can_inherit{$_})} @{$srcpkg->pkgnames()};
+ if (defined($known_key) && !$can_inherit{$known_key}) {
+ next SRCPKG;
+ }
+ $pkg{$key} = $srcpkg;
+ }
+ }
+
+ # Get list of intermediate "packages"
+ # ----------------------------------------------------------------------------
+ for my $name (keys %pkg) {
+ # Name of current package
+ my @names = split /__/, $name;
+
+ my $cur = $name;
+
+ while ($cur) {
+ # Name of parent package
+ pop @names;
+ my $parent = @names ? join ('__', @names) : '';
+
+ # If parent package does not exist, create it
+ $pkg{$parent} = FCM1::BuildSrc->new (PKGNAME => $parent)
+ unless exists $pkg{$parent};
+
+ # Current package is a child of the parent package
+ push @{ $pkg{$parent}->children }, $pkg{$cur}
+ unless grep {$_->pkgname eq $cur} @{ $pkg{$parent}->children };
+
+ # Go up a package
+ $cur = $parent;
+ }
+ }
+
+ $self->srcpkg (\%pkg);
+
+ # Dummy: e.g. "foo/bar/baz.egg" belongs to the "foo/bar/baz" dummy.
+ # ----------------------------------------------------------------------------
+ SRCPKG:
+ while (my ($name, $srcpkg) = each(%pkg)) {
+ if (!$srcpkg->src()) { # ensure that $srcpkg represents a source file
+ next SRCPKG;
+ }
+ my @names = split('__', $name);
+ if (@names) {
+ $names[-1] =~ s{\.\w+ \z}{}msx;
+ }
+ my $dummy_name = join('__', @names);
+ if ($dummy_name eq $name || defined($self->srcpkg($dummy_name))) {
+ next SRCPKG;
+ }
+ if (!defined($self->dummysrcpkg($dummy_name))) {
+ $self->dummysrcpkg($dummy_name, FCM1::BuildSrc->new(PKGNAME => $dummy_name));
+ }
+ push(@{$self->dummysrcpkg($dummy_name)->children()}, $srcpkg);
+ }
+
+ # Make sure a package is defined
+ # ----------------------------------------------------------------------------
+ if (not %{$self->srcpkg}) {
+ w_report 'ERROR: ', $self->cfg->actual_src, ': no source file to build.';
+ $rc = 0;
+ }
+
+ return $rc;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = $self->parse_cfg_target (\@cfg_lines);
+#
+# DESCRIPTION
+# This method parses the target settings in the @cfg_lines.
+# ------------------------------------------------------------------------------
+
+sub parse_cfg_target {
+ my ($self, $cfg_lines) = @_;
+
+ # NAME declaraions
+ # ----------------------------------------------------------------------------
+ for my $line (grep {$_->slabel_starts_with_cfg ('NAME')} @$cfg_lines) {
+ $self->name ($line->value);
+ $line->parsed (1);
+ }
+
+ # TARGET declarations
+ # ----------------------------------------------------------------------------
+ for my $line (grep {$_->slabel_starts_with_cfg ('TARGET')} @$cfg_lines) {
+ # Value is a space delimited list
+ push @{ $self->target }, split (/\s+/, $line->value);
+ $line->parsed (1);
+ }
+
+ # INHERIT::TARGET declarations
+ # ----------------------------------------------------------------------------
+ # By default, do not inherit target
+ my $inherit_flag = 0;
+
+ for (grep {$_->slabel_starts_with_cfg (qw/INHERIT TARGET/)} @$cfg_lines) {
+ $inherit_flag = $_->bvalue;
+ $_->parsed (1);
+ }
+
+ # Inherit targets from inherited build, if $inherit_flag is set to true
+ # ----------------------------------------------------------------------------
+ if ($inherit_flag) {
+ for my $use (reverse @{ $self->inherit }) {
+ unshift @{ $self->target }, @{ $use->target };
+ }
+ }
+
+ return 1;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = $self->parse_cfg_tool (\@cfg_lines);
+#
+# DESCRIPTION
+# This method parses the tool settings in the @cfg_lines.
+# ------------------------------------------------------------------------------
+
+sub parse_cfg_tool {
+ my ($self, $cfg_lines) = @_;
+
+ my $rc = 1;
+
+ my %tools = %{ $self->setting ('TOOL') };
+ my @package_tools = split(/$DELIMITER_LIST/, $self->setting('TOOL_PACKAGE'));
+
+ # TOOL declaration
+ # ----------------------------------------------------------------------------
+ for my $line (grep {$_->slabel_starts_with_cfg ('TOOL')} @$cfg_lines) {
+ # Separate label into a list, delimited by double-colon, remove TOOL
+ my @flds = $line->slabel_fields;
+ shift @flds;
+
+ # Check that there is a field after TOOL
+ if (not @flds) {
+ $line->error ('TOOL: not followed by a valid label.');
+ $rc = 0;
+ next;
+ }
+
+ # The first field is the tool iteself, identified in uppercase
+ $flds[0] = uc ($flds[0]);
+
+ # Check that the tool is recognised
+ if (not exists $tools{$flds[0]}) {
+ $line->error ($flds[0] . ': not a valid TOOL.');
+ $rc = 0;
+ next;
+ }
+
+ # Check sub-package declaration
+ if (@flds > 1 and not grep {$_ eq $flds[0]} @package_tools) {
+ $line->error ($flds[0] . ': sub-package not accepted with this TOOL.');
+ $rc = 0;
+ next;
+ }
+
+ # Name of declared package
+ my $pk = join ('__', @flds[1 .. $#flds]);
+
+ # Check whether package exists
+ if (not ($self->srcpkg ($pk) or $self->dummysrcpkg ($pk))) {
+ $line->error ($line->label . ': invalid sub-package in declaration.');
+ $rc = 0;
+ next;
+ }
+
+ $self->setting (['TOOL', join ('__', @flds)], $line->value);
+ $line->parsed (1);
+ }
+
+ return $rc;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $string = $self->_write_makefile_perl5lib ();
+#
+# DESCRIPTION
+# This method returns a makefile $string for defining $PERL5LIB.
+# ------------------------------------------------------------------------------
+
+sub _write_makefile_perl5lib {
+ my $self = shift;
+
+ my $classpath = File::Spec->catfile (split (/::/, ref ($self))) . '.pm';
+
+ my $libdir = dirname (dirname ($INC{$classpath}));
+ my @libpath = split (/:/, (exists $ENV{PERL5LIB} ? $ENV{PERL5LIB} : ''));
+
+ my $string = ((grep {$_ eq $libdir} @libpath)
+ ? ''
+ : 'export PERL5LIB := ' . $libdir .
+ (exists $ENV{PERL5LIB} ? ':$(PERL5LIB)' : '') . "\n\n");
+
+ return $string;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $string = $self->_write_makefile_target ();
+#
+# DESCRIPTION
+# This method returns a makefile $string for defining the default targets.
+# ------------------------------------------------------------------------------
+
+sub _write_makefile_target {
+ my $self = shift;
+
+ # Targets of the build
+ # ----------------------------------------------------------------------------
+ my @targets = @{ $self->target };
+ if (not @targets) {
+ # Build targets not specified by user, default to building all main programs
+ my @programs = ();
+
+ # Get all main programs from all packages
+ for my $pkg (values %{ $self->srcpkg }) {
+ push @programs, $pkg->exebase if $pkg->exebase;
+ }
+
+ @programs = sort (@programs);
+
+ if (@programs) {
+ # Build main programs, if there are any
+ @targets = @programs;
+
+ } else {
+ # No main program in source tree, build the default library
+ @targets = ($self->srcpkg ('')->libbase);
+ }
+ }
+
+ my $return = 'FCM_BLD_TARGETS = ' . join (' ', @targets) . "\n\n";
+
+ # Default targets
+ $return .= '.PHONY : all' . "\n\n";
+ $return .= 'all : $(FCM_BLD_TARGETS)' . "\n\n";
+
+ # Targets for copy dummy
+ $return .= sprintf("%s:\n\ttouch \$@\n\n", $self->setting(qw/BLD_CPDUMMY/));
+
+ return $return;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $string = $self->_write_makefile_tool ();
+#
+# DESCRIPTION
+# This method returns a makefile $string for defining the build tools.
+# ------------------------------------------------------------------------------
+
+sub _write_makefile_tool {
+ my $self = shift;
+
+ # List of build tools
+ my $tool = $self->setting ('TOOL');
+
+ # List of tools local to FCM, (will not be exported)
+ my %localtool = map {($_, 1)} split ( # map into a hash table
+ /$DELIMITER_LIST/, $self->setting ('TOOL_LOCAL'),
+ );
+
+ # Export required tools
+ my $count = 0;
+ my $return = '';
+ for my $name (sort keys %$tool) {
+ # Ignore local tools
+ next if exists $localtool{(split (/__/, $name))[0]};
+
+ if ($name =~ /^\w+$/) {
+ # Tools with normal name, just export it as an environment variable
+ $return .= 'export ' . $name . ' = ' . $tool->{$name} . "\n";
+
+ } else {
+ # Tools with unusual characters, export using a label/value pair
+ $return .= 'export FCM_UNUSUAL_TOOL_LABEL' . $count . ' = ' . $name . "\n";
+ $return .= 'export FCM_UNUSUAL_TOOL_VALUE' . $count . ' = ' .
+ $tool->{$name} . "\n";
+ $count++;
+ }
+ }
+
+ $return .= "\n";
+
+ return $return;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $string = $self->_write_makefile_vpath ();
+#
+# DESCRIPTION
+# This method returns a makefile $string for defining vpath directives.
+# ------------------------------------------------------------------------------
+
+sub _write_makefile_vpath {
+ my $self = shift();
+ my $FMT = 'vpath %%%s $(FCM_%sPATH)';
+ my %SETTING_OF = %{$self->setting('BLD_VPATH')};
+ my %EXT_OF = %{$self->setting('OUTFILE_EXT')};
+ # Note: each setting can be either an empty string or a comma-separated list
+ # of output file extension keys.
+ join(
+ "\n",
+ (
+ map
+ {
+ my $key = $_;
+ my @types = split(qr{$DELIMITER_LIST}msx, $SETTING_OF{$key});
+ @types ? (map {sprintf($FMT, $EXT_OF{$_}, $key)} sort @types)
+ : sprintf($FMT, q{}, $key)
+ ;
+ }
+ sort keys(%SETTING_OF)
+ ),
+ ) . "\n\n";
+}
+
+# Wraps chdir. Returns the old working directory.
+sub _chdir {
+ my ($self, $path) = @_;
+ if ($self->verbose() >= 3) {
+ printf("cd %s\n", $path);
+ }
+ my $old_cwd = cwd();
+ chdir($path) || croak(sprintf("%s: cannot change directory ($!)\n", $path));
+ $old_cwd;
+}
+
+# ------------------------------------------------------------------------------
+
+1;
+
+__END__
diff --git a/lib/FCM1/Build/Fortran.pm b/lib/FCM1/Build/Fortran.pm
new file mode 100644
index 0000000..452e32f
--- /dev/null
+++ b/lib/FCM1/Build/Fortran.pm
@@ -0,0 +1,549 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+
+# ------------------------------------------------------------------------------
+package FCM1::Build::Fortran;
+
+use Text::Balanced qw{extract_bracketed extract_delimited};
+
+# Actions of this class
+my %ACTION_OF = (extract_interface => \&_extract_interface);
+
+# Regular expressions
+# Matches a variable attribute
+my $RE_ATTR = qr{
+ allocatable|dimension|external|intent|optional|parameter|pointer|save|target
+}imsx;
+# Matches a name
+my $RE_NAME = qr{[A-Za-z]\w*}imsx;
+# Matches a specification type
+my $RE_SPEC = qr{
+ character|complex|double\s*precision|integer|logical|real|type
+}imsx;
+# Matches the identifier of a program unit that does not have arguments
+my $RE_UNIT_BASE = qr{block\s*data|module|program}imsx;
+# Matches the identifier of a program unit that has arguments
+my $RE_UNIT_CALL = qr{function|subroutine}imsx;
+# Matches the identifier of any program unit
+my $RE_UNIT = qr{$RE_UNIT_BASE|$RE_UNIT_CALL}msx;
+my %RE = (
+ # A comment line
+ COMMENT => qr{\A\s*(?:!|\z)}msx,
+ # A trailing comment, capture the expression before the comment
+ COMMENT_END => qr{\A([^'"]*?)\s*!.*\z}msx,
+ # A contination marker, capture the expression before the marker
+ CONT => qr{\A(.*)&\s*\z}msx,
+ # A contination marker at the beginning of a line, capture the marker and
+ # the expression after the marker
+ CONT_LEAD => qr{\A(\s*&)(.*)\z}msx,
+ # Capture a variable identifier, removing any type component expression
+ NAME_COMP => qr{\b($RE_NAME)(?:\s*\%\s*$RE_NAME)*\b}msx,
+ # Matches the first identifier in a line
+ NAME_LEAD => qr{\A\s*$RE_NAME\s*}msx,
+ # Captures a name identifier after a comma, and the expression after
+ NAME_LIST => qr{\A(?:.*?)\s*,\s*($RE_NAME)\b(.*)\z}msx,
+ # Captures the next quote character
+ QUOTE => qr{\A[^'"]*(['"])}msx,
+ # Matches an attribute declaration
+ TYPE_ATTR => qr{\A\s*($RE_ATTR)\b}msx,
+ # Matches a type declaration
+ TYPE_SPEC => qr{\A\s*($RE_SPEC)\b}msx,
+ # Captures the expression after one or more program unit attributes
+ UNIT_ATTR => qr{\A\s*(?:(?:elemental|recursive|pure)\s+)+(.*)\z}imsx,
+ # Captures the identifier and the symbol of a program unit with no arguments
+ UNIT_BASE => qr{\A\s*($RE_UNIT_BASE)\s+($RE_NAME)\s*\z}imsx,
+ # Captures the identifier and the symbol of a program unit with arguments
+ UNIT_CALL => qr{\A\s*($RE_UNIT_CALL)\s+($RE_NAME)\b}imsx,
+ # Captures the end of a program unit, its identifier and its symbol
+ UNIT_END => qr{\A\s*(end)(?:\s+($RE_NAME)(?:\s+($RE_NAME))?)?\s*\z}imsx,
+ # Captures the expression after a program unit type specification
+ UNIT_SPEC => qr{\A\s*$RE_SPEC\b(.*)\z}imsx,
+);
+
+# Keywords in type declaration statements
+my %TYPE_DECL_KEYWORD_SET = map { ($_, 1) } qw{
+ allocatable
+ dimension
+ in
+ inout
+ intent
+ kind
+ len
+ optional
+ out
+ parameter
+ pointer
+ save
+ target
+};
+
+# Creates and returns an instance of this class.
+sub new {
+ my ($class) = @_;
+ bless(
+ sub {
+ my $key = shift();
+ if (!exists($ACTION_OF{$key})) {
+ return;
+ }
+ $ACTION_OF{$key}->(@_);
+ },
+ $class,
+ );
+}
+
+# Methods.
+for my $key (keys(%ACTION_OF)) {
+ no strict qw{refs};
+ *{$key} = sub { my $self = shift(); $self->($key, @_) };
+}
+
+# Extracts the calling interfaces of top level subroutines and functions from
+# the $handle for reading Fortran sources.
+sub _extract_interface {
+ my ($handle) = @_;
+ map { _present_line($_) } @{_reduce_to_interface(_load($handle))};
+}
+
+# Reads $handle for the next Fortran statement, handling continuations.
+sub _load {
+ my ($handle) = @_;
+ my $ctx = {signature_token_set_of => {}, statements => []};
+ my $state = {
+ in_contains => undef, # in a "contains" section of a program unit
+ in_interface => undef, # in an "interface" block
+ in_quote => undef, # in a multi-line quote
+ stack => [], # program unit stack
+ };
+ my $NEW_STATEMENT = sub {
+ { name => q{}, # statement name, e.g. function, integer, ...
+ lines => [], # original lines in the statement
+ line_number => 0, # line number (start) in the original source
+ symbol => q{}, # name of a program unit (signature, end)
+ type => q{}, # e.g. signature, use, type, attr, end
+ value => q{}, # the actual value of the statement
+ };
+ };
+ my $statement;
+LINE:
+ while (my $line = readline($handle)) {
+ if (!defined($statement)) {
+ $statement = $NEW_STATEMENT->();
+ }
+ my $value = $line;
+ chomp($value);
+ # Pre-processor directives and continuation
+ if (!$statement->{line_number} && index($value, '#') == 0) {
+ $statement->{line_number} = $.;
+ $statement->{name} = 'cpp';
+ }
+ if ($statement->{name} eq 'cpp') {
+ push(@{$statement->{lines}}, $line);
+ $statement->{value} .= $value;
+ if (rindex($value, '\\') != length($value) - 1) {
+ $statement = undef;
+ }
+ next LINE;
+ }
+ # Normal Fortran
+ if ($value =~ $RE{COMMENT}) {
+ next LINE;
+ }
+ if (!$statement->{line_number}) {
+ $statement->{line_number} = $.;
+ }
+ my ($cont_head, $cont_tail);
+ if ($statement->{line_number} != $.) { # is a continuation
+ ($cont_head, $cont_tail) = $value =~ $RE{CONT_LEAD};
+ if ($cont_head) {
+ $value = $cont_tail;
+ }
+ }
+ # Correctly handle ! and & in quotes
+ my ($head, $tail) = (q{}, $value);
+ if ($state->{in_quote} && index($value, $state->{in_quote}) >= 0) {
+ my $index = index($value, $state->{in_quote});
+ $head = substr($value, 0, $index + 1);
+ $tail
+ = length($value) > $index + 1
+ ? substr($value, $index + 2)
+ : q{};
+ $state->{in_quote} = undef;
+ }
+ if (!$state->{in_quote}) {
+ while ($tail) {
+ if (index($tail, q{!}) >= 0) {
+ if (!($tail =~ s/$RE{COMMENT_END}/$1/)) {
+ ($head, $tail, $state->{in_quote})
+ = _load_extract_quote($head, $tail);
+ }
+ }
+ else {
+ while (index($tail, q{'}) > 0
+ || index($tail, q{"}) > 0)
+ {
+ ($head, $tail, $state->{in_quote})
+ = _load_extract_quote($head, $tail);
+ }
+ $head .= $tail;
+ $tail = q{};
+ }
+ }
+ }
+ $cont_head ||= q{};
+ push(@{$statement->{lines}}, $cont_head . $head . $tail . "\n");
+ $statement->{value} .= $head . $tail;
+ # Process a statement only if it is marked with a continuation
+ if (!($statement->{value} =~ s/$RE{CONT}/$1/)) {
+ $statement->{value} =~ s{\s+\z}{}msx;
+ if (_process($statement, $ctx, $state)) {
+ push(@{$ctx->{statements}}, $statement);
+ }
+ $statement = undef;
+ }
+ }
+ return $ctx;
+}
+
+# Helper, removes a quoted string from $tail.
+sub _load_extract_quote {
+ my ($head, $tail) = @_;
+ my ($extracted, $remainder, $prefix)
+ = extract_delimited($tail, q{'"}, qr{[^'"]*}msx, q{});
+ if ($extracted) {
+ return ($head . $prefix . $extracted, $remainder);
+ }
+ else {
+ my ($quote) = $tail =~ $RE{QUOTE};
+ return ($head . $tail, q{}, $quote);
+ }
+}
+
+# Study statements and put attributes into array $statements
+sub _process {
+ my ($statement, $ctx, $state) = @_;
+ my $name;
+
+ # End Interface
+ if ($state->{in_interface}) {
+ if ($statement->{value} =~ qr{\A\s*end\s*interface\b}imsx) {
+ $state->{in_interface} = 0;
+ }
+ return;
+ }
+
+ # End Program Unit
+ if (@{$state->{stack}} && $statement->{value} =~ qr{\A\s*end\b}imsx) {
+ my ($end, $type, $symbol) = lc($statement->{value}) =~ $RE{UNIT_END};
+ if (!$end) {
+ return;
+ }
+ my ($top_type, $top_symbol) = @{$state->{stack}->[-1]};
+ if (!$type
+ || $top_type eq $type && (!$symbol || $top_symbol eq $symbol))
+ {
+ pop(@{$state->{stack}});
+ if ($state->{in_contains} && !@{$state->{stack}}) {
+ $state->{in_contains} = 0;
+ }
+ if (!$state->{in_contains}) {
+ $statement->{name} = $top_type;
+ $statement->{symbol} = $top_symbol;
+ $statement->{type} = 'end';
+ return $statement;
+ }
+ }
+ return;
+ }
+
+ # Interface/Contains
+ ($name) = $statement->{value} =~ qr{\A\s*(contains|interface)\b}imsx;
+ if ($name) {
+ $state->{'in_' . lc($name)} = 1;
+ return;
+ }
+
+ # Program Unit
+ my ($type, $symbol, @tokens) = _process_prog_unit($statement->{value});
+ if ($type) {
+ push(@{$state->{stack}}, [$type, $symbol]);
+ if ($state->{in_contains}) {
+ return;
+ }
+ $statement->{name} = lc($type);
+ $statement->{type} = 'signature';
+ $statement->{symbol} = lc($symbol);
+ $ctx->{signature_token_set_of}{$symbol}
+ = {map { (lc($_) => 1) } @tokens};
+ return $statement;
+ }
+ if ($state->{in_contains}) {
+ return;
+ }
+
+ # Use
+ if ($statement->{value} =~ qr{\A\s*(use)\b}imsx) {
+ $statement->{name} = 'use';
+ $statement->{type} = 'use';
+ return $statement;
+ }
+
+ # Type Declarations
+ ($name) = $statement->{value} =~ $RE{TYPE_SPEC};
+ if ($name) {
+ $name =~ s{\s}{}gmsx;
+ $statement->{name} = lc($name);
+ $statement->{type} = 'type';
+ return $statement;
+ }
+
+ # Attribute Statements
+ ($name) = $statement->{value} =~ $RE{TYPE_ATTR};
+ if ($name) {
+ $statement->{name} = $name;
+ $statement->{type} = 'attr';
+ return $statement;
+ }
+}
+
+# Parse a statement for program unit header. Returns a list containing the type,
+# the symbol and the signature tokens of the program unit.
+sub _process_prog_unit {
+ my ($string) = @_;
+ my ($type, $symbol, @args) = (q{}, q{});
+ # Is it a blockdata, module or program?
+ ($type, $symbol) = $string =~ $RE{UNIT_BASE};
+ if ($type) {
+ $type = lc($type);
+ $type =~ s{\s*}{}gmsx;
+ return ($type, $symbol);
+ }
+ # Remove the attribute and type declaration of a procedure
+ $string =~ s/$RE{UNIT_ATTR}/$1/;
+ my ($match) = $string =~ $RE{UNIT_SPEC};
+ if ($match) {
+ $string = $match;
+ extract_bracketed($string);
+ }
+ # Is it a function or subroutine?
+ ($type, $symbol) = lc($string) =~ $RE{UNIT_CALL};
+ if (!$type) {
+ return;
+ }
+ my $extracted = extract_bracketed($string, q{()}, qr{[^(]*}msx);
+
+ # Get signature tokens from SUBROUTINE/FUNCTION
+ if ($extracted) {
+ $extracted =~ s{\s}{}gmsx;
+ @args = split(q{,}, substr($extracted, 1, length($extracted) - 2));
+ if ($type eq 'function') {
+ my $result = extract_bracketed($string, q{()}, qr{[^(]*}msx);
+ if ($result) {
+ $result =~ s{\A\(\s*(.*?)\s*\)\z}{$1}msx; # remove braces
+ push(@args, $result);
+ }
+ else {
+ push(@args, $symbol);
+ }
+ }
+ }
+ return (lc($type), lc($symbol), map { lc($_) } @args);
+}
+
+# Reduces the list of statements to contain only the interface block.
+sub _reduce_to_interface {
+ my ($ctx) = @_;
+ my (%token_set, @interface_statements);
+STATEMENT:
+ for my $statement (reverse(@{$ctx->{statements}})) {
+ if ($statement->{type} eq 'end'
+ && grep { $_ eq $statement->{name} } qw{subroutine function})
+ {
+ push(@interface_statements, $statement);
+ %token_set
+ = %{$ctx->{signature_token_set_of}{$statement->{symbol}}};
+ next STATEMENT;
+ }
+ if ($statement->{type} eq 'signature'
+ && grep { $_ eq $statement->{name} } qw{subroutine function})
+ {
+ push(@interface_statements, $statement);
+ %token_set = ();
+ next STATEMENT;
+ }
+ if ($statement->{type} eq 'use') {
+ my ($head, $tail)
+ = split(qr{\s*:\s*}msx, lc($statement->{value}), 2);
+ if ($tail) {
+ my @imports = map { [split(qr{\s*=>\s*}msx, $_, 2)] }
+ split(qr{\s*,\s*}msx, $tail);
+ my @useful_imports
+ = grep { exists($token_set{$_->[0]}) } @imports;
+ if (!@useful_imports) {
+ next STATEMENT;
+ }
+ if (@imports != @useful_imports) {
+ my @token_strings
+ = map { $_->[0] . ($_->[1] ? ' => ' . $_->[1] : q{}) }
+ @useful_imports;
+ my ($last, @rest) = reverse(@token_strings);
+ my @token_lines
+ = (reverse(map { $_ . q{,&} } @rest), $last);
+ push(
+ @interface_statements,
+ { lines => [
+ sprintf("%s:&\n", $head),
+ (map { sprintf(" & %s\n", $_) } @token_lines),
+ ]
+ },
+ );
+ next STATEMENT;
+ }
+ }
+ push(@interface_statements, $statement);
+ next STATEMENT;
+ }
+ if ($statement->{type} eq 'attr') {
+ my ($spec, @tokens) = ($statement->{value} =~ /$RE{NAME_COMP}/g);
+ if (grep { exists($token_set{$_}) } @tokens) {
+ for my $token (@tokens) {
+ $token_set{$token} = 1;
+ }
+ push(@interface_statements, $statement);
+ next STATEMENT;
+ }
+ }
+ if ($statement->{type} eq 'type') {
+ my ($variable_string, $spec_string)
+ = reverse(split('::', lc($statement->{value}), 2));
+ if ($spec_string) {
+ $spec_string =~ s{$RE{NAME_LEAD}}{}msx;
+ }
+ else {
+ # The first expression in the statement is the type + attrib
+ $variable_string =~ s{$RE{NAME_LEAD}}{}msx;
+ $spec_string = extract_bracketed($variable_string, '()',
+ qr{[\s\*]*}msx);
+ }
+ # Useful tokens are those that comes after a comma
+ my $tail = q{,} . lc($variable_string);
+ my @tokens;
+ while ($tail) {
+ if ($tail =~ qr{\A\s*['"]}msx) {
+ extract_delimited($tail, q{'"}, qr{\A[^'"]*}msx, q{});
+ }
+ elsif ($tail =~ qr{\A\s*\(}msx) {
+ extract_bracketed($tail, '()', qr{\A[^(]*}msx);
+ }
+ else {
+ my $token;
+ ($token, $tail) = $tail =~ $RE{NAME_LIST};
+ if ($token && $token_set{$token}) {
+ @tokens = ($variable_string =~ /$RE{NAME_COMP}/g);
+ $tail = q{};
+ }
+ }
+ }
+ if (@tokens && $spec_string) {
+ my @spec_tokens = (lc($spec_string) =~ /$RE{NAME_COMP}/g);
+ push(
+ @tokens,
+ ( grep { !exists($TYPE_DECL_KEYWORD_SET{$_}) }
+ @spec_tokens
+ ),
+ );
+ }
+ if (grep { exists($token_set{$_}) } @tokens) {
+ for my $token (@tokens) {
+ $token_set{$token} = 1;
+ }
+ push(@interface_statements, $statement);
+ next STATEMENT;
+ }
+ }
+ }
+ if (!@interface_statements) {
+ return [];
+ }
+ [ {lines => ["interface\n"]},
+ reverse(@interface_statements),
+ {lines => ["end interface\n"]},
+ ];
+}
+
+# Processes and returns the line of the statement.
+sub _present_line {
+ my ($statement) = @_;
+ map {
+ s{\s+}{ }gmsx; # collapse multiple spaces
+ s{\s+\z}{\n}msx; # remove trailing spaces
+ $_;
+ } @{$statement->{lines}};
+}
+
+# ------------------------------------------------------------------------------
+1;
+__END__
+
+=head1 NAME
+
+FCM1::Build::Fortran
+
+=head1 SYNOPSIS
+
+ use FCM1::Build::Fortran;
+ my $fortran_util = FCM1::Build::Fortran->new();
+ open(my($handle), '<', $path_to_a_fortran_source_file);
+ print($fortran_util->extract_interface($handle)); # prints interface
+ close($handle);
+
+=head1 DESCRIPTION
+
+A class to analyse Fortran source. Currently, it has a single method to extract
+the calling interfaces of top level subroutines and functions in a Fortran
+source.
+
+=head1 METHODS
+
+=over 4
+
+=item $class->new()
+
+Creates and returns an instance of this class.
+
+=item $instance->extract_interface($handle)
+
+Extracts the calling interfaces of top level subroutines and functions in a
+Fortran source that can be read from $handle. Returns an interface block as a
+list of lines.
+
+=back
+
+=head1 ACKNOWLEDGEMENT
+
+This module is inspired by the logic developed by the European Centre
+for Medium-Range Weather Forecasts (ECMWF).
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM1/BuildSrc.pm b/lib/FCM1/BuildSrc.pm
new file mode 100644
index 0000000..cd36a52
--- /dev/null
+++ b/lib/FCM1/BuildSrc.pm
@@ -0,0 +1,1508 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# NAME
+# FCM1::BuildSrc
+#
+# DESCRIPTION
+# This is a class to group functionalities of source in a build.
+#
+# ------------------------------------------------------------------------------
+
+use strict;
+use warnings;
+
+package FCM1::BuildSrc;
+use base qw{FCM1::Base};
+
+use Carp qw{croak};
+use Cwd qw{cwd};
+use FCM1::Build::Fortran;
+use FCM1::CfgFile;
+use FCM1::CfgLine;
+use FCM1::Config;
+use FCM1::Timer qw{timestamp_command};
+use FCM1::Util qw{find_file_in_path run_command};
+use File::Basename qw{basename dirname};
+use File::Spec;
+
+# List of scalar property methods for this class
+my @scalar_properties = (
+ 'children', # list of children packages
+ 'is_updated', # is this source (or its associated settings) updated?
+ 'mtime', # modification time of src
+ 'ppmtime', # modification time of ppsrc
+ 'ppsrc', # full path of the pre-processed source
+ 'pkgname', # package name of the source
+ 'progname', # program unit name in the source
+ 'src', # full path of the source
+ 'type', # type of the source
+);
+
+# List of hash property methods for this class
+my @hash_properties = (
+ 'dep', # dependencies
+ 'ppdep', # pre-process dependencies
+ 'rules', # make rules
+);
+
+# Error message formats
+my %ERR_MESS_OF = (
+ CHDIR => '%s: cannot change directory (%s), abort',
+ OPEN => '%s: cannot open (%s), abort',
+ CLOSE_PIPE => '%s: failed (%d), abort',
+);
+
+# Event message formats and levels
+my %EVENT_SETTING_OF = (
+ CHDIR => ['%s: change directory' , 2],
+ F_INTERFACE_NONE => ['%s: Fortran interface generation is off', 3],
+ GET_DEPENDENCY => ['%s: %d line(s), %d auto dependency(ies)', 3],
+);
+
+my %RE_OF = (
+ F_PREFIX => qr{
+ (?:
+ (?:ELEMENTAL|PURE(?:\s+RECURSIVE)?|RECURSIVE(?:\s+PURE)?)
+ \s+
+ )?
+ }imsx,
+ F_SPEC => qr{
+ (?:
+ (?:CHARACTER|COMPLEX|DOUBLE\s*PRECISION|INTEGER|LOGICAL|REAL|TYPE)
+ (?: \s* \( .+ \) | \s* \* \d+ \s*)??
+ \s+
+ )?
+ }imsx,
+);
+
+{
+ # Returns a singleton instance of FCM1::Build::Fortran.
+ my $FORTRAN_UTIL;
+ sub _get_fortran_util {
+ $FORTRAN_UTIL ||= FCM1::Build::Fortran->new();
+ return $FORTRAN_UTIL;
+ }
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $obj = FCM1::BuildSrc->new (%args);
+#
+# DESCRIPTION
+# This method constructs a new instance of the FCM1::BuildSrc class. See
+# above for allowed list of properties. (KEYS should be in uppercase.)
+# ------------------------------------------------------------------------------
+
+sub new {
+ my ($class, %args) = @_;
+ my $self = bless(FCM1::Base->new(%args), $class);
+ for my $key (@scalar_properties, @hash_properties) {
+ $self->{$key}
+ = exists($args{uc($key)}) ? $args{uc($key)}
+ : undef
+ ;
+ }
+ $self;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $value = $obj->X;
+# $obj->X ($value);
+#
+# DESCRIPTION
+# Details of these properties are explained in @scalar_properties.
+# ------------------------------------------------------------------------------
+
+for my $name (@scalar_properties) {
+ no strict 'refs';
+
+ *$name = sub {
+ my $self = shift;
+
+ # Argument specified, set property to specified argument
+ if (@_) {
+ $self->{$name} = $_[0];
+
+ if ($name eq 'ppsrc') {
+ $self->ppmtime (undef);
+
+ } elsif ($name eq 'src') {
+ $self->mtime (undef);
+ }
+ }
+
+ # Default value for property
+ if (not defined $self->{$name}) {
+ if ($name eq 'children') {
+ # Reference to an empty array
+ $self->{$name} = [];
+
+ } elsif ($name =~ /^(?:is_cur|pkgname|ppsrc|src)$/) {
+ # Empty string
+ $self->{$name} = '';
+
+ } elsif ($name eq 'mtime') {
+ # Modification time
+ $self->{$name} = (stat $self->src)[9] if $self->src;
+
+ } elsif ($name eq 'ppmtime') {
+ # Modification time
+ $self->{$name} = (stat $self->ppsrc)[9] if $self->ppsrc;
+
+ } elsif ($name eq 'type') {
+ # Attempt to get the type if src is set
+ $self->{$name} = $self->get_type if $self->src;
+ }
+ }
+
+ return $self->{$name};
+ }
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# %hash = %{ $obj->X () };
+# $obj->X (\%hash);
+#
+# $value = $obj->X ($index);
+# $obj->X ($index, $value);
+#
+# DESCRIPTION
+# Details of these properties are explained in @hash_properties.
+#
+# If no argument is set, this method returns a hash containing a list of
+# objects. If an argument is set and it is a reference to a hash, the objects
+# are replaced by the specified hash.
+#
+# If a scalar argument is specified, this method returns a reference to an
+# object, if the indexed object exists or undef if the indexed object does
+# not exist. If a second argument is set, the $index element of the hash will
+# be set to the value of the argument.
+# ------------------------------------------------------------------------------
+
+for my $name (@hash_properties) {
+ no strict 'refs';
+
+ *$name = sub {
+ my ($self, $arg1, $arg2) = @_;
+
+ # Ensure property is defined as a reference to a hash
+ if (not defined $self->{$name}) {
+ if ($name eq 'rules') {
+ $self->{$name} = $self->get_rules;
+
+ } else {
+ $self->{$name} = {};
+ }
+ }
+
+ # Argument 1 can be a reference to a hash or a scalar index
+ my ($index, %hash);
+
+ if (defined $arg1) {
+ if (ref ($arg1) eq 'HASH') {
+ %hash = %$arg1;
+
+ } else {
+ $index = $arg1;
+ }
+ }
+
+ if (defined $index) {
+ # A scalar index is defined, set and/or return the value of an element
+ $self->{$name}{$index} = $arg2 if defined $arg2;
+
+ return (
+ exists $self->{$name}{$index} ? $self->{$name}{$index} : undef
+ );
+
+ } else {
+ # A scalar index is not defined, set and/or return the hash
+ $self->{$name} = \%hash if defined $arg1;
+ return $self->{$name};
+ }
+ }
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $value = $obj->X;
+# $obj->X ($value);
+#
+# DESCRIPTION
+# This method returns/sets property X, all derived from src, where X is:
+# base - (read-only) basename of src
+# dir - (read-only) dirname of src
+# ext - (read-only) file extension of src
+# root - (read-only) basename of src without the file extension
+# ------------------------------------------------------------------------------
+
+sub base {
+ return &basename ($_[0]->src);
+}
+
+# ------------------------------------------------------------------------------
+
+sub dir {
+ return &dirname ($_[0]->src);
+}
+
+# ------------------------------------------------------------------------------
+
+sub ext {
+ return substr $_[0]->base, length ($_[0]->root);
+}
+
+# ------------------------------------------------------------------------------
+
+sub root {
+ (my $root = $_[0]->base) =~ s/\.\w+$//;
+ return $root;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $value = $obj->X;
+# $obj->X ($value);
+#
+# DESCRIPTION
+# This method returns/sets property X, all derived from ppsrc, where X is:
+# ppbase - (read-only) basename of ppsrc
+# ppdir - (read-only) dirname of ppsrc
+# ppext - (read-only) file extension of ppsrc
+# pproot - (read-only) basename of ppsrc without the file extension
+# ------------------------------------------------------------------------------
+
+sub ppbase {
+ return &basename ($_[0]->ppsrc);
+}
+
+# ------------------------------------------------------------------------------
+
+sub ppdir {
+ return &dirname ($_[0]->ppsrc);
+}
+
+# ------------------------------------------------------------------------------
+
+sub ppext {
+ return substr $_[0]->ppbase, length ($_[0]->pproot);
+}
+
+# ------------------------------------------------------------------------------
+
+sub pproot {
+ (my $root = $_[0]->ppbase) =~ s/\.\w+$//;
+ return $root;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $value = $obj->X;
+#
+# DESCRIPTION
+# This method returns/sets property X, derived from src or ppsrc, where X is:
+# curbase - (read-only) basename of cursrc
+# curdir - (read-only) dirname of cursrc
+# curext - (read-only) file extension of cursrc
+# curmtime - (read-only) modification time of cursrc
+# curroot - (read-only) basename of cursrc without the file extension
+# cursrc - ppsrc or src
+# ------------------------------------------------------------------------------
+
+for my $name (qw/base dir ext mtime root src/) {
+ no strict 'refs';
+
+ my $subname = 'cur' . $name;
+
+ *$subname = sub {
+ my $self = shift;
+ my $method = $self->ppsrc ? 'pp' . $name : $name;
+ return $self->$method (@_);
+ }
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $base = $obj->X ();
+#
+# DESCRIPTION
+# This method returns a basename X for the source, where X is:
+# donebase - "done" file name
+# etcbase - target for copying data files
+# exebase - executable name for source containing a main program
+# interfacebase - Fortran interface file name
+# libbase - library file name
+# objbase - object name for source containing compilable source
+# If the source file contains a compilable procedure, this method returns
+# the name of the object file.
+# ------------------------------------------------------------------------------
+
+sub donebase {
+ my $self = shift;
+
+ my $return;
+ if ($self->is_type_all ('SOURCE')) {
+ if ($self->objbase and not $self->is_type_all ('PROGRAM')) {
+ $return = ($self->progname ? $self->progname : lc ($self->curroot)) .
+ $self->setting (qw/OUTFILE_EXT DONE/);
+ }
+
+ } elsif ($self->is_type_all ('INCLUDE')) {
+ $return = $self->curbase . $self->setting (qw/OUTFILE_EXT IDONE/);
+ }
+
+ return $return;
+}
+
+# ------------------------------------------------------------------------------
+
+sub etcbase {
+ my $self = shift;
+
+ my $return = @{ $self->children }
+ ? $self->pkgname . $self->setting (qw/OUTFILE_EXT ETC/)
+ : undef;
+
+ return $return;
+}
+
+# ------------------------------------------------------------------------------
+
+sub exebase {
+ my $self = shift;
+
+ my $return;
+ if ($self->objbase and $self->is_type_all ('PROGRAM')) {
+ if ($self->setting ('BLD_EXE_NAME', $self->curroot)) {
+ $return = $self->setting ('BLD_EXE_NAME', $self->curroot);
+
+ } else {
+ $return = $self->curroot . $self->setting (qw/OUTFILE_EXT EXE/);
+ }
+ }
+
+ return $return;
+}
+
+# ------------------------------------------------------------------------------
+
+sub interfacebase {
+ my $self = shift();
+ if (
+ uc($self->get_setting(qw/TOOL GENINTERFACE/)) ne 'NONE'
+ && $self->progname()
+ && $self->is_type_all(qw/SOURCE/)
+ && $self->is_type_any(qw/FORTRAN9X FPP9X/)
+ && !$self->is_type_any(qw/PROGRAM MODULE BLOCKDATA/)
+ ) {
+ my $flag = lc($self->get_setting(qw/TOOL INTERFACE/));
+ my $ext = $self->setting(qw/OUTFILE_EXT INTERFACE/);
+
+ return (($flag eq 'program' ? $self->progname() : $self->curroot()) . $ext);
+ }
+ return;
+}
+
+# ------------------------------------------------------------------------------
+
+sub objbase {
+ my $self = shift;
+
+ my $return;
+
+ if ($self->is_type_all ('SOURCE')) {
+ my $ext = $self->setting (qw/OUTFILE_EXT OBJ/);
+
+ if ($self->is_type_any (qw/FORTRAN FPP/)) {
+ $return = lc ($self->progname) . $ext if $self->progname;
+
+ } else {
+ $return = lc ($self->curroot) . $ext;
+ }
+ }
+
+ return $return;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $value = $obj->flagsbase ($flag, [$index,]);
+#
+# DESCRIPTION
+# Returns the base name of the flags file for the current package namespace
+# for a given $flag. The returned base name should look like
+# "LABEL___PACKAGE__NAME__SPACE.flags", where "LABEL" is normally the $flag,
+# and "PACKAGE__NAME__SPACE" is the current package namespace without the file
+# extension. If $flag is FLAGS or PPKEYS and $self->lang() is defined, it
+# will attempt to determine the correct label for the language. E.g. If
+# $self->lang() is 'C', the label will be "CFLAGS". If $index is set, returns
+# the base name of the flags file for the $index'th element in package name
+# space (as described in "pkgnames" method) instead of the current package
+# name space.
+# ------------------------------------------------------------------------------
+
+sub flagsbase {
+ my ($self, $flag, $index) = @_;
+ my $name = $index ? $self->pkgnames()->[$index] : $self->pkgname();
+ my @names = split('__', $name);
+ if (@names && $self->src() && $name eq $self->pkgname()) {
+ $names[-1] =~ s{\.\w+ \z}{}msx;
+ }
+ my $label = $flag;
+ if ($self->lang() && ($flag eq 'FLAGS' || $flag eq 'PPKEYS')) {
+ if (!exists($self->setting('TOOL_SRC')->{$self->lang()}{$flag})) {
+ return;
+ }
+ $label = $self->setting('TOOL_SRC')->{$self->lang()}{$flag};
+ }
+ join('__', $label, @names) . $self->setting(qw/OUTFILE_EXT FLAGS/);
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $value = $obj->libbase ([$prefix], [$suffix]);
+#
+# DESCRIPTION
+# This method returns the property libbase (derived from pkgname) the base
+# name of the library archive. $prefix and $suffix defaults to 'lib' and '.a'
+# respectively.
+# ------------------------------------------------------------------------------
+
+sub libbase {
+ my ($self, $prefix, $suffix) = @_;
+ $prefix ||= 'lib';
+ $suffix ||= $self->setting(qw/OUTFILE_EXT LIB/);
+ if ($self->src()) { # applies to directories only
+ return;
+ }
+ my $name = $self->setting('BLD_LIB', $self->pkgname());
+ if (!defined($name)) {
+ $name = $self->pkgname();
+ }
+ $prefix . $name . $suffix;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $value = $obj->lang ([$setting]);
+#
+# DESCRIPTION
+# This method returns the property lang (derived from type) the programming
+# language name if type matches one supported in the TOOL_SRC setting. If
+# $setting is specified, use $setting instead of TOOL_SRC.
+# ------------------------------------------------------------------------------
+
+sub lang {
+ my ($self, $setting) = @_;
+
+ my @keys = keys %{ $self->setting ($setting ? $setting : 'TOOL_SRC') };
+
+ my $return = undef;
+ for my $key (@keys) {
+ next unless $self->is_type_all ('SOURCE', $key);
+ $return = $key;
+ last;
+ }
+
+ return $return;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $value = $obj->pkgnames;
+#
+# DESCRIPTION
+# This method returns a list of container packages, derived from pkgname:
+# ------------------------------------------------------------------------------
+
+sub pkgnames {
+ my $self = shift;
+
+ my $return = [];
+ if ($self->pkgname) {
+ my @names = split (/__/, $self->pkgname);
+
+ for my $i (0 .. $#names) {
+ push @$return, join ('__', (@names[0 .. $i]));
+ }
+
+ unshift @$return, '';
+ }
+
+ return $return;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# %dep = %{$obj->get_dep()};
+# %dep = %{$obj->get_dep($flag)};
+#
+# DESCRIPTION
+# This method scans the current source file for dependencies and returns the
+# dependency hash (keys = dependencies, values = dependency types). If $flag
+# is specified, the config setting for $flag is used to determine the types of
+# types. Otherwise, those specified in 'BLD_TYPE_DEP' is used.
+# ------------------------------------------------------------------------------
+
+sub get_dep {
+ my ($self, $flag) = @_;
+ # Work out list of exclude for this file, using its sub-package name
+ my %EXCLUDE_SET = map {($_, 1)} @{$self->get_setting('BLD_DEP_EXCL')};
+ # Determine what dependencies are supported by this known type
+ my %DEP_TYPE_OF = %{$self->setting($flag ? $flag : 'BLD_TYPE_DEP')};
+ my %PATTERN_OF = %{$self->setting('BLD_DEP_PATTERN')};
+ my @dep_types = ();
+ if (!$self->get_setting('BLD_DEP_N')) {
+ DEP_TYPE:
+ while (my ($key, $dep_type_string) = each(%DEP_TYPE_OF)) {
+ # Check if current file is a type of file requiring dependency scan
+ if (!$self->is_type_all($key)) {
+ next DEP_TYPE;
+ }
+ # Get list of dependency type for this file
+ for my $dep_type (split(/$FCM1::Config::DELIMITER/, $dep_type_string)) {
+ if (exists($PATTERN_OF{$dep_type}) && !exists($EXCLUDE_SET{$dep_type})) {
+ push(@dep_types, $dep_type);
+ }
+ }
+ }
+ }
+
+ # Automatic dependencies
+ my %dep_of;
+ my $can_get_symbol # Also scan for program unit name in Fortran source
+ = !$flag
+ && $self->is_type_all('SOURCE')
+ && $self->is_type_any(qw/FPP FORTRAN/)
+ ;
+ my $has_read_file;
+ if ($can_get_symbol || @dep_types) {
+ my $handle = _open($self->cursrc());
+ LINE:
+ while (my $line = readline($handle)) {
+ chomp($line);
+ if ($line =~ qr{\A \s* \z}msx) { # empty lines
+ next LINE;
+ }
+ if ($can_get_symbol) {
+ my $symbol = _get_dep_symbol($line);
+ if ($symbol) {
+ $self->progname($symbol);
+ $can_get_symbol = 0;
+ next LINE;
+ }
+ }
+ DEP_TYPE:
+ for my $dep_type (@dep_types) {
+ my ($match) = $line =~ /$PATTERN_OF{$dep_type}/i;
+ if (!$match) {
+ next DEP_TYPE;
+ }
+ # $match may contain multiple items delimited by space
+ for my $item (split(qr{\s+}msx, $match)) {
+ my $key = uc($dep_type . $FCM1::Config::DELIMITER . $item);
+ if (!exists($EXCLUDE_SET{$key})) {
+ $dep_of{$item} = $dep_type;
+ }
+ }
+ next LINE;
+ }
+ }
+ $self->_event('GET_DEPENDENCY', $self->pkgname(), $., scalar(keys(%dep_of)));
+ close($handle);
+ $has_read_file = 1;
+ }
+
+ # Manual dependencies
+ my $manual_deps_ref
+ = $self->setting('BLD_DEP' . ($flag ? '_PP' : ''), $self->pkgname());
+ if (defined($manual_deps_ref)) {
+ for (@{$manual_deps_ref}) {
+ my ($dep_type, $item) = split(/$FCM1::Config::DELIMITER/, $_, 2);
+ $dep_of{$item} = $dep_type;
+ }
+ }
+
+ return ($has_read_file, \%dep_of);
+}
+
+# Returns, if possible, the program unit declared in the $line.
+sub _get_dep_symbol {
+ my $line = shift();
+ for my $pattern (
+ qr{\A \s* $RE_OF{F_PREFIX} SUBROUTINE \s+ ([A-Za-z]\w*)}imsx,
+ qr{\A \s* MODULE (?!\s+PROCEDURE) \s+ ([A-Za-z]\w*)}imsx,
+ qr{\A \s* PROGRAM \s+ ([A-Za-z]\w*)}imsx,
+ qr{\A \s* $RE_OF{F_PREFIX} $RE_OF{F_SPEC} FUNCTION \s+ ([A-Za-z]\w*)}imsx,
+ qr{\A \s* BLOCK\s*DATA \s+ ([A-Za-z]\w*)}imsx,
+ ) {
+ my ($match) = $line =~ $pattern;
+ if ($match) {
+ return lc($match);
+ }
+ }
+ return;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# @out = @{ $obj->get_fortran_interface () };
+#
+# DESCRIPTION
+# This method invokes the Fortran interface block generator to generate
+# an interface block for the current source file. It returns a reference to
+# an array containing the lines of the interface block.
+# ------------------------------------------------------------------------------
+
+sub get_fortran_interface {
+ my $self = shift();
+ my %ACTION_OF = (
+ q{} => \&_get_fortran_interface_by_internal_code,
+ f90aib => \&_get_fortran_interface_by_f90aib,
+ none => sub {$self->_event('F_INTERFACE_NONE', $self->root()); []},
+ );
+ my $key = lc($self->get_setting(qw/TOOL GENINTERFACE/));
+ if (!$key || !exists($ACTION_OF{$key})) {
+ $key = q{};
+ }
+ $ACTION_OF{$key}->($self->cursrc());
+}
+
+# Generates Fortran interface block using "f90aib".
+sub _get_fortran_interface_by_f90aib {
+ my $path = shift();
+ my $command = sprintf(q{f90aib <'%s' 2>'%s'}, $path, File::Spec->devnull());
+ my $pipe = _open($command, '-|');
+ my @lines = readline($pipe);
+ close($pipe) || croak($ERR_MESS_OF{CLOSE_PIPE}, $command, $?);
+ \@lines;
+}
+
+# Generates Fortran interface block using internal code.
+sub _get_fortran_interface_by_internal_code {
+ my $path = shift();
+ my $handle = _open($path);
+ my @lines = _get_fortran_util()->extract_interface($handle);
+ close($handle);
+ \@lines;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# @out = @{ $obj->get_pre_process () };
+#
+# DESCRIPTION
+# This method invokes the pre-processor on the source file and returns a
+# reference to an array containing the lines of the pre-processed source on
+# success.
+# ------------------------------------------------------------------------------
+
+sub get_pre_process {
+ my $self = shift;
+
+ # Supported source files
+ my $lang = $self->lang ('TOOL_SRC_PP');
+ return unless $lang;
+
+ # List of include directories
+ my @inc = @{ $self->setting (qw/PATH INC/) };
+
+ # Build the pre-processor command according to file type
+ my %tool = %{ $self->setting ('TOOL') };
+ my %tool_src_pp = %{ $self->setting ('TOOL_SRC_PP', $lang) };
+
+ # The pre-processor command and its options
+ my @command = ($tool{$tool_src_pp{COMMAND}});
+ my @ppflags = split /\s+/, $self->get_setting ('TOOL', $tool_src_pp{FLAGS});
+
+ # List of defined macros, add "-D" in front of each macro
+ my @ppkeys = split /\s+/, $self->get_setting ('TOOL', $tool_src_pp{PPKEYS});
+ @ppkeys = map {($tool{$tool_src_pp{DEFINE}} . $_)} @ppkeys;
+
+ # Add "-I" in front of each include directories
+ @inc = map {($tool{$tool_src_pp{INCLUDE}} . $_)} @inc;
+
+ push @command, (@ppflags, @ppkeys, @inc, $self->base);
+
+ # Change to container directory of source file
+ my $old_cwd = $self->_chdir($self->dir());
+
+ # Execute the command, getting the output lines
+ my $verbose = $self->verbose;
+ my @outlines = &run_command (
+ \@command, METHOD => 'qx', PRINT => $verbose > 1, TIME => $verbose > 2,
+ );
+
+ # Change back to original directory
+ $self->_chdir($old_cwd);
+
+ return \@outlines;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rules = %{ $self->get_rules };
+#
+# DESCRIPTION
+# This method returns a reference to a hash in the following format:
+# $rules = {
+# target => {ACTION => action, DEP => [dependencies], ...},
+# ... => {...},
+# };
+# where the 1st rank keys are the available targets for building this source
+# file, the second rank keys are ACTION and DEP. The value of ACTION is the
+# action for building the target, which can be "COMPILE", "LOAD", "TOUCH",
+# "CP" or "AR". The value of DEP is a refernce to an array containing a list
+# of dependencies suitable for insertion into the Makefile.
+# ------------------------------------------------------------------------------
+
+sub get_rules {
+ my $self = shift;
+
+ my $rules;
+ my %outfile_ext = %{ $self->setting ('OUTFILE_EXT') };
+
+ if ($self->is_type_all (qw/SOURCE/)) {
+ # Source file
+ # --------------------------------------------------------------------------
+ # Determine whether the language of the source file is supported
+ my %tool_src = %{ $self->setting ('TOOL_SRC') };
+
+ return () unless $self->lang;
+
+ # Compile object
+ # --------------------------------------------------------------------------
+ if ($self->objbase) {
+ # Depends on the source file
+ my @dep = ($self->rule_src);
+
+ # Depends on the compiler flags flags-file
+ my @flags;
+ push @flags, ('FLAGS' )
+ if $self->flagsbase ('FLAGS' );
+ push @flags, ('PPKEYS')
+ if $self->flagsbase ('PPKEYS') and not $self->ppsrc;
+
+ push @dep, $self->flagsbase ($_) for (@flags);
+
+ # Source file dependencies
+ for my $name (sort keys %{ $self->dep }) {
+ # A Fortran 9X module, lower case object file name
+ if ($self->dep ($name) eq 'USE') {
+ (my $root = $name) =~ s/\.\w+$//;
+ push @dep, lc ($root) . $outfile_ext{OBJ};
+
+ # An include file
+ } elsif ($self->dep ($name) =~ /^(?:INC|H|INTERFACE)$/) {
+ push @dep, $name;
+ }
+ }
+
+ $rules->{$self->objbase} = {ACTION => 'COMPILE', DEP => \@dep};
+
+ # Touch flags-files
+ # ------------------------------------------------------------------------
+ for my $flag (@flags) {
+ next unless $self->flagsbase ($flag);
+
+ $rules->{$self->flagsbase ($flag)} = {
+ ACTION => 'TOUCH',
+ DEP => [
+ $self->flagsbase ($tool_src{$self->lang}{$flag}, -2),
+ ],
+ DEST => '$(FCM_FLAGSDIR)',
+ };
+ }
+ }
+
+ if ($self->exebase) {
+ # Link into an executable
+ # ------------------------------------------------------------------------
+ my @dep = ();
+ push @dep, $self->objbase if $self->objbase;
+ push @dep, $self->flagsbase ('LD' ) if $self->flagsbase ('LD' );
+ push @dep, $self->flagsbase ('LDFLAGS') if $self->flagsbase ('LDFLAGS');
+
+ # Depends on BLOCKDATA program units, for Fortran programs
+ my %blockdata = %{ $self->setting ('BLD_BLOCKDATA') };
+ my @blkobj = ();
+
+ if ($self->is_type_any (qw/FPP FORTRAN/) and keys %blockdata) {
+ # List of BLOCKDATA object files
+ if (exists $blockdata{$self->exebase}) {
+ @blkobj = split /\s+/, $blockdata{$self->exebase};
+
+ } elsif (exists $blockdata{''}) {
+ @blkobj = split /\s+/, $blockdata{''};
+ }
+
+ for my $name (@blkobj) {
+ (my $root = $name) =~ s/\.\w+$//;
+ $name = $root . $outfile_ext{OBJ};
+ push @dep, $root . $outfile_ext{DONE};
+ }
+ }
+
+ # Extra executable dependencies
+ my %exe_dep = %{ $self->setting ('BLD_DEP_EXE') };
+ if (keys %exe_dep) {
+ my @exe_deps;
+ if (exists $exe_dep{$self->exebase}) {
+ @exe_deps = split /\s+/, $exe_dep{$self->exebase};
+
+ } elsif (exists $exe_dep{''}) {
+ @exe_deps = $exe_dep{''} ? split (/\s+/, $exe_dep{''}) : ('');
+ }
+
+ my $pattern = '\\' . $outfile_ext{OBJ} . '$';
+
+ for my $name (@exe_deps) {
+ if ($name =~ /$pattern/) {
+ # Extra dependency is an object
+ (my $root = $name) =~ s/\.\w+$//;
+ push @dep, $root . $outfile_ext{DONE};
+
+ } else {
+ # Extra dependency is a sub-package
+ my $var;
+ if ($self->setting ('FCM_PCK_OBJECTS', $name)) {
+ # sub-package name contains unusual characters
+ $var = $self->setting ('FCM_PCK_OBJECTS', $name);
+
+ } else {
+ # sub-package name contains normal characters
+ $var = $name ? join ('__', ('OBJECTS', $name)) : 'OBJECTS';
+ }
+
+ push @dep, '$(' . $var . ')';
+ }
+ }
+ }
+
+ # Source file dependencies
+ for my $name (sort keys %{ $self->dep }) {
+ (my $root = $name) =~ s/\.\w+$//;
+
+ # Lowercase name for object dependency
+ $root = lc ($root) unless $self->dep ($name) =~ /^(?:INC|H)$/;
+
+ # Select "done" file extension
+ if ($self->dep ($name) =~ /^(?:INC|H)$/) {
+ push @dep, $name . $outfile_ext{IDONE};
+
+ } else {
+ push @dep, $root . $outfile_ext{DONE};
+ }
+ }
+
+ $rules->{$self->exebase} = {
+ ACTION => 'LOAD', DEP => \@dep, BLOCKDATA => \@blkobj,
+ };
+
+ # Touch Linker flags-file
+ # ------------------------------------------------------------------------
+ for my $flag (qw/LD LDFLAGS/) {
+ $rules->{$self->flagsbase ($flag)} = {
+ ACTION => 'TOUCH',
+ DEP => [$self->flagsbase ($flag, -2)],
+ DEST => '$(FCM_FLAGSDIR)',
+ };
+ }
+
+ }
+
+ if ($self->donebase) {
+ # Touch done file
+ # ------------------------------------------------------------------------
+ my @dep = ($self->objbase);
+
+ for my $name (sort keys %{ $self->dep }) {
+ (my $root = $name) =~ s/\.\w+$//;
+
+ # Lowercase name for object dependency
+ $root = lc ($root) unless $self->dep ($name) =~ /^(?:INC|H)$/;
+
+ # Select "done" file extension
+ if ($self->dep ($name) =~ /^(?:INC|H)$/) {
+ push @dep, $name . $outfile_ext{IDONE};
+
+ } else {
+ push @dep, $root . $outfile_ext{DONE};
+ }
+ }
+
+ $rules->{$self->donebase} = {
+ ACTION => 'TOUCH', DEP => \@dep, DEST => '$(FCM_DONEDIR)',
+ };
+ }
+
+ if ($self->interfacebase) {
+ # Interface target
+ # ------------------------------------------------------------------------
+ # Source file dependencies
+ my @dep = ();
+ for my $name (sort keys %{ $self->dep }) {
+ # Depends on Fortran 9X modules
+ push @dep, lc ($name) . $outfile_ext{OBJ}
+ if $self->dep ($name) eq 'USE';
+ }
+
+ $rules->{$self->interfacebase} = {ACTION => '', DEP => \@dep};
+ }
+
+ } elsif ($self->is_type_all ('INCLUDE')) {
+ # Copy include target
+ # --------------------------------------------------------------------------
+ my @dep = ($self->rule_src);
+
+ for my $name (sort keys %{ $self->dep }) {
+ # A Fortran 9X module, lower case object file name
+ if ($self->dep ($name) eq 'USE') {
+ (my $root = $name) =~ s/\.\w+$//;
+ push @dep, lc ($root) . $outfile_ext{OBJ};
+
+ # An include file
+ } elsif ($self->dep ($name) =~ /^(?:INC|H|INTERFACE)$/) {
+ push @dep, $name;
+ }
+ }
+
+ $rules->{$self->curbase} = {
+ ACTION => 'CP', DEP => \@dep, DEST => '$(FCM_INCDIR)',
+ };
+
+ # Touch IDONE file
+ # --------------------------------------------------------------------------
+ if ($self->donebase) {
+ my @dep = ($self->rule_src);
+
+ for my $name (sort keys %{ $self->dep }) {
+ (my $root = $name) =~ s/\.\w+$//;
+
+ # Lowercase name for object dependency
+ $root = lc ($root) unless $self->dep ($name) =~ /^(?:INC|H)$/;
+
+ # Select "done" file extension
+ if ($self->dep ($name) =~ /^(?:INC|H)$/) {
+ push @dep, $name . $outfile_ext{IDONE};
+
+ } else {
+ push @dep, $root . $outfile_ext{DONE};
+ }
+ }
+
+ $rules->{$self->donebase} = {
+ ACTION => 'TOUCH', DEP => \@dep, DEST => '$(FCM_DONEDIR)',
+ };
+ }
+
+ } elsif ($self->is_type_any (qw/EXE SCRIPT/)) {
+ # Copy executable file
+ # --------------------------------------------------------------------------
+ my @dep = ($self->rule_src);
+
+ # Depends on dummy copy file, if file is an "always build type"
+ push @dep, $self->setting (qw/BLD_CPDUMMY/)
+ if $self->is_type_any (split (
+ /$FCM1::Config::DELIMITER_LIST/, $self->setting ('BLD_TYPE_ALWAYS_BUILD')
+ ));
+
+ # Depends on other executable files
+ for my $name (sort keys %{ $self->dep }) {
+ push @dep, $name if $self->dep ($name) eq 'EXE';
+ }
+
+ $rules->{$self->curbase} = {
+ ACTION => 'CP', DEP => \@dep, DEST => '$(FCM_BINDIR)',
+ };
+
+ } elsif (@{ $self->children }) {
+ # Targets for top level and package flags files and dummy dependencies
+ # --------------------------------------------------------------------------
+ my %tool_src = %{ $self->setting ('TOOL_SRC') };
+ my %flags_tool = (LD => '', LDFLAGS => '');
+
+ for my $key (keys %tool_src) {
+ $flags_tool{$tool_src{$key}{FLAGS}} = $tool_src{$key}{COMMAND}
+ if exists $tool_src{$key}{FLAGS};
+
+ $flags_tool{$tool_src{$key}{PPKEYS}} = ''
+ if exists $tool_src{$key}{PPKEYS};
+ }
+
+ for my $name (sort keys %flags_tool) {
+ my @dep = $self->pkgname eq '' ? () : $self->flagsbase ($name, -2);
+ push @dep, $self->flagsbase ($flags_tool{$name})
+ if $self->pkgname eq '' and $flags_tool{$name};
+
+ $rules->{$self->flagsbase ($flags_tool{$name})} = {
+ ACTION => 'TOUCH',
+ DEST => '$(FCM_FLAGSDIR)',
+ } if $self->pkgname eq '' and $flags_tool{$name};
+
+ $rules->{$self->flagsbase ($name)} = {
+ ACTION => 'TOUCH',
+ DEP => \@dep,
+ DEST => '$(FCM_FLAGSDIR)',
+ };
+ }
+
+ # Package object and library
+ # --------------------------------------------------------------------------
+ {
+ my @dep;
+ # Add objects from children
+ for my $child (sort {$a->pkgname cmp $b->pkgname} @{ $self->children }) {
+ push @dep, $child->rule_obj_var (1)
+ if $child->libbase and $child->rules ($child->libbase);
+ push @dep, $child->objbase
+ if $child->cursrc and $child->objbase and
+ not $child->is_type_any (qw/PROGRAM BLOCKDATA/);
+ }
+
+ if (@dep) {
+ $rules->{$self->libbase} = {ACTION => 'AR', DEP => \@dep};
+ }
+ }
+
+ # Package data files
+ # --------------------------------------------------------------------------
+ {
+ my @dep;
+ for my $child (@{ $self->children }) {
+ push @dep, $child->rule_src if $child->src and not $child->type;
+ }
+
+ if (@dep) {
+ push @dep, $self->setting (qw/BLD_CPDUMMY/);
+ $rules->{$self->etcbase} = {
+ ACTION => 'CP_DATA', DEP => \@dep, DEST => '$(FCM_ETCDIR)',
+ };
+ }
+ }
+ }
+
+ return $rules;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $value = $obj->get_setting ($setting[, @prefix]);
+#
+# DESCRIPTION
+# This method gets the correct $setting for the current source by following
+# its package name. If @prefix is set, get the setting with the given prefix.
+# ------------------------------------------------------------------------------
+
+sub get_setting {
+ my ($self, $setting, @prefix) = @_;
+
+ my $val;
+ for my $name (reverse @{ $self->pkgnames }) {
+ my @names = split /__/, $name;
+ $val = $self->setting ($setting, join ('__', (@prefix, @names)));
+
+ $val = $self->setting ($setting, join ('__', (@prefix, @names)))
+ if (not defined $val) and @names and $names[-1] =~ s/\.[^\.]+$//;
+ last if defined $val;
+ }
+
+ return $val;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $type = $self->get_type();
+#
+# DESCRIPTION
+# This method determines whether the source is a type known to the
+# build system. If so, it returns the type flags delimited by "::".
+# ------------------------------------------------------------------------------
+
+sub get_type {
+ my $self = shift();
+ my @IGNORE_LIST
+ = split(/$FCM1::Config::DELIMITER_LIST/, $self->setting('INFILE_IGNORE'));
+ if (grep {$self->curbase() eq $_} @IGNORE_LIST) {
+ return q{};
+ }
+ # User defined
+ my $type = $self->setting('BLD_TYPE', $self->pkgname());
+ # Extension
+ if (!defined($type)) {
+ my $ext = $self->curext() ? substr($self->curext(), 1) : q{};
+ $type = $self->setting('INFILE_EXT', $ext);
+ }
+ # Pattern of name
+ if (!defined($type)) {
+ my %NAME_PATTERN_TO_TYPE_HASH = %{$self->setting('INFILE_PAT')};
+ PATTERN:
+ while (my ($pattern, $value) = each(%NAME_PATTERN_TO_TYPE_HASH)) {
+ if ($self->curbase() =~ $pattern) {
+ $type = $value;
+ last PATTERN;
+ }
+ }
+ }
+ # Pattern of #! line
+ if (!defined($type) && -s $self->cursrc() && -T _) {
+ my $handle = _open($self->cursrc());
+ my $line = readline($handle);
+ close($handle);
+ my %SHEBANG_PATTERN_TO_TYPE_HASH = %{$self->setting('INFILE_TXT')};
+ PATTERN:
+ while (my ($pattern, $value) = each(%SHEBANG_PATTERN_TO_TYPE_HASH)) {
+ if ($line =~ qr{^\#!.*$pattern}msx) {
+ $type = $value;
+ last PATTERN;
+ }
+ }
+ }
+ if (!$type) {
+ return $type;
+ }
+ # Extra type information for selected file types
+ my %EXTRA_FOR = (
+ qr{\b (?:FORTRAN|FPP) \b}msx => \&_get_type_extra_for_fortran,
+ qr{\b C \b}msx => \&_get_type_extra_for_c,
+ );
+ EXTRA:
+ while (my ($key, $code_ref) = each(%EXTRA_FOR)) {
+ if ($type =~ $key) {
+ my $handle = _open($self->cursrc());
+ LINE:
+ while (my $line = readline($handle)) {
+ my $extra = $code_ref->($line);
+ if ($extra) {
+ $type .= $FCM1::Config::DELIMITER . $extra;
+ last LINE;
+ }
+ }
+ close($handle);
+ last EXTRA;
+ }
+ }
+ return $type;
+}
+
+sub _get_type_extra_for_fortran {
+ my ($match) = $_[0] =~ qr{\A \s* (PROGRAM|MODULE|BLOCK\s*DATA) \b}imsx;
+ if (!$match) {
+ return;
+ }
+ $match =~ s{\s}{}g;
+ uc($match)
+}
+
+sub _get_type_extra_for_c {
+ ($_[0] =~ qr{int\s+main\s*\(}msx) ? 'PROGRAM' : undef;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $flag = $obj->is_in_package ($name);
+#
+# DESCRIPTION
+# This method returns true if current package is in the package $name.
+# ------------------------------------------------------------------------------
+
+sub is_in_package {
+ my ($self, $name) = @_;
+
+ my $return = 0;
+ for (@{ $self->pkgnames }) {
+ next unless /^$name(?:\.\w+)?$/;
+ $return = 1;
+ last;
+ }
+
+ return $return;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $flag = $obj->is_type_all ($arg, ...);
+# $flag = $obj->is_type_any ($arg, ...);
+#
+# DESCRIPTION
+# This method returns a flag for the following:
+# is_type_all - does type match all of the arguments?
+# is_type_any - does type match any of the arguments?
+# ------------------------------------------------------------------------------
+
+for my $name ('all', 'any') {
+ no strict 'refs';
+
+ my $subname = 'is_type_' . $name;
+
+ *$subname = sub {
+ my ($self, @intypes) = @_;
+
+ my $rc = 0;
+ if ($self->type) {
+ my %types = map {($_, 1)} split /$FCM1::Config::DELIMITER/, $self->type;
+
+ for my $intype (@intypes) {
+ $rc = exists $types{$intype};
+ last if ($name eq 'all' and not $rc) or ($name eq 'any' and $rc);
+ }
+ }
+
+ return $rc;
+ }
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $string = $obj->rule_obj_var ([$read]);
+#
+# DESCRIPTION
+# This method returns a string containing the make rule object variable for
+# the current package. If $read is set, return $($string)
+# ------------------------------------------------------------------------------
+
+sub rule_obj_var {
+ my ($self, $read) = @_;
+
+ my $return;
+ if ($self->setting ('FCM_PCK_OBJECTS', $self->pkgname)) {
+ # Package name registered in unusual list
+ $return = $self->setting ('FCM_PCK_OBJECTS', $self->pkgname);
+
+ } else {
+ # Package name not registered in unusual list
+ $return = $self->pkgname
+ ? join ('__', ('OBJECTS', $self->pkgname)) : 'OBJECTS';
+ }
+
+ $return = $read ? '$(' . $return . ')' : $return;
+
+ return $return;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $string = $obj->rule_src ();
+#
+# DESCRIPTION
+# This method returns a string containing the location of the source file
+# relative to the build root. This string will be suitable for use in a
+# "Make" rule file for FCM.
+# ------------------------------------------------------------------------------
+
+sub rule_src {
+ my $self = shift;
+
+ my $return = $self->cursrc;
+ LABEL: for my $name (qw/SRC PPSRC/) {
+ for my $i (0 .. @{ $self->setting ('PATH', $name) } - 1) {
+ my $dir = $self->setting ('PATH', $name)->[$i];
+ next unless index ($self->cursrc, $dir) == 0;
+
+ $return = File::Spec->catfile (
+ '$(FCM_' . $name . 'DIR' . ($i ? $i : '') . ')',
+ File::Spec->abs2rel ($self->cursrc, $dir),
+ );
+ last LABEL;
+ }
+ }
+
+ return $return;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = $obj->write_lib_dep_excl ();
+#
+# DESCRIPTION
+# This method writes a set of exclude dependency configurations for the
+# library of this package.
+# ------------------------------------------------------------------------------
+
+sub write_lib_dep_excl {
+ my $self = shift();
+ if (!find_file_in_path($self->libbase(), $self->setting(qw/PATH LIB/))) {
+ return 0;
+ }
+
+ my $ETC_DIR = $self->setting(qw/PATH ETC/)->[0];
+ my $CFG_EXT = $self->setting(qw/OUTFILE_EXT CFG/);
+ my $LABEL_OF_EXCL_DEP = $self->cfglabel('BLD_DEP_EXCL');
+ my @SETTINGS = (
+ #dependency #source file type list #dependency name function
+ ['H' , [qw{INCLUDE CPP }], sub {$_[0]->base()} ],
+ ['INTERFACE', [qw{INCLUDE INTERFACE }], sub {$_[0]->base()} ],
+ ['INC' , [qw{INCLUDE }], sub {$_[0]->base()} ],
+ ['USE' , [qw{SOURCE FORTRAN MODULE}], sub {$_[0]->root()} ],
+ ['INTERFACE', [qw{SOURCE FORTRAN }], sub {$_[0]->interfacebase()}],
+ ['OBJ' , [qw{SOURCE }], sub {$_[0]->root()} ],
+ );
+
+ my $cfg = FCM1::CfgFile->new();
+ my @stack = ($self);
+ NODE:
+ while (my $node = pop(@stack)) {
+ # Is a directory
+ if (@{$node->children()}) {
+ push(@stack, reverse(@{$node->children()}));
+ next NODE;
+ }
+ # Is a typed file
+ if (
+ $node->cursrc()
+ && $node->type()
+ && !$node->is_type_any(qw{PROGRAM BLOCKDATA})
+ ) {
+ for (@SETTINGS) {
+ my ($key, $type_list_ref, $name_func_ref) = @{$_};
+ my $name = $name_func_ref->($node);
+ if ($name && $node->is_type_all(@{$type_list_ref})) {
+ push(
+ @{$cfg->lines()},
+ FCM1::CfgLine->new(
+ label => $LABEL_OF_EXCL_DEP,
+ value => $key . $FCM1::Config::DELIMITER . $name,
+ ),
+ );
+ next NODE;
+ }
+ }
+ }
+ }
+
+ # Write to configuration file
+ $cfg->print_cfg(
+ File::Spec->catfile($ETC_DIR, $self->libbase('lib', $CFG_EXT)),
+ );
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $string = $obj->write_rules ();
+#
+# DESCRIPTION
+# This method returns a string containing the "Make" rules for building the
+# source file.
+# ------------------------------------------------------------------------------
+
+sub write_rules {
+ my $self = shift;
+ my $mk = '';
+
+ for my $target (sort keys %{ $self->rules }) {
+ my $rule = $self->rules ($target);
+ next unless defined ($rule->{ACTION});
+
+ if ($rule->{ACTION} eq 'AR') {
+ my $var = $self->rule_obj_var;
+ $mk .= ($var eq 'OBJECTS' ? 'export ' : '') . $var . ' =';
+ $mk .= ' ' . join (' ', @{ $rule->{DEP} });
+ $mk .= "\n\n";
+ }
+
+ $mk .= $target . ':';
+
+ if ($rule->{ACTION} eq 'AR') {
+ $mk .= ' ' . $self->rule_obj_var (1);
+
+ } else {
+ for my $dep (@{ $rule->{DEP} }) {
+ $mk .= ' ' . $dep;
+ }
+ }
+
+ $mk .= "\n";
+
+ if (exists $rule->{ACTION}) {
+ if ($rule->{ACTION} eq 'AR') {
+ $mk .= "\t" . 'fcm_internal archive $@ $^' . "\n";
+
+ } elsif ($rule->{ACTION} eq 'CP') {
+ $mk .= "\t" . 'cp $< ' . $rule->{DEST} . "\n";
+ $mk .= "\t" . 'chmod u+w ' .
+ File::Spec->catfile ($rule->{DEST}, '$@') . "\n";
+
+ } elsif ($rule->{ACTION} eq 'CP_DATA') {
+ $mk .= "\t" . 'cp $^ ' . $rule->{DEST} . "\n";
+ $mk .= "\t" . 'touch ' .
+ File::Spec->catfile ($rule->{DEST}, '$@') . "\n";
+
+ } elsif ($rule->{ACTION} eq 'COMPILE') {
+ if ($self->lang) {
+ $mk .= "\t" . 'fcm_internal compile:' . substr ($self->lang, 0, 1) .
+ ' ' . $self->pkgnames->[-2] . ' $< $@';
+ $mk .= ' 1' if ($self->flagsbase ('PPKEYS') and not $self->ppsrc);
+ $mk .= "\n";
+ }
+
+ } elsif ($rule->{ACTION} eq 'LOAD') {
+ if ($self->lang) {
+ $mk .= "\t" . 'fcm_internal load:' . substr ($self->lang, 0, 1) .
+ ' ' . $self->pkgnames->[-2] . ' $< $@';
+ $mk .= ' ' . join (' ', @{ $rule->{BLOCKDATA} })
+ if @{ $rule->{BLOCKDATA} };
+ $mk .= "\n";
+ }
+
+ } elsif ($rule->{ACTION} eq 'TOUCH') {
+ $mk .= "\t" . 'touch ' .
+ File::Spec->catfile ($rule->{DEST}, '$@') . "\n";
+ }
+ }
+
+ $mk .= "\n";
+ }
+
+ return $mk;
+}
+
+# Wraps "chdir". Returns old directory.
+sub _chdir {
+ my ($self, $dir) = @_;
+ my $old_cwd = cwd();
+ $self->_event('CHDIR', $dir);
+ chdir($dir) || croak(sprintf($ERR_MESS_OF{CHDIR}, $dir));
+ $old_cwd;
+}
+
+# Wraps an event.
+sub _event {
+ my ($self, $key, @args) = @_;
+ my ($format, $level) = @{$EVENT_SETTING_OF{$key}};
+ $level ||= 1;
+ if ($self->verbose() >= $level) {
+ printf($format . ".\n", @args);
+ }
+}
+
+# Wraps "open".
+sub _open {
+ my ($path, $mode) = @_;
+ $mode ||= '<';
+ open(my $handle, $mode, $path) || croak(sprintf($ERR_MESS_OF{OPEN}, $path, $!));
+ $handle;
+}
+
+# ------------------------------------------------------------------------------
+
+1;
+
+__END__
diff --git a/lib/FCM1/BuildTask.pm b/lib/FCM1/BuildTask.pm
new file mode 100644
index 0000000..6b9d0ba
--- /dev/null
+++ b/lib/FCM1/BuildTask.pm
@@ -0,0 +1,353 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# NAME
+# FCM1::BuildTask
+#
+# DESCRIPTION
+# This class hosts information of a build task in the FCM build system.
+#
+# ------------------------------------------------------------------------------
+
+package FCM1::BuildTask;
+ at ISA = qw(FCM1::Base);
+
+# Standard pragma
+use strict;
+use warnings;
+
+# Standard modules
+use Carp;
+use File::Compare;
+use File::Copy;
+use File::Basename;
+use File::Path;
+use File::Spec::Functions;
+
+# FCM component modules
+use FCM1::Base;
+use FCM1::Timer;
+use FCM1::Util;
+
+# List of property methods for this class
+my @scalar_properties = (
+ 'actiontype', # type of action
+ 'dependency', # list of dependencies for this target
+ 'srcfile', # reference to input FCM1::BuildSrc instance
+ 'output', # output file
+ 'outputmtime', # output file modification time
+ 'target', # target name for this task
+ 'targetpath', # search path for the target
+);
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $obj = FCM1::BuildTask->new (%args);
+#
+# DESCRIPTION
+# This method constructs a new instance of the FCM1::BuildTask class. See
+# above for allowed list of properties. (KEYS should be in uppercase.)
+# ------------------------------------------------------------------------------
+
+sub new {
+ my $this = shift;
+ my %args = @_;
+ my $class = ref $this || $this;
+
+ my $self = FCM1::Base->new (%args);
+
+ bless $self, $class;
+
+ for my $name (@scalar_properties) {
+ $self->{$name} = exists $args{uc ($name)} ? $args{uc ($name)} : undef;
+ }
+
+ return $self;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $value = $obj->X;
+# $obj->X ($value);
+#
+# DESCRIPTION
+# Details of these properties are explained in @scalar_properties.
+# ------------------------------------------------------------------------------
+
+for my $name (@scalar_properties) {
+ no strict 'refs';
+
+ *$name = sub {
+ my $self = shift;
+
+ # Argument specified, set property to specified argument
+ if (@_) {
+ $self->{$name} = $_[0];
+
+ if ($name eq 'output') {
+ $self->{outputmtime} = $_[0] ? (stat $_[0]) [9] : undef;
+ }
+ }
+
+ # Default value for property
+ if (not defined $self->{$name}) {
+ if ($name eq 'dependency' or $name eq 'targetpath') {
+ # Reference to an array
+ $self->{$name} = [];
+ }
+ }
+
+ return $self->{$name};
+ }
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = $obj->action (TASKLIST => \%tasklist);
+#
+# DESCRIPTION
+# This method performs the task action and sets the output accordingly. The
+# argument TASKLIST must be a reference to a hash containing the other tasks
+# of the build, which this task may depend on. The keys of the hash must the
+# name of the target names of the tasks, and the values of the hash must be
+# the references to the corresponding FCM1::BuildTask instances. The method
+# returns true if the task has been performed to create a new version of the
+# target.
+# ------------------------------------------------------------------------------
+
+sub action {
+ my $self = shift;
+ my %args = @_;
+ my $tasklist = exists $args{TASKLIST} ? $args{TASKLIST} : {};
+
+ return unless $self->actiontype;
+
+ my $uptodate = 1;
+ my $dep_uptodate = 1;
+
+ # Check if dependencies are up to date
+ # ----------------------------------------------------------------------------
+ for my $depend (@{ $self->dependency }) {
+ if (exists $tasklist->{$depend}) {
+ if (not $tasklist->{$depend}->output) {
+ # Dependency task output is not set, performs its task action
+ if ($tasklist->{$depend}->action (TASKLIST => $tasklist)) {
+ $uptodate = 0;
+ $dep_uptodate = 0;
+ }
+ }
+
+ } elsif ($self->verbose > 1) {
+ w_report 'Warning: Task for "', $depend,
+ '" does not exist, may be required by ', $self->target;
+ }
+ }
+
+ # Check if the target exists in the search path
+ # ----------------------------------------------------------------------------
+ if (@{ $self->targetpath }) {
+ my $output = find_file_in_path ($self->target, $self->targetpath);
+ $self->output ($output) if $output;
+ }
+
+ # Target is out of date if it does not exist
+ if ($uptodate) {
+ $uptodate = 0 if not $self->output;
+ }
+
+ # Check if current target is older than its dependencies
+ # ----------------------------------------------------------------------------
+ if ($uptodate) {
+ for my $depend (@{ $self->dependency }) {
+ next unless exists $tasklist->{$depend};
+
+ if ($tasklist->{$depend}->outputmtime > $self->outputmtime) {
+ $uptodate = 0;
+ $dep_uptodate = 0;
+ }
+ }
+
+ if ($uptodate and ref $self->srcfile) {
+ $uptodate = 0 if $self->srcfile->mtime > $self->outputmtime;
+ }
+ }
+
+ if ($uptodate) {
+ # Current target and its dependencies are up to date
+ # --------------------------------------------------------------------------
+ if ($self->actiontype eq 'PP') {
+ # "done" file up to date, set name of pre-processed source file
+ # ------------------------------------------------------------------------
+ my $base = $self->srcfile->root . lc ($self->srcfile->ext);
+ my @pknames = split '__', (@{ $self->srcfile->pkgnames })[-2];
+ my @path = map {
+ catfile ($_, @pknames);
+ } @{ $self->setting (qw/PATH PPSRC/) };
+ my $oldfile = find_file_in_path ($base, \@path);
+ $self->srcfile->ppsrc ($oldfile);
+ }
+
+ } else {
+ # Perform action is not up to date
+ # --------------------------------------------------------------------------
+ # (For GENINTERFACE and PP, perform action if "done" file not up to date)
+ my $new_output = @{ $self->targetpath }
+ ? catfile ($self->targetpath->[0], $self->target)
+ : $self->target;
+
+ # Create destination container directory if necessary
+ my $destdir = dirname $new_output;
+
+ if (not -d $destdir) {
+ print 'Make directory: ', $destdir, "\n" if $self->verbose > 2;
+ mkpath $destdir;
+ }
+
+ # List of actions
+ if ($self->actiontype eq 'UPDATE') {
+ # Action is UPDATE: Update file
+ # ------------------------------------------------------------------------
+ print 'Update: ', $new_output, "\n" if $self->verbose > 2;
+ touch_file $new_output
+ or croak 'Unable to update "', $new_output, '", abort';
+ $self->output ($new_output);
+
+ } elsif ($self->actiontype eq 'COPY') {
+ # Action is COPY: copy file to destination if necessary
+ # ------------------------------------------------------------------------
+ my $copy_required = ($dep_uptodate and $self->output and -f $self->output)
+ ? compare ($self->output, $self->srcfile->src)
+ : 1;
+
+ if ($copy_required) {
+ # Set up copy command
+ my $srcfile = $self->srcfile->src;
+ my $destfile = catfile ($destdir, basename($srcfile));
+ print 'Copy: ', $srcfile, "\n", ' to: ', $destfile, "\n"
+ if $self->verbose > 2;
+ © ($srcfile, $destfile)
+ or die $srcfile, ': copy to ', $destfile, ' failed (', $!, '), abort';
+ chmod (((stat ($srcfile))[2] & 07777), $destfile);
+
+ $self->output ($new_output);
+
+ } else {
+ $uptodate = 1;
+ }
+
+ } elsif ($self->actiontype eq 'PP' or $self->actiontype eq 'GENINTERFACE') {
+ # Action is PP or GENINTERFACE: process file
+ # ------------------------------------------------------------------------
+ my ($newlines, $base, @path);
+
+ if ($self->actiontype eq 'PP') {
+ # Invoke the pre-processor on the source file
+ # ----------------------------------------------------------------------
+ # Get lines in the pre-processed source
+ $newlines = $self->srcfile->get_pre_process;
+ $base = $self->srcfile->root . lc ($self->srcfile->ext);
+
+ # Get search path for the existing pre-processed file
+ my @pknames = split '__', (@{ $self->srcfile->pkgnames })[-2];
+ @path = map {
+ catfile ($_, @pknames);
+ } @{ $self->setting (qw/PATH PPSRC/) };
+
+ } else { # if ($self->actiontype eq 'GENINTERFACE')
+ # Invoke the interface generator
+ # ----------------------------------------------------------------------
+ # Get new interface lines
+ $newlines = $self->srcfile->get_fortran_interface;
+
+ # Get search path for the existing interface file
+ $base = $self->srcfile->interfacebase;
+ @path = @{ $self->setting (qw/PATH INC/) },
+ }
+
+
+ # If pre-processed or interface file exists,
+ # compare its content with new lines to see if it has been updated
+ my $update_required = 1;
+ my $oldfile = find_file_in_path ($base, \@path);
+
+ if ($oldfile and -f $oldfile) {
+ # Read old file
+ open FILE, '<', $oldfile;
+ my @oldlines = readline 'FILE';
+ close FILE;
+
+ # Compare old contents and new contents
+ if (@oldlines eq @$newlines) {
+ $update_required = grep {
+ $oldlines[$_] ne $newlines->[$_];
+ } (0 .. $#oldlines);
+ }
+ }
+
+ if ($update_required) {
+ # Update the pre-processed source or interface file
+ # ----------------------------------------------------------------------
+ # Determine container directory of the pre-processed or interface file
+ my $newfile = @path ? catfile ($path[0], $base) : $base;
+
+ # Create the container directory if necessary
+ if (not -d $path[0]) {
+ print 'Make directory: ', $path[0], "\n"
+ if $self->verbose > 1;
+ mkpath $path[0];
+ }
+
+ # Update the pre-processor or interface file
+ open FILE, '>', $newfile
+ or croak 'Cannot write to "', $newfile, '" (', $!, '), abort';
+ print FILE @$newlines;
+ close FILE
+ or croak 'Cannot write to "', $newfile, '" (', $!, '), abort';
+ print 'Generated: ', $newfile, "\n" if $self->verbose > 1;
+
+ # Set the name of the pre-processed file
+ $self->srcfile->ppsrc ($newfile) if $self->actiontype eq 'PP';
+
+ } else {
+ # Content in pre-processed source or interface file is up to date
+ # ----------------------------------------------------------------------
+ $uptodate = 1;
+
+ # Set the name of the pre-processed file
+ $self->srcfile->ppsrc ($oldfile) if $self->actiontype eq 'PP';
+ }
+
+ # Update the "done" file
+ print 'Update: ', $new_output, "\n" if $self->verbose > 2;
+ touch_file $new_output
+ or croak 'Unable to update "', $new_output, '", abort';
+ $self->output ($new_output);
+
+ } else {
+ carp 'Action type "', $self->actiontype, "' not supported";
+ }
+ }
+
+ return not $uptodate;
+}
+
+# ------------------------------------------------------------------------------
+
+1;
+
+__END__
diff --git a/lib/FCM1/CfgFile.pm b/lib/FCM1/CfgFile.pm
new file mode 100644
index 0000000..8f947f8
--- /dev/null
+++ b/lib/FCM1/CfgFile.pm
@@ -0,0 +1,597 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# NAME
+# FCM1::CfgFile
+#
+# DESCRIPTION
+# This class is used for reading and writing FCM config files. A FCM config
+# file is a line-based text file that provides information on how to perform
+# a particular task using the FCM system.
+#
+# ------------------------------------------------------------------------------
+
+package FCM1::CfgFile;
+ at ISA = qw(FCM1::Base);
+
+# Standard pragma
+use warnings;
+use strict;
+
+# Standard modules
+use Carp;
+use File::Basename;
+use File::Path;
+use File::Spec;
+
+# FCM component modules
+use FCM1::Base;
+use FCM1::CfgLine;
+use FCM1::Config;
+use FCM1::Keyword;
+use FCM1::Util;
+
+# List of property methods for this class
+my @scalar_properties = (
+ 'actual_src', # actual source of configuration file
+ 'lines', # list of lines, FCM1::CfgLine objects
+ 'pegrev', # peg revision of configuration file
+ 'src', # source of configuration file
+ 'type', # type of configuration file
+ 'version', # version of configuration file
+);
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $obj = FCM1::CfgFile->new (%args);
+#
+# DESCRIPTION
+# This method constructs a new instance of the FCM1::CfgFile class. See above
+# for allowed list of properties. (KEYS should be in uppercase.)
+# ------------------------------------------------------------------------------
+
+sub new {
+ my $this = shift;
+ my %args = @_;
+ my $class = ref $this || $this;
+
+ my $self = FCM1::Base->new (%args);
+
+ bless $self, $class;
+
+ for (@scalar_properties) {
+ $self->{$_} = exists $args{uc ($_)} ? $args{uc ($_)} : undef;
+ }
+
+ return $self;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $value = $obj->X;
+# $obj->X ($value);
+#
+# DESCRIPTION
+# Details of these properties are explained in @scalar_properties.
+# ------------------------------------------------------------------------------
+
+for my $name (@scalar_properties) {
+ no strict 'refs';
+
+ *$name = sub {
+ my $self = shift;
+
+ if (@_) {
+ $self->{$name} = $_[0];
+ }
+
+ if (not defined $self->{$name}) {
+ if ($name eq 'lines') {
+ $self->{$name} = [];
+ }
+ }
+
+ return $self->{$name};
+ }
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $mtime = $obj->mtime ();
+#
+# DESCRIPTION
+# This method returns the modified time of the configuration file source.
+# ------------------------------------------------------------------------------
+
+sub mtime {
+ my $self = shift;
+ my $mtime = undef;
+
+ if (-f $self->src) {
+ $mtime = (stat $self->src)[9];
+ }
+
+ return $mtime;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $read = $obj->read_cfg($is_for_inheritance);
+#
+# DESCRIPTION
+# This method reads the current configuration file. It returns the number of
+# lines read from the config file, or "undef" if it fails. The result is
+# placed in the LINES array of the current instance, and can be accessed via
+# the "lines" method.
+# ------------------------------------------------------------------------------
+
+sub read_cfg {
+ my ($self, $is_for_inheritance) = @_;
+
+ my @lines = $self->_get_cfg_lines($is_for_inheritance);
+
+ # List of CFG types that need INC declarations expansion
+ my %exp_inc = ();
+ for (split (/$FCM1::Config::DELIMITER_LIST/, $self->setting ('CFG_EXP_INC'))) {
+ $exp_inc{uc ($_)} = 1;
+ }
+
+ # List of CFG labels that are reserved keywords
+ my %cfg_keywords = ();
+ for (split (/$FCM1::Config::DELIMITER_LIST/, $self->setting ('CFG_KEYWORD'))) {
+ $cfg_keywords{$self->cfglabel ($_)} = 1;
+ }
+
+ # Loop each line, to separate lines into label : value pairs
+ my $cont = undef;
+ my $here = undef;
+ for my $line_num (1 .. @lines) {
+ my $line = $lines[$line_num - 1];
+ chomp $line;
+
+ my $label = '';
+ my $value = '';
+ my $comment = '';
+
+ # If this line is a continuation, set $start to point to the line that
+ # starts this continuation. Otherwise, set $start to undef
+ my $start = defined ($cont) ? $self->lines->[$cont] : undef;
+ my $warning = undef;
+
+ if ($line =~ /^(\s*#.*)$/) { # comment line
+ $comment = $1;
+
+ } elsif ($line =~ /\S/) { # non-blank line
+ if (defined $cont) {
+ # Previous line has a continuation mark
+ $value = $line;
+
+ # Separate value and comment
+ if ($value =~ s/((?:\s+|^)#\s+.*)$//) {
+ $comment = $1;
+ }
+
+ # Remove leading spaces
+ $value =~ s/^\s*\\?//;
+
+ # Expand environment variables
+ my $warn;
+ ($value, $warn) = $self->_expand_variable ($value, 1) if $value;
+ $warning .= ($warning ? ', ' : '') . $warn if $warn;
+
+ # Expand internal variables
+ ($value, $warn) = $self->_expand_variable ($value, 0) if $value;
+ $warning .= ($warning ? ', ' : '') . $warn if $warn;
+
+ # Get "line" that begins the current continuation
+ my $v = $start->value . $value;
+ $v =~ s/\\$//;
+ $start->value ($v);
+
+ } else {
+ # Previous line does not have a continuation mark
+ if ($line =~ /^\s*(\S+)(?:\s+(.*))?$/) {
+ # Check line contains a valid label:value pair
+ $label = $1;
+ $value = defined ($2) ? $2 : '';
+
+ # Separate value and comment
+ if ($value =~ s/((?:\s+|^)#\s+.*)$//) {
+ $comment = $1;
+ }
+
+ # Remove trailing spaces
+ $value =~ s/\s+$//;
+
+ # Value begins with $HERE?
+ $here = ($value =~ /\$\{?HERE\}?(?:[^A-Z_]|$)/);
+
+ # Expand environment variables
+ my $warn;
+ ($value, $warn) = $self->_expand_variable ($value, 1) if $value;
+ $warning .= ($warning ? ', ' : '') . $warn if $warn;
+
+ # Expand internal variables
+ ($value, $warn) = $self->_expand_variable ($value, 0) if $value;
+ $warning .= ($warning ? ', ' : '') . $warn if $warn;
+ }
+ }
+
+ # Determine whether current line ends with a continuation mark
+ if ($value =~ s/\\$//) {
+ $cont = scalar (@{ $self->lines }) unless defined $cont;
+
+ } else {
+ $cont = undef;
+ }
+ }
+
+ if (exists $exp_inc{uc ($self->type)} and
+ uc ($start ? $start->label : $label) eq $self->cfglabel ('INC') and
+ not defined $cont) {
+ # Current configuration file requires expansion of INC declarations
+ # The start/current line is an INC declaration
+ # The current line is not a continuation or is the end of the continuation
+
+ # Get lines from an "include" configuration file
+ my $src = ($start ? $start->value : $value);
+ $src .= '@' . $self->pegrev if $here and $self->pegrev;
+
+ if ($src) {
+ # Invoke a new instance to read the source
+ my $cfg = FCM1::CfgFile->new (
+ SRC => expand_tilde ($src), TYPE => $self->type,
+ );
+
+ $cfg->read_cfg;
+
+ # Add lines to the lines array in the current configuration file
+ $comment = 'INC ' . $src . ' ';
+ push @{$self->lines}, FCM1::CfgLine->new (
+ comment => $comment . '# Start',
+ number => ($start ? $start->number : $line_num),
+ src => $self->actual_src,
+ warning => $warning,
+ );
+ push @{ $self->lines }, @{ $cfg->lines };
+ push @{$self->lines}, FCM1::CfgLine->new (
+ comment => $comment . '# End',
+ src => $self->actual_src,
+ );
+
+ } else {
+ push @{$self->lines}, FCM1::CfgLine->new (
+ number => $line_num,
+ src => $self->actual_src,
+ warning => 'empty INC declaration.'
+ );
+ }
+
+ } else {
+ # Push label:value pair into lines array
+ push @{$self->lines}, FCM1::CfgLine->new (
+ label => $label,
+ value => ($label ? $value : ''),
+ comment => $comment,
+ number => $line_num,
+ src => $self->actual_src,
+ warning => $warning,
+ );
+ }
+
+ next if defined $cont; # current line not a continuation
+
+ my $slabel = ($start ? $start->label : $label);
+ my $svalue = ($start ? $start->value : $value);
+ next unless $slabel;
+
+ # Check config file type and version
+ if (index (uc ($slabel), $self->cfglabel ('CFGFILE')) == 0) {
+ my @words = split /$FCM1::Config::DELIMITER_PATTERN/, $slabel;
+ shift @words;
+
+ my $name = @words ? lc ($words[0]) : 'type';
+
+ if ($self->can ($name)) {
+ $self->$name ($svalue);
+ }
+ }
+
+ # Set internal variable
+ $slabel =~ s/^\%//; # Remove leading "%" from label
+
+ $self->config->variable ($slabel, $svalue)
+ unless exists $cfg_keywords{$slabel};
+ }
+
+ # Report and reset warnings
+ # ----------------------------------------------------------------------------
+ for my $line (@{ $self->lines }) {
+ w_report $line->format_warning if $line->warning;
+ $line->warning (undef);
+ }
+
+ return @{ $self->lines };
+
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = $obj->print_cfg ($file, [$force]);
+#
+# DESCRIPTION
+# This method prints the content of current configuration file. If no
+# argument is specified, it prints output to the standard output. If $file is
+# specified, and is a writable file name, the output is sent to the file. If
+# the file already exists, its content is compared to the current output.
+# Nothing will be written if the content is unchanged unless $force is
+# specified. Otherwise, for typed configuration files, the existing file is
+# renamed using a prefix that contains its last modified time. The method
+# returns 1 if there is no error.
+# ------------------------------------------------------------------------------
+
+sub print_cfg {
+ my ($self, $file, $force) = @_;
+
+ # Count maximum number of characters in the labels, (for pretty printing)
+ my $max_label_len = 0;
+ for my $line (@{ $self->lines }) {
+ next unless $line->label;
+ my $label_len = length $line->label;
+ $max_label_len = $label_len if $label_len > $max_label_len;
+ }
+
+ # Output string
+ my $out = '';
+
+ # Append each line of the config file to the output string
+ for my $line (@{ $self->lines }) {
+ $out .= $line->print_line ($max_label_len - length ($line->label) + 1);
+ $out .= "\n";
+ }
+
+ if ($out) {
+ my $out_handle = select();
+
+ # Open file if necessary
+ if ($file) {
+ # Make sure the host directory exists and is writable
+ my $dirname = dirname $file;
+ if (not -d $dirname) {
+ print 'Make directory: ', $dirname, "\n" if $self->verbose;
+ mkpath $dirname;
+ }
+ croak $dirname, ': cannot write to config file directory, abort'
+ unless -d $dirname;
+
+ if (-f $file and not $force) {
+ # Read old config file to see if content has changed
+ open(my $handle, '<', $file) || croak("$file: $!\n");
+ my $in_lines = '';
+ while (my $line = readline($handle)) {
+ $in_lines .= $line;
+ }
+ close($handle);
+
+ # Return if content is up-to-date
+ if ($in_lines eq $out) {
+ print 'No change in ', lc ($self->type), ' cfg: ', $file, "\n"
+ if $self->verbose > 1 and $self->type;
+ return 1;
+ }
+
+ # If config file already exists, make sure it is writable
+ if ($self->type) {
+ # Existing config file writable, rename it using its time stamp
+ my $mtime = (stat $file)[9];
+ my ($sec, $min, $hour, $mday, $mon, $year) = (gmtime $mtime)[0 .. 5];
+ my $timestamp = sprintf '%4d%2.2d%2.2d_%2.2d%2.2d%2.2d_',
+ $year + 1900, $mon + 1, $mday, $hour, $min, $sec;
+ my $oldfile = File::Spec->catfile (
+ $dirname, $timestamp . basename ($file)
+ );
+ rename $file, $oldfile;
+ print 'Rename existing ', lc ($self->type), ' cfg: ',
+ $oldfile, "\n" if $self->verbose > 1;
+ }
+ }
+
+ # Open file and select file handle
+ open(my $handle, '>', $file) || croak("$file: $!\n");
+ $out_handle = $handle;
+ }
+
+ # Print output
+ print($out_handle $out);
+
+ # Close file if necessary
+ if ($file) {
+ close($out_handle) || croak("$file: $!\n");
+
+ if ($self->type and $self->verbose > 1) {
+ print 'Generated ', lc ($self->type), ' cfg: ', $file, "\n";
+
+ } elsif ($self->verbose > 2) {
+ print 'Generated cfg: ', $file, "\n";
+ }
+ }
+
+ } else {
+ # Warn if nothing to print
+ my $warning = 'Empty configuration';
+ $warning .= ' - nothing written to file: ' . $file if $file;
+ carp $warning if $self->type;
+ }
+
+ return 1;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# @lines = $self->_get_cfg_lines($is_for_inheritance);
+#
+# DESCRIPTION
+# This internal method opens the configuration file and returns its contents
+# as an array of lines. If the $self->src() is given as a URI, the method
+# tries to read it with "svn cat". Otherwise, the method tries to read
+# $self->src() with open() and readline(). If $self->type() is not a known
+# type, $self->src() can only be a regular file and $is_for_inheritance is
+# ignored. Otherwise, $self->src() can be a regular file or a directory and
+# $is_for_inheritance is used to determine the behaviour for searching the
+# directory for a configuration file. If $is_for_inheritance is set, the
+# config file may be located at "$src/cfg/$type.cfg". If $is_for_inheritance
+# is not set, the config file may be located at "$src/$type.cfg" or
+# "$src/cfg/$type.cfg".
+# ------------------------------------------------------------------------------
+
+sub _get_cfg_lines {
+ my ($self, $is_for_inheritance) = @_;
+ my $DIAG = sub {};
+ my @paths_refs = ([]);
+ if ($self->type() && exists($self->setting('CFG_NAME')->{uc($self->type())})) {
+ my $base = $self->setting('CFG_NAME')->{uc($self->type())};
+ if (!$is_for_inheritance) {
+ push(@paths_refs, [$base]);
+ }
+ push(@paths_refs, [$self->setting(qw/DIR CFG/), $base]);
+ if ($self->verbose()) {
+ $DIAG = sub {printf("Config file (%s): %s\n", $self->type(), @_)};
+ }
+ }
+ if ($self->src() =~ qr{\A([A-Za-z][\w\+-\.]*):}xms) {
+ # $self->src() is a URI, try "svn cat"
+ my $src = FCM1::Util::tidy_url(FCM1::Keyword::expand($self->src()));
+ my ($uri, $rev) = $src =~ qr{\A(.+?)(?:\@([^\@]+))?\z}msx;
+ $rev ||= 'HEAD';
+ for my $paths_ref (@paths_refs) {
+ my $path = join('/', $uri, @{$paths_ref}) . '@' . $rev;
+ local($@);
+ my @lines = eval {
+ run_command([qw/svn cat/, $path], METHOD => 'qx', DEVNULL => 1);
+ };
+ if (!$@) {
+ $self->pegrev($rev);
+ $self->actual_src($path);
+ $DIAG->($path);
+ return @lines;
+ }
+ }
+ }
+ else {
+ # $self->src() is not a URI, assume that it resides in the file system
+ for my $paths_ref (@paths_refs) {
+ my $path = File::Spec->catfile($self->src(), @{$paths_ref});
+ if (-e $path && !-d $path) { # "-f $path" returns false for "/dev/null"
+ open(my $handle, '<', $path)
+ || croak("$path: cannot open config file, abort: $!");
+ my @lines = readline($handle);
+ close($handle);
+ $self->actual_src($path);
+ $DIAG->($path);
+ return @lines;
+ }
+ }
+ }
+ croak(sprintf("%s: cannot locate config file, abort", $self->src()));
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $string = $self->_expand_variable ($string, $env[, \%recursive_set]);
+#
+# DESCRIPTION
+# This internal method expands variables in $string. If $env is true, it
+# expands environment variables. Otherwise, it expands local variables. If
+# %recursive_set is specified, it indicates that this method is being called
+# recursively. In which case, it must not attempt to expand a variable that
+# exists in the keys of %recursive_set.
+# ------------------------------------------------------------------------------
+
+sub _expand_variable {
+ my ($self, $string, $env, $recursive_set_ref) = @_;
+
+ # Pattern for environment/local variable
+ my @patterns = $env
+ ? (qr#\$([A-Z][A-Z0-9_]+)#, qr#\$\{([A-Z][A-Z0-9_]+)\}#)
+ : (qr#%(\w+(?:::[\w\.-]+)*)#, qr#%\{(\w+(?:(?:::|/)[\w\.-]+)*)\}#);
+
+ my $ret = '';
+ my $warning = undef;
+ while ($string) {
+ # Find the first match in $string
+ my ($prematch, $match, $postmatch, $var_label);
+ for my $pattern (@patterns) {
+ next unless $string =~ /$pattern/;
+ if ((not defined $prematch) or length ($`) < length ($prematch)) {
+ $prematch = $`;
+ $match = $&;
+ $var_label = $1;
+ $postmatch = $';
+ }
+ }
+
+ if ($match) {
+ $ret .= $prematch;
+ $string = $postmatch;
+
+ # Get variable value from environment or local configuration
+ my $variable = $env
+ ? (exists $ENV{$var_label} ? $ENV{$var_label} : undef)
+ : $self->config->variable ($var_label);
+
+ if ($env and $var_label eq 'HERE' and not defined $variable) {
+ $variable = dirname ($self->actual_src);
+ $variable = File::Spec->rel2abs ($variable) if not &is_url ($variable);
+ }
+
+ # Substitute match with value of variable
+ if (defined $variable) {
+ my %set = (($recursive_set_ref ? %{$recursive_set_ref} : ()));
+ if (exists($set{$var_label})) {
+ $warning .= ', ' if $warning;
+ $warning .= $match . ': cyclic dependency, variable not expanded';
+ $ret .= $variable;
+
+ } else {
+ my ($r, $w)
+ = $self->_expand_variable($variable, $env, {%set, $var_label => 1});
+ $ret .= $r;
+ if ($w) {
+ $warning .= ', ' if $warning;
+ $warning .= $w;
+ }
+ }
+
+ } else {
+ $warning .= ', ' if $warning;
+ $warning .= $match . ': variable not expanded';
+ $ret .= $match;
+ }
+
+ } else {
+ $ret .= $string;
+ $string = "";
+ }
+ }
+
+ return ($ret, $warning);
+}
+
+1;
+
+__END__
diff --git a/lib/FCM1/CfgLine.pm b/lib/FCM1/CfgLine.pm
new file mode 100644
index 0000000..4819024
--- /dev/null
+++ b/lib/FCM1/CfgLine.pm
@@ -0,0 +1,346 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# NAME
+# FCM1::CfgLine
+#
+# DESCRIPTION
+# This class is used for grouping the settings in each line of a FCM
+# configuration file.
+#
+# ------------------------------------------------------------------------------
+
+package FCM1::CfgLine;
+ at ISA = qw(FCM1::Base);
+
+# Standard pragma
+use warnings;
+use strict;
+
+# Standard modules
+use File::Basename;
+
+# In-house modules
+use FCM1::Base;
+use FCM1::Config;
+use FCM1::Util;
+
+# List of property methods for this class
+my @scalar_properties = (
+ 'bvalue', # line value, in boolean
+ 'comment', # (in)line comment
+ 'error', # error message for incorrect usage while parsing the line
+ 'label', # line label
+ 'line', # content of the line
+ 'number', # line number in source file
+ 'parsed', # has this line been parsed (by the extract/build system)?
+ 'prefix', # optional prefix for line label
+ 'slabel', # label without the optional prefix
+ 'src', # name of source file
+ 'value', # line value
+ 'warning', # warning message for deprecated usage
+);
+
+# Useful variables
+our $COMMENT_RULER = '-' x 78;
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# @cfglines = FCM1::CfgLine->comment_block (@comment);
+#
+# DESCRIPTION
+# This method returns a list of FCM1::CfgLine objects representing a comment
+# block with the comment string @comment.
+# ------------------------------------------------------------------------------
+
+sub comment_block {
+ my @return = (
+ FCM1::CfgLine->new (comment => $COMMENT_RULER),
+ (map {FCM1::CfgLine->new (comment => $_)} @_),
+ FCM1::CfgLine->new (comment => $COMMENT_RULER),
+ FCM1::CfgLine->new (),
+ );
+
+ return @return;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $obj = FCM1::CfgLine->new (%args);
+#
+# DESCRIPTION
+# This method constructs a new instance of the FCM1::CfgLine class. See above
+# for allowed list of properties. (KEYS should be in uppercase.)
+# ------------------------------------------------------------------------------
+
+sub new {
+ my $this = shift;
+ my %args = @_;
+ my $class = ref $this || $this;
+
+ my $self = FCM1::Base->new (%args);
+
+ for (@scalar_properties) {
+ $self->{$_} = exists $args{uc ($_)} ? $args{uc ($_)} : undef;
+ $self->{$_} = $args{$_} if exists $args{$_};
+ }
+
+ bless $self, $class;
+ return $self;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $value = $obj->X;
+# $obj->X ($value);
+#
+# DESCRIPTION
+# Details of these properties are explained in @scalar_properties.
+# ------------------------------------------------------------------------------
+
+for my $name (@scalar_properties) {
+ no strict 'refs';
+
+ *$name = sub {
+ my $self = shift;
+
+ if (@_) {
+ $self->{$name} = $_[0];
+
+ if ($name eq 'line' or $name eq 'label') {
+ $self->{slabel} = undef;
+
+ } elsif ($name eq 'line' or $name eq 'value') {
+ $self->{bvalue} = undef;
+ }
+ }
+
+ # Default value for property
+ if (not defined $self->{$name}) {
+ if ($name =~ /^(?:comment|error|label|line|prefix|src|value)$/) {
+ # Blank
+ $self->{$name} = '';
+
+ } elsif ($name eq 'slabel') {
+ if ($self->prefix and $self->label_starts_with ($self->prefix)) {
+ $self->{$name} = $self->label_from_field (1);
+
+ } else {
+ $self->{$name} = $self->label;
+ }
+
+ } elsif ($name eq 'bvalue') {
+ if (defined ($self->value)) {
+ $self->{$name} = ($self->value =~ /^(\s*|false|no|off|0*)$/i)
+ ? 0 : $self->value;
+ }
+ }
+ }
+
+ return $self->{$name};
+ }
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# @fields = $obj->label_fields ();
+# @fields = $obj->slabel_fields ();
+#
+# DESCRIPTION
+# These method returns a list of fields in the (s)label.
+# ------------------------------------------------------------------------------
+
+for my $name (qw/label slabel/) {
+ no strict 'refs';
+
+ my $sub_name = $name . '_fields';
+ *$sub_name = sub {
+ return (split (/$FCM1::Config::DELIMITER_PATTERN/, $_[0]->$name));
+ }
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $string = $obj->label_from_field ($index);
+# $string = $obj->slabel_from_field ($index);
+#
+# DESCRIPTION
+# These method returns the (s)label from field $index onwards.
+# ------------------------------------------------------------------------------
+
+for my $name (qw/label slabel/) {
+ no strict 'refs';
+
+ my $sub_name = $name . '_from_field';
+ *$sub_name = sub {
+ my ($self, $index) = @_;
+ my $method = $name . '_fields';
+ my @fields = $self->$method;
+ return join ($FCM1::Config::DELIMITER, @fields[$index .. $#fields]);
+ }
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $flag = $obj->label_starts_with (@fields);
+# $flag = $obj->slabel_starts_with (@fields);
+#
+# DESCRIPTION
+# These method returns a true if (s)label starts with the labels in @fields
+# (ignore case).
+# ------------------------------------------------------------------------------
+
+for my $name (qw/label slabel/) {
+ no strict 'refs';
+
+ my $sub_name = $name . '_starts_with';
+ *$sub_name = sub {
+ my ($self, @fields) = @_;
+ my $return = 1;
+
+ my $method = $name . '_fields';
+ my @all_fields = $self->$method;
+
+ for my $i (0 .. $#fields) {
+ next if lc ($fields[$i]) eq lc ($all_fields[$i]);
+ $return = 0;
+ last;
+ }
+
+ return $return;
+ }
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $flag = $obj->label_starts_with_cfg (@fields);
+# $flag = $obj->slabel_starts_with_cfg (@fields);
+#
+# DESCRIPTION
+# These method returns a true if (s)label starts with the configuration file
+# labels in @fields (ignore case).
+# ------------------------------------------------------------------------------
+
+for my $name (qw/label slabel/) {
+ no strict 'refs';
+
+ my $sub_name = $name . '_starts_with_cfg';
+ *$sub_name = sub {
+ my ($self, @fields) = @_;
+
+ for my $field (@fields) {
+ $field = $self->cfglabel ($field);
+ }
+
+ my $method = $name . '_starts_with';
+ return $self->$method (@fields);
+ }
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $mesg = $obj->format_error ();
+#
+# DESCRIPTION
+# This method returns a string containing a formatted error message for
+# anything reported to the current line.
+# ------------------------------------------------------------------------------
+
+sub format_error {
+ my ($self) = @_;
+ my $mesg = '';
+
+ $mesg .= $self->format_warning;
+
+ if ($self->error or not $self->parsed) {
+ $mesg = 'ERROR: ' . $self->src . ': LINE ' . $self->number . ':' . "\n";
+ if ($self->error) {
+ $mesg .= ' ' . $self->error;
+
+ } else {
+ $mesg .= ' ' . $self->label . ': label not recognised.';
+ }
+ }
+
+ return $mesg;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $mesg = $obj->format_warning ();
+#
+# DESCRIPTION
+# This method returns a string containing a formatted warning message for
+# any warning reported to the current line.
+# ------------------------------------------------------------------------------
+
+sub format_warning {
+ my ($self) = @_;
+ my $mesg = '';
+
+ if ($self->warning) {
+ $mesg = 'WARNING: ' . $self->src . ': LINE ' . $self->number . ':' . "\n";
+ $mesg .= ' ' . $self->warning;
+ }
+
+ return $mesg;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $line = $obj->print_line ([$space]);
+#
+# DESCRIPTION
+# This method returns a configuration line using $self->label, $self->value
+# and $self->comment. The value in $self->line is re-set. If $space is set
+# and is a positive integer, it sets the spacing between the label and the
+# value in the line. The default is 1.
+# ------------------------------------------------------------------------------
+
+sub print_line {
+ my ($self, $space) = @_;
+
+ # Set space between label and value, default to 1 character
+ $space = 1 unless $space and $space =~ /^[1-9]\d*$/;
+
+ my $line = '';
+
+ # Add label and value, if label is set
+ if ($self->label) {
+ $line .= $self->label . ' ' x $space;
+ $line .= $self->value if defined $self->value;
+ }
+
+ # Add comment if necessary
+ my $comment = $self->comment;
+ $comment =~ s/^\s*//;
+
+ if ($comment) {
+ $comment = '# ' . $comment if $comment !~ /^#/;
+ $line .= ' ' if $line;
+ $line .= $comment;
+ }
+
+ return $self->line ($line);
+}
+
+# ------------------------------------------------------------------------------
+
+1;
+
+__END__
diff --git a/lib/FCM1/Cm.pm b/lib/FCM1/Cm.pm
new file mode 100644
index 0000000..6e3c8a6
--- /dev/null
+++ b/lib/FCM1/Cm.pm
@@ -0,0 +1,2264 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# NAME
+# FCM1::Cm
+#
+# DESCRIPTION
+# This module contains the FCM code management functionalities and wrappers
+# to Subversion commands.
+#
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+
+package FCM1::Cm;
+use base qw{Exporter};
+
+our @EXPORT_OK = qw(cm_check_missing cm_check_unknown cm_switch cm_update);
+
+use Cwd qw{cwd};
+use FCM::System::Exception;
+use FCM1::Config;
+use FCM1::CmBranch;
+use FCM1::CmUrl;
+use FCM1::Keyword;
+use FCM1::Util qw{
+ get_url_of_wc
+ get_url_peg_of_wc
+ is_url
+ is_wc
+ tidy_url
+};
+use File::Basename qw{basename dirname};
+use File::Path qw{mkpath rmtree};
+use File::Spec;
+use Text::ParseWords qw{shellwords};
+
+# ------------------------------------------------------------------------------
+
+# CLI message handler
+our $CLI_MESSAGE = \&_cli_message;
+
+# List of CLI messages
+our %CLI_MESSAGE_FOR = (
+ q{} => "%s",
+ BRANCH_LIST => "%s at %s: %d branch(es) found for %s.\n",
+ CHDIR_WCT => "%s: working directory changed to top of working copy.\n",
+ CF => "Conflicts in: %s\n",
+ MERGE_ACTUAL => "-" x 74 . "actual\n%s" . "-" x 74 . "actual\n",
+ MERGE_COMPARE => "Merge: %s\n c.f.: %s\n",
+ MERGE_OK => "Merge succeeded.\n",
+ MERGE_DRYRUN => "-" x 73 . "dry-run\n%s" . "-" x 73 . "dry-run\n",
+ MERGE_REVS => "Eligible merge(s) from %s: %s\n",
+ OUT_DIR => "Output directory: %s\n",
+ PATCH_DONE => "%s: patch generated.\n",
+ PATCH_REV => "Patch created for changeset %s\n",
+ SEPARATOR => q{-} x 80 . "\n",
+ STATUS => "%s: status of %s:\n%s\n",
+);
+
+# CLI abort and error messages
+our %CLI_MESSAGE_FOR_ABORT = (
+ FAIL => "%s: command failed.\n",
+ NULL => "%s: command will result in no change.\n",
+ USER => "%s: abort by user.\n",
+);
+
+# CLI abort and error messages
+our %CLI_MESSAGE_FOR_ERROR = (
+ CHDIR => "%s: cannot change to directory.\n",
+ CLI => "%s",
+ CLI_HELP => "Type 'fcm help %s' for usage.\n",
+ CLI_MERGE_ARG1 => "Arg 1 must be the source in auto/custom mode.\n",
+ CLI_MERGE_ARG2 => "Arg 2 must be the source in custom mode"
+ . " if --revision not set.\n",
+ CLI_OPT_ARG => "--%s: invalid argument [%s].\n",
+ CLI_OPT_WITH_OPT => "--%s: must be specified with --%s.\n",
+ CLI_USAGE => "incorrect value for the %s argument",
+ DIFF_PROJECTS => "%s (target) and %s (source) are not related.\n",
+ INVALID_BRANCH => "%s: not a valid URL of a standard FCM branch.\n",
+ INVALID_PROJECT => "%s: not a valid URL of a standard FCM project.\n",
+ INVALID_TARGET => "%s: not a valid working copy or URL.\n",
+ INVALID_URL => "%s: not a valid URL.\n",
+ INVALID_WC => "%s: not a valid working copy.\n",
+ MERGE_REV_INVALID => "%s: not a revision in the available merge list.\n",
+ MERGE_SELF => "%s: cannot be merged to its own working copy: %s.\n",
+ MERGE_UNRELATED => "%s: target and %s: source not directly related.\n",
+ MERGE_UNSAFE => "%s: source contains changes outside the target"
+ . " sub-directory. Please merge with a full tree.\n",
+ MKPATH => "%s: cannot create directory.\n",
+ NOT_EXIST => "%s: does not exist.\n",
+ PARENT_NOT_EXIST => "%s: parent %s no longer exists.\n",
+ RMTREE => "%s: cannot remove.\n",
+ ST_CI_MESG_FILE => "Attempt to add commit message file:\n%s",
+ ST_CONFLICT => "File(s) in conflicts:\n%s",
+ ST_MISSING => "File(s) missing:\n%s",
+ ST_OOD => "File(s) out of date:\n%s",
+ SWITCH_UNSAFE => "%s: merge template exists."
+ . " Please remove before retrying.\n",
+ WC_INVALID_BRANCH => "%s: not a working copy of a standard FCM branch.\n",
+ WC_URL_NOT_EXIST => "%s: working copy URL does not exists at HEAD.\n",
+);
+
+# List of CLI prompt messages
+our %CLI_MESSAGE_FOR_PROMPT = (
+ CF_OVERWRITE => qq{%s: existing changes will be overwritten.\n}
+ . qq{ Do you wish to continue?},
+ CI => qq{Would you like to commit this change?},
+ CI_BRANCH_SHARED => qq{\n}
+ . qq{*** WARNING: YOU ARE COMMITTING TO A %s BRANCH.\n}
+ . qq{*** Please ensure that you have the}
+ . qq{ owner's permission.\n\n}
+ . qq{Would you like to commit this change?},
+ CI_BRANCH_USER => qq{\n}
+ . qq{*** WARNING: YOU ARE COMMITTING TO A BRANCH}
+ . qq{ NOT OWNED BY YOU.\n}
+ . qq{*** Please ensure that you have the}
+ . qq{ owner's permission.\n\n}
+ . qq{Would you like to commit this change?},
+ CI_TRUNK => qq{\n}
+ . qq{*** WARNING: YOU ARE COMMITTING TO THE TRUNK.\n}
+ . qq{*** Please ensure that your change conforms to}
+ . qq{ your project's working practices.\n\n}
+ . qq{Would you like to commit this change?},
+ CONTINUE => qq{%s: continue?},
+ MERGE => qq{Would you like to go ahead with the merge?},
+ MERGE_REV => qq{Enter a revision},
+ MKPATCH_OVERWRITE => qq{%s: output location exists. OK to overwrite?},
+ RUN_SVN_COMMAND => qq{Would you like to run "svn %s"?},
+);
+
+# List of CLI warning messages
+our %CLI_MESSAGE_FOR_WARNING = (
+ BRANCH_SUBDIR => "%s: is a sub-directory of a branch in a FCM project.\n",
+ CF_BINARY => "%s: ignoring binary file, please resolve manually.\n",
+ INVALID_BRANCH => $CLI_MESSAGE_FOR_ERROR{INVALID_BRANCH},
+ ST_IN_TRAC_DIFF => "%s: local changes cannot be displayed in Trac.\n"
+);
+
+# CLI prompt handler and title prefix
+our $CLI_PROMPT = \&_cli_prompt;
+our $CLI_PROMPT_PREFIX = q{fcm };
+
+# Event handlers
+our %CLI_HANDLER_OF = (
+ 'WC_STATUS' => \&_cli_handler_of_wc_status,
+ 'WC_STATUS_PATH' => \&_cli_handler_of_wc_status_path,
+);
+
+# Common patterns
+our %PATTERN_OF = (
+ # A CLI option
+ CLI_OPT => qr{
+ \A (?# beginning)
+ (--\w[\w-]*=) (?# capture 1, a long option label)
+ (.*) (?# capture 2, the value of the option)
+ \z (?# end)
+ }xms,
+ # A CLI revision option
+ CLI_OPT_REV => qr{
+ \A (?# beginning)
+ (--revision(?:=|\z)|-r) (?# capture 1, --revision, --revision= or -r)
+ (.*) (?# capture 2, trailing value)
+ \z (?# end)
+ }xms,
+ # A CLI revision option range
+ CLI_OPT_REV_RANGE => qr{
+ \A (?# beginning)
+ ( (?# capture 1, begin)
+ (?:\{[^\}]+\}+) (?# a date in curly braces)
+ | (?# or)
+ [^:]+ (?# anything but a colon)
+ ) (?# capture 1, end)
+ (?::(.*))? (?# colon, and capture 2 til the end)
+ \z (?# end)
+ }xms,
+ # A FCM branch path look-alike, should be configurable in the future
+ FCM_BRANCH_PATH => qr{
+ \A (?# beginning)
+ /* (?# some slashes)
+ (?: (?# group 1, begin)
+ (?:trunk/*(?:@\d+)?\z) (?# trunk at a revision)
+ | (?# or)
+ (?:trunk|branches|tags)/+ (?# trunk, branch or tags)
+ ) (?# group 1, end)
+ }xms,
+ # Last line of output from "svn status -u"
+ ST_AGAINST_REV => qr{
+ \A (?# beginning)
+ Status\sagainst\srevision:.* (?# output of svn status -u)
+ \z (?# end)
+ }xms,
+ # Extract path from "svn status"
+ ST_PATH => qr{
+ \A (?# beginning)
+ .{6} (?# 6 columns)
+ \s+ (?# spaces)
+ (.+) (?# capture 1, target path)
+ \z (?# end)
+ }xms,
+ # A legitimate "svn" revision
+ SVN_REV => qr{
+ \A (?# beginning)
+ (?:\d+|HEAD|BASE|COMMITTED|PREV|\{.+\}) (?# digit, reserved words, date)
+ \z (?# end)
+ }ixms,
+);
+
+# Status matchers
+our %ST_MATCHER_FOR = (
+ CONFLICT => sub {substr($_[0], 0, 1) eq 'C' || substr($_[0], 6, 1) eq 'C'},
+ MISSING => sub {substr($_[0], 0, 1) eq '!'},
+ MODIFIED => sub {substr($_[0], 0, 7) =~ qr{\S}xms},
+ OOD => sub {substr($_[0], 8, 1) eq '*'},
+ UNKNOWN => sub {substr($_[0], 0, 1) eq '?'},
+);
+
+# Set the FCM::Util object by FCM::System::CM.
+our $UTIL;
+sub set_util {
+ $UTIL = shift();
+}
+
+# Set the commit message utility provided by FCM::System::CM.
+our $COMMIT_MESSAGE_UTIL;
+sub set_commit_message_util {
+ $COMMIT_MESSAGE_UTIL = shift();
+ FCM1::CmBranch::set_commit_message_util($COMMIT_MESSAGE_UTIL);
+}
+
+# Set the SVN utility provided by FCM::System::CM.
+our $SVN;
+sub set_svn_util {
+ $SVN = shift();
+ FCM1::CmUrl::set_svn_util($SVN);
+ FCM1::CmBranch::set_svn_util($SVN);
+}
+
+# Returns the branch URL as an instance of FCM1::CmUrl.
+sub _branch_url {
+ my $arg = shift();
+ my $url
+ = $arg && is_url($arg) ? FCM1::CmUrl->new(URL => $arg)
+ : $arg && is_wc($arg) ? FCM1::CmUrl->new(URL => get_url_of_wc($arg))
+ : is_wc() ? FCM1::CmUrl->new(URL => get_url_of_wc())
+ : undef
+ ;
+ if (!$url) {
+ return _cm_err(FCM1::Cm::Exception->INVALID_TARGET, '.');
+ }
+ $url;
+}
+
+# Branch delete.
+sub cm_branch_delete {
+ my ($option_ref, $arg) = @_;
+ my $branch = cm_branch_info($option_ref, $arg);
+ $branch->del(
+ PASSWORD => $option_ref->{password},
+ NON_INTERACTIVE => $option_ref->{'non-interactive'},
+ SVN_NON_INTERACTIVE => $option_ref->{'svn-non-interactive'},
+ );
+ if (!$arg && $option_ref->{'switch'}) {
+ cm_switch($option_ref, $branch->layout()->get_config()->{'dir-trunk'});
+ }
+}
+
+# Branch diff.
+sub cm_branch_diff {
+ my ($option_ref, $target) = @_;
+ local(%ENV) = %ENV;
+ $ENV{FCM_GRAPHIC_DIFF} ||= $UTIL->external_cfg_get('graphic-diff');
+ my @diff_cmd
+ = $option_ref->{graphical} ? (qw{--diff-cmd fcm_graphic_diff})
+ : $option_ref->{'diff-cmd'} ? ('--diff-cmd', $option_ref->{'diff-cmd'})
+ : ()
+ ;
+ if ($option_ref->{extensions}) {
+ push(@diff_cmd, '--extensions', shellwords($option_ref->{extensions}));
+ }
+
+ # Target can be a URL/path, default to $PWD.
+ $target ||= q{.};
+ my $target_is_path = !is_url($target);
+
+ # Get repository and branch information
+ my $url = bless(_branch_url($target), 'FCM1::CmBranch');
+
+ # Check that URL is a standard FCM branch
+ if (!$url->is_branch()) {
+ return _cm_err(FCM1::Cm::Exception->INVALID_BRANCH, $url->url_peg());
+ }
+
+ # Save and remove sub-directory part of the URL
+ my $subdir = $url->subdir();
+ $url->url_peg($url->branch_url_peg());
+
+ # Check that $url exists
+ if (!$url->url_exists()) {
+ return _cm_err(FCM1::Cm::Exception->INVALID_URL, $url->url_peg());
+ }
+
+ # Compare current branch with its parent
+ my $parent = FCM1::CmBranch->new(URL => $url->parent()->url());
+ if ($url->pegrev()) {
+ $parent->url_peg($parent->url() . '@' . $url->pegrev());
+ }
+
+ if (!$parent->url_exists()) {
+ return _cm_err(
+ FCM1::Cm::Exception->PARENT_NOT_EXIST, $url->url_peg(), $parent->url(),
+ );
+ }
+
+ my $base = $parent->base_of_merge_from($url);
+
+ # Ensure the correct diff (syntax) is displayed
+ # Reinstate the sub-tree part into the URL
+ if ($subdir) {
+ $url->url_peg($url->branch_url() . '/' . $subdir . '@' . $url->pegrev());
+ $base->url_peg($base->branch_url() . '/' . $subdir . '@' . $base->pegrev());
+ }
+
+ if ($option_ref->{trac} || $option_ref->{wiki}) {
+ if ($target_is_path && _svn_status_get([$target])) {
+ $CLI_MESSAGE->('ST_IN_TRAC_DIFF', $target);
+ }
+
+ # Trac wiki syntax
+ my $wiki_syntax = 'diff:' . $base->path_peg() . '//' . $url->path_peg();
+
+ if ($option_ref->{wiki}) {
+ $CLI_MESSAGE->(q{}, "$wiki_syntax\n");
+ }
+ else { # if $option_ref->{trac}
+ my $browser = $UTIL->external_cfg_get('browser');
+ my $trac_url = FCM1::Keyword::get_browser_url($url->project_url());
+ # FIXME: assuming that the browser URL uses the InterTrac syntax
+ $trac_url =~ s{/intertrac/.*$}{/intertrac/$wiki_syntax}xms;
+ my %value_of = %{$UTIL->shell_simple([$browser, $trac_url])};
+ if ($value_of{rc}) {
+ return FCM::System::Exception->throw(
+ FCM::System::Exception->SHELL,
+ {command_list => [$browser, $trac_url], %value_of},
+ $value_of{e},
+ );
+ }
+ }
+ }
+ else {
+ $SVN->call(
+ 'diff', @diff_cmd,
+ ($option_ref->{summarize} ? ('--summarize') : ()),
+ ($option_ref->{xml} ? ('--xml') : ()),
+ '--old', $base->url_peg(),
+ '--new', ($target_is_path ? $target : $url->url_peg()),
+ );
+ }
+}
+
+# Branch info.
+sub cm_branch_info {
+ my ($option_ref, $arg) = @_;
+ my $url = _branch_url($arg);
+ FCM1::Config->instance()->verbose($option_ref->{verbose} ? 1 : 0);
+ my $branch = FCM1::CmBranch->new(URL => $url->url_peg());
+ if (!$branch->branch()) {
+ return _cm_err(FCM1::Cm::Exception->INVALID_BRANCH, $branch->url_peg());
+ }
+ if (!$branch->url_exists()) {
+ return _cm_err(FCM1::Cm::Exception->NOT_EXIST, $branch->url_peg());
+ }
+ $branch->url_peg($branch->branch_url_peg());
+ $option_ref->{'show-children'} ||= $option_ref->{'show-all'};
+ $option_ref->{'show-other' } ||= $option_ref->{'show-all'};
+ $option_ref->{'show-siblings'} ||= $option_ref->{'show-all'};
+ $branch->display_info(
+ SHOW_CHILDREN => $option_ref->{'show-children'},
+ SHOW_OTHER => $option_ref->{'show-other' },
+ SHOW_SIBLINGS => $option_ref->{'show-siblings'},
+ );
+ $branch;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# &FCM1::Cm::cm_commit ();
+#
+# DESCRIPTION
+# This is a FCM wrapper to the "svn commit" command.
+# ------------------------------------------------------------------------------
+
+sub cm_commit {
+ my ($option_ref, $path) = @_;
+ $path ||= cwd();
+ if (!-e $path) {
+ return _cm_err(FCM1::Cm::Exception->NOT_EXIST, $path);
+ }
+
+ # Make sure we are in a working copy
+ if (!is_wc($path)) {
+ return _cm_err(FCM1::Cm::Exception->INVALID_WC, $path);
+ }
+
+ # Make sure we are at the top level of the working copy
+ # (otherwise we might miss any template commit message)
+ my $dir = $SVN->get_wc_root($path);
+
+ if ($dir ne cwd ()) {
+ chdir($dir) || return _cm_err(FCM1::Cm::Exception->CHDIR, $dir);
+ $CLI_MESSAGE->('CHDIR_WCT', $dir);
+ }
+
+ # Get update status of working copy
+ # Check working copy files are not in conflict, missing, or out of date
+ my @status = _svn_status_get([], 1);
+ if (!defined($option_ref->{'dry-run'})) {
+ my %st_lines_of = (CONFLICT => [], MISSING => [], OOD => []);
+
+ LINE:
+ for my $line (@status) {
+ for my $key (keys(%st_lines_of)) {
+ if ($ST_MATCHER_FOR{$key}->($line)) {
+ push(@{$st_lines_of{$key}}, $line);
+ next LINE;
+ }
+ }
+ # Check that all files which have been added have the svn:executable
+ # property set correctly (in case the developer adds a script before they
+ # remember to set the execute bit)
+ my ($file) = $line =~ qr/\AA.{8}\s*\d+\s+(.*)/msx;
+ if (!$file || !-f $file) {
+ next LINE;
+ }
+ my ($command, @arguments)
+ = (-x $file && !-l $file) ? ('propset', '*') : ('propdel');
+ $SVN->call($command, qw{-q svn:executable}, @arguments, $file);
+ }
+
+ # Abort commit if files are in conflict, missing, or out of date
+ my @keys = grep {@{$st_lines_of{$_}}} keys(%st_lines_of);
+ if (@keys) {
+ for my $key (sort(@keys)) {
+ my @lines = map {"$_\n"} @{$st_lines_of{$key}};
+ $CLI_MESSAGE->('ST_' . $key, join(q{}, @lines));
+ }
+ return _cm_abort(FCM1::Cm::Abort->FAIL);
+ }
+ }
+
+ # Read in any existing message
+ my $commit_message_ctx = $COMMIT_MESSAGE_UTIL->load();
+
+ # Execute "svn status" for a list of changed items
+ @status = map {$_ . "\n"} grep {$_ =~ qr/\A[^\?]/msx} _svn_status_get();
+
+ # Abort if there is no change in the working copy
+ if (!@status) {
+ return _cm_abort(FCM1::Cm::Abort->NULL);
+ }
+
+ # Abort if attempt to add commit message file
+ my $ci_mesg_file_base = $COMMIT_MESSAGE_UTIL->path_base();
+ my @bad_status = grep {$_ =~ qr{^A.*?\s$ci_mesg_file_base\n}m} @status;
+ if (@bad_status) {
+ for my $bad_status (@bad_status) {
+ $CLI_MESSAGE->('ST_CI_MESG_FILE', $bad_status);
+ }
+ return _cm_abort(FCM1::Cm::Abort->FAIL);
+ }
+
+ # Get associated URL of current working copy
+ my $layout = $SVN->get_layout($SVN->get_info()->[0]->{url});
+
+ # Include URL, or project, branch and sub-directory info in @status
+ unshift @status, "\n";
+
+ if ($layout->get_branch()) {
+ unshift(@status,
+ map {sprintf("[%-7s: %s]\n", @{$_})} (
+ ['Root' , $layout->get_root() ],
+ ['Project', $layout->get_project() ],
+ ['Branch' , $layout->get_branch() ],
+ ['Sub-dir', $layout->get_sub_tree()],
+ ),
+ );
+ }
+ else {
+ unshift(@status,
+ map {sprintf("[%s: %s]\n", @{$_})} (
+ ['Root', $layout->get_root()],
+ ['Path', $layout->get_path()],
+ ),
+ );
+ }
+
+ # Use a temporary file to store the final commit log message
+ $commit_message_ctx->set_info_part(join(q{}, @status));
+ $COMMIT_MESSAGE_UTIL->edit($commit_message_ctx);
+ $COMMIT_MESSAGE_UTIL->notify($commit_message_ctx);
+
+ # Check with the user to see if he/she wants to go ahead
+ my $reply = 'n';
+ if (!defined($option_ref->{'dry-run'})) {
+ $reply = $CLI_PROMPT->('commit', (
+ $layout->is_trunk() ? ('CI_TRUNK')
+ : !$layout->get_branch_owner()? ('CI')
+ : $layout->is_owned_by_user() ? ('CI')
+ : $layout->is_shared() ? ('CI_BRANCH_SHARED',
+ $layout->get_branch_owner())
+ : ('CI_BRANCH_USER')
+ ));
+ }
+
+ if ($reply eq 'y') {
+ # Commit the change if user replies "y" for "yes"
+ my $temp = $COMMIT_MESSAGE_UTIL->temp($commit_message_ctx);
+ eval {$SVN->call(
+ qw{commit -F}, "$temp",
+ ($option_ref->{'svn-non-interactive'} ? '--non-interactive' : ()),
+ ( defined($option_ref->{password})
+ ? ('--password', $option_ref->{password}) : ()
+ ),
+ )};
+ if ($@) {
+ $COMMIT_MESSAGE_UTIL->save($commit_message_ctx);
+ die($@);
+ }
+
+ # Remove commit message file
+ unlink($COMMIT_MESSAGE_UTIL->path());
+
+ # Update the working copy
+ _svn_update();
+
+ } else {
+ $COMMIT_MESSAGE_UTIL->save($commit_message_ctx);
+ if (!$option_ref->{'dry-run'}) {
+ return _cm_abort();
+ }
+ }
+
+ return;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# &FCM1::Cm::cm_merge ();
+#
+# DESCRIPTION
+# This is a wrapper to "svn merge".
+# ------------------------------------------------------------------------------
+
+sub cm_merge {
+ my ($option_ref, @args) = @_;
+ # Find out the URL of the working copy
+ if (!is_wc()) {
+ return _cm_err(FCM1::Cm::Exception->INVALID_WC, '.');
+ }
+ my $wct = $SVN->get_wc_root();
+ if ($wct ne cwd()) {
+ chdir($wct) || return _cm_err(FCM1::Cm::Exception->CHDIR, $wct);
+ $CLI_MESSAGE->('CHDIR_WCT', $wct);
+ }
+ my $target = FCM1::CmBranch->new(URL => get_url_of_wc($wct));
+ if (!$target->url_exists()) {
+ return _cm_err(FCM1::Cm::Exception->WC_URL_NOT_EXIST, '.');
+ }
+
+ # The target must be at the top of a branch
+ # $subdir will be used later to determine whether the merge is allowed or not
+ my $subdir = $target->subdir();
+ if ($subdir) {
+ $target->url_peg($target->branch_url_peg());
+ }
+
+ # Check for any local modifications
+ # ----------------------------------------------------------------------------
+ if (!$option_ref->{'dry-run'} && !$option_ref->{'non-interactive'}) {
+ _svn_status_checker('merge', 'MODIFIED', $CLI_HANDLER_OF{WC_STATUS})->();
+ }
+
+ # Determine the SOURCE URL
+ # ----------------------------------------------------------------------------
+ my $source;
+
+ if ($option_ref->{reverse}) {
+ # Reverse merge, the SOURCE is the working copy URL
+ $source = FCM1::CmBranch->new (URL => $target->url);
+
+ } else {
+ # Automatic/custom merge, argument 1 is the SOURCE of the merge
+ my $source_url = shift (@args);
+ if (!$source_url) {
+ _cli_err('CLI_MERGE_ARG1');
+ }
+
+ $source = _cm_get_source($source_url, $target);
+ }
+
+ # Parse the revision option
+ # ----------------------------------------------------------------------------
+ if ($option_ref->{reverse} && !$option_ref->{revision}) {
+ _cli_err('CLI_OPT_WITH_OPT', 'revision', 'reverse');
+ }
+ my @revs
+ = (grep {$option_ref->{$_}} qw{reverse custom}) && $option_ref->{revision}
+ ? split(qr{:}xms, $option_ref->{revision})
+ : ();
+
+ # Determine the merge delta and the commit log message
+ # ----------------------------------------------------------------------------
+ my (@delta, $mesg, @logs);
+ my $separator = '-' x 80 . "\n";
+
+ if ($option_ref->{reverse}) {
+ # Reverse merge
+ # --------------------------------------------------------------------------
+ if (@revs == 1) {
+ $revs[1] = ($revs[0] - 1);
+
+ } else {
+ @revs = sort {$b <=> $a} @revs;
+ }
+ $source->url_peg(
+ $source->branch_url() . '/' . $subdir . '@' . $source->pegrev(),
+ );
+
+ # "Delta" of the "svn merge" command
+ @delta = ('-r' . $revs[0] . ':' . $revs[1], $source->url_peg);
+
+ # Template message
+ $mesg = 'Reversed r' . $revs[0] .
+ (($revs[1] < $revs[0] - 1) ? ':' . $revs[1] : '') . ' of ' .
+ $source->path . "\n";
+
+ } elsif ($option_ref->{custom}) {
+ # Custom merge
+ # --------------------------------------------------------------------------
+ if (@revs) {
+ # Revision specified
+ # ------------------------------------------------------------------------
+ # Only one revision N specified, use (N - 1):N as the delta
+ unshift @revs, ($revs[0] - 1) if @revs == 1;
+ $source->url_peg(
+ $source->branch_url() . '/' . $subdir . '@' . $source->pegrev(),
+ );
+ $target->url_peg(
+ $target->branch_url() . '/' . $subdir . '@' . $target->pegrev(),
+ );
+
+ # "Delta" of the "svn merge" command
+ @delta = ('-r' . $revs[0] . ':' . $revs[1], $source->url_peg);
+
+ # Template message
+ $mesg = 'Custom merge into ' . $target->path . ': r' . $revs[1] .
+ ' cf. r' . $revs[0] . ' of ' . $source->path_peg . "\n";
+
+ } else {
+ # Revision not specified
+ # ------------------------------------------------------------------------
+ # Get second source URL
+ my $source2_url = shift (@args);
+ if (!$source2_url) {
+ _cli_err('CLI_MERGE_ARG2');
+ }
+
+ my $source2 = _cm_get_source($source2_url, $target);
+ for my $item ($source, $source2, $target) {
+ $item->url_peg($item->branch_url() . '/' . $subdir . '@' . $item->pegrev());
+ }
+
+ # "Delta" of the "svn merge" command
+ @delta = ($source->url_peg, $source2->url_peg);
+
+ # Template message
+ $mesg = 'Custom merge into ' . $target->path . ': ' . $source->path_peg .
+ ' cf. ' . $source2->path_peg . "\n";
+ }
+
+ } else {
+ # Automatic merge
+ # --------------------------------------------------------------------------
+ # Check to ensure source branch is not the same as the target branch
+ if (!$target->branch()) {
+ return _cm_err(FCM1::Cm::Exception->WC_INVALID_BRANCH, $wct);
+ }
+ if ($source->branch() eq $target->branch()) {
+ return _cm_err(FCM1::Cm::Exception->MERGE_SELF, $target->url_peg(), $wct);
+ }
+
+ # Only allow the merge if the source and target are "directly related"
+ # --------------------------------------------------------------------------
+ my $anc = $target->ancestor ($source);
+ return _cm_err(
+ FCM1::Cm::Exception->MERGE_UNRELATED, $target->url_peg(), $source->url_peg
+ ) unless
+ ($anc->url eq $target->url and $anc->url_peg eq $source->parent->url_peg)
+ or
+ ($anc->url eq $source->url and $anc->url_peg eq $target->parent->url_peg)
+ or
+ ($anc->url eq $source->parent->url and $anc->url eq $target->parent->url);
+
+ # Check for available merges from the source
+ # --------------------------------------------------------------------------
+ my @revs = $target->avail_merge_from ($source, 1);
+
+ if (@revs) {
+ if ($option_ref->{verbose}) {
+ # Verbose mode, print log messages of available merges
+ $CLI_MESSAGE->('MERGE_REVS', $source->path_peg(), q{});
+ for (@revs) {
+ $CLI_MESSAGE->('SEPARATOR');
+ $CLI_MESSAGE->(q{}, $source->display_svnlog($_));
+ }
+ $CLI_MESSAGE->('SEPARATOR');
+ }
+ else {
+ # Normal mode, list revisions of available merges
+ $CLI_MESSAGE->('MERGE_REVS', $source->path_peg(), join(q{ }, @revs));
+ }
+
+ } else {
+ return _cm_abort(FCM1::Cm::Abort->NULL);
+ }
+
+ # If more than one merge available, prompt user to enter a revision number
+ # to merge from, default to $revs [0]
+ # --------------------------------------------------------------------------
+ if ($option_ref->{'non-interactive'} || @revs == 1) {
+ $source->url_peg($source->url() . '@' . $revs[0]);
+ }
+ else {
+ my $reply = $CLI_PROMPT->(
+ {type => q{}, default => $revs[0]}, 'merge', 'MERGE_REV',
+ );
+ if (!defined($reply)) {
+ return _cm_abort();
+ }
+ # Expand revision keyword if necessary
+ if ($reply) {
+ $reply = (FCM1::Keyword::expand($target->project_url(), $reply))[1];
+ }
+ # Check that the reply is a number in the available merges list
+ if (!grep {$_ eq $reply} @revs) {
+ return _cm_err(FCM1::Cm::Exception->MERGE_REV_INVALID, $reply)
+ }
+ $source->url_peg($source->url() . '@' . $reply);
+ }
+
+ # If the working copy top is pointing to a sub-directory of a branch,
+ # we need to check whether the merge will result in losing changes made in
+ # other sub-directories of the source.
+ if ($subdir and not $target->allow_subdir_merge_from ($source, $subdir)) {
+ return _cm_err(FCM1::Cm::Exception->MERGE_UNSAFE, $source->url_peg());
+ }
+
+ # Calculate the base of the merge
+ my $base = $target->base_of_merge_from ($source);
+
+ # $source and $base must take into account the sub-directory
+ my $source_full = FCM1::CmBranch->new (URL => $source->url_peg);
+ my $base_full = FCM1::CmBranch->new (URL => $base->url_peg);
+
+ if ($subdir) {
+ $source_full->url_peg(
+ $source_full->branch_url() . '/' . $subdir . '@' . $source_full->pegrev()
+ );
+ $base_full->url_peg(
+ $base_full->branch_url() . '/' . $subdir . '@' . $base_full->pegrev()
+ );
+ }
+
+ # Diagnostic
+ $CLI_MESSAGE->('SEPARATOR');
+ $CLI_MESSAGE->('MERGE_COMPARE', $source->path_peg(), $base->path_peg());
+ # Delta of the "svn merge" command
+ @delta = ($base_full->url_peg, $source_full->url_peg);
+
+ # Template message
+ $mesg = sprintf(
+ "Merged into %s: %s cf. %s",
+ $target->path(), $source->path_peg(), $base->path_peg(),
+ );
+
+ if (exists($option_ref->{'auto-log'})) {
+ my $last_merge_from_source = ($target->last_merge_from($source))[1];
+ if (!defined($last_merge_from_source)) {
+ $last_merge_from_source = $target->ancestor($source);
+ }
+ my %log_entries = $source->svnlog(
+ REV => [$last_merge_from_source->pegrev() + 1, $source->pegrev()],
+ );
+ @logs = sort {$b->{'revision'} <=> $a->{'revision'}} values(%log_entries);
+ }
+ }
+
+ # Run "svn merge" in "--dry-run" mode to see the result
+ # ----------------------------------------------------------------------------
+ my $dry_run_output
+ = $SVN->stdout(qw{svn merge --dry-run --non-interactive}, @delta);
+
+ # Abort merge if it will result in no change
+ if (!$dry_run_output) {
+ return _cm_abort(FCM1::Cm::Abort->NULL);
+ }
+
+ # Report result of "svn merge --dry-run"
+ if ($option_ref->{'dry-run'} || !$option_ref->{'non-interactive'}) {
+ $CLI_MESSAGE->('MERGE_DRYRUN', $dry_run_output);
+ }
+
+ return if $option_ref->{'dry-run'};
+
+ # Prompt the user to see if (s)he would like to go ahead
+ # ----------------------------------------------------------------------------
+ # Go ahead with merge only if user replies "y"
+ if (
+ !$option_ref->{'non-interactive'} && $CLI_PROMPT->('merge', 'MERGE') ne 'y'
+ ) {
+ return _cm_abort();
+ }
+ $SVN->call('cleanup');
+ my $output = $SVN->stdout(qw{svn merge --non-interactive}, @delta);
+ $CLI_MESSAGE->('MERGE_OK');
+ if ($output ne $dry_run_output) {
+ $CLI_MESSAGE->('MERGE_ACTUAL', $output);
+ }
+
+ # Prepare the commit log
+ # ----------------------------------------------------------------------------
+ my $commit_message_ctx = $COMMIT_MESSAGE_UTIL->load();
+ my @auto_log = map {
+ my $log_entry = $_;
+ my @msg_list = (
+ map {q{> } . $_}
+ grep {
+ $_
+ && $_ !~ qr{\AMerged\sinto\s\S+:\s(?:\S+)\scf\.\s(?:\S+)\z}msx
+ && $_ !~ qr{\A(?:\#\d+(?:,\#\d+)*:\s)?Created\s\S+\sfrom\s\S+\.\z}msx
+ && $_ !~ qr{\Ar\d+:\z}msx
+ && $_ !~ qr{\A>\s.+\z}msx
+ }
+ split("\n", $log_entry->{'msg'})
+ );
+ @msg_list ? ('----', 'r' . $log_entry->{'revision'} . ':', @msg_list) : ();
+ } @logs;
+ my @messages = (
+ $mesg,
+ (@auto_log ? (@auto_log, '----'): ()),
+ $commit_message_ctx->get_auto_part()
+ );
+ $commit_message_ctx->set_auto_part(join("\n", @messages));
+ $COMMIT_MESSAGE_UTIL->save($commit_message_ctx);
+
+ return;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# &FCM1::Cm::cm_mkpatch ();
+#
+# DESCRIPTION
+# This is a FCM command to create a patching script from particular revisions
+# of a URL.
+# ------------------------------------------------------------------------------
+
+sub cm_mkpatch {
+ my ($option_ref, $u, $outdir) = @_;
+ # Process command line options and arguments
+ my @exclude = $option_ref->{exclude} ? @{$option_ref->{exclude}} : ();
+ my $organisation = $option_ref->{organisation};
+ my $revision = $option_ref->{revision};
+
+ # Excluded paths, convert glob into regular patterns
+ @exclude = split (/:/, join (':', @exclude));
+ for (@exclude) {
+ s#\*#[^/]*#; # match any number of non-slash character
+ s#\?#[^/]#; # match a non-slash character
+ s#/*$##; # remove trailing slash
+ }
+
+ # Organisation prefix
+ $organisation ||= 'original';
+
+ # Make sure revision option is set correctly
+ my @revs = $revision ? split (/:/, $revision) : ();
+ @revs = @revs [0, 1] if @revs > 2;
+
+ if (!$u) {
+ _cli_err('CLI_USAGE', 'URL');
+ }
+
+ my $url = FCM1::CmUrl->new (URL => $u);
+ if (!$url->is_url()) {
+ return _cm_err(FCM1::Cm::Exception->INVALID_URL, $u);
+ }
+ if (!$url->url_exists()) {
+ return _cm_err(FCM1::Cm::Exception->NOT_EXIST, $u);
+ }
+ if (!$url->branch()) {
+ $CLI_MESSAGE->('INVALID_BRANCH', $u);
+ }
+ elsif ($url->subdir()) {
+ $CLI_MESSAGE->('BRANCH_SUBDIR', $u);
+ }
+
+ if (@revs) {
+ # If HEAD revision is given, convert it into a number
+ # --------------------------------------------------------------------------
+ for my $rev (@revs) {
+ $rev = $url->svninfo(FLAG => 'revision') if uc ($rev) eq 'HEAD';
+ }
+
+ } else {
+ # If no revision is given, use the HEAD
+ # --------------------------------------------------------------------------
+ $revs[0] = $url->svninfo(FLAG => 'revision');
+ }
+
+ $revs[1] = $revs[0] if @revs == 1;
+
+ # Check that output directory is set
+ # ----------------------------------------------------------------------------
+ $outdir = File::Spec->catfile (cwd (), 'fcm-mkpatch-out') if not $outdir;
+
+ if (-e $outdir) {
+ # Ask user to confirm removal of old output directory if it exists
+ if ($CLI_PROMPT->('mkpatch', 'MKPATCH_OVERWRITE', $outdir) ne 'y') {
+ return _cm_abort();
+ }
+
+ rmtree($outdir) || return _cm_err(FCM1::Cm::Exception->RMTREE, $outdir);
+ }
+
+ # (Re-)create output directory
+ mkpath($outdir) || return _cm_err(FCM1::Cm::Exception->MKPATH, $outdir);
+ $CLI_MESSAGE->('OUT_DIR', $outdir);
+
+ # Get and process log of URL
+ # ----------------------------------------------------------------------------
+ my @script = (); # main output script
+ my %log = $url->svnlog (REV => \@revs);
+ my $url_path = $url->path;
+
+ for my $rev (sort {$a <=> $b} keys %log) {
+ # Look at the changed paths for each revision
+ my $use_patch = 1; # OK to use a patch file?
+ my $only_modified = 1; # Change only contains modifications?
+ my @paths;
+ PATH: for my $path (sort keys %{ $log{$rev}{paths} }) {
+ my $file = $path;
+
+ # Skip paths outside of the branch
+ next PATH unless $file =~ s#^$url_path/##;
+
+ # Skip excluded paths
+ for my $exclude (@exclude) {
+ if ($file =~ m#^$exclude(?:/|$)#) {
+ # Can't use a patch file if any files have been excluded
+ $use_patch = 0;
+ next PATH;
+ }
+ }
+
+ # Can't use a patch file if any files have been added or replaced
+ $use_patch = 0 if $log{$rev}{paths}{$path}{action} eq 'A' or
+ $log{$rev}{paths}{$path}{action} eq 'R';
+
+ $only_modified = 0 unless $log{$rev}{paths}{$path}{action} eq 'M';
+
+ push @paths, $path;
+ }
+
+ # If the change only contains modifications, make sure they aren't
+ # just property changes
+ if ($only_modified) {
+ my @changedpaths;
+ for my $path (@paths) {
+ (my $file = $path) =~ s#^$url_path/*##;
+ my @diff = $SVN->stdout(
+ qw{svn diff --no-diff-deleted --summarize -c}, $rev,
+ sprintf("%s/%s@%s", $url->url(), $file, $rev),
+ );
+ next unless $diff[-1] =~ /^[A-Z]/;
+ push @changedpaths, $path;
+ }
+ @paths = @changedpaths;
+ }
+
+ next unless @paths;
+
+ # Create the patch using "svn diff"
+ my $patch = ();
+ if ($use_patch) {
+ $patch = $SVN->stdout(
+ qw{svn diff --no-diff-deleted -c}, $rev, $url->url(),
+ );
+ if ($patch) {
+ # Don't use the patch if it may contain subversion keywords or
+ # any changes to PDF files or any changes to symbolic links or
+ # any carriage returns in the middle of a line
+ for (split(qr{\n}msx, $patch)) {
+ if (/\$[a-zA-Z:]+ *\$/ or /^--- .+\.pdf\t/ or /^\+link / or /\r.+/) {
+ $use_patch = 0;
+ last;
+ }
+ }
+ } else {
+ $use_patch = 0;
+ }
+ }
+
+ # Create a directory for this revision in the output directory
+ my $outdir_rev = File::Spec->catfile ($outdir, $rev);
+ mkpath($outdir_rev)
+ || return _cm_err(FCM1::Cm::Exception->MKPATH, $outdir_rev);
+
+ # Parse commit log message
+ my @msg = split /\n/, $log{$rev}{msg};
+ for (@msg) {
+ # Re-instate line break
+ $_ .= "\n";
+
+ # Remove line if it matches a merge template
+ $_ = '' if /^Reversed r\d+(?::\d+)? of \S+$/;
+ $_ = '' if /^Custom merge into \S+:.+$/;
+ $_ = '' if /^Merged into \S+: \S+ cf\. \S+$/;
+
+ # Modify Trac ticket link
+ s/(?:#|ticket:)(\d+)/${organisation}_ticket:$1/g;
+
+ # Modify Trac changeset link
+ s/(?:r|changeset:)(\d+)/${organisation}_changeset:$1/g;
+ s/\[(\d+)\]/${organisation}_changeset:$1/g;
+ }
+
+ push @msg, '(' . $organisation . '_changeset:' . $rev . ')' . "\n";
+
+ # Write commit log message in a file
+ my $f_revlog = File::Spec->catfile ($outdir_rev, 'log-message');
+ open FILE, '>', $f_revlog or die $f_revlog, ': cannot open (', $!, ')';
+ print FILE @msg;
+ close FILE or die $f_revlog, ': cannot close (', $!, ')';
+
+ # Handle each changed path
+ my $export_file = 1; # name for next exported file (gets incremented)
+ my $patch_needed = 0; # is a patch file required?
+ my @before_script = (); # patch script to run before patch applied
+ my @after_script = (); # patch script to run after patch applied
+ my @copied_dirs = (); # copied directories
+ CHANGED: for my $path (@paths) {
+ (my $file = $path) =~ s#^$url_path/*##;
+ my $url_file = $url->url . '/' . $file . '@' . $rev;
+
+ # Skip paths within copied directories
+ for my $copied_dir (@copied_dirs) {
+ next CHANGED if $file =~ m#^$copied_dir(?:/|$)#;
+ }
+
+ # Handle deleted files
+ if ($log{$rev}{paths}{$path}{action} eq 'D') {
+ push @after_script, 'svn delete "' . $file . '"';
+
+ } else {
+ # Skip property changes (if not done earlier)
+ if (not $only_modified and $log{$rev}{paths}{$path}{action} eq 'M') {
+ my @diff = $SVN->stdout(
+ qw{svn diff --no-diff-deleted --summarize -c}, $rev, $url_file,
+ );
+ next CHANGED unless $diff[-1] =~ /^[A-Z]/;
+ }
+
+ # Determine if the file is a directory
+ my $is_dir
+ = $log{$rev}{paths}{$path}{action} ne 'M'
+ && $SVN->get_info($url_file)->[0]->{'kind'} eq 'dir';
+
+ # Decide how to treat added files
+ my $export_required = 0;
+ if ($log{$rev}{paths}{$path}{action} eq 'A') {
+ my $is_newfile = 0;
+ # Determine if the file is copied
+ if (exists $log{$rev}{paths}{$path}{'copyfrom-path'}) {
+ if ($is_dir) {
+ # A copied directory needs to be exported and added recursively
+ push @after_script, 'svn add "' . $file . '"';
+ $export_required = 1;
+ push @copied_dirs, $file;
+ } else {
+ # History exists for this file
+ my $copyfrom_path = $log{$rev}{paths}{$path}{'copyfrom-path'};
+ my $copyfrom_rev = $log{$rev}{paths}{$path}{'copyfrom-rev'};
+ my $cp_url = FCM1::CmUrl->new (
+ URL => $url->root . $copyfrom_path . '@' . $copyfrom_rev,
+ );
+
+ if ($copyfrom_path =~ s#^$url_path/*##) {
+ # File is copied from a file under the specified URL
+ # Check source exists
+ $is_newfile = 1 unless $cp_url->url_exists ($rev - 1);
+ } else {
+ # File copied from outside of the specified URL
+ $is_newfile = 1;
+
+ # Check branches can be determined
+ if ($url->branch and $cp_url->branch) {
+
+ # Follow its history, stop on copy
+ my %cp_log = $cp_url->svnlog (STOP_ON_COPY => 1);
+
+ # "First" revision of the copied file
+ my $cp_rev = (sort {$a <=> $b} keys %cp_log) [0];
+ my %attrib = %{ $cp_log{$cp_rev}{paths}{$cp_url->path} }
+ if $cp_log{$cp_rev}{paths}{$cp_url->path};
+
+ # Check whether the "first" revision is copied from elsewhere.
+ if (exists $attrib{'copyfrom-path'}) {
+ # If source exists in the specified URL, set up the copy
+ my $cp_cp_url = FCM1::CmUrl->new (
+ URL => $url->root . $attrib{'copyfrom-path'} . '@' .
+ $attrib{'copyfrom-rev'},
+ );
+ if ($cp_cp_url->subdir()) {
+ $cp_cp_url->url_peg(
+ $cp_cp_url->project_url()
+ . '/' . $url->branch()
+ . '/' . $cp_cp_url->subdir()
+ . '@' . $cp_cp_url->pegrev(),
+ );
+ if ($cp_cp_url->url_exists ($rev - 1)) {
+ ($copyfrom_path = $cp_cp_url->path) =~ s#^$url_path/*##;
+ # Check path is defined - if not it probably means the
+ # branch doesn't follow the FCM naming convention
+ $is_newfile = 0 if $copyfrom_path;
+ }
+ }
+ }
+
+ # Note: The logic above does not cover all cases. However, it
+ # should do the right thing for the most common case. Even
+ # where it gets it wrong the file contents should always be
+ # correct even if the file history is not.
+ }
+ }
+
+ # Check whether file is copied from an excluded or copied path
+ if (not $is_newfile) {
+ for my $path (@exclude, at copied_dirs) {
+ if ($copyfrom_path =~ m#^$path(?:/|$)#) {
+ $is_newfile = 1;
+ last;
+ }
+ }
+ }
+
+ # Check whether file is copied from a file which has been replaced
+ if (not $is_newfile) {
+ my $copyfrom_fullpath = $url->branch_path . "/" . $copyfrom_path;
+ $is_newfile = 1 if ($log{$rev}{paths}{$copyfrom_fullpath}{action} and
+ $log{$rev}{paths}{$copyfrom_fullpath}{action} eq 'R');
+ }
+
+ # Copy the file, if required
+ push @before_script, 'svn copy ' . $copyfrom_path . ' "' . $file . '"'
+ if not $is_newfile;
+ }
+
+ } else {
+ # History does not exist, must be a new file
+ if ($is_dir) {
+ # If it's a directory then create it and add it immediately
+ # (in case it contains any copied files)
+ push @before_script, 'mkdir "' . $file. '"';
+ push @before_script, 'svn add "' . $file . '"';
+ } else {
+ $is_newfile = 1;
+ }
+ }
+
+ # Add the file, if required
+ if ($is_newfile) {
+ push @after_script, 'svn add "' . $file . '"';
+ }
+ }
+
+ if ($is_dir and $log{$rev}{paths}{$path}{action} eq 'R') {
+ # Subversion does not appear to support replacing a directory in a
+ # single transaction from a working copy (other than as the result
+ # of a merge). Therefore the delete of the old directory must be
+ # done in advance as a separate commit.
+ push @script, 'svn delete -m "Delete directory in preparation for' .
+ ' replacing it (part of ' . $organisation . '_changeset:' . $rev .
+ ')" $target/' . $file;
+ push @script, 'svn update --non-interactive';
+ # The replaced directory needs to be exported and added recursively
+ push @after_script, 'svn add "' . $file . '"';
+ $export_required = 1;
+ push @copied_dirs, $file;
+ }
+
+ if (not $is_dir and $log{$rev}{paths}{$path}{action} ne 'A') {
+ my ($was_symlink) = $SVN->stdout(
+ qw{svn propget svn:special},
+ sprintf("%s/%s@%d", $url->url(), $file, ($rev - 1)),
+ );
+ my ($is_symlink) = $SVN->stdout(
+ qw{svn propget svn:special}, $url_file,
+ );
+ if ($was_symlink and not $is_symlink) {
+ # A symbolic link has been changed to a normal file
+ push @before_script, 'svn propdel -q svn:special "' . $file . '"';
+ push @before_script, 'rm "' . $file . '"';
+ } elsif ($log{$rev}{paths}{$path}{action} eq 'R') {
+ # Delete the old file and then add the new file
+ push @before_script, 'svn delete "' . $file . '"';
+ push @after_script, 'svn add "' . $file . '"';
+ } elsif ($is_symlink and not $was_symlink) {
+ # A normal file has been changed to a symbolic link
+ push @after_script, 'svn propset -q svn:special \* "' . $file . '"';
+ } elsif ($is_symlink and $was_symlink) {
+ # If a symbolic link has been modified then remove the old
+ # copy first to allow the copy to work
+ push @before_script, 'rm "' . $file . '"';
+ }
+ }
+
+ # Decide whether the file needs to be exported
+ if (not $is_dir) {
+ if (not $use_patch) {
+ $export_required = 1;
+ } else {
+ # Export the file if it is binary
+ my @file_diff = $SVN->stdout(
+ qw{svn diff --no-diff-deleted -c}, $rev, $url_file,
+ );
+ for (@file_diff) {
+ $export_required = 1 if /Cannot display: file marked as a binary type./;
+ }
+ # Only create a patch file if necessary
+ $patch_needed = 1 if not $export_required;
+ }
+ }
+
+ if ($export_required) {
+ # Download the file using "svn export"
+ my $export = File::Spec->catfile ($outdir_rev, $export_file);
+ $SVN->call(qw{export -q -r}, $rev, $url_file, $export);
+
+ # Copy the exported file into the file
+ push @before_script,
+ 'cp -r ${fcm_patch_dir}/' . $export_file . ' "' . $file . '"';
+ $export_file++;
+ }
+ }
+ }
+
+ # Write the patch file
+ if ($patch_needed) {
+ my $patchfile = File::Spec->catfile ($outdir_rev, 'patchfile');
+ open FILE, '>', $patchfile
+ or die $patchfile, ': cannot open (', $!, ')';
+ print FILE $patch;
+ close FILE or die $patchfile, ': cannot close (', $!, ')';
+ }
+
+ # Add line break to each line in @before_script and @after_script
+ @before_script = map {($_ ? $_ . ' || exit 1' . "\n" : "\n")}
+ @before_script if (@before_script);
+ @after_script = map {($_ ? $_ . ' || exit 1' . "\n" : "\n")}
+ @after_script if (@after_script);
+
+ # Write patch script to output
+ my $out = File::Spec->catfile ($outdir_rev, 'apply-patch');
+ open FILE, '>', $out or die $out, ': cannot open (', $!, ')';
+
+ # Script header
+ print FILE <<EOF;
+#!/usr/bin/env bash
+# ------------------------------------------------------------------------------
+# NAME
+# apply-patch
+#
+# DESCRIPTION
+# This script is generated automatically by the "fcm mkpatch" command. It
+# applies the patch to the current working directory which must be a working
+# copy of a valid project tree that can accept the import of the patches.
+#
+# Patch created from $organisation URL: $u
+# Changeset: $rev
+# ------------------------------------------------------------------------------
+
+this=`basename \$0`
+echo "\$this: Applying patch for changeset $rev."
+
+# Location of the patch, base on the location of this script
+cd `dirname \$0` || exit 1
+fcm_patch_dir=\$PWD
+
+# Change directory back to the working copy
+cd \$OLDPWD || exit 1
+
+# Check working copy does not have local changes
+status=`svn status`
+if [[ -n \$status ]]; then
+ echo "\$this: working copy contains changes, abort." >&2
+ exit 1
+fi
+if [[ -a "#commit_message#" ]]; then
+ echo "\$this: existing commit message in "#commit_message#", abort." >&2
+ exit 1
+fi
+
+# Apply the changes
+patch_command=\${patch_command:-"patch --no-backup-if-mismatch -p0"}
+EOF
+
+ # Script content
+ print FILE @before_script if @before_script;
+ print FILE "\$patch_command <\${fcm_patch_dir}/patchfile || exit 1\n"
+ if $patch_needed;
+ print FILE @after_script if @after_script;
+
+ # Script footer
+ print FILE <<EOF;
+
+# Copy in the commit message
+cp \${fcm_patch_dir}/log-message "#commit_message#"
+
+echo "\$this: finished normally."
+#EOF
+EOF
+
+ close FILE or die $out, ': cannot close (', $!, ')';
+
+ # Add executable permission
+ chmod 0755, $out;
+
+ # Script to commit the change
+ push @script, '${fcm_patches_dir}/' . $rev . '/apply-patch';
+ push @script, 'svn commit -F "#commit_message#"';
+ push @script, 'rm -f "#commit_message#"';
+ push @script, 'svn update --non-interactive';
+ push @script, '';
+
+ $CLI_MESSAGE->('PATCH_REV', $rev);
+ }
+
+ # Write the main output script if necessary. Otherwise remove output directory
+ # ----------------------------------------------------------------------------
+ if (@script) {
+ # Add line break to each line in @script
+ @script = map {($_ ? $_ . ' || exit 1' . "\n" : "\n")} @script;
+
+ # Write script to output
+ my $out = File::Spec->catfile ($outdir, 'fcm-import-patch');
+ open FILE, '>', $out or die $out, ': cannot open (', $!, ')';
+
+ # Script header
+ print FILE <<EOF;
+#!/usr/bin/env bash
+# ------------------------------------------------------------------------------
+# NAME
+# fcm-import-patch
+#
+# SYNOPSIS
+# fcm-import-patch TARGET
+#
+# DESCRIPTION
+# This script is generated automatically by the "fcm mkpatch" command, as are
+# the revision "patches" created in the same directory. The script imports the
+# patches into TARGET, which must either be a URL or a working copy of a valid
+# project tree that can accept the import of the patches.
+#
+# Patch created from $organisation URL: $u
+# ------------------------------------------------------------------------------
+
+this=`basename \$0`
+
+# Check argument
+target=\$1
+
+# First argument must be a URL or working copy
+if [[ -z \$target ]]; then
+ echo "\$this: the first argument must be a URL or a working copy, abort." >&2
+ exit 1
+fi
+
+if [[ \$target == svn://* || \$target == svn+ssh://* || \\
+ \$target == http://* || \$target == https://* || \\
+ \$target == file://* ]]; then
+ # A URL, checkout a working copy in a temporary location
+ fcm_tmp_dir=`mktemp -d \${TMPDIR:=/tmp}/\$this.XXXXXX`
+ fcm_working_copy=\$fcm_tmp_dir
+ svn checkout -q \$target \$fcm_working_copy || exit 1
+else
+ fcm_working_copy=\$target
+ target=`svn info \$fcm_working_copy | grep "^URL: " | sed 's/URL: //'` || exit 1
+fi
+
+# Location of the patches, base on the location of this script
+cd `dirname \$0` || exit 1
+fcm_patches_dir=\$PWD
+
+# Change directory to the working copy
+cd \$fcm_working_copy || exit 1
+
+# Set the language to avoid encoding problems
+if locale -a | grep -q en_GB\$; then
+ export LANG=en_GB
+fi
+
+# Commands to apply patches
+EOF
+
+ # Script content
+ print FILE @script;
+
+ # Script footer
+ print FILE <<EOF;
+# Check working copy does not have local changes
+status=`svn status`
+if [[ -n \$status ]]; then
+ echo "\$this: working copy still contains changes, abort." >&2
+ exit 1
+fi
+
+# Remove temporary working copy, if necessary
+if [[ -d \$fcm_tmp_dir && -w \$fcm_tmp_dir ]]; then
+ rm -rf \$fcm_tmp_dir
+fi
+
+echo "\$this: finished normally."
+#EOF
+EOF
+
+ close FILE or die $out, ': cannot close (', $!, ')';
+
+ # Add executable permission
+ chmod 0755, $out;
+
+ # Diagnostic
+ $CLI_MESSAGE->('PATCH_DONE', $outdir);
+
+ } else {
+ # Remove output directory
+ rmtree $outdir or die $outdir, ': cannot remove';
+
+ # Diagnostic
+ return _cm_abort(FCM1::Cm::Abort->NULL);
+ }
+
+ return 1;
+}
+
+# ------------------------------------------------------------------------------
+# CLI error.
+sub _cli_err {
+ my ($key, @args) = @_;
+ my $message = sprintf($CLI_MESSAGE_FOR_ERROR{$key}, @args);
+ die(FCM1::CLI::Exception->new({message => $message}));
+}
+
+# ------------------------------------------------------------------------------
+# The default handler of the "WC_STATUS" event.
+sub _cli_handler_of_wc_status {
+ my ($name, $target_list_ref, $status_list_ref) = @_;
+ $target_list_ref ||= [q{.}];
+ if (@{$status_list_ref}) {
+ $CLI_MESSAGE->(
+ 'STATUS',
+ $name,
+ q{"} . join(q{", "}, @{$target_list_ref}) . q{"},
+ join("\n", @{$status_list_ref}),
+ );
+ if ($CLI_PROMPT->($name, 'CONTINUE', $name) ne 'y') {
+ return _cm_abort();
+ }
+ }
+ return @{$status_list_ref};
+}
+
+# ------------------------------------------------------------------------------
+# The default handler of the "WC_STATUS_PATH" event.
+sub _cli_handler_of_wc_status_path {
+ my ($name, $target_list_ref, $status_list_ref) = @_;
+ my $message
+ = @{$status_list_ref} ? (join("\n", @{$status_list_ref}) . "\n") : q{};
+ $CLI_MESSAGE->(q{}, $message);
+ my @paths = map {chomp(); ($_ =~ $PATTERN_OF{ST_PATH})} @{$status_list_ref};
+ my @paths_of_interest;
+ while (my $path = shift(@paths)) {
+ my %handler_of = (
+ a => sub {push(@paths_of_interest, $path, @paths); @paths = ()},
+ n => sub {},
+ y => sub {push(@paths_of_interest, $path)},
+ );
+ my $reply = $CLI_PROMPT->(
+ {type => 'yna'}, $name, 'RUN_SVN_COMMAND', "$name $path",
+ );
+ $handler_of{$reply}->();
+ }
+ return @paths_of_interest;
+}
+
+# ------------------------------------------------------------------------------
+# Expands location keywords in a list.
+sub _cli_keyword_expand_url {
+ my ($arg_list_ref) = @_;
+ ARG:
+ for my $arg (@{$arg_list_ref}) {
+ my ($label, $value) = ($arg =~ $PATTERN_OF{CLI_OPT});
+ if (!$label) {
+ ($label, $value) = (q{}, $arg);
+ }
+ if (!$value) {
+ next ARG;
+ }
+ eval {
+ $value = FCM1::Util::tidy_url(FCM1::Keyword::expand($value));
+ };
+ if ($@) {
+ if ($value ne 'fcm:revision') {
+ die($@);
+ }
+ }
+ $arg = $label . $value;
+ }
+}
+
+# ------------------------------------------------------------------------------
+# Expands revision keywords in -r and --revision options in a list.
+sub _cli_keyword_expand_rev {
+ my ($arg_list_ref) = @_;
+ my @targets;
+ for my $arg (@{$arg_list_ref}) {
+ if (-e $arg && is_wc($arg) || is_url($arg)) {
+ push(@targets, $arg);
+ }
+ }
+ if (!@targets) {
+ push(@targets, get_url_of_wc());
+ }
+ if (!@targets) {
+ return;
+ }
+ my @old_arg_list = @{$arg_list_ref};
+ my @new_arg_list = ();
+ ARG:
+ while (defined(my $arg = shift(@old_arg_list))) {
+ my ($key, $value) = $arg =~ $PATTERN_OF{CLI_OPT_REV};
+ if (!$key) {
+ push(@new_arg_list, $arg);
+ next ARG;
+ }
+ push(@new_arg_list, '--revision');
+ if (!$value) {
+ $value = shift(@old_arg_list);
+ }
+ my @revs = grep {defined()} ($value =~ $PATTERN_OF{CLI_OPT_REV_RANGE});
+ my ($url, @url_list) = @targets;
+ for my $rev (@revs) {
+ if ($rev !~ $PATTERN_OF{SVN_REV}) {
+ $rev = (FCM1::Keyword::expand($url, $rev))[1];
+ }
+ if (@url_list) {
+ $url = shift(@url_list);
+ }
+ }
+ push(@new_arg_list, join(q{:}, @revs));
+ }
+ @{$arg_list_ref} = @new_arg_list;
+}
+
+# ------------------------------------------------------------------------------
+# Prints a message.
+sub _cli_message {
+ my ($key, @args) = @_;
+ for (
+ [\*STDOUT, \%CLI_MESSAGE_FOR , q{} ],
+ [\*STDERR, \%CLI_MESSAGE_FOR_WARNING, q{[WARNING] }],
+ [\*STDERR, \%CLI_MESSAGE_FOR_ABORT , q{[ABORT] } ],
+ [\*STDERR, \%CLI_MESSAGE_FOR_ERROR , q{[ERROR] } ],
+ ) {
+ my ($handle, $hash_ref, $prefix) = @{$_};
+ if (exists($hash_ref->{$key})) {
+ return printf({$handle} $prefix . $hash_ref->{$key}, @args);
+ }
+ }
+}
+
+# ------------------------------------------------------------------------------
+# Wrapper for FCM1::Interactive::get_input.
+sub _cli_prompt {
+ my %option
+ = (type => 'yn', default => 'n', (ref($_[0]) ? %{shift(@_)} : ()));
+ my ($name, $key, @args) = @_;
+ return FCM1::Interactive::get_input(
+ title => $CLI_PROMPT_PREFIX . $name,
+ message => sprintf($CLI_MESSAGE_FOR_PROMPT{$key}, @args),
+ %option,
+ );
+}
+
+# ------------------------------------------------------------------------------
+# Check missing status and delete.
+sub cm_check_missing {
+ my %option = %{shift()};
+ my $checker
+ = _svn_status_checker('delete', 'MISSING', $option{st_check_handler});
+ my @paths = $checker->(\@_);
+ if (@paths) {
+ $SVN->call('delete', @paths);
+ }
+}
+
+# ------------------------------------------------------------------------------
+# Check unknown status and add.
+sub cm_check_unknown {
+ my %option = %{shift()};
+ my $checker
+ = _svn_status_checker('add', 'UNKNOWN', $option{st_check_handler});
+ my @paths = $checker->(\@_);
+ if (@paths) {
+ $SVN->call('add', @paths);
+ }
+}
+
+# ------------------------------------------------------------------------------
+# FCM wrapper to SVN switch.
+sub cm_switch {
+ my %option = %{shift()};
+ my ($source, $path) = @_;
+ $path ||= cwd();
+ if (!$source) {
+ return _cm_err(FCM1::Cm::Exception->INVALID_TARGET, q{});
+ }
+ if (!-e $path) {
+ return _cm_err(FCM1::Cm::Exception->NOT_EXIST, $path);
+ }
+ if (!is_wc($path)) {
+ return _cm_err(FCM1::Cm::Exception->INVALID_WC, $path);
+ }
+
+ # Check for merge template in the commit log file in the working copy
+ my $path_of_wc = $SVN->get_wc_root($path);
+ my $commit_message_file = $COMMIT_MESSAGE_UTIL->path($path_of_wc);
+ my $commit_message_ctx = $COMMIT_MESSAGE_UTIL->load($commit_message_file);
+ if ($commit_message_ctx->get_auto_part()) {
+ return _cm_err(
+ FCM1::Cm::Exception->SWITCH_UNSAFE,
+ ($path eq $path_of_wc
+ ? File::Spec->abs2rel($commit_message_file)
+ : $commit_message_file
+ ),
+ );
+ }
+
+ # Check for any local modifications
+ if (defined($option{st_check_handler})) {
+ _svn_status_checker('switch', 'MODIFIED', $option{st_check_handler})->(
+ [$path_of_wc],
+ );
+ }
+
+ my @targets = $path_of_wc eq cwd() ? () : ($path_of_wc);
+ $SVN->call('cleanup', @targets);
+ $SVN->call(
+ 'switch',
+ '--non-interactive',
+ ($option{revision} ? ('-r', $option{revision}) : ()),
+ ($option{quiet} ? '--quiet' : ()),
+ _cm_get_source(
+ $source,
+ FCM1::CmBranch->new(URL => $path_of_wc),
+ )->url_peg(),
+ @targets,
+ );
+}
+
+# ------------------------------------------------------------------------------
+# FCM wrapper to SVN update.
+sub cm_update {
+ my %option = %{shift()};
+ my @targets = @_;
+ if (!@targets) {
+ @targets = (cwd());
+ }
+ for my $target (@targets) {
+ if (!-e $target) {
+ return _cm_err(FCM1::Cm::Exception->NOT_EXIST, $target);
+ }
+ if (!is_wc($target)) {
+ return _cm_err(FCM1::Cm::Exception->INVALID_WC, $target);
+ }
+ $target = $SVN->get_wc_root($target);
+ if ($target eq cwd()) {
+ $target = q{.};
+ }
+ }
+ if (defined($option{st_check_handler})) {
+ my ($matcher_keys_ref, $show_updates)
+ = defined($option{revision}) ? (['MODIFIED' ], undef)
+ : (['MODIFIED', 'OOD'], 1 )
+ ;
+ my $matcher = sub {
+ for my $key (@{$matcher_keys_ref}) {
+ $ST_MATCHER_FOR{$key}->(@_) && return 1;
+ }
+ };
+ _svn_status_checker(
+ 'update', $matcher, $option{st_check_handler}, $show_updates,
+ )->(\@targets);
+ }
+ if ($option{revision} && $option{revision} !~ $PATTERN_OF{SVN_REV}) {
+ $option{revision} = (
+ FCM1::Keyword::expand(get_url_of_wc($targets[0]), $option{revision})
+ )[1];
+ }
+ _svn_update(\@targets, \%option);
+}
+
+# ------------------------------------------------------------------------------
+# Raises an abort exception.
+sub _cm_abort {
+ my ($code) = @_;
+ $code ||= FCM1::Cm::Abort->USER;
+ die(bless({code => $code, message => 'abort'}, 'FCM1::Cm::Abort'));
+}
+
+# ------------------------------------------------------------------------------
+# Raises a failure.
+sub _cm_err {
+ my ($code, @targets) = @_;
+ die(bless(
+ {code => $code, message => "ERROR: $code", targets => \@targets},
+ 'FCM1::Cm::Exception',
+ ));
+}
+
+# ------------------------------------------------------------------------------
+# Return a corresponding FCM1::CmBranch instance for $source_url w.r.t. $target.
+sub _cm_get_source {
+ my ($source_url, $target) = @_;
+ if (!$UTIL->uri_match($source_url)) {
+ # Source not full URL, construct source URL based on target URL
+ my ($path, $peg) = $source_url =~ qr{\A(.*?)(@[^@/]+)?\z}msx;
+ my $project = $target->project_path();
+ if (index($path, $project . '/') == 0) {
+ # $path contains the full path under the repository root
+ $path = substr($path, length($project));
+ }
+ my %layout_config = %{$target->layout()->get_config()};
+ if (!grep {!defined($layout_config{"dir-$_"})} qw{trunk branch tag}) {
+ # $path must be under the specified sub-directories for the trunk,
+ # branches and tags
+ my @dirs = map {$layout_config{"dir-$_"}} qw{trunk branch tag};
+ my @paths = split(qr{/+}msx, $path);
+ if (!@paths || !grep {$_ eq $paths[0]} @dirs) {
+ $path = $layout_config{'dir-branch'} . '/' . $path;
+ }
+ }
+ $peg ||= q{};
+ $source_url = join('/', $target->project_url(), $path) . $peg;
+ }
+ my $source = FCM1::CmBranch->new(URL => $source_url);
+ my $layout = eval {$source->layout()};
+ if ($@) {
+ $@ = undef;
+ return _cm_err(FCM1::Cm::Exception->INVALID_URL, $source_url);
+ }
+ if (!$layout->get_branch()) {
+ return _cm_err(FCM1::Cm::Exception->INVALID_BRANCH, $source_url);
+ }
+ $source->url_peg(
+ $source->branch_url() . '/' . $target->subdir() . '@' . $source->pegrev()
+ );
+ # Ensure that the source and target URLs are in the same project
+ if ($source->project_url() ne $target->project_url()) {
+ return _cm_err(
+ FCM1::Cm::Exception->DIFF_PROJECTS,
+ $target->url_peg(),
+ $source->url_peg(),
+ );
+ }
+ return $source;
+}
+
+# ------------------------------------------------------------------------------
+# Returns the results of "svn status".
+sub _svn_status_get {
+ my ($targets_ref, $show_updates) = @_;
+ my @targets = (defined($targets_ref) ? @{$targets_ref} : ());
+ for my $target (@targets) {
+ if ($target eq cwd()) {
+ $target = q{.};
+ }
+ }
+ my @options = ($show_updates ? qw{--show-updates} : ());
+ $SVN->stdout(qw{svn status}, @options, @targets);
+}
+
+# ------------------------------------------------------------------------------
+# Returns a "svn status" checker.
+sub _svn_status_checker {
+ my ($name, $matcher, $handler, $show_updates) = @_;
+ if (!ref($matcher)) {
+ $matcher = $ST_MATCHER_FOR{$matcher};
+ }
+ my $P = $PATTERN_OF{ST_PATH};
+ sub {
+ my ($targets_ref) = @_;
+ my @status = _svn_status_get($targets_ref, $show_updates);
+ if ($show_updates) {
+ @status = map {$_ =~ $PATTERN_OF{ST_AGAINST_REV} ? () : $_} @status;
+ }
+ my @status_of_interest = grep {$matcher->($_)} @status;
+ # Note: for future expansion...
+ #my @paths;
+ #if (!$show_updates) {
+ # @paths = map {chomp(); $_} map {($_ =~ $P)} @status_of_interest;
+ #}
+ #defined($handler)
+ #? $handler->($name, $targets_ref, \@status_of_interest, \@paths)
+ #: @status_of_interest
+ #;
+ defined($handler)
+ ? $handler->($name, $targets_ref, \@status_of_interest)
+ : @status_of_interest
+ ;
+ };
+}
+
+# ------------------------------------------------------------------------------
+# Runs "svn update".
+sub _svn_update {
+ my ($targets_ref, $option_hash_ref) = @_;
+ my %option = (defined($option_hash_ref) ? %{$option_hash_ref} : ());
+ my @targets = defined($targets_ref) ? @{$targets_ref} : ();
+ $SVN->call('cleanup', @targets);
+ $SVN->call(
+ 'update',
+ '--non-interactive',
+ ($option{revision} ? ('-r', $option{revision}) : ()),
+ ($option{quiet} ? '--quiet' : ()),
+ @targets,
+ );
+}
+
+# ------------------------------------------------------------------------------
+# CLI exception.
+package FCM1::CLI::Exception;
+use base qw{FCM1::Exception};
+
+# ------------------------------------------------------------------------------
+# Abort exception.
+package FCM1::Cm::Abort;
+use base qw{FCM1::Exception};
+use constant {FAIL => 'FAIL', NULL => 'NULL', USER => 'USER'};
+
+sub get_code {
+ return $_[0]->{code};
+}
+
+# ------------------------------------------------------------------------------
+# Resource exception.
+package FCM1::Cm::Exception;
+our @ISA = qw{FCM1::Cm::Abort};
+use constant {
+ CHDIR => 'CHDIR',
+ DIFF_PROJECTS => 'DIFF_PROJECTS',
+ INVALID_BRANCH => 'INVALID_BRANCH',
+ INVALID_PROJECT => 'INVALID_PROJECT',
+ INVALID_TARGET => 'INVALID_TARGET',
+ INVALID_URL => 'INVALID_URL',
+ INVALID_WC => 'INVALID_WC',
+ MERGE_REV_INVALID => 'MERGE_REV_INVALID',
+ MERGE_SELF => 'MERGE_SELF',
+ MERGE_UNRELATED => 'MERGE_UNRELATED',
+ MERGE_UNSAFE => 'MERGE_UNSAFE',
+ MKPATH => 'MKPATH',
+ NOT_EXIST => 'NOT_EXIST',
+ PARENT_NOT_EXIST => 'PARENT_NOT_EXIST',
+ RMTREE => 'RMTREE',
+ SWITCH_UNSAFE => 'SWITCH_UNSAFE',
+ WC_INVALID_BRANCH => 'WC_INVALID_BRANCH',
+ WC_URL_NOT_EXIST => 'WC_URL_NOT_EXIST',
+};
+
+sub get_targets {
+ return @{$_[0]->{targets}};
+}
+
+1;
+__END__
+
+=pod
+
+=head1 NAME
+
+FCM1::Cm
+
+=head1 SYNOPSIS
+
+ use FCM1::Cm qw{cm_check_missing cm_check_unknown cm_switch cm_update};
+
+ # Checks status for "missing" items and "svn delete" them
+ $missing_st_handler = sub {
+ my ($name, $targets_ref, $status_list_ref) = @_;
+ # ...
+ return @paths_of_interest;
+ };
+ cm_check_missing({st_check_handler => $missing_st_handler}, @targets);
+
+ # Checks status for "unknown" items and "svn add" them
+ $unknown_st_handler = sub {
+ my ($name, $targets_ref, $status_list_ref) = @_;
+ # ...
+ return @paths_of_interest;
+ };
+ cm_check_unknown({st_check_handler => $unknown_st_handler}, @targets);
+
+ # Sets up a status checker
+ $st_check_handler = sub {
+ my ($name, $targets_ref, $status_list_ref) = @_;
+ # ...
+ };
+ # Switches a "working copy" at the "root" level to a new URL target
+ cm_switch(
+ {
+ 'non-interactive' => $non_interactive_flag,
+ 'quiet' => $quiet_flag,
+ 'revision' => $revision,
+ 'st_check_handler' => $st_check_handler,
+ },
+ $target, $path_of_wc,
+ );
+ # Runs "svn update" on each working copy from their "root" level
+ cm_update(
+ {
+ 'non-interactive' => $non_interactive_flag,
+ 'quiet' => $quiet_flag,
+ 'revision' => $revision,
+ 'st_check_handler' => $st_check_handler,
+ },
+ @targets,
+ );
+
+=head1 DESCRIPTION
+
+Wraps the Subversion client and implements other FCM code management
+functionalities.
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item cm_check_missing(\%option, at targets)
+
+Use "svn status" to check for missing items in @targets. If @targets is an empty
+list, the function adds the current working directory to it. Expects
+$option{st_check_handler} to be a CODE reference. Calls
+$option{st_check_handler} with ($name, $targets_ref, $status_list_ref) where
+$name is "delete", $targets_ref is \@targets, and $status_list_ref is an
+ARRAY reference to a list of "svn status" output with the "missing" status.
+$option{st_check_handler} should return a list of interesting paths, which will
+be scheduled for removal using "svn delete".
+
+=item cm_check_unknown(\%option, at targets)
+
+Similar to cm_check_missing(\%option, at targets) but checks for "unknown" items,
+which will be scheduled for addition using "svn add".
+
+=item cm_switch(\%option,$target,$path_of_wc)
+
+Invokes "svn switch" at the root of a working copy specified by $path_of_wc (or
+the current working directory if $path_of_wc is not specified).
+$option{'non-interactive'}, $option{quiet}, $option{revision} determines the
+options (of the same name) that are passed to "svn switch". If
+$option{st_check_handler} is set, it should be a CODE reference, and will be
+called with ('switch', [$path_of_wc], $status_list_ref), where $status_list_ref
+is an ARRAY reference to the output returned by "svn status" on $path_of_wc.
+This can be used for the application to display the working copy status to the
+user before prompting him/her to continue. The return value of
+$option{st_check_handler} is ignored.
+
+=item cm_update(\%option, at targets)
+
+Invokes "svn update" at the root of each working copy specified by @targets. If
+ at targets is an empty list, the function adds the current working directory to
+it. $option{'non-interactive'}, $option{quiet}, $option{revision} determines the
+options (of the same name) that are passed to "svn update". If
+$option{st_check_handler} is set, it should be a CODE reference, and will be
+called with ($name, $targets_ref, $status_list_ref), where $name is
+'update', $targets_ref is \@targets and $status_list_ref is an ARRAY
+reference to the output returned by "svn status -u" on the @targets. This can be
+used for the application to display the working copy update status to the user
+before prompting him/her to continue. The return value of
+$option{st_check_handler} is ignored.
+
+=back
+
+=head1 DIAGNOSTICS
+
+The following exceptions can be raised:
+
+=over 4
+
+=item FCM1::Cm::Abort
+
+This exception @ISA L<FCM1::Exception|FCM1::Exception>. It is raised if a command
+is aborted for some reason. The $e->get_code() method can be used to retrieve an
+error code, which can be one of the following:
+
+=over 4
+
+=item $e->FAIL
+
+The command aborts because of a failure.
+
+=item $e->NULL
+
+The command aborts because it will result in no change.
+
+=item $e->USER
+
+The command aborts because of an action by the user.
+
+=back
+
+=item FCM1::Cm::Exception
+
+This exception @ISA L<FCM1::Abort|FCM1::Abort>. It is raised if a command fails
+with a known reason. The $e->get_targets() method can be used to retrieve a list
+of targets/resources associated with this exception. The $e->get_code() method
+can be used to retrieve an error code, which can be one of the following:
+
+=over 4
+
+=item $e->CHDIR
+
+Fails to change directory to a target.
+
+=item $e->INVALID_BRANCH
+
+A target is not a valid branch URL in the standard FCM project layout.
+
+=item $e->INVALID_PROJECT
+
+A target is not a valid project URL in the standard FCM project layout.
+
+=item $e->INVALID_TARGET
+
+A target is not a valid Subversion URL or working copy.
+
+=item $e->INVALID_URL
+
+A target is not a valid Subversion URL.
+
+=item $e->INVALID_WC
+
+A target is not a valid Subversion working copy.
+
+=item $e->MERGE_REV_INVALID
+
+An invalid revision (target element 0) is specified for a merge.
+
+=item $e->MERGE_SELF
+
+Attempt to merge a URL (target element 0) to its own working copy (target
+element 1).
+
+=item $e->MERGE_UNRELATED
+
+The merge target (target element 0) is not directly related to the merge source
+(target element 1).
+
+=item $e->MERGE_UNSAFE
+
+A merge source (target element 0) contains changes outside the target
+sub-directory.
+
+=item $e->MKPATH
+
+Fail to create a directory (target element 0) recursively.
+
+=item $e->NOT_EXIST
+
+A target does not exist.
+
+=item $e->PARENT_NOT_EXIST
+
+The parent of the target no longer exists.
+
+=item $e->RMTREE
+
+Fail to remove a directory (target element 0) recursively.
+
+=item $e->SWITCH_UNSAFE
+
+A merge template exists in the commit message file (target element 0) in a
+working copy target.
+
+=item $e->WC_INVALID_BRANCH
+
+The URL of the target working copy is not a valid branch URL in the standard FCM
+project layout.
+
+=item $e->WC_URL_NOT_EXIST
+
+The URL of the target working copy no longer exists at the HEAD revision.
+
+=back
+
+=back
+
+=head1 TO DO
+
+Reintegrate with L<FCM1::CmUrl|FCM1::CmUrl> and L<FCM1::CmBranch|FCM1::CmBranch>,
+but separate this module into the CLI part and the CM part. Expose the remaining
+CM functions when this is done.
+
+Use L<SVN::Client|SVN::Client> to interface with Subversion.
+
+Move C<mkpatch> out of this module.
+
+=head1 COPYRIGHT
+
+E<169> Crown copyright Met Office. All rights reserved.
+
+=cut
+opy already exists.
+
+=item $e->WC_INVALID_BRANCH
+
+The URL of the target working copy is not a valid branch URL in the standard FCM
+project layout.
+
+=item $e->WC_URL_NOT_EXIST
+
+The URL of the target working copy no longer exists at the HEAD revision.
+
+=back
+
+=back
+
+=head1 TO DO
+
+Reintegrate with L<FCM1::CmUrl|FCM1::CmUrl> and L<FCM1::CmBranch|FCM1::CmBranch>,
+but separate this module into the CLI part and the CM part. Expose the remaining
+CM functions when this is done.
+
+Use L<SVN::Client|SVN::Client> to interface with Subversion.
+
+Move C<mkpatch> out of this module.
+
+=head1 COPYRIGHT
+
+E<169> Crown copyright Met Office. All rights reserved.
+
+=cut
+$e->CHDIR
+
+Fails to change directory to a target.
+
+=item $e->INVALID_BRANCH
+
+A target is not a valid branch URL in the standard FCM project layout.
+
+=item $e->INVALID_PROJECT
+
+A target is not a valid project URL in the standard FCM project layout.
+
+=item $e->INVALID_TARGET
+
+A target is not a valid Subversion URL or working copy.
+
+=item $e->INVALID_URL
+
+A target is not a valid Subversion URL.
+
+=item $e->INVALID_WC
+
+A target is not a valid Subversion working copy.
+
+=item $e->MERGE_REV_INVALID
+
+An invalid revision (target element 0) is specified for a merge.
+
+=item $e->MERGE_SELF
+
+Attempt to merge a URL (target element 0) to its own working copy (target
+element 1).
+
+=item $e->MERGE_UNRELATED
+
+The merge target (target element 0) is not directly related to the merge source
+(target element 1).
+
+=item $e->MERGE_UNSAFE
+
+A merge source (target element 0) contains changes outside the target
+sub-directory.
+
+=item $e->MKPATH
+
+Fail to create a directory (target element 0) recursively.
+
+=item $e->NOT_EXIST
+
+A target does not exist.
+
+=item $e->PARENT_NOT_EXIST
+
+The parent of the target no longer exists.
+
+=item $e->RMTREE
+
+Fail to remove a directory (target element 0) recursively.
+
+=item $e->SWITCH_UNSAFE
+
+A merge template exists in the commit message file (target element 0) in a
+working copy target.
+
+=item $e->WC_INVALID_BRANCH
+
+The URL of the target working copy is not a valid branch URL in the standard FCM
+project layout.
+
+=item $e->WC_URL_NOT_EXIST
+
+The URL of the target working copy no longer exists at the HEAD revision.
+
+=back
+
+=back
+
+=head1 TO DO
+
+Migrate to FCM::System hierarchy.
+
+Move C<mkpatch> out of this module.
+
+=head1 COPYRIGHT
+
+E<169> Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM1/CmBranch.pm b/lib/FCM1/CmBranch.pm
new file mode 100644
index 0000000..075d6cf
--- /dev/null
+++ b/lib/FCM1/CmBranch.pm
@@ -0,0 +1,1009 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# NAME
+# FCM1::CmBranch
+#
+# DESCRIPTION
+# This class contains methods for manipulating a branch. It is a sub-class of
+# FCM1::CmUrl, and inherits all methods from that class.
+#
+# ------------------------------------------------------------------------------
+
+package FCM1::CmBranch;
+use base qw{FCM1::CmUrl};
+
+use strict;
+use warnings;
+
+use FCM1::Config;
+use FCM1::Interactive;
+use FCM1::Keyword;
+use FCM1::Util qw/e_report w_report svn_date/;
+
+my @properties = (
+ 'CREATE_REV', # revision at which the branch is created
+ 'DELETE_REV', # revision at which the branch is deleted
+ 'PARENT', # reference to parent branch FCM1::CmBranch
+ 'ANCESTOR', # list of common ancestors with other branches
+ # key = URL, value = ancestor FCM1::CmBranch
+ 'LAST_MERGE', # list of last merges from branches
+ # key = URL at REV, value = [TARGET, UPPER, LOWER]
+ 'AVAIL_MERGE', # list of available revisions for merging
+ # key = URL at REV, value = [REV ...]
+ 'CHILDREN', # list of children of this branch
+ 'SIBLINGS', # list of siblings of this branch
+);
+
+# Set the commit message utility provided by FCM::System::CM.
+our $COMMIT_MESSAGE_UTIL;
+sub set_commit_message_util {
+ $COMMIT_MESSAGE_UTIL = shift();
+}
+
+# Set the SVN utility provided by FCM::System::CM.
+our $SVN;
+sub set_svn_util {
+ $SVN = shift();
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $cm_branch = FCM1::CmBranch->new (URL => $url,);
+#
+# DESCRIPTION
+# This method constructs a new instance of the FCM1::CmBranch class.
+#
+# ARGUMENTS
+# URL - URL of a branch
+# ------------------------------------------------------------------------------
+
+sub new {
+ my $this = shift;
+ my %args = @_;
+ my $class = ref $this || $this;
+
+ my $self = FCM1::CmUrl->new (%args);
+
+ $self->{$_} = undef for (@properties);
+
+ bless $self, $class;
+ return $self;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $url = $cm_branch->url_peg;
+# $cm_branch->url_peg ($url);
+#
+# DESCRIPTION
+# This method returns/sets the current URL.
+# ------------------------------------------------------------------------------
+
+sub url_peg {
+ my $self = shift;
+
+ if (@_) {
+ if (! $self->{URL} or $_[0] ne $self->{URL}) {
+ # Re-set URL and other essential variables in the SUPER-class
+ $self->SUPER::url_peg (@_);
+
+ # Re-set essential variables
+ $self->{$_} = undef for (@properties);
+ }
+ }
+
+ return $self->{URL};
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rev = $cm_branch->create_rev;
+#
+# DESCRIPTION
+# This method returns the revision at which the branch was created.
+# ------------------------------------------------------------------------------
+
+sub create_rev {
+ my $self = shift;
+
+ if (not $self->{CREATE_REV}) {
+ return unless $self->url_exists ($self->pegrev);
+
+ # Use "svn log" to find out the first revision of the branch
+ my %log = $self->svnlog (STOP_ON_COPY => 1);
+
+ # Look at log in ascending order
+ my $rev = (sort {$a <=> $b} keys %log) [0];
+ my $paths = $log{$rev}{paths};
+
+ # Get revision when URL is first added to the repository
+ if (exists $paths->{$self->branch_path}) {
+ $self->{CREATE_REV} = $rev if $paths->{$self->branch_path}{action} eq 'A';
+ }
+ }
+
+ return $self->{CREATE_REV};
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $parent = $cm_branch->parent;
+#
+# DESCRIPTION
+# This method returns the parent (a FCM1::CmBranch object) of the current
+# branch.
+# ------------------------------------------------------------------------------
+
+sub parent {
+ my $self = shift;
+
+ if (not $self->{PARENT}) {
+ # Use the log to find out the parent revision
+ my %log = $self->svnlog (REV => $self->create_rev);
+
+ if (exists $log{paths}{$self->branch_path}) {
+ my $path = $log{paths}{$self->branch_path};
+
+ if ($path->{action} eq 'A') {
+ if (exists $path->{'copyfrom-path'}) {
+ # Current branch is copied from somewhere, set the source as the parent
+ my $url = $self->root . $path->{'copyfrom-path'};
+ my $rev = $path->{'copyfrom-rev'};
+ $self->{PARENT} = FCM1::CmBranch->new (URL => $url . '@' . $rev);
+
+ } else {
+ # Current branch is not copied from somewhere
+ $self->{PARENT} = $self;
+ }
+ }
+ }
+ }
+
+ return $self->{PARENT};
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rev = $cm_branch->delete_rev;
+#
+# DESCRIPTION
+# This method returns the revision at which the branch was deleted.
+# ------------------------------------------------------------------------------
+
+sub delete_rev {
+ my $self = shift;
+
+ if (not $self->{DELETE_REV}) {
+ return if $self->url_exists ('HEAD');
+
+ # Container of the current URL
+ (my $dir_url = $self->branch_url) =~ s#/+[^/]+/*$##;
+
+ # Use "svn log" on the container between a revision where the branch exists
+ # and the HEAD
+ my $dir = FCM1::CmUrl->new (URL => $dir_url);
+ my %log = $dir->svnlog (
+ REV => ['HEAD', ($self->pegrev ? $self->pegrev : $self->create_rev)],
+ );
+
+ # Go through the log to see when branch no longer exists
+ for my $rev (sort {$a <=> $b} keys %log) {
+ next unless exists $log{$rev}{paths}{$self->branch_path} and
+ $log{$rev}{paths}{$self->branch_path}{action} eq 'D';
+
+ $self->{DELETE_REV} = $rev;
+ last;
+ }
+ }
+
+ return $self->{DELETE_REV};
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $flag = $cm_branch->is_child_of ($branch);
+#
+# DESCRIPTION
+# This method returns true if the current branch is a child of $branch.
+# ------------------------------------------------------------------------------
+
+sub is_child_of {
+ my ($self, $branch) = @_;
+ !$self->is_trunk()
+ && $self->parent()->url() eq $branch->url()
+ && (!$branch->is_branch() || $self->create_rev() >= $branch->create_rev());
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $flag = $cm_branch->is_sibling_of ($branch);
+#
+# DESCRIPTION
+# This method returns true if the current branch is a sibling of $branch.
+# ------------------------------------------------------------------------------
+
+sub is_sibling_of {
+ my ($self, $branch) = @_;
+
+ # The trunk cannot be a sibling branch
+ return if $branch->is_trunk;
+
+ return if $self->parent->url ne $branch->parent->url;
+
+ # If the parent is a branch, ensure they are actually the same branch
+ return if $branch->parent->is_branch and
+ $self->parent->create_rev != $branch->parent->create_rev;
+
+ return 1;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $self->_get_relatives ($relation);
+#
+# DESCRIPTION
+# This method sets the $self->{$relation} variable by inspecting the list of
+# branches at the current revision of the current branch. $relation can be
+# either "CHILDREN" or "SIBLINGS".
+# ------------------------------------------------------------------------------
+
+sub _get_relatives {
+ my ($self, $relation) = @_;
+
+ $self->{$relation} = [];
+
+ # If we are searching for CHILDREN, get list of SIBLINGS, and vice versa
+ my $other = ($relation eq 'CHILDREN' ? 'SIBLINGS' : 'CHILDREN');
+ my %other_list;
+ if ($self->{$other}) {
+ %other_list = map {$_->url(), 1} @{$self->{$other}};
+ }
+
+ my @url_peg_list = $self->branch_list();
+ URL:
+ for my $url_peg (@url_peg_list) {
+ my ($url, $peg) = $SVN->split_by_peg($url_peg);
+ # Ignore URL of current branch and its parent
+ if ( $url eq $self->url()
+ # Ignore parent
+ || $self->is_branch() && $url eq $self->parent()->url()
+ # Ignore the other type of relatives
+ || exists($other_list{$url})
+ ) {
+ next URL;
+ }
+
+ my $branch = FCM1::CmBranch->new(URL => $url_peg);
+
+ # Test whether $branch is a relative we are looking for
+ my $can_return = $relation eq 'CHILDREN'
+ ? $branch->is_child_of($self) : $branch->is_sibling_of($self);
+ if ($can_return) {
+ push(@{$self->{$relation}}, $branch);
+ }
+ }
+
+ return;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# @children = $cm_branch->children;
+#
+# DESCRIPTION
+# This method returns a list of children (FCM1::CmBranch objects) of the
+# current branch that exists in the current revision.
+# ------------------------------------------------------------------------------
+
+sub children {
+ my $self = shift;
+
+ $self->_get_relatives ('CHILDREN') if not $self->{CHILDREN};
+
+ return @{ $self->{CHILDREN} };
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# @siblings = $cm_branch->siblings;
+#
+# DESCRIPTION
+# This method returns a list of siblings (FCM1::CmBranch objects) of the
+# current branch that exists in the current revision.
+# ------------------------------------------------------------------------------
+
+sub siblings {
+ my $self = shift;
+
+ $self->_get_relatives ('SIBLINGS') if not $self->{SIBLINGS};
+
+ return @{ $self->{SIBLINGS} };
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $ancestor = $cm_branch->ancestor ($branch);
+#
+# DESCRIPTION
+# This method returns the common ancestor (a FCM1::CmBranch object) of a
+# specified $branch and the current branch. The argument $branch must be a
+# FCM1::CmBranch object. Both the current branch and $branch are assumed to be
+# in the same project.
+# ------------------------------------------------------------------------------
+
+sub ancestor {
+ my ($self, $branch) = @_;
+
+ if (not exists $self->{ANCESTOR}{$branch->url_peg}) {
+ if ($self->url_peg eq $branch->url_peg) {
+ $self->{ANCESTOR}{$branch->url_peg} = $self;
+
+ } else {
+ # Get family tree of current branch, from trunk to current branch
+ my @this_family = ($self);
+ while (not $this_family [0]->is_trunk) {
+ unshift @this_family, $this_family [0]->parent;
+ }
+
+ # Get family tree of $branch, from trunk to $branch
+ my @that_family = ($branch);
+ while (not $that_family [0]->is_trunk) {
+ unshift @that_family, $that_family [0]->parent;
+ }
+
+ # Find common ancestor from list of parents
+ my $ancestor = undef;
+
+ while (not $ancestor) {
+ # $this and $that should both start as some revisions on the trunk.
+ # Walk down a generation each time it loops around.
+ my $this = shift @this_family;
+ my $that = shift @that_family;
+
+ if ($this->url eq $that->url) {
+ if ($this->is_trunk or $this->create_rev eq $that->create_rev) {
+ # $this and $that are the same branch
+ if (@this_family and @that_family) {
+ # More generations in both branches, try comparing the next
+ # generations.
+ next;
+
+ } else {
+ # End of lineage in one of the branches, ancestor is at the lower
+ # revision of the current URL.
+ if ($this->pegrev and $that->pegrev) {
+ $ancestor = $this->pegrev < $that->pegrev ? $this : $that;
+
+ } else {
+ $ancestor = $this->pegrev ? $this : $that;
+ }
+ }
+
+ } else {
+ # Despite the same URL, $this and $that are different branches as
+ # they are created at different revisions. The ancestor must be the
+ # parent with the lower revision. (This should not occur at the
+ # start.)
+ $ancestor = $this->parent->pegrev < $that->parent->pegrev
+ ? $this->parent : $that->parent;
+ }
+
+ } else {
+ # Different URLs, ancestor must be the parent with the lower revision.
+ # (This should not occur at the start.)
+ $ancestor = $this->parent->pegrev < $that->parent->pegrev
+ ? $this->parent : $that->parent;
+ }
+ }
+
+ $self->{ANCESTOR}{$branch->url_peg} = $ancestor;
+ }
+ }
+
+ return $self->{ANCESTOR}{$branch->url_peg};
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# ($target, $upper, $lower) = $cm_branch->last_merge_from (
+# $branch, $stop_on_copy,
+# );
+#
+# DESCRIPTION
+# This method returns a 3-element list with information of the last merge
+# into the current branch from a specified $branch. The first element in the
+# list $target (a FCM1::CmBranch object) is the target at which the merge was
+# performed. (This can be the current branch or a parent branch up to the
+# common ancestor with the specified $branch.) The second and third elements,
+# $upper and $lower, (both FCM1::CmBranch objects), are the upper and lower
+# ends of the source delta. If there is no merge from $branch into the
+# current branch from their common ancestor to the current revision, this
+# method will return an empty list. If $stop_on_copy is specified, it ignores
+# merges from parents of $branch, and merges into parents of the current
+# branch.
+# ------------------------------------------------------------------------------
+
+sub last_merge_from {
+ my ($self, $branch, $stop_on_copy) = @_;
+
+ if (not exists $self->{LAST_MERGE}{$branch->url_peg}) {
+ # Get "log" of current branch down to the common ancestor
+ my %log = $self->svnlog (
+ REV => [
+ ($self->pegrev ? $self->pegrev : 'HEAD'),
+ $self->ancestor ($branch)->pegrev,
+ ],
+
+ STOP_ON_COPY => $stop_on_copy,
+ );
+
+ my $cr = $self;
+
+ # Go down the revision log, checking for merge template messages
+ REV: for my $rev (sort {$b <=> $a} keys %log) {
+ # Loop each line of the log message at each revision
+ my @msg = split /\n/, $log{$rev}{msg};
+
+ # Also consider merges into parents of current branch
+ $cr = $cr->parent if ($cr->is_branch and $rev < $cr->create_rev);
+
+ for (@msg) {
+ # Ignore unless log message matches a merge template
+ next unless /Merged into \S+: (\S+) cf\. (\S+)/;
+
+ # Upper $1 and lower $2 ends of the source delta
+ my $u_path = $1;
+ my $l_path = $2;
+
+ # Add the root directory to the paths if necessary
+ $u_path = '/' . $u_path if substr ($u_path, 0, 1) ne '/';
+ $l_path = '/' . $l_path if substr ($l_path, 0, 1) ne '/';
+
+ # Only consider merges with specified branch (and its parent)
+ (my $path = $u_path) =~ s/@(\d+)$//;
+ my $u_rev = $1;
+
+ my $br = $branch;
+ $br = $br->parent while (
+ $br->is_branch and $u_rev < $br->create_rev and not $stop_on_copy
+ );
+
+ next unless $br->branch_path eq $path;
+
+ # If $br is a parent of branch, ignore those merges with the parent
+ # above the branch point of the current branch
+ next if $br->pegrev and $br->pegrev < $u_rev;
+
+ # Set the return values
+ $self->{LAST_MERGE}{$branch->url_peg} = [
+ FCM1::CmBranch->new (URL => $cr->url . '@' . $rev), # target
+ FCM1::CmBranch->new (URL => $self->root . $u_path), # delta upper
+ FCM1::CmBranch->new (URL => $self->root . $l_path), # delta lower
+ ];
+
+ last REV;
+ }
+ }
+ }
+
+ return (exists $self->{LAST_MERGE}{$branch->url_peg}
+ ? @{ $self->{LAST_MERGE}{$branch->url_peg} } : ());
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# @revs = $cm_branch->avail_merge_from ($branch[, $stop_on_copy]);
+#
+# DESCRIPTION
+# This method returns a list of revisions of a specified $branch, which are
+# available for merging into the current branch. If $stop_on_copy is
+# specified, it will not list available merges from the parents of $branch.
+# ------------------------------------------------------------------------------
+
+sub avail_merge_from {
+ my ($self, $branch, $stop_on_copy) = @_;
+
+ if (not exists $self->{AVAIL_MERGE}{$branch->url_peg}) {
+ # Find out the revision of the upper delta at the last merge from $branch
+ # If no merge is found, use revision of common ancestor with $branch
+ my @last_merge = $self->last_merge_from ($branch);
+ my $rev = $self->ancestor ($branch)->pegrev;
+ $rev = $last_merge [1]->pegrev
+ if @last_merge and $last_merge [1]->pegrev > $rev;
+
+ # Get the "log" of the $branch down to $rev
+ my %log = $branch->svnlog (
+ REV => [($branch->pegrev ? $branch->pegrev : 'HEAD'), $rev],
+ STOP_ON_COPY => $stop_on_copy,
+ );
+
+ # No need to include $rev itself, as it has already been merged
+ delete $log{$rev};
+
+ # No need to include the branch create revision
+ delete $log{$branch->create_rev}
+ if $branch->is_branch and exists $log{$branch->create_rev};
+
+ if (keys %log) {
+ # Check whether there is a latest merge from $self into $branch, if so,
+ # all revisions of $branch below that merge should become unavailable
+ my @last_merge_into = $branch->last_merge_from ($self);
+
+ if (@last_merge_into) {
+ for my $rev (keys %log) {
+ delete $log{$rev} if $rev < $last_merge_into [0]->pegrev;
+ }
+ }
+ }
+
+ # Available merges include all revisions above the branch creation revision
+ # or the revision of the last merge
+ $self->{AVAIL_MERGE}{$branch->url_peg} = [sort {$b <=> $a} keys %log];
+ }
+
+ return @{ $self->{AVAIL_MERGE}{$branch->url_peg} };
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $lower = $cm_branch->base_of_merge_from ($branch);
+#
+# DESCRIPTION
+# This method returns the lower delta (a FCM1::CmBranch object) for the next
+# merge from $branch.
+# ------------------------------------------------------------------------------
+
+sub base_of_merge_from {
+ my ($self, $branch) = @_;
+
+ # Base is the ancestor if there is no merge between $self and $branch
+ my $return = $self->ancestor ($branch);
+
+ # Get configuration for the last merge from $branch to $self
+ my @merge_from = $self->last_merge_from ($branch);
+
+ # Use the upper delta of the last merge from $branch, as all revisions below
+ # that have already been merged into the $self
+ $return = $merge_from [1]
+ if @merge_from and $merge_from [1]->pegrev > $return->pegrev;
+
+ # Get configuration for the last merge from $self to $branch
+ my @merge_into = $branch->last_merge_from ($self);
+
+ # Use the upper delta of the last merge from $self, as the current revision
+ # of $branch already contains changes of $self up to the peg revision of the
+ # upper delta
+ $return = $merge_into [1]
+ if @merge_into and $merge_into [0]->pegrev > $return->pegrev;
+
+ return $return;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $flag = $cm_branch->allow_subdir_merge_from ($branch, $subdir);
+#
+# DESCRIPTION
+# This method returns true if a merge from the sub-directory $subdir in
+# $branch is allowed - i.e. it does not result in losing changes made in
+# $branch outside of $subdir.
+# ------------------------------------------------------------------------------
+
+sub allow_subdir_merge_from {
+ my ($self, $branch, $subdir) = @_;
+
+ # Get revision at last merge from $branch or ancestor
+ my @merge_from = $self->last_merge_from ($branch);
+ my $last = @merge_from ? $merge_from [1] : $self->ancestor ($branch);
+ my $rev = $last->pegrev;
+
+ my $return = 1;
+ if ($branch->pegrev > $rev) {
+ # Use "svn diff --summarize" to work out what's changed between last
+ # merge/ancestor and current revision
+ my $range = $branch->pegrev . ':' . $rev;
+ my @out = $SVN->stdout(
+ qw{svn diff --summarize -r}, $range, $branch->url_peg(),
+ );
+
+ # Returns false if there are changes outside of $subdir
+ my $url = join ('/', $branch->url, $subdir);
+ for my $line (@out) {
+ chomp $line;
+ $line = substr ($line, 7); # file name begins at column 7
+ if ($line !~ m#^$url(?:/|$)#) {
+ $return = 0;
+ last;
+ }
+ }
+ }
+
+ return $return;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $cm_branch->delete (
+# [NON_INTERACTIVE => 1,]
+# [PASSWORD => $password,]
+# [SVN_NON_INTERACTIVE => 1,]
+# );
+#
+# DESCRIPTION
+# This method deletes the current branch from the Subversion repository.
+#
+# OPTIONS
+# NON_INTERACTIVE - Do no interactive prompting, set SVN_NON_INTERACTIVE
+# to true automatically.
+# PASSWORD - specify the password for commit access.
+# SVN_NON_INTERACTIVE - Do no interactive prompting when running svn commit,
+# etc. This option is implied by NON_INTERACTIVE.
+# ------------------------------------------------------------------------------
+
+sub del {
+ my $self = shift;
+ my %args = @_;
+
+ # Options
+ # ----------------------------------------------------------------------------
+ my $password = exists $args{PASSWORD} ? $args{PASSWORD} : undef;
+ my $non_interactive = exists $args{NON_INTERACTIVE}
+ ? $args{NON_INTERACTIVE} : 0;
+ my $svn_non_interactive = exists $args{SVN_NON_INTERACTIVE}
+ ? $args{SVN_NON_INTERACTIVE} : 0;
+ $svn_non_interactive = $non_interactive ? 1 : $svn_non_interactive;
+
+ # Ensure URL is a branch
+ # ----------------------------------------------------------------------------
+ e_report $self->url_peg, ': not a branch, abort.' if not $self->is_branch;
+
+ # Create a temporary file for the commit log message
+ my $temp_handle = $self->_commit_message(
+ sprintf("Deleted %s.\n", $self->branch_path()), 'D', $non_interactive,
+ );
+
+ # Check with the user to see if he/she wants to go ahead
+ # ----------------------------------------------------------------------------
+ if (not $non_interactive) {
+ my $mesg = '';
+ if (!$self->layout()->is_owned_by_user()) {
+ $mesg .= "\n";
+
+ if (exists $FCM1::CmUrl::owner_keywords{$self->branch_owner()}) {
+ my $type = $FCM1::CmUrl::owner_keywords{$self->branch_owner()};
+ $mesg .= '*** WARNING: YOU ARE DELETING A ' . uc ($type) .
+ ' BRANCH.';
+
+ } else {
+ $mesg .= '*** WARNING: YOU ARE DELETING A BRANCH NOT OWNED BY YOU.';
+ }
+
+ $mesg .= "\n" .
+ '*** Please ensure that you have the owner\'s permission.' .
+ "\n\n";
+ }
+
+ $mesg .= 'Would you like to go ahead and delete this branch?';
+
+ my $reply = FCM1::Interactive::get_input (
+ title => 'fcm branch',
+ message => $mesg,
+ type => 'yn',
+ default => 'n',
+ );
+
+ return unless $reply eq 'y';
+ }
+
+ # Delete branch if answer is "y" for "yes"
+ # ----------------------------------------------------------------------------
+ print 'Deleting branch ', $self->url, ' ...', "\n";
+ $SVN->call(
+ 'delete',
+ '-F', $temp_handle->filename(),
+ (defined $password ? ('--password', $password) : ()),
+ ($svn_non_interactive ? '--non-interactive' : ()),
+ $self->url(),
+ );
+
+ return;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $cm_branch->display_info (
+# [SHOW_CHILDREN => 1],
+# [SHOW_OTHER => 1]
+# [SHOW_SIBLINGS => 1]
+# );
+#
+# DESCRIPTION
+# This method displays information of the current branch. If SHOW_CHILDREN is
+# set, it shows information of all current children branches of the current
+# branch. If SHOW_SIBLINGS is set, it shows information of siblings that have
+# been merged recently with the current branch. If SHOW_OTHER is set, it shows
+# information of custom/reverse merges.
+# ------------------------------------------------------------------------------
+
+sub display_info {
+ my $self = shift;
+ my %args = @_;
+
+ # Arguments
+ # ----------------------------------------------------------------------------
+ my $show_children = exists $args{SHOW_CHILDREN} ? $args{SHOW_CHILDREN} : 0;
+ my $show_other = exists $args{SHOW_OTHER } ? $args{SHOW_OTHER} : 0;
+ my $show_siblings = exists $args{SHOW_SIBLINGS} ? $args{SHOW_SIBLINGS} : 0;
+
+ # Useful variables
+ # ----------------------------------------------------------------------------
+ my $separator = '-' x 80 . "\n";
+ my $separator2 = ' ' . '-' x 78 . "\n";
+
+ # Print "info" as returned by "svn info"
+ # ----------------------------------------------------------------------------
+ for (
+ ['URL', 'url' ],
+ ['Repository Root', 'repository:root'],
+ ['Revision', 'revision' ],
+ ['Last Changed Author', 'commit:author' ],
+ ['Last Changed Rev', 'commit:revision'],
+ ['Last Changed Date', 'commit:date' ],
+ ) {
+ my ($key, $flag) = @{$_};
+ if ($self->svninfo(FLAG => $flag)) {
+ printf("%s: %s\n", $key, $self->svninfo(FLAG => $flag));
+ }
+ }
+
+ if ($self->config->verbose) {
+ # Verbose mode, print log message at last changed revision
+ my %log = $self->svnlog (REV => $self->svninfo(FLAG => 'commit:revision'));
+ my @log = split /\n/, $log{msg};
+ print 'Last Changed Log:', "\n\n", map ({' ' . $_ . "\n"} @log), "\n";
+ }
+
+ if ($self->is_branch) {
+ # Print create information
+ # --------------------------------------------------------------------------
+ my %log = $self->svnlog (REV => $self->create_rev);
+
+ print $separator;
+ print 'Branch Create Author: ', $log{author}, "\n" if $log{author};
+ print 'Branch Create Rev: ', $self->create_rev, "\n";
+ print 'Branch Create Date: ', &svn_date ($log{date}), "\n";
+
+ if ($self->config->verbose) {
+ # Verbose mode, print log message at last create revision
+ my @log = split /\n/, $log{msg};
+ print 'Branch Create Log:', "\n\n", map ({' ' . $_ . "\n"} @log), "\n";
+ }
+
+ # Print delete information if branch no longer exists
+ # --------------------------------------------------------------------------
+ print 'Branch Delete Rev: ', $self->delete_rev, "\n" if $self->delete_rev;
+
+ # Report merges into/from the parent
+ # --------------------------------------------------------------------------
+ # Print the URL at REV of the parent branch
+ print $separator, 'Branch Parent: ', $self->parent->url_peg, "\n";
+
+ # Set up a new object for the parent at the current revision
+ # --------------------------------------------------------------------------
+ my $p_url = $self->parent->url;
+ $p_url .= '@' . $self->pegrev if $self->pegrev;
+ my $parent = FCM1::CmBranch->new (URL => $p_url);
+
+ if (not $parent->url_exists) {
+ print 'Branch parent deleted.', "\n";
+ return;
+ }
+
+ # Report merges into/from the parent
+ # --------------------------------------------------------------------------
+ print $self->_report_merges ($parent, 'Parent');
+ }
+
+ # Report merges with siblings
+ # ----------------------------------------------------------------------------
+ if ($show_siblings) {
+ # Report number of sibling branches found
+ print $separator, 'Searching for siblings ... ';
+ my @siblings = $self->siblings;
+ print scalar (@siblings), ' ', (@siblings> 1 ? 'siblings' : 'sibling'),
+ ' found.', "\n";
+
+ # Report branch name and merge information only if there are recent merges
+ my $out = '';
+ for my $sibling (@siblings) {
+ my $string = $self->_report_merges ($sibling, 'Sibling');
+
+ $out .= $separator2 . ' ' . $sibling->url . "\n" . $string if $string;
+ }
+
+ if (@siblings) {
+ if ($out) {
+ print 'Merges with existing siblings:', "\n", $out;
+
+ } else {
+ print 'No merges with existing siblings.', "\n";
+ }
+ }
+ }
+
+ # Report children
+ # ----------------------------------------------------------------------------
+ if ($show_children) {
+ # Report number of child branches found
+ print $separator, 'Searching for children ... ';
+ my @children = $self->children;
+ print scalar (@children), ' ', (@children > 1 ? 'children' : 'child'),
+ ' found.', "\n";
+
+ # Report children if they exist
+ print 'Current children:', "\n" if @children;
+
+ for my $child (@children) {
+ print $separator2, ' ', $child->url, "\n";
+ print ' Child Create Rev: ', $child->create_rev, "\n";
+ print $self->_report_merges ($child, 'Child');
+ }
+ }
+
+ # Report custom/reverse merges into the branch
+ # ----------------------------------------------------------------------------
+ if ($show_other) {
+ my %log = $self->svnlog (STOP_ON_COPY => 1);
+ my @out;
+
+ # Go down the revision log, checking for merge template messages
+ REV: for my $rev (sort {$b <=> $a} keys %log) {
+ # Loop each line of the log message at each revision
+ my @msg = split /\n/, $log{$rev}{msg};
+
+ for (@msg) {
+ # Ignore unless log message matches a merge template
+ if (/^Reversed r\d+(:\d+)? of \S+$/ or
+ s/^(Custom merge) into \S+(:.+)$/$1$2/) {
+ push @out, ('r' . $rev . ': ' . $_) . "\n";
+ }
+ }
+ }
+
+ print $separator, 'Other merges:', "\n", @out if @out;
+ }
+
+ return;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $string = $self->_report_merges ($branch, $relation);
+#
+# DESCRIPTION
+# This method returns a string for displaying merge information with a
+# branch, the $relation of which can be a Parent, a Sibling or a Child.
+# ------------------------------------------------------------------------------
+
+sub _report_merges {
+ my ($self, $branch, $relation) = @_;
+
+ my $indent = ($relation eq 'Parent') ? '' : ' ';
+ my $separator = ($relation eq 'Parent') ? ('-' x 80) : (' ' . '-' x 78);
+ $separator .= "\n";
+
+ my $return = '';
+
+ # Report last merges into/from the $branch
+ # ----------------------------------------------------------------------------
+ my %merge = (
+ 'Last Merge From ' . $relation . ':'
+ => [$self->last_merge_from ($branch, 1)],
+ 'Last Merge Into ' . $relation . ':'
+ => [$branch->last_merge_from ($self, 1)],
+ );
+
+ if ($self->config->verbose) {
+ # Verbose mode, print the log of the merge
+ for my $key (keys %merge) {
+ next if not @{ $merge{$key} };
+
+ # From: target (0) is self, upper delta (1) is $branch
+ # Into: target (0) is $branch, upper delta (1) is self
+ my $t = ($key =~ /From/) ? $self : $branch;
+
+ $return .= $indent . $key . "\n";
+ $return .= $separator . $t->display_svnlog ($merge{$key}[0]->pegrev);
+ }
+
+ } else {
+ # Normal mode, print in simplified form (rREV Parent at REV)
+ for my $key (keys %merge) {
+ next if not @{ $merge{$key} };
+
+ # From: target (0) is self, upper delta (1) is $branch
+ # Into: target (0) is $branch, upper delta (1) is self
+ $return .= $indent . $key . ' r' . $merge{$key}[0]->pegrev . ' ' .
+ $merge{$key}[1]->path_peg . ' cf. ' .
+ $merge{$key}[2]->path_peg . "\n";
+ }
+ }
+
+ if ($relation eq 'Sibling') {
+ # For sibling, do not report further if there is no recent merge
+ my @values = values %merge;
+
+ return $return unless (@{ $values[0] } or @{ $values[1] });
+ }
+
+ # Report available merges into/from the $branch
+ # ----------------------------------------------------------------------------
+ my %avail = (
+ 'Merges Avail From ' . $relation . ':'
+ => ($self->delete_rev ? [] : [$self->avail_merge_from ($branch, 1)]),
+ 'Merges Avail Into ' . $relation . ':'
+ => [$branch->avail_merge_from ($self, 1)],
+ );
+
+ if ($self->config->verbose) {
+ # Verbose mode, print the log of each revision
+ for my $key (keys %avail) {
+ next unless @{ $avail{$key} };
+
+ $return .= $indent . $key . "\n";
+
+ my $s = ($key =~ /From/) ? $branch: $self;
+
+ for my $rev (@{ $avail{$key} }) {
+ $return .= $separator . $s->display_svnlog ($rev);
+ }
+ }
+
+ } else {
+ # Normal mode, print only the revisions
+ for my $key (keys %avail) {
+ next unless @{ $avail{$key} };
+
+ $return .= $indent . $key . ' ' . join (' ', @{ $avail{$key} }) . "\n";
+ }
+ }
+
+ return $return;
+}
+
+# Returns a File::Temp object containing the commit log for create/del.
+sub _commit_message {
+ my ($self, $message, $action, $non_interactive) = @_;
+ my $commit_message_ctx = $COMMIT_MESSAGE_UTIL->ctx();
+ $commit_message_ctx->set_auto_part($message);
+ $commit_message_ctx->set_info_part(
+ sprintf("%s %s\n", $action, $self->url())
+ );
+ if (!$non_interactive) {
+ $COMMIT_MESSAGE_UTIL->edit($commit_message_ctx);
+ }
+ $COMMIT_MESSAGE_UTIL->notify($commit_message_ctx);
+ $COMMIT_MESSAGE_UTIL->temp($commit_message_ctx);
+}
+
+# ------------------------------------------------------------------------------
+
+1;
+
+__END__
diff --git a/lib/FCM1/CmUrl.pm b/lib/FCM1/CmUrl.pm
new file mode 100644
index 0000000..67bddc0
--- /dev/null
+++ b/lib/FCM1/CmUrl.pm
@@ -0,0 +1,639 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# NAME
+# FCM1::CmUrl
+#
+# DESCRIPTION
+# This class contains methods for manipulating a Subversion URL in a standard
+# FCM project.
+#
+# ------------------------------------------------------------------------------
+
+package FCM1::CmUrl;
+use base qw{FCM1::Base};
+
+use strict;
+use warnings;
+
+use FCM::System::Exception;
+use FCM1::Keyword;
+use FCM1::Util qw/svn_date/;
+
+# Special branches
+our %owner_keywords = (Share => 'shared', Config => 'config', Rel => 'release');
+
+# Revision pattern
+my $rev_pattern = '\d+|HEAD|BASE|COMMITTED|PREV|\{.+\}';
+
+my $E = 'FCM::System::Exception';
+
+# "svn log --xml" handlers.
+# -> element node start tag handlers
+my %SVN_LOG_ELEMENT_0_HANDLER_FOR = (
+# tag => handler
+ 'logentry' => \&_svn_log_handle_element_0_logentry,
+ 'path' => \&_svn_log_handle_element_0_path,
+);
+# -> text node (after a start tag) handlers
+my %SVN_LOG_TEXT_HANDLER_FOR = (
+# tag => handler
+ 'date' => \&_svn_log_handle_text_date,
+ 'path' => \&_svn_log_handle_text_path,
+);
+
+# Set the SVN utility provided by FCM::System::CM.
+our $SVN;
+sub set_svn_util {
+ $SVN = shift();
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $cm_url = FCM1::CmUrl->new ([URL => $url,]);
+#
+# DESCRIPTION
+# This method constructs a new instance of the FCM1::CmUrl class.
+#
+# ARGUMENTS
+# URL - URL of a branch
+# ------------------------------------------------------------------------------
+
+sub new {
+ my $this = shift;
+ my %args = @_;
+ my $class = ref $this || $this;
+
+ my $self = FCM1::Base->new (%args);
+
+ $self->{URL} = (exists $args{URL} ? $args{URL} : '');
+
+ for (qw/LAYOUT BRANCH_LIST INFO LIST LOG LOG_RANGE RLIST/) {
+ $self->{$_} = undef;
+ }
+
+ bless $self, $class;
+ return $self;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $url = $cm_url->url_peg;
+# $cm_url->url_peg ($url);
+#
+# DESCRIPTION
+# This method returns/sets the current URL at PEG.
+# ------------------------------------------------------------------------------
+
+sub url_peg {
+ my $self = shift;
+
+ if (@_) {
+ if (! $self->{URL} or $_[0] ne $self->{URL}) {
+ # Re-set URL
+ $self->{URL} = shift;
+
+ # Re-set essential variables
+ $self->{$_} = undef for (qw/LAYOUT RLIST LIST INFO LOG LOG_RANGE/);
+ }
+ }
+
+ return $self->{URL};
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $flag = $cm_url->is_url ();
+#
+# DESCRIPTION
+# Returns true if current url is a valid Subversion URL.
+# ------------------------------------------------------------------------------
+
+sub is_url {
+ my $self = shift;
+
+ # This should handle URL beginning with svn://, http:// and svn+ssh://
+ return ($self->url_peg =~ qr{^[\w\+\-]+://}msx);
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $flag = $cm_url->url_exists ([$rev]);
+#
+# DESCRIPTION
+# Returns true if current url exists (at operative revision $rev) in a
+# Subversion repository.
+# ------------------------------------------------------------------------------
+
+sub url_exists {
+ my ($self, $rev) = @_;
+
+ my $url = eval {$self->svninfo(FLAG => 'url', REV => $rev)};
+ if ($@) {
+ $@ = undef;
+ }
+
+ defined($url);
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $string = $cm_url->svninfo([FLAG => $flag], [REV => $rev]);
+#
+# DESCRIPTION
+# Returns the value of $flag, where $flag is a field returned by "svn info
+# --xml". The original hierarchy below the entry element is delimited by a
+# colon in the name. (If $flag is not set, default to "url".) If REV is
+# specified, it will be used as the operative revision.
+# ------------------------------------------------------------------------------
+
+sub svninfo {
+ my ($self, %args) = @_;
+ if (!$self->is_url()) {
+ return;
+ }
+ my $flag = exists($args{FLAG}) ? $args{FLAG} : 'url';
+ my $rev = exists($args{REV}) ? $args{REV} : undef;
+ $rev ||= ($self->pegrev ? $self->pegrev : 'HEAD');
+ # Get "info" for the specified revision if necessary
+ if (!exists($self->{INFO}{$rev})) {
+ $self->{INFO}{$rev}
+ = $SVN->get_info({'revision' => $rev}, $self->url_peg())->[0];
+ }
+ exists($self->{INFO}{$rev}{$flag}) ? $self->{INFO}{$rev}{$flag} : undef;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# %logs = $cm_url->svnlog (
+# [REV => $rev,]
+# [REV => \@revs,] # reference to a 2-element array
+# [STOP_ON_COPY => 1,]
+# );
+#
+# DESCRIPTION
+# Returns the logs for the current URL. If REV is a range of revisions or not
+# specified, return a hash where the keys are revision numbers and the values
+# are the entries (which are hash references). If a single REV is specified,
+# return the entry (a hash reference) at the specified REV. Each entry in the
+# returned list is a hash reference, with the following structure:
+#
+# $entry = {
+# author => $author, # the commit author
+# date => $date, # the commit date (in seconds since epoch)
+# msg => $msg, # the log message
+# paths => { # list of changed paths
+# $path1 => { # a changed path
+# copyfrom-path => $frompath, # copy-from-path
+# copyfrom-rev => $fromrev, # copy-from-revision
+# action => $action, # action status code
+# },
+# ... => { ... }, # ... more changed paths ...
+# },
+# }
+# ------------------------------------------------------------------------------
+
+sub svnlog {
+ my $self = shift;
+ my %args = @_;
+
+ my $stop_on_copy = exists $args{STOP_ON_COPY} ? $args{STOP_ON_COPY} : undef;
+ my $rev_arg = exists $args{REV} ? $args{REV} : 0;
+
+ my @revs;
+
+ # Get revision options
+ # ----------------------------------------------------------------------------
+ if ($rev_arg) {
+ if (ref ($rev_arg)) {
+ # Revision option is an array, a range of revisions specified?
+ ($revs [0], $revs [1]) = @$rev_arg;
+
+ } else {
+ # A single revision specified
+ $revs [0] = $rev_arg;
+ }
+
+ # Expand 'HEAD' revision
+ for my $rev (@revs) {
+ next unless uc ($rev) eq 'HEAD';
+ $rev = $self->svninfo(FLAG => 'revision', REV => 'HEAD');
+ }
+
+ } else {
+ # No revision option specified, get log for all revisions
+ $revs [0] = $self->svninfo(FLAG => 'revision');
+ $revs [1] = 1;
+ }
+
+ $revs [1] = $revs [0] if not $revs [1];
+ @revs = sort {$b <=> $a} @revs;
+
+ # Check whether a "svn log" run is necessary
+ # ----------------------------------------------------------------------------
+ my $need_update = ! ($revs [0] == $revs [1] and exists $self->{LOG}{$revs [0]});
+ my @ranges = @revs;
+ if ($need_update and $self->{LOG_RANGE}) {
+ my %log_range = %{ $self->{LOG_RANGE} };
+
+ if ($stop_on_copy) {
+ $ranges [1] = $log_range{UPPER} if $ranges [1] >= $log_range{LOWER_SOC};
+
+ } else {
+ $ranges [1] = $log_range{UPPER} if $ranges [1] >= $log_range{LOWER};
+ }
+ }
+
+ $need_update = 0 if $ranges [0] < $ranges [1];
+
+ if ($need_update) {
+ my @entries = @{$SVN->get_log(
+ {'revision' => join(':', @ranges), 'stop-on-copy' => $stop_on_copy},
+ $self->url_peg(),
+ )};
+ for my $entry (@entries) {
+ $self->{LOG}{$entry->{revision}} = $entry;
+ $entry->{paths} = {map {($_->{path} => $_)} @{$entry->{paths}}};
+ }
+
+ # Update the range cache
+ # --------------------------------------------------------------------------
+ # Upper end of the range
+ $self->{LOG_RANGE}{UPPER} = $ranges [0]
+ if ! $self->{LOG_RANGE}{UPPER} or $ranges [0] > $self->{LOG_RANGE}{UPPER};
+
+ # Lower end of the range, need to take into account the stop-on-copy option
+ if ($stop_on_copy) {
+ # Lower end of the range with stop-on-copy option
+ $self->{LOG_RANGE}{LOWER_SOC} = $ranges [1]
+ if ! $self->{LOG_RANGE}{LOWER_SOC} or
+ $ranges [1] < $self->{LOG_RANGE}{LOWER_SOC};
+
+ my $low = (sort {$a <=> $b} keys %{ $self->{LOG} }) [0];
+ $self->{LOG_RANGE}{LOWER} = $low
+ if ! $self->{LOG_RANGE}{LOWER} or $low < $self->{LOG_RANGE}{LOWER};
+
+ } else {
+ # Lower end of the range without the stop-on-copy option
+ $self->{LOG_RANGE}{LOWER} = $ranges [1]
+ if ! $self->{LOG_RANGE}{LOWER} or
+ $ranges [1] < $self->{LOG_RANGE}{LOWER};
+
+ $self->{LOG_RANGE}{LOWER_SOC} = $ranges [1]
+ if ! $self->{LOG_RANGE}{LOWER_SOC} or
+ $ranges [1] < $self->{LOG_RANGE}{LOWER_SOC};
+ }
+ }
+
+ my %return = ();
+
+ if (! $rev_arg or ref ($rev_arg)) {
+ # REV is an array, return log entries if they are within range
+ for my $rev (sort {$b <=> $a} keys %{ $self->{LOG} }) {
+ next if $rev > $revs [0] or $revs [1] > $rev;
+
+ $return{$rev} = $self->{LOG}{$rev};
+
+ if ($stop_on_copy) {
+ last if exists $self->{LOG}{$rev}{paths}{$self->branch_path} and
+ $self->{LOG}{$rev}{paths}{$self->branch_path}{action} eq 'A';
+ }
+ }
+
+ } else {
+ # REV is a scalar, return log of the specified revision if it exists
+ %return = %{ $self->{LOG}{$revs [0]} } if exists $self->{LOG}{$revs [0]};
+ }
+
+ return %return;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $string = $cm_branch->display_svnlog ($rev, [$wiki]);
+#
+# DESCRIPTION
+# This method returns a string for displaying the log of the current branch
+# at a $rev. If $wiki is set, returns a string for displaying in a Trac wiki
+# table. The value of $wiki should be the Subversion URL of a FCM project
+# associated with the intended Trac system.
+# ------------------------------------------------------------------------------
+
+sub display_svnlog {
+ my ($self, $rev, $wiki) = @_;
+ my $return = '';
+
+ my %log = $self->svnlog (REV => $rev);
+
+ if ($wiki) {
+ # Output in Trac wiki format
+ # --------------------------------------------------------------------------
+ $return .= '|| ' . &svn_date ($log{date}) . ' || ' . $log{author} . ' || ';
+
+ my $trac_url = FCM1::Keyword::get_browser_url($self->url);
+
+ # Get list of tickets from log
+ my @tickets;
+ while ($log{msg} =~ /(?:(\w+):)?(?:#|ticket:)(\d+)/g) {
+ push @tickets, [$1, $2];
+ }
+ @tickets = sort {
+ if ($a->[0] and $b->[0]) {
+ $a->[0] cmp $b->[0] or $a->[1] <=> $b->[1];
+
+ } elsif ($a->[0]) {
+ 1;
+
+ } else {
+ $a->[1] <=> $b->[1];
+ }
+ } @tickets;
+
+ if ($trac_url =~ qr{^$wiki(?:/*|$)}msx) {
+ # URL is in the specified $wiki, use Trac link
+ $return .= '[' . $rev . '] ||';
+
+ for my $ticket (@tickets) {
+ $return .= ' ';
+ $return .= $ticket->[0] . ':' if $ticket->[0];
+ $return .= '#' . $ticket->[1];
+ }
+
+ $return .= ' ||';
+
+ } else {
+ # URL is not in the specified $wiki, use full URL
+ my $rev_url = $trac_url;
+ $rev_url =~ s{/intertrac/source:.*\z}{/intertrac/changeset:$rev}xms;
+ $return .= '[' . $rev_url . ' ' . $rev . '] ||';
+
+ my $ticket_url = $trac_url;
+ $ticket_url =~ s{/intertrac/source:.*\z}{/intertrac/}xms;
+
+ for my $ticket (@tickets) {
+ $return .= ' [' . $ticket_url;
+ $return .= $ticket->[0] . ':' if $ticket->[0];
+ $return .= 'ticket:' . $ticket->[1] . ' ' . $ticket->[1] . ']';
+ }
+
+ $return .= ' ||';
+ }
+
+ } else {
+ # Output in plain text format
+ # --------------------------------------------------------------------------
+ my @msg = split /\n/, $log{msg};
+ my $line = (@msg > 1 ? ' lines' : ' line');
+
+ $return .= join (
+ ' | ',
+ ('r' . $rev, $log{author}, &svn_date ($log{date}), scalar (@msg) . $line),
+ );
+ $return .= "\n\n";
+ $return .= $log{msg};
+ }
+
+ return $return;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# @list = $cm_url->branch_list ($rev);
+#
+# DESCRIPTION
+# The method returns a list of branches in the current project, assuming the
+# FCM naming convention. If $rev if specified, it returns the list of
+# branches at $rev.
+# ------------------------------------------------------------------------------
+
+sub branch_list {
+ my ($self, $rev) = @_;
+ if (!defined($self->project())) {
+ return;
+ }
+ $rev = $self->svninfo(FLAG => 'revision', REV => $rev);
+ if (!exists($self->{BRANCH_LIST}{$rev})) {
+ my %layout_config = %{$self->layout()->get_config()};
+ my $url0 = $self->project_url();
+ my @d1_filters = ();
+ if ($layout_config{'dir-branch'}) {
+ $url0 .= '/' . $layout_config{'dir-branch'};
+ }
+ else {
+ for my $key (qw{trunk tag}) {
+ if ($layout_config{"dir-$key"}) {
+ push(@d1_filters, $layout_config{"dir-$key"});
+ }
+ }
+ }
+ $self->{BRANCH_LIST}{$rev} = [$SVN->get_list(
+ $url0 . '@' . $self->pegrev(),
+ sub {
+ my ($this_url, $this_name, $is_dir, $depth) = @_;
+ if ($depth == 1 && @d1_filters && grep {$this_name eq $_} @d1_filters) {
+ return (0, 0);
+ }
+ my $can_return = $depth >= $layout_config{'depth-branch'};
+ ($can_return, ($is_dir && !$can_return));
+ },
+ )];
+ }
+ @{$self->{BRANCH_LIST}{$rev}};
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $layout = $self->layout();
+#
+# DESCRIPTION
+# Wrap FCM::System::CM::SVN->get_layout($url).
+# ------------------------------------------------------------------------------
+
+sub layout {
+ my ($self) = @_;
+ if (defined($self->{LAYOUT})) {
+ return $self->{LAYOUT};
+ }
+ my $url = $self->url_peg();
+ my $layout = $SVN->get_layout($url);
+ $self->{URL} = $layout->get_url();
+ $self->{LAYOUT} = $layout;
+
+ $layout;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $url = $cm_url->url();
+# $url = $cm_url->pegrev();
+# $url = $cm_url->root();
+# $url = $cm_url->path();
+# $url = $cm_url->path_peg();
+#
+# DESCRIPTION
+# Return the relevant part of the current URL. The url method returns the URL
+# without the peg revision. The pegrev method returns the peg revision. The
+# root method returns the repository root. The path method returns the path in
+# URL under root. The path_peg method returns the path in URL with a peg
+# revision.
+# ------------------------------------------------------------------------------
+
+sub url {
+ my $layout = $_[0]->layout();
+ $layout->get_root() . $layout->get_path();
+}
+
+sub pegrev {
+ $_[0]->layout()->get_peg_rev();
+}
+
+sub root {
+ $_[0]->layout()->get_root();
+}
+
+sub path {
+ $_[0]->layout()->get_path();
+}
+
+sub path_peg {
+ my $layout = $_[0]->layout();
+ $layout->get_path() . '@' . $layout->get_peg_rev();
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $url = $cm_url->project_url_peg();
+# $url = $cm_url->project_url();
+# $url = $cm_url->project_path();
+# $url = $cm_url->project();
+# $url = $cm_url->branch_url();
+# $url = $cm_url->branch_url_peg();
+# $url = $cm_url->branch_path();
+# $url = $cm_url->branch();
+# $url = $cm_url->subdir();
+#
+# DESCRIPTION
+# Return the relevant part of the current URL. The "project_*" methods return
+# the "project" part. The "branch_*" methods return the "branch" part.
+# The "*_url_peg" methods return the URL at PEG, and the "*_url" methods return
+# the URL without the peg revision. The "*_path" methods return the path in
+# the URL under the root.
+# ------------------------------------------------------------------------------
+
+sub project_url_peg {
+ my $layout = $_[0]->layout();
+ if (!defined($layout->get_project())) {
+ return;
+ }
+ my $path = $layout->get_project() ? '/' . $layout->get_project() : q{};
+ $layout->get_root() . $path . '@' . $layout->get_peg_rev();
+}
+
+sub project_url {
+ my $layout = $_[0]->layout();
+ if (!defined($layout->get_project())) {
+ return;
+ }
+ my $path = $layout->get_project() ? '/' . $layout->get_project() : q{};
+ $layout->get_root() . $path;
+}
+
+sub project_path {
+ my $layout = $_[0]->layout();
+ if (!defined($layout->get_project())) {
+ return;
+ }
+ '/' . $layout->get_project();
+}
+
+sub project {
+ $_[0]->layout()->get_project();
+}
+
+sub branch_url_peg {
+ my $layout = $_[0]->layout();
+ if (!$layout->get_branch()) {
+ return;
+ }
+ $_[0]->project_url() . '/' . $layout->get_branch()
+ . '@' . $layout->get_peg_rev();
+}
+
+sub branch_url {
+ my $layout = $_[0]->layout();
+ if (!$layout->get_branch()) {
+ return;
+ }
+ $_[0]->project_url() . '/' . $layout->get_branch();
+}
+
+sub branch_path {
+ my $layout = $_[0]->layout();
+ if (!$layout->get_branch()) {
+ return;
+ }
+ ($_[0]->project() ? '/' . $_[0]->project() : q{}) . '/' . $layout->get_branch();
+}
+
+sub branch {
+ $_[0]->layout()->get_branch();
+}
+
+sub subdir {
+ $_[0]->layout()->get_sub_tree();
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $string = $obj->branch_owner();
+#
+# DESCRIPTION
+# This method returns the owner of the branch (based on the default layout).
+# ------------------------------------------------------------------------------
+
+sub branch_owner {
+ $_[0]->layout()->get_branch_owner();
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $flag = $cm_url->is_trunk();
+# $flag = $cm_url->is_branch();
+# $flag = $cm_url->is_tag();
+#
+# DESCRIPTION
+# Return true if the branch of current URL belongs to a given category (i.e.
+# trunk, branch or tag).
+# ------------------------------------------------------------------------------
+
+sub is_trunk {
+ $_[0]->layout()->is_trunk();
+}
+
+sub is_branch {
+ $_[0]->layout()->is_branch();
+}
+
+sub is_tag {
+ $_[0]->layout()->is_tag();
+}
+
+# ------------------------------------------------------------------------------
+
+1;
+__END__
diff --git a/lib/FCM1/Config.pm b/lib/FCM1/Config.pm
new file mode 100644
index 0000000..35dc3c3
--- /dev/null
+++ b/lib/FCM1/Config.pm
@@ -0,0 +1,898 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# NAME
+# FCM1::Config
+#
+# DESCRIPTION
+# This is a class for reading and processing central and user configuration
+# settings for FCM.
+#
+# ------------------------------------------------------------------------------
+
+package FCM1::Config;
+
+# Standard pragma
+use warnings;
+use strict;
+
+# Standard modules
+use File::Basename;
+use File::Spec::Functions;
+use FindBin;
+use POSIX qw/setlocale LC_ALL/;
+
+# FCM component modules
+use FCM1::CfgFile;
+
+# Other declarations:
+sub _get_hash_value;
+
+# Delimiter for setting and for list
+our $DELIMITER = '::';
+our $DELIMITER_PATTERN = qr{::|/};
+our $DELIMITER_LIST = ',';
+
+my $INSTANCE;
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $config = FCM1::Config->instance();
+#
+# DESCRIPTION
+# Returns an instance of this class.
+# ------------------------------------------------------------------------------
+
+sub instance {
+ my ($class) = @_;
+ if (!defined($INSTANCE)) {
+ $INSTANCE = $class->new();
+ $INSTANCE->get_config();
+ $INSTANCE->is_initialising(0);
+ }
+ return $INSTANCE;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $obj = FCM1::Config->new (VERBOSE => $verbose);
+#
+# DESCRIPTION
+# This method constructs a new instance of the FCM1::Config class.
+#
+# ARGUMENTS
+# VERBOSE - Set the verbose level of diagnostic output
+# ------------------------------------------------------------------------------
+
+sub new {
+ my $this = shift;
+ my %args = @_;
+ my $class = ref $this || $this;
+
+ # Ensure that all subsequent Subversion output is in UK English
+ if (setlocale (LC_ALL, 'en_GB')) {
+ $ENV{LANG} = 'en_GB';
+ }
+
+ my $self = {
+ initialising => 1,
+ central_config => undef,
+ user_config => undef,
+ user_id => undef,
+ verbose => exists $args{VERBOSE} ? $args{VERBOSE} : undef,
+ variable => {},
+
+ # Primary settings
+ setting => {
+ # Fortran BLOCKDATA dependencies
+ BLD_BLOCKDATA => {},
+
+ # Copy dummy target
+ BLD_CPDUMMY => '$(FCM_DONEDIR)/FCM_CP.dummy',
+
+ # No dependency check
+ BLD_DEP_N => {},
+
+ # Additional (PP) dependencies
+ BLD_DEP => {},
+ BLD_DEP_PP => {},
+
+ # Excluded dependency
+ BLD_DEP_EXCL => {
+ '' => [
+ # Fortran intrinsic modules
+ 'USE' . $DELIMITER . 'ISO_C_BINDING',
+ 'USE' . $DELIMITER . 'IEEE_EXCEPTIONS',
+ 'USE' . $DELIMITER . 'IEEE_ARITHMETIC',
+ 'USE' . $DELIMITER . 'IEEE_FEATURES',
+
+ # Fortran intrinsic subroutines
+ 'OBJ' . $DELIMITER . 'CPU_TIME',
+ 'OBJ' . $DELIMITER . 'GET_COMMAND',
+ 'OBJ' . $DELIMITER . 'GET_COMMAND_ARGUMENT',
+ 'OBJ' . $DELIMITER . 'GET_ENVIRONMENT_VARIABLE',
+ 'OBJ' . $DELIMITER . 'MOVE_ALLOC',
+ 'OBJ' . $DELIMITER . 'MVBITS',
+ 'OBJ' . $DELIMITER . 'RANDOM_NUMBER',
+ 'OBJ' . $DELIMITER . 'RANDOM_SEED',
+ 'OBJ' . $DELIMITER . 'SYSTEM_CLOCK',
+
+ # Dummy statements
+ 'OBJ' . $DELIMITER . 'NONE',
+ 'EXE' . $DELIMITER . 'NONE',
+ ],
+ },
+
+ # Extra executable dependencies
+ BLD_DEP_EXE => {},
+
+ # Dependency pattern for each type
+ BLD_DEP_PATTERN => {
+ H => q/^#\s*include\s*['"](\S+)['"]/,
+ USE => q/^\s*use\s+(\w+)/,
+ INTERFACE => q/^#?\s*include\s+['"](\S+##OUTFILE_EXT/ . $DELIMITER .
+ q/INTERFACE##)['"]/,
+ INC => q/^\s*include\s+['"](\S+)['"]/,
+ OBJ => q#^\s*(?:/\*|!)\s*depends\s*on\s*:\s*(\S+)#,
+ EXE => q/^\s*(?:#|;)\s*(?:calls|list|if|interface)\s*:\s*(\S+)/,
+ },
+
+ # Rename main program targets
+ BLD_EXE_NAME => {},
+
+ # Rename library targets
+ BLD_LIB => {'' => 'fcm_default'},
+
+ # Name of Makefile and run environment shell script
+ BLD_MISC => {
+ 'BLDMAKEFILE' => 'Makefile',
+ 'BLDRUNENVSH' => 'fcm_env.sh',
+ },
+
+ # PP flags
+ BLD_PP => {},
+
+ # Custom source file type
+ BLD_TYPE => {},
+
+ # Types that always need to be built
+ BLD_TYPE_ALWAYS_BUILD => 'PVWAVE' .
+ $DELIMITER_LIST . 'GENLIST' .
+ $DELIMITER_LIST . 'SQL',
+
+ # Dependency scan types
+ BLD_TYPE_DEP => {
+ FORTRAN => 'USE' .
+ $DELIMITER . 'INTERFACE' .
+ $DELIMITER . 'INC' .
+ $DELIMITER . 'OBJ',
+ FPP => 'USE' .
+ $DELIMITER . 'INTERFACE' .
+ $DELIMITER . 'INC' .
+ $DELIMITER . 'H' .
+ $DELIMITER . 'OBJ',
+ CPP => 'H' .
+ $DELIMITER . 'OBJ',
+ C => 'H' .
+ $DELIMITER . 'OBJ',
+ SCRIPT => 'EXE',
+ },
+
+ # Dependency scan types for pre-processing
+ BLD_TYPE_DEP_PP => {
+ FPP => 'H',
+ CPP => 'H',
+ C => 'H',
+ },
+
+ # Types that cannot have duplicated targets
+ BLD_TYPE_NO_DUPLICATED_TARGET => '',
+
+ # BLD_VPATH, each value must be a comma separate list
+ # '' translates to %
+ # 'FLAG' translates to {OUTFILE_EXT}{FLAG}
+ BLD_VPATH => {
+ BIN => q{},
+ ETC => 'ETC',
+ DONE => join($DELIMITER_LIST, qw{DONE IDONE}),
+ FLAGS => 'FLAGS',
+ INC => q{},
+ LIB => 'LIB',
+ OBJ => 'OBJ',
+ },
+
+ # Cache basename
+ CACHE => '.config',
+ CACHE_DEP => '.config_dep',
+ CACHE_DEP_PP => '.config_dep_pp',
+ CACHE_FILE_SRC => '.config_file_src',
+
+ # Types of "inc" statements expandable CFG files
+ CFG_EXP_INC => 'BLD' .
+ $DELIMITER_LIST . 'EXT' .
+ $DELIMITER_LIST . 'FCM',
+
+ # Configuration file labels that can be declared more than once
+ CFG_KEYWORD => 'USE' .
+ $DELIMITER_LIST . 'INC' .
+ $DELIMITER_LIST . 'TARGET' .
+ $DELIMITER_LIST . 'BLD_DEP_EXCL',
+
+ # Labels for all types of FCM configuration files
+ CFG_LABEL => {
+ CFGFILE => 'CFG', # config file information
+ INC => 'INC', # "include" from an configuration file
+
+ # Labels for central/user internal config setting
+ SETTING => 'SET',
+
+ # Labels for systems that allow inheritance
+ DEST => 'DEST', # destination
+ USE => 'USE', # use (inherit) a previous configuration
+
+ # Labels for bld and pck cfg
+ TARGET => 'TARGET', # BLD: declare targets, PCK: target of source file
+
+ # Labels for bld cfg
+ BLD_BLOCKDATA => 'BLOCKDATA', # declare Fortran BLOCKDATA dependencies
+ BLD_DEP => 'DEP', # additional dependencies
+ BLD_DEP_N => 'NO_DEP', # no dependency check
+ BLD_DEP_EXCL => 'EXCL_DEP', # exclude automatic dependencies
+ BLD_DEP_EXE => 'EXE_DEP', # declare dependencies for program
+ BLD_EXE_NAME => 'EXE_NAME', # rename a main program
+ BLD_LIB => 'LIB', # rename library
+ BLD_PP => 'PP', # sub-package needs pre-process?
+ BLD_TYPE => 'SRC_TYPE', # custom source file type
+ DIR => 'DIR', # DEPRECATED, same as DEST
+ INFILE_EXT => 'INFILE_EXT', # change input file name extension type
+ INHERIT => 'INHERIT', # inheritance flag
+ NAME => 'NAME', # name the build
+ OUTFILE_EXT => 'OUTFILE_EXT', # change output file type extension
+ FILE => 'SRC', # declare a sub-package
+ SEARCH_SRC => 'SEARCH_SRC', # search src/ sub-directory?
+ TOOL => 'TOOL', # declare a tool
+
+ # Labels for ext cfg
+ BDECLARE => 'BLD', # build declaration
+ CONFLICT => 'CONFLICT', # set conflict mode
+ DIRS => 'SRC', # declare source directory
+ EXPDIRS => 'EXPSRC', # declare expandable source directory
+ MIRROR => 'MIRROR', # DEPRECATED, same as RDEST::MIRROR_CMD
+ OVERRIDE => 'OVERRIDE', # DEPRECATED, replaced by CONFLICT
+ RDEST => 'RDEST', # declare remote destionation
+ REVISION => 'REVISION', # declare branch revision in a project
+ REVMATCH => 'REVMATCH', # branch revision must match changed revision
+ REPOS => 'REPOS', # declare branch in a project
+ VERSION => 'VERSION', # DEPRECATED, same as REVISION
+ },
+
+ # Default names of known FCM configuration files
+ CFG_NAME => {
+ BLD => 'bld.cfg', # build configuration file
+ EXT => 'ext.cfg', # extract configuration file
+ PARSED => 'parsed_', # as-parsed configuration file prefix
+ },
+
+ # Latest version of known FCM configuration files
+ CFG_VERSION => {
+ BLD => '1.0', # bld cfg
+ EXT => '1.0', # ext cfg
+ },
+
+ # Standard sub-directories for extract/build
+ DIR => {
+ BIN => 'bin', # executable
+ BLD => 'bld', # build
+ CACHE => '.cache', # cache
+ CFG => 'cfg', # configuration
+ DONE => 'done', # "done"
+ ETC => 'etc', # miscellaneous items
+ FLAGS => 'flags', # "flags"
+ INC => 'inc', # include
+ LIB => 'lib', # library
+ OBJ => 'obj', # object
+ PPSRC => 'ppsrc', # pre-processed source
+ SRC => 'src', # source
+ TMP => 'tmp', # temporary directory
+ },
+
+ # A flag to indicate whether the revision of a given branch for extract
+ # must match with the revision of a changed revision of the branch
+ EXT_REVMATCH => 0, # default is false (allow any revision)
+
+ # Input file name extension and type
+ # (may overlap with output (below) and vpath (above))
+ INFILE_EXT => {
+ # General extensions
+ 'f' => 'FORTRAN' .
+ $DELIMITER . 'SOURCE',
+ 'for' => 'FORTRAN' .
+ $DELIMITER . 'SOURCE',
+ 'ftn' => 'FORTRAN' .
+ $DELIMITER . 'SOURCE',
+ 'f77' => 'FORTRAN' .
+ $DELIMITER . 'SOURCE',
+ 'f90' => 'FORTRAN' .
+ $DELIMITER . 'FORTRAN9X' .
+ $DELIMITER . 'SOURCE',
+ 'f95' => 'FORTRAN' .
+ $DELIMITER . 'FORTRAN9X' .
+ $DELIMITER . 'SOURCE',
+ 'F' => 'FPP' .
+ $DELIMITER . 'SOURCE',
+ 'FOR' => 'FPP' .
+ $DELIMITER . 'SOURCE',
+ 'FTN' => 'FPP' .
+ $DELIMITER . 'SOURCE',
+ 'F77' => 'FPP' .
+ $DELIMITER . 'SOURCE',
+ 'F90' => 'FPP' .
+ $DELIMITER . 'FPP9X' .
+ $DELIMITER . 'SOURCE',
+ 'F95' => 'FPP' .
+ $DELIMITER . 'FPP9X' .
+ $DELIMITER . 'SOURCE',
+ 'c' => 'C' .
+ $DELIMITER . 'SOURCE',
+ 'cpp' => 'C' .
+ $DELIMITER . 'C++' .
+ $DELIMITER . 'SOURCE',
+ 'h' => 'CPP' .
+ $DELIMITER . 'INCLUDE',
+ 'o' => 'BINARY' .
+ $DELIMITER . 'OBJ',
+ 'obj' => 'BINARY' .
+ $DELIMITER . 'OBJ',
+ 'exe' => 'BINARY' .
+ $DELIMITER . 'EXE',
+ 'a' => 'BINARY' .
+ $DELIMITER . 'LIB',
+ 'sh' => 'SCRIPT' .
+ $DELIMITER . 'SHELL',
+ 'ksh' => 'SCRIPT' .
+ $DELIMITER . 'SHELL',
+ 'bash' => 'SCRIPT' .
+ $DELIMITER . 'SHELL',
+ 'csh' => 'SCRIPT' .
+ $DELIMITER . 'SHELL',
+ 'pl' => 'SCRIPT' .
+ $DELIMITER . 'PERL',
+ 'pm' => 'SCRIPT' .
+ $DELIMITER . 'PERL',
+ 'py' => 'SCRIPT' .
+ $DELIMITER . 'PYTHON',
+ 'tcl' => 'SCRIPT' .
+ $DELIMITER . 'TCL',
+ 'pro' => 'SCRIPT' .
+ $DELIMITER . 'PVWAVE',
+
+ # Local extensions
+ 'cfg' => 'CFGFILE',
+ 'h90' => 'CPP' .
+ $DELIMITER . 'INCLUDE',
+ 'inc' => 'FORTRAN' .
+ $DELIMITER . 'FORTRAN9X' .
+ $DELIMITER . 'INCLUDE',
+ 'interface' => 'FORTRAN' .
+ $DELIMITER . 'FORTRAN9X' .
+ $DELIMITER . 'INCLUDE' .
+ $DELIMITER . 'INTERFACE',
+ },
+
+ # Ignore input files matching the following names (comma-separated list)
+ INFILE_IGNORE => 'fcm_env.ksh' .
+ $DELIMITER_LIST . 'fcm_env.sh',
+
+ # Input file name pattern and type
+ INFILE_PAT => {
+ '\w+Scr_\w+' => 'SCRIPT' .
+ $DELIMITER . 'SHELL',
+ '\w+Comp_\w+' => 'SCRIPT' .
+ $DELIMITER . 'SHELL' .
+ $DELIMITER . 'GENTASK',
+ '\w+(?:IF|Interface)_\w+' => 'SCRIPT' .
+ $DELIMITER . 'SHELL' .
+ $DELIMITER . 'GENIF',
+ '\w+Suite_\w+' => 'SCRIPT' .
+ $DELIMITER . 'SHELL' .
+ $DELIMITER . 'GENSUITE',
+ '\w+List_\w+' => 'SCRIPT' .
+ $DELIMITER . 'SHELL' .
+ $DELIMITER . 'GENLIST',
+ '\w+Sql_\w+' => 'SCRIPT' .
+ $DELIMITER . 'SQL',
+ },
+
+ # Input text file pattern and type
+ INFILE_TXT => {
+ '(?:[ck]|ba)?sh' => 'SCRIPT' .
+ $DELIMITER . 'SHELL',
+ 'perl' => 'SCRIPT' .
+ $DELIMITER . 'PERL',
+ 'python' => 'SCRIPT' .
+ $DELIMITER . 'PYTHON',
+ 'tcl(?:sh)?|wish' => 'SCRIPT' .
+ $DELIMITER . 'TCL',
+ },
+
+ # Lock file
+ LOCK => {
+ BLDLOCK => 'fcm.bld.lock', # build lock file
+ EXTLOCK => 'fcm.ext.lock', # extract lock file
+ },
+
+ # Output file type and extension
+ # (may overlap with input and vpath (above))
+ OUTFILE_EXT => {
+ CFG => '.cfg', # FCM configuration file
+ DONE => '.done', # "done" files for compiled source
+ ETC => '.etc', # "etc" dummy file
+ EXE => '.exe', # binary executables
+ FLAGS => '.flags', # "flags" files, compiler flags config
+ IDONE => '.idone', # "done" files for included source
+ INTERFACE => '.interface', # interface for F90 subroutines/functions
+ LIB => '.a', # archive object library
+ MOD => '.mod', # compiled Fortran module information files
+ OBJ => '.o', # compiled object files
+ PDONE => '.pdone', # "done" files for pre-processed files
+ TAR => '.tar', # TAR archive
+ },
+
+ # Build commands and options (i.e. tools)
+ TOOL => {
+ SHELL => '/bin/sh', # Default shell
+
+ CPP => 'cpp', # C pre-processor
+ CPPFLAGS => '-C', # CPP flags
+ CPP_INCLUDE => '-I', # CPP flag, specify "include" path
+ CPP_DEFINE => '-D', # CPP flag, define macro
+ CPPKEYS => '', # CPP keys (definition macro)
+
+ CC => 'cc', # C compiler
+ CFLAGS => '', # CC flags
+ CC_COMPILE => '-c', # CC flag, compile only
+ CC_OUTPUT => '-o', # CC flag, specify output file name
+ CC_INCLUDE => '-I', # CC flag, specify "include" path
+ CC_DEFINE => '-D', # CC flag, define macro
+
+ FPP => 'cpp', # Fortran pre-processor
+ FPPFLAGS => '-P -traditional', # FPP flags
+ FPP_INCLUDE => '-I', # FPP flag, specify "include" path
+ FPP_DEFINE => '-D', # FPP flag, define macro
+ FPPKEYS => '', # FPP keys (definition macro)
+
+ FC => 'f90', # Fortran compiler
+ FFLAGS => '', # FC flags
+ FC_COMPILE => '-c', # FC flag, compile only
+ FC_OUTPUT => '-o', # FC flag, specify output file name
+ FC_INCLUDE => '-I', # FC flag, specify "include" path
+ FC_MODSEARCH => '', # FC flag, specify "module" path
+ FC_DEFINE => '-D', # FC flag, define macro
+
+ LD => '', # linker
+ LDFLAGS => '', # LD flags
+ LD_OUTPUT => '-o', # LD flag, specify output file name
+ LD_LIBSEARCH => '-L', # LD flag, specify "library" path
+ LD_LIBLINK => '-l', # LD flag, specify link library
+
+ AR => 'ar', # library archiver
+ ARFLAGS => 'rs', # AR flags
+
+ MAKE => 'make', # make command
+ MAKEFLAGS => '', # make flags
+ MAKE_FILE => '-f', # make flag, path to Makefile
+ MAKE_SILENT => '-s', # make flag, silent diagnostic
+ MAKE_JOB => '-j', # make flag, number of jobs
+
+ INTERFACE => 'file', # name interface after file/program
+ GENINTERFACE => '', # Fortran 9x interface generator
+
+ DIFF3 => 'diff3', # extract diff3 merge
+ DIFF3FLAGS => '-E -m', # DIFF3 flags
+ GRAPHIC_DIFF => 'xxdiff', # graphical diff tool
+ GRAPHIC_MERGE=> 'xxdiff', # graphical merge tool
+ },
+
+ # List of tools that are local to FCM, (will not be exported to a Makefile)
+ TOOL_LOCAL => 'CPP' .
+ $DELIMITER_LIST . 'CPPFLAGS' .
+ $DELIMITER_LIST . 'CPP_INCLUDE' .
+ $DELIMITER_LIST . 'CPP_DEFINE' .
+ $DELIMITER_LIST . 'DIFF3' .
+ $DELIMITER_LIST . 'DIFF3_FLAGS' .
+ $DELIMITER_LIST . 'FPP' .
+ $DELIMITER_LIST . 'FPPFLAGS' .
+ $DELIMITER_LIST . 'FPP_INCLUDE' .
+ $DELIMITER_LIST . 'FPP_DEFINE' .
+ $DELIMITER_LIST . 'GRAPHIC_DIFF' .
+ $DELIMITER_LIST . 'GRAPHIC_MERGE' .
+ $DELIMITER_LIST . 'MAKE' .
+ $DELIMITER_LIST . 'MAKEFLAGS' .
+ $DELIMITER_LIST . 'MAKE_FILE' .
+ $DELIMITER_LIST . 'MAKE_SILENT' .
+ $DELIMITER_LIST . 'MAKE_JOB' .
+ $DELIMITER_LIST . 'INTERFACE' .
+ $DELIMITER_LIST . 'GENINTERFACE' .
+ $DELIMITER_LIST . 'MIRROR' .
+ $DELIMITER_LIST . 'REMOTE_SHELL',
+
+ # List of tools that allow sub-package declarations
+ TOOL_PACKAGE => 'CPPFLAGS' .
+ $DELIMITER_LIST . 'CPPKEYS' .
+ $DELIMITER_LIST . 'CFLAGS' .
+ $DELIMITER_LIST . 'FPPFLAGS' .
+ $DELIMITER_LIST . 'FPPKEYS' .
+ $DELIMITER_LIST . 'FFLAGS' .
+ $DELIMITER_LIST . 'LD' .
+ $DELIMITER_LIST . 'LDFLAGS' .
+ $DELIMITER_LIST . 'INTERFACE' .
+ $DELIMITER_LIST . 'GENINTERFACE',
+
+ # Supported tools for compilable source
+ TOOL_SRC_PP => {
+ FPP => {
+ COMMAND => 'FPP',
+ FLAGS => 'FPPFLAGS',
+ PPKEYS => 'FPPKEYS',
+ INCLUDE => 'FPP_INCLUDE',
+ DEFINE => 'FPP_DEFINE',
+ },
+
+ C => {
+ COMMAND => 'CPP',
+ FLAGS => 'CPPFLAGS',
+ PPKEYS => 'CPPKEYS',
+ INCLUDE => 'CPP_INCLUDE',
+ DEFINE => 'CPP_DEFINE',
+ },
+ },
+
+ # Supported tools for compilable source
+ TOOL_SRC => {
+ FORTRAN => {
+ COMMAND => 'FC',
+ FLAGS => 'FFLAGS',
+ OUTPUT => 'FC_OUTPUT',
+ INCLUDE => 'FC_INCLUDE',
+ },
+
+ FPP => {
+ COMMAND => 'FC',
+ FLAGS => 'FFLAGS',
+ PPKEYS => 'FPPKEYS',
+ OUTPUT => 'FC_OUTPUT',
+ INCLUDE => 'FC_INCLUDE',
+ DEFINE => 'FC_DEFINE',
+ },
+
+ C => {
+ COMMAND => 'CC',
+ FLAGS => 'CFLAGS',
+ PPKEYS => 'CPPKEYS',
+ OUTPUT => 'CC_OUTPUT',
+ INCLUDE => 'CC_INCLUDE',
+ DEFINE => 'CC_DEFINE',
+ },
+ },
+
+ # FCM URL keyword and prefix, FCM revision keyword, and FCM Trac URL
+ URL => {},
+ URL_REVISION => {},
+
+ URL_BROWSER_MAPPING => {},
+ URL_BROWSER_MAPPING_DEFAULT => {
+ LOCATION_COMPONENT_PATTERN
+ => qr{\A // ([^/]+) /+ ([^/]+)_svn /+(.*) \z}xms,
+ BROWSER_URL_TEMPLATE
+ => 'http://{1}/projects/{2}/intertrac/source:{3}{4}',
+ BROWSER_REV_TEMPLATE => '@{1}',
+ },
+
+ # Default web browser
+ WEB_BROWSER => 'firefox',
+ },
+ };
+
+ # Backward compatibility: the REPOS setting is equivalent to the URL setting
+ $self->{setting}{REPOS} = $self->{setting}{URL};
+
+ # Alias the REVISION and TRAC setting to URL_REVISION and URL_TRAC
+ $self->{setting}{REVISION} = $self->{setting}{URL_REVISION};
+
+ bless $self, $class;
+ return $self;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $value = $obj->X;
+# $obj->X ($value);
+#
+# DESCRIPTION
+# Details of these properties are explained in the "new" method.
+# ------------------------------------------------------------------------------
+
+for my $name (qw/central_config user_config user_id verbose/) {
+ no strict 'refs';
+
+ *$name = sub {
+ my $self = shift;
+
+ # Argument specified, set property to specified argument
+ if (@_) {
+ $self->{$name} = $_[0];
+ }
+
+ # Default value for property
+ if (not defined $self->{$name}) {
+ if ($name eq 'central_config') {
+ # Central configuration file
+ if (-f catfile (dirname ($FindBin::Bin), 'etc', 'fcm.cfg')) {
+ $self->{$name} = catfile (
+ dirname ($FindBin::Bin), 'etc', 'fcm.cfg'
+ );
+
+ } elsif (-f catfile ($FindBin::Bin, 'fcm.cfg')) {
+ $self->{$name} = catfile ($FindBin::Bin, 'fcm.cfg');
+ }
+
+ } elsif ($name eq 'user_config') {
+ # User configuration file
+ my $home = (getpwuid ($<))[7];
+ $home = $ENV{HOME} if not defined $home;
+ $self->{$name} = catfile ($home, '.fcm')
+ if defined ($home) and -f catfile ($home, '.fcm');
+
+ } elsif ($name eq 'user_id') {
+ # User ID of current process
+ my $user = (getpwuid ($<))[0];
+ $user = $ENV{LOGNAME} if not defined $user;
+ $user = $ENV{USER} if not defined $user;
+ $self->{$name} = $user;
+
+ } elsif ($name eq 'verbose') {
+ # Verbose mode
+ $self->{$name} = exists $ENV{FCM_VERBOSE} ? $ENV{FCM_VERBOSE} : 1;
+ }
+ }
+
+ return $self->{$name};
+ }
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $flag = $obj->is_initialising();
+#
+# DESCRIPTION
+# Returns true if this object is initialising.
+# ------------------------------------------------------------------------------
+sub is_initialising {
+ my ($self, $value) = @_;
+ if (defined($value)) {
+ $self->{initialising} = $value;
+ }
+ return $self->{initialising};
+}
+
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# %hash = %{ $obj->X () };
+# $obj->X (\%hash);
+#
+# $value = $obj->X ($index);
+# $obj->X ($index, $value);
+#
+# DESCRIPTION
+# Details of these properties are explained in the "new" method.
+#
+# If no argument is set, this method returns a hash containing a list of
+# objects. If an argument is set and it is a reference to a hash, the objects
+# are replaced by the specified hash.
+#
+# If a scalar argument is specified, this method returns a reference to an
+# object, if the indexed object exists or undef if the indexed object does
+# not exist. If a second argument is set, the $index element of the hash will
+# be set to the value of the argument.
+# ------------------------------------------------------------------------------
+
+for my $name (qw/variable/) {
+ no strict 'refs';
+
+ *$name = sub {
+ my ($self, $arg1, $arg2) = @_;
+
+ # Ensure property is defined as a reference to a hash
+ $self->{$name} = {} if not defined ($self->{$name});
+
+ # Argument 1 can be a reference to a hash or a scalar index
+ my ($index, %hash);
+
+ if (defined $arg1) {
+ if (ref ($arg1) eq 'HASH') {
+ %hash = %$arg1;
+
+ } else {
+ $index = $arg1;
+ }
+ }
+
+ if (defined $index) {
+ # A scalar index is defined, set and/or return the value of an element
+ $self->{$name}{$index} = $arg2 if defined $arg2;
+
+ return (
+ exists $self->{$name}{$index} ? $self->{$name}{$index} : undef
+ );
+
+ } else {
+ # A scalar index is not defined, set and/or return the hash
+ $self->{$name} = \%hash if defined $arg1;
+ return $self->{$name};
+ }
+ }
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $setting = $obj->setting (@labels);
+# $obj->setting (\@labels, $setting);
+#
+# DESCRIPTION
+# This method returns/sets an item under the setting hash table. The depth
+# within the hash table is given by the list of arguments @labels, which
+# should match with the keys in the multi-dimension setting hash table.
+# ------------------------------------------------------------------------------
+
+sub setting {
+ my $self = shift;
+
+ if (@_) {
+ my $arg1 = shift;
+ my $s = $self->{setting};
+
+ if (ref ($arg1) eq 'ARRAY') {
+ # Assign setting
+ # ------------------------------------------------------------------------
+ my $value = shift;
+
+ while (defined (my $label = shift @$arg1)) {
+ if (exists $s->{$label}) {
+ if (ref $s->{$label} eq 'HASH') {
+ $s = $s->{$label};
+
+ } else {
+ $s->{$label} = $value;
+ last;
+ }
+
+ } else {
+ if (@$arg1) {
+ $s->{$label} = {};
+ $s = $s->{$label};
+
+ } else {
+ $s->{$label} = $value;
+ }
+ }
+ }
+
+ } else {
+ # Get setting
+ # ------------------------------------------------------------------------
+ return _get_hash_value ($s->{$arg1}, @_) if exists $s->{$arg1};
+ }
+ }
+
+ return undef;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $obj->get_config ();
+#
+# DESCRIPTION
+# This method reads the configuration settings from the central and the user
+# configuration files.
+# ------------------------------------------------------------------------------
+
+sub get_config {
+ my $self = shift;
+
+ $self->_read_config_file ($self->central_config);
+ $self->_read_config_file ($self->user_config);
+
+ return;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $obj->_read_config_file ();
+#
+# DESCRIPTION
+# This internal method reads a configuration file and assign values to the
+# attributes of the current instance.
+# ------------------------------------------------------------------------------
+
+sub _read_config_file {
+ my $self = shift;
+ my $config_file = $_[0];
+
+ if (!$config_file || !-f $config_file) {
+ return;
+ }
+
+ my $cfgfile = FCM1::CfgFile->new (SRC => $config_file, TYPE => 'FCM');
+ $cfgfile->read_cfg ();
+
+ LINE: for my $line (@{ $cfgfile->lines }) {
+ next unless $line->label;
+
+ # "Environment variables" start with $
+ if ($line->label =~ /^\$([A-Za-z_]\w*)$/) {
+ $ENV{$1} = $line->value;
+ next LINE;
+ }
+
+ # "Settings variables" start with "set"
+ if ($line->label_starts_with_cfg ('SETTING')) {
+ my @tags = $line->label_fields;
+ shift @tags;
+ @tags = map {uc} @tags;
+ $self->setting (\@tags, $line->value);
+ next LINE;
+ }
+
+ # Not a standard setting variable, put in internal variable list
+ (my $label = $line->label) =~ s/^\%//;
+ $self->variable ($label, $line->value);
+ }
+
+ 1;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $ref = _get_hash_value (arg1, arg2, ...);
+#
+# DESCRIPTION
+# This internal method recursively gets a value from a multi-dimensional
+# hash.
+# ------------------------------------------------------------------------------
+
+sub _get_hash_value {
+ my $value = shift;
+
+ while (defined (my $arg = shift)) {
+ if (exists $value->{$arg}) {
+ $value = $value->{$arg};
+
+ } else {
+ return undef;
+ }
+ }
+
+ return $value;
+}
+
+# ------------------------------------------------------------------------------
+
+1;
+
+__END__
diff --git a/lib/FCM1/ConfigSystem.pm b/lib/FCM1/ConfigSystem.pm
new file mode 100644
index 0000000..144681c
--- /dev/null
+++ b/lib/FCM1/ConfigSystem.pm
@@ -0,0 +1,752 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# NAME
+# FCM1::ConfigSystem
+#
+# DESCRIPTION
+# This is the base class for FCM systems that are based on inherited
+# configuration files, e.g. the extract and the build systems.
+#
+# ------------------------------------------------------------------------------
+
+package FCM1::ConfigSystem;
+use base qw{FCM1::Base};
+
+use strict;
+use warnings;
+
+use FCM1::CfgFile;
+use FCM1::CfgLine;
+use FCM1::Dest;
+use FCM1::Util qw{expand_tilde e_report w_report};
+use Sys::Hostname qw{hostname};
+
+# List of property methods for this class
+my @scalar_properties = (
+ 'cfg', # configuration file
+ 'cfg_methods', # list of sub-methods for parse_cfg
+ 'cfg_prefix', # optional prefix in configuration declaration
+ 'dest', # destination for output
+ 'inherit', # list of inherited configurations
+ 'inherited', # list of inheritance hierarchy
+ 'type', # system type
+);
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $obj = FCM1::ConfigSystem->new;
+#
+# DESCRIPTION
+# This method constructs a new instance of the FCM1::ConfigSystem class.
+# ------------------------------------------------------------------------------
+
+sub new {
+ my $this = shift;
+ my %args = @_;
+ my $class = ref $this || $this;
+
+ my $self = FCM1::Base->new (%args);
+
+ $self->{$_} = undef for (@scalar_properties);
+
+ bless $self, $class;
+
+ # List of sub-methods for parse_cfg
+ $self->cfg_methods ([qw/header inherit dest/]);
+
+ return $self;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $value = $obj->X;
+# $obj->X ($value);
+#
+# DESCRIPTION
+# Details of these properties are explained in @scalar_properties.
+# ------------------------------------------------------------------------------
+
+for my $name (@scalar_properties) {
+ no strict 'refs';
+
+ *$name = sub {
+ my $self = shift;
+
+ # Argument specified, set property to specified argument
+ if (@_) {
+ $self->{$name} = $_[0];
+ }
+
+ # Default value for property
+ if (not defined $self->{$name}) {
+ if ($name eq 'cfg') {
+ # New configuration file
+ $self->{$name} = FCM1::CfgFile->new (TYPE => $self->type);
+
+ } elsif ($name =~ /^(?:cfg_methods|inherit|inherited)$/) {
+ # Reference to an array
+ $self->{$name} = [];
+
+ } elsif ($name eq 'cfg_prefix' or $name eq 'type') {
+ # Reference to an array
+ $self->{$name} = '';
+
+ } elsif ($name eq 'dest') {
+ # New destination
+ $self->{$name} = FCM1::Dest->new (TYPE => $self->type);
+ }
+ }
+
+ return $self->{$name};
+ }
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# ($rc, $out_of_date) = $obj->check_cache ();
+#
+# DESCRIPTION
+# This method returns $rc = 1 on success or undef on failure. It returns
+# $out_of_date = 1 if current cache file is out of date relative to those in
+# inherited runs or 0 otherwise.
+# ------------------------------------------------------------------------------
+
+sub check_cache {
+ my $self = shift;
+
+ my $rc = 1;
+ my $out_of_date = 0;
+
+ if (@{ $self->inherit } and -f $self->dest->cache) {
+ # Get modification time of current cache file
+ my $cur_mtime = (stat ($self->dest->cache))[9];
+
+ # Compare with modification times of inherited cache files
+ for my $use (@{ $self->inherit }) {
+ next unless -f $use->dest->cache;
+ my $use_mtime = (stat ($use->dest->cache))[9];
+ $out_of_date = 1 if $use_mtime > $cur_mtime;
+ }
+ }
+
+ return ($rc, $out_of_date);
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = $obj->check_lock ();
+#
+# DESCRIPTION
+# This method returns true if no lock is found in the destination or if the
+# locks found are allowed.
+# ------------------------------------------------------------------------------
+
+sub check_lock {
+ my $self = shift;
+
+ # Check all types of locks
+ for my $method (@FCM1::Dest::lockfiles) {
+ my $lock = $self->dest->$method;
+
+ # Check whether lock exists
+ next unless -e $lock;
+
+ # Check whether this lock is allowed
+ next if $self->check_lock_is_allowed ($lock);
+
+ # Throw error if a lock exists
+ w_report 'ERROR: ', $lock, ': lock file exists,';
+ w_report ' ', $self->dest->rootdir, ': destination is busy.';
+ return;
+ }
+
+ return 1;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = $self->check_lock_is_allowed ($lock);
+#
+# DESCRIPTION
+# This method returns true if it is OK for $lock to exist in the destination.
+# ------------------------------------------------------------------------------
+
+sub check_lock_is_allowed {
+ my ($self, $lock) = @_;
+
+ # Disallow all types of locks by default
+ return 0;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = $self->compare_setting (
+# METHOD_LIST => \@method_list,
+# [METHOD_ARGS => \@method_args,]
+# [CACHEBASE => $cachebase,]
+# );
+#
+# DESCRIPTION
+# This method gets settings from the previous cache and updates the current.
+#
+# METHOD
+# The method returns true on success. @method_list must be a list of method
+# names for processing the cached lines in the previous run. If an existing
+# cache exists, its content is read into $old_lines, which is a list of
+# FCM1::CfgLine objects. Otherwise, $old_lines is set to undef. If $cachebase
+# is set, it is used for as the cache basename. Otherwise, the default for
+# the current system is used. It calls each method in the @method_list using
+# $self->$method ($old_lines, @method_args), which should return a
+# two-element list. The first element should be a return code (1 for out of
+# date, 0 for up to date and undef for failure). The second element should be
+# a reference to a list of FCM1::CfgLine objects for the output.
+# ------------------------------------------------------------------------------
+
+sub compare_setting {
+ my ($self, %args) = @_;
+
+ my @method_list = exists ($args{METHOD_LIST}) ? @{ $args{METHOD_LIST} } : ();
+ my @method_args = exists ($args{METHOD_ARGS}) ? @{ $args{METHOD_ARGS} } : ();
+ my $cachebase = exists ($args{CACHEBASE}) ? $args{CACHEBASE} : undef;
+
+ my $rc = 1;
+
+ # Read cache if the file exists
+ # ----------------------------------------------------------------------------
+ my $cache = $cachebase
+ ? File::Spec->catfile ($self->dest->cachedir, $cachebase)
+ : $self->dest->cache;
+ my @in_caches = ();
+ if (-f $cache) {
+ push @in_caches, $cache;
+
+ } else {
+ for my $use (@{ $self->inherit }) {
+ my $use_cache = $cachebase
+ ? File::Spec->catfile ($use->dest->cachedir, $cachebase)
+ : $use->dest->cache;
+ push @in_caches, $use_cache if -f $use_cache;
+ }
+ }
+
+ my $old_lines = undef;
+ for my $in_cache (@in_caches) {
+ next unless -f $in_cache;
+ my $cfg = FCM1::CfgFile->new (SRC => $in_cache);
+
+ if ($cfg->read_cfg) {
+ $old_lines = [] if not defined $old_lines;
+ push @$old_lines, @{ $cfg->lines };
+ }
+ }
+
+ # Call methods in @method_list to see if cache is out of date
+ # ----------------------------------------------------------------------------
+ my @new_lines = ();
+ my $out_of_date = 0;
+ for my $method (@method_list) {
+ my ($return, $lines);
+ ($return, $lines) = $self->$method ($old_lines, @method_args) if $rc;
+
+ if (defined $return) {
+ # Method succeeded
+ push @new_lines, @$lines;
+ $out_of_date = 1 if $return;
+
+ } else {
+ # Method failed
+ $rc = $return;
+ last;
+ }
+ }
+
+ # Update the cache in the current run
+ # ----------------------------------------------------------------------------
+ if ($rc) {
+ if (@{ $self->inherited } and $out_of_date) {
+ # If this is an inherited configuration, the cache must not be changed
+ w_report 'ERROR: ', $self->cfg->src,
+ ': inherited configuration does not match with its cache.';
+ $rc = undef;
+
+ } elsif ((not -f $cache) or $out_of_date) {
+ my $cfg = FCM1::CfgFile->new;
+ $cfg->lines ([sort {$a->label cmp $b->label} @new_lines]);
+ $rc = $cfg->print_cfg ($cache, 1);
+ }
+ }
+
+ return $rc;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# ($changed_hash_ref, $new_lines_array_ref) =
+# $self->compare_setting_in_config($prefix, \@old_lines);
+#
+# DESCRIPTION
+# This method compares old and current settings for a specified item.
+#
+# METHOD
+# This method does two things.
+#
+# It uses the current configuration for the $prefix item to generate a list of
+# new FCM1::CfgLine objects (which is returned as a reference in the second
+# element of the returned list).
+#
+# The values of the old lines are then compared with those of the new lines.
+# Any settings that are changed are stored in a hash, which is returned as a
+# reference in the first element of the returned list. The key of the hash is
+# the name of the changed setting, and the value is the value of the new
+# setting or undef if the setting no longer exists.
+#
+# ARGUMENTS
+# $prefix - the name of an item in FCM1::Config to be compared
+# @old_lines - a list of FCM1::CfgLine objects containing the old settings
+# ------------------------------------------------------------------------------
+
+sub compare_setting_in_config {
+ my ($self, $prefix, $old_lines_ref) = @_;
+
+ my %changed = %{$self->setting($prefix)};
+ my (@new_lines, %new_val_of);
+ while (my ($key, $val) = each(%changed)) {
+ $new_val_of{$key} = (ref($val) eq 'ARRAY' ? join(q{ }, sort(@{$val})) : $val);
+ push(@new_lines, FCM1::CfgLine->new(
+ LABEL => $prefix . $FCM1::Config::DELIMITER . $key,
+ VALUE => $new_val_of{$key},
+ ));
+ }
+
+ if (defined($old_lines_ref)) {
+ my %old_val_of
+ = map {($_->label_from_field(1), $_->value())} # converts into a hash
+ grep {$_->label_starts_with($prefix)} # gets relevant lines
+ @{$old_lines_ref};
+
+ while (my ($key, $val) = each(%old_val_of)) {
+ if (exists($changed{$key})) {
+ if ($val eq $new_val_of{$key}) { # no change from old to new
+ delete($changed{$key});
+ }
+ }
+ else { # exists in old but not in new
+ $changed{$key} = undef;
+ }
+ }
+ }
+
+ return (\%changed, \@new_lines);
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = $obj->invoke ([CLEAN => 1, ]%args);
+#
+# DESCRIPTION
+# This method invokes the system. If CLEAN is set to true, it will only parse
+# the configuration and set up the destination, but will not invoke the
+# system. See the invoke_setup_dest and the invoke_system methods for list of
+# other arguments in %args.
+# ------------------------------------------------------------------------------
+
+sub invoke {
+ my $self = shift;
+ my %args = @_;
+
+ # Print diagnostic at beginning of run
+ # ----------------------------------------------------------------------------
+ # Name of the system
+ (my $name = ref ($self)) =~ s/^FCM1:://;
+
+ # Print start time on system run, if verbose is true
+ my $date = localtime;
+ print $name, ' command started on ', $date, '.', "\n"
+ if $self->verbose;
+
+ # Start time (seconds since epoch)
+ my $otime = time;
+
+ # Parse the configuration file
+ my $rc = $self->invoke_stage ('Parse configuration', 'parse_cfg');
+
+ # Set up the destination
+ $rc = $self->invoke_stage ('Setup destination', 'invoke_setup_dest', %args)
+ if $rc;
+
+ # Invoke the system
+ # ----------------------------------------------------------------------------
+ $rc = $self->invoke_system (%args) if $rc and not $args{CLEAN};
+
+ # Remove empty directories
+ $rc = $self->dest->clean (MODE => 'EMPTY') if $rc;
+
+ # Print diagnostic at end of run
+ # ----------------------------------------------------------------------------
+ # Print lapse time at the end, if verbose is true
+ if ($self->verbose) {
+ my $total = time - $otime;
+ my $s_str = $total > 1 ? 'seconds' : 'second';
+ print '->TOTAL: ', $total, ' ', $s_str, "\n";
+ }
+
+ # Report end of system run
+ $date = localtime;
+ if ($rc) {
+ # Success
+ print $name, ' command finished on ', $date, '.', "\n"
+ if $self->verbose;
+
+ } else {
+ # Failure
+ e_report $name, ' failed on ', $date, '.';
+ }
+
+ return $rc;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = $obj->invoke_setup_dest ([CLEAN|FULL => 1], [IGNORE_LOCK => 1]);
+#
+# DESCRIPTION
+# This method sets up the destination and returns true on success.
+#
+# ARGUMENTS
+# CLEAN|FULL - If set to "true", set up the system in "clean|full" mode.
+# Sub-directories and files in the root directory created by
+# the previous invocation of the system will be removed. If
+# not set, the default is to run in "incremental" mode.
+# IGNORE_LOCK - If set to "true", it ignores any lock files that may exist in
+# the destination root directory.
+# ------------------------------------------------------------------------------
+
+sub invoke_setup_dest {
+ my $self = shift;
+ my %args = @_;
+
+ # Set up destination
+ # ----------------------------------------------------------------------------
+ # Print destination in verbose mode
+ if ($self->verbose()) {
+ printf(
+ "Destination: %s@%s:%s\n",
+ scalar(getpwuid($<)),
+ hostname(),
+ $self->dest()->rootdir(),
+ );
+ }
+
+ my $rc = 1;
+ my $out_of_date = 0;
+
+ # Check whether lock exists in the destination root
+ $rc = $self->check_lock if $rc and not $args{IGNORE_LOCK};
+
+ # Check whether current cache is out of date relative to the inherited ones
+ ($rc, $out_of_date) = $self->check_cache if $rc;
+
+ # Remove sub-directories and files in destination in "full" mode
+ $rc = $self->dest->clean (MODE => 'ALL')
+ if $rc and ($args{FULL} or $args{CLEAN} or $out_of_date);
+
+ # Create build root directory if necessary
+ $rc = $self->dest->create if $rc;
+
+ # Set a lock in the destination root
+ $rc = $self->dest->set_lock if $rc;
+
+ # Generate an as-parsed configuration file
+ $self->cfg->print_cfg ($self->dest->parsedcfg);
+
+ return $rc;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = $self->invoke_stage ($name, $method, @args);
+#
+# DESCRIPTION
+# This method invokes a named stage of the system, where $name is the name of
+# the stage, $method is the name of the method for invoking the stage and
+# @args are the arguments to the &method.
+# ------------------------------------------------------------------------------
+
+sub invoke_stage {
+ my ($self, $name, $method, @args) = @_;
+
+ # Print diagnostic at beginning of a stage
+ print '->', $name, ': start', "\n" if $self->verbose;
+ my $stime = time;
+
+ # Invoke the stage
+ my $rc = $self->$method (@args);
+
+ # Print diagnostic at end of a stage
+ my $total = time - $stime;
+ my $s_str = $total > 1 ? 'seconds' : 'second';
+ print '->', $name, ': ', $total, ' ', $s_str, "\n";
+
+ return $rc;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = $self->invoke_system (%args);
+#
+# DESCRIPTION
+# This is a prototype method for invoking the system.
+# ------------------------------------------------------------------------------
+
+sub invoke_system {
+ my $self = shift;
+ my %args = @_;
+
+ print "Dummy code.\n";
+
+ return 0;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = $obj->parse_cfg($is_for_inheritance);
+#
+# DESCRIPTION
+# This method calls other methods to parse the configuration file.
+# ------------------------------------------------------------------------------
+
+sub parse_cfg {
+ my ($self, $is_for_inheritance) = @_;
+
+ # Read config file
+ # ----------------------------------------------------------------------------
+ if (!$self->cfg()->src() || !$self->cfg()->read_cfg($is_for_inheritance)) {
+ return;
+ }
+
+ if ($self->cfg->type ne $self->type) {
+ w_report 'ERROR: ', $self->cfg->src, ': not a ', $self->type,
+ ' config file.';
+ return;
+ }
+
+ # Strip out optional prefix from all labels
+ # ----------------------------------------------------------------------------
+ if ($self->cfg_prefix) {
+ for my $line (@{ $self->cfg->lines }) {
+ $line->prefix ($self->cfg_prefix);
+ }
+ }
+
+ # Filter lines from the configuration file
+ # ----------------------------------------------------------------------------
+ my @cfg_lines = grep {
+ $_->slabel and # ignore empty/comment lines
+ index ($_->slabel, '%') != 0 and # ignore user variable
+ not $_->slabel_starts_with_cfg ('INC') # ignore INC line
+ } @{ $self->cfg->lines };
+
+ # Parse the lines to read in the various settings, by calling the methods:
+ # $self->parse_cfg_XXX, where XXX is: header, inherit, dest, and the values
+ # in the list @{ $self->cfg_methods }.
+ # ----------------------------------------------------------------------------
+ my $rc = 1;
+ for my $name (@{ $self->cfg_methods }) {
+ my $method = 'parse_cfg_' . $name;
+ $self->$method (\@cfg_lines) or $rc = 0;
+ }
+
+ # Report warnings/errors
+ # ----------------------------------------------------------------------------
+ for my $line (@cfg_lines) {
+ $rc = 0 if not $line->parsed;
+ my $mesg = $line->format_error;
+ w_report $mesg if $mesg;
+ }
+
+ return ($rc);
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = $self->parse_cfg_dest (\@cfg_lines);
+#
+# DESCRIPTION
+# This method parses the destination settings in the @cfg_lines.
+# ------------------------------------------------------------------------------
+
+sub parse_cfg_dest {
+ my ($self, $cfg_lines) = @_;
+
+ my $rc = 1;
+
+ # DEST/DIR declarations
+ # ----------------------------------------------------------------------------
+ my @lines = grep {
+ $_->slabel_starts_with_cfg ('DEST') or $_->slabel_starts_with_cfg ('DIR')
+ } @$cfg_lines;
+
+ # Only ROOTDIR declarations are accepted
+ for my $line (@lines) {
+ my ($d, $method) = $line->slabel_fields;
+ $d = lc $d;
+ $method = lc $method;
+
+ # Backward compatibility
+ $d = 'dest' if $d eq 'dir';
+
+ # Default to "rootdir"
+ $method = 'rootdir' if (not $method) or $method eq 'root';
+
+ # Only "rootdir" can be set
+ next unless $method eq 'rootdir';
+
+ $self->$d->$method (&expand_tilde ($line->value));
+ $line->parsed (1);
+ }
+
+ # Make sure root directory is set
+ # ----------------------------------------------------------------------------
+ if (not $self->dest->rootdir) {
+ w_report 'ERROR: ', $self->cfg->actual_src,
+ ': destination root directory not set.';
+ $rc = 0;
+ }
+
+ # Inherit destinations
+ # ----------------------------------------------------------------------------
+ @{$self->dest()->inherit()} = ();
+ my @nodes = @{$self->inherit()};
+ while (my $node = pop(@nodes)) {
+ push(@nodes, @{$node->inherit()});
+ push(@{$self->dest()->inherit()}, $node->dest());
+ }
+ @{$self->dest()->inherit()} = reverse(@{$self->dest()->inherit()});
+
+ return $rc;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = $self->parse_cfg_header (\@cfg_lines);
+#
+# DESCRIPTION
+# This method parses the header setting in the @cfg_lines.
+# ------------------------------------------------------------------------------
+
+sub parse_cfg_header {
+ my ($self, $cfg_lines) = @_;
+
+ # Set header lines as "parsed"
+ map {$_->parsed (1)} grep {$_->slabel_starts_with_cfg ('CFGFILE')} @$cfg_lines;
+
+ return 1;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = $self->parse_cfg_inherit (\@cfg_lines);
+#
+# DESCRIPTION
+# This method parses the inherit setting in the @cfg_lines.
+# ------------------------------------------------------------------------------
+
+sub parse_cfg_inherit {
+ my ($self, $cfg_lines) = @_;
+
+ # USE declaration
+ # ----------------------------------------------------------------------------
+ my @lines = grep {$_->slabel_starts_with_cfg ('USE')} @$cfg_lines;
+
+ # Check for cyclic dependency
+ if (@lines and grep {$_ eq $self->cfg->actual_src} @{ $self->inherited }) {
+ # Error if current configuration file is in its own inheritance hierarchy
+ w_report 'ERROR: ', $self->cfg->actual_src, ': attempt to inherit itself.';
+ $_->error ($_->label . ': ignored due to cyclic dependency.') for (@lines);
+ return 0;
+ }
+
+ my $rc = 1;
+
+ for my $line (@lines) {
+ # Invoke new instance of the current class
+ my $use = ref ($self)->new;
+
+ # Set configuration file, inheritance hierarchy
+ # and attempt to parse the configuration
+ $use->cfg->src (&expand_tilde ($line->value));
+ $use->inherited ([$self->cfg->actual_src, @{ $self->inherited }]);
+ $use->parse_cfg(1); # 1 = is for inheritance
+
+ # Add to list of inherit configurations
+ push @{ $self->inherit }, $use;
+
+ $line->parsed (1);
+ }
+
+ # Check locks in inherited destination
+ # ----------------------------------------------------------------------------
+ for my $use (@{ $self->inherit }) {
+ $rc = 0 unless $use->check_lock;
+ }
+
+ return $rc;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# @cfglines = $obj->to_cfglines ();
+#
+# DESCRIPTION
+# This method returns the configuration lines of this object.
+# ------------------------------------------------------------------------------
+
+sub to_cfglines {
+ my ($self) = @_;
+
+ my @inherited_dests = map {
+ FCM1::CfgLine->new (
+ label => $self->cfglabel ('USE'), value => $_->dest->rootdir
+ );
+ } @{ $self->inherit };
+
+ return (
+ FCM1::CfgLine::comment_block ('File header'),
+ FCM1::CfgLine->new (
+ label => $self->cfglabel ('CFGFILE') . $FCM1::Config::DELIMITER . 'TYPE',
+ value => $self->type,
+ ),
+ FCM1::CfgLine->new (
+ label => $self->cfglabel ('CFGFILE') . $FCM1::Config::DELIMITER . 'VERSION',
+ value => '1.0',
+ ),
+ FCM1::CfgLine->new (),
+
+ @inherited_dests,
+
+ FCM1::CfgLine::comment_block ('Destination'),
+ ($self->dest->to_cfglines()),
+ );
+}
+
+# ------------------------------------------------------------------------------
+
+1;
+
+__END__
diff --git a/lib/FCM1/Dest.pm b/lib/FCM1/Dest.pm
new file mode 100644
index 0000000..af65084
--- /dev/null
+++ b/lib/FCM1/Dest.pm
@@ -0,0 +1,899 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# NAME
+# FCM1::Dest
+#
+# DESCRIPTION
+# This class contains methods to set up a destination location of an FCM
+# extract/build.
+#
+# ------------------------------------------------------------------------------
+use warnings;
+use strict;
+
+package FCM1::Dest;
+use base qw{FCM1::Base};
+
+use Carp qw{croak} ;
+use Cwd qw{cwd} ;
+use FCM1::CfgLine ;
+use FCM1::Timer qw{timestamp_command} ;
+use FCM1::Util qw{run_command touch_file w_report};
+use File::Basename qw{basename dirname} ;
+use File::Find qw{find} ;
+use File::Path qw{mkpath rmtree} ;
+use File::Spec ;
+use Sys::Hostname qw{hostname} ;
+use Text::ParseWords qw{shellwords} ;
+
+# Useful variables
+# ------------------------------------------------------------------------------
+# List of configuration files
+our @cfgfiles = (
+ 'bldcfg', # default location of the build configuration file
+ 'extcfg', # default location of the extract configuration file
+);
+
+# List of cache and configuration files, according to the dest type
+our @cfgfiles_type = (
+ 'cache', # default location of the cache file
+ 'cfg', # default location of the configuration file
+ 'parsedcfg', # default location of the as-parsed configuration file
+);
+
+# List of lock files
+our @lockfiles = (
+ 'bldlock', # the build lock file
+ 'extlock', # the extract lock file
+);
+
+# List of misc files
+our @miscfiles_bld = (
+ 'bldrunenvsh', # the build run environment shell script
+ 'bldmakefile', # the build Makefile
+);
+
+# List of sub-directories created by extract
+our @subdirs_ext = (
+ 'cfgdir', # sub-directory for configuration files
+ 'srcdir', # sub-directory for source tree
+);
+
+# List of sub-directories that can be archived by "tar" at end of build
+our @subdirs_tar = (
+ 'donedir', # sub-directory for "done" files
+ 'flagsdir', # sub-directory for "flags" files
+ 'incdir', # sub-directory for include files
+ 'ppsrcdir', # sub-directory for pre-process source tree
+ 'objdir', # sub-directory for object files
+);
+
+# List of sub-directories created by build
+our @subdirs_bld = (
+ 'bindir', # sub-directory for executables
+ 'etcdir', # sub-directory for miscellaneous files
+ 'libdir', # sub-directory for object libraries
+ 'tmpdir', # sub-directory for temporary build files
+ @subdirs_tar, # -see above-
+);
+
+# List of sub-directories under rootdir
+our @subdirs = (
+ 'cachedir', # sub-directory for caches
+ @subdirs_ext, # -see above-
+ @subdirs_bld, # -see above-
+);
+
+# List of inherited search paths
+# "rootdir" + all @subdirs, with "XXXdir" replaced with "XXXpath"
+our @paths = (
+ 'rootpath',
+ (map {my $key = $_; $key =~ s{dir\z}{path}msx; $key} @subdirs),
+);
+
+# List of properties and their default values.
+my %PROP_OF = (
+ # the original destination (if current destination is a mirror)
+ 'dest0' => undef,
+ # list of inherited FCM1::Dest objects
+ 'inherit' => [],
+ # remote login name
+ 'logname' => scalar(getpwuid($<)),
+ # lock file
+ 'lockfile' => undef,
+ # remote machine
+ 'machine' => hostname(),
+ # mirror command to use
+ 'mirror_cmd' => 'rsync',
+ # (for rsync) remote mkdir, the remote shell command
+ 'rsh_mkdir_rsh' => 'ssh',
+ # (for rsync) remote mkdir, the remote shell command flags
+ 'rsh_mkdir_rshflags' => '-n -oBatchMode=yes',
+ # (for rsync) remote mkdir, the remote shell command
+ 'rsh_mkdir_mkdir' => 'mkdir',
+ # (for rsync) remote mkdir, the remote shell command flags
+ 'rsh_mkdir_mkdirflags' => '-p',
+ # (for rsync) remote mkdir, the remote shell command
+ 'rsync' => 'rsync',
+ # (for rsync) remote mkdir, the remote shell command flags
+ 'rsyncflags' => q{-a --exclude='.*' --delete-excluded}
+ . q{ --timeout=900 --rsh='ssh -oBatchMode=yes'},
+ # destination root directory
+ 'rootdir' => undef,
+ # destination type, "bld" (default) or "ext"
+ 'type' => 'bld',
+);
+# Hook for property setter
+my %PROP_HOOK_OF = (
+ 'inherit' => \&_reset_inherit,
+ 'rootdir' => \&_reset_rootdir,
+);
+
+# Mirror implementations
+my %MIRROR_IMPL_OF = (
+ rdist => \&_mirror_with_rdist,
+ rsync => \&_mirror_with_rsync,
+);
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $obj = FCM1::Dest->new(%args);
+#
+# DESCRIPTION
+# This method constructs a new instance of the FCM1::Dest class. See above for
+# allowed list of properties. (KEYS should be in uppercase.)
+# ------------------------------------------------------------------------------
+
+sub new {
+ my ($class, %args) = @_;
+ my $self = bless(FCM1::Base->new(%args), $class);
+ while (my ($key, $value) = each(%args)) {
+ $key = lc($key);
+ if (exists($PROP_OF{$key})) {
+ $self->{$key} = $value;
+ }
+ }
+ for my $key (@subdirs, @paths, @lockfiles, @cfgfiles) {
+ $self->{$key} = undef;
+ }
+ return $self;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $self->DESTROY;
+#
+# DESCRIPTION
+# This method is called automatically when the FCM1::Dest object is
+# destroyed.
+# ------------------------------------------------------------------------------
+
+sub DESTROY {
+ my $self = shift;
+
+ # Remove the lockfile if it is set
+ unlink $self->lockfile if $self->lockfile and -f $self->lockfile;
+
+ return;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $value = $obj->X($value);
+#
+# DESCRIPTION
+# Details of these properties are explained in %PROP_OF.
+# ------------------------------------------------------------------------------
+
+while (my ($key, $default) = each(%PROP_OF)) {
+ no strict 'refs';
+ *{$key} = sub {
+ my $self = shift();
+ # Set property to specified value
+ if (@_) {
+ $self->{$key} = $_[0];
+ if (exists($PROP_HOOK_OF{$key})) {
+ $PROP_HOOK_OF{$key}->($self, $key);
+ }
+ }
+ # Sets default where possible
+ if (!defined($self->{$key})) {
+ $self->{$key} = $default;
+ }
+ return $self->{$key};
+ };
+}
+
+# Remote shell property: deprecated.
+sub remote_shell {
+ my $self = shift();
+ $self->rsh_mkdir_rsh(@_);
+}
+
+# Resets properties associated with root directory.
+sub _reset_rootdir {
+ my $self = shift();
+ for my $key (@cfgfiles, @lockfiles, @miscfiles_bld, @subdirs) {
+ $self->{$key} = undef;
+ }
+}
+
+# Reset properties associated with inherited paths.
+sub _reset_inherit {
+ my $self = shift();
+ for my $key (@paths) {
+ $self->{$key} = undef;
+ }
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $value = $obj->X;
+#
+# DESCRIPTION
+# This method returns X, where X is a location derived from rootdir, and can
+# be one of:
+# bindir, bldcfg, blddir, bldlock, bldrunenv, cache, cachedir, cfg, cfgdir,
+# donedir, etcdir, extcfg, extlock, flagsdir, incdir, libdir, parsedcfg,
+# ppsrcdir, objdir, or tmpdir.
+#
+# Details of these properties are explained earlier.
+# ------------------------------------------------------------------------------
+
+for my $name (@cfgfiles, @cfgfiles_type, @lockfiles, @miscfiles_bld, @subdirs) {
+ no strict 'refs';
+
+ *$name = sub {
+ my $self = shift;
+
+ # If variable not set, derive it from rootdir
+ if ($self->rootdir and not defined $self->{$name}) {
+ if ($name eq 'cache') {
+ # Cache file under root/.cache
+ $self->{$name} = File::Spec->catfile (
+ $self->cachedir, $self->setting ('CACHE'),
+ );
+
+ } elsif ($name eq 'cfg') {
+ # Configuration file of current type
+ my $method = $self->type . 'cfg';
+ $self->{$name} = $self->$method;
+
+ } elsif (grep {$name eq $_} @cfgfiles) {
+ # Configuration files under the root/cfg
+ (my $label = uc ($name)) =~ s/CFG//;
+ $self->{$name} = File::Spec->catfile (
+ $self->cfgdir, $self->setting ('CFG_NAME', $label),
+ );
+
+ } elsif (grep {$name eq $_} @lockfiles) {
+ # Lock file
+ $self->{$name} = File::Spec->catfile (
+ $self->rootdir, $self->setting ('LOCK', uc ($name)),
+ );
+
+ } elsif (grep {$name eq $_} @miscfiles_bld) {
+ # Misc file
+ $self->{$name} = File::Spec->catfile (
+ $self->rootdir, $self->setting ('BLD_MISC', uc ($name)),
+ );
+
+ } elsif ($name eq 'parsedcfg') {
+ # As-parsed configuration file of current type
+ $self->{$name} = File::Spec->catfile (
+ dirname ($self->cfg),
+ $self->setting (qw/CFG_NAME PARSED/) . basename ($self->cfg),
+ )
+
+ } elsif (grep {$name eq $_} @subdirs) {
+ # Sub-directories under the root
+ (my $label = uc ($name)) =~ s/DIR//;
+ $self->{$name} = File::Spec->catfile (
+ $self->rootdir,
+ $self->setting ('DIR', $label),
+ ($name eq 'cachedir' ? '.' . $self->type : ()),
+ );
+ }
+ }
+
+ return $self->{$name};
+ }
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $value = $obj->X;
+#
+# DESCRIPTION
+# This method returns X, an array containing the search path of a destination
+# directory, which can be one of:
+# binpath, bldpath, cachepath, cfgpath, donepath, etcpath, flagspath,
+# incpath, libpath, ppsrcpath, objpath, rootpath, srcpath, or tmppath,
+#
+# Details of these properties are explained earlier.
+# ------------------------------------------------------------------------------
+
+for my $name (@paths) {
+ no strict 'refs';
+
+ *$name = sub {
+ my $self = shift;
+
+ (my $dir = $name) =~ s/path/dir/;
+
+ if ($self->$dir and not defined $self->{$name}) {
+ my @path = ();
+
+ # Recursively inherit the search path
+ for my $d (@{ $self->inherit }) {
+ unshift @path, $d->$dir;
+ }
+
+ # Place the path of the current build in the front
+ unshift @path, $self->$dir;
+
+ $self->{$name} = \@path;
+ }
+
+ return $self->{$name};
+ }
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = $obj->archive ();
+#
+# DESCRIPTION
+# This method creates TAR archives for selected sub-directories.
+# ------------------------------------------------------------------------------
+
+sub archive {
+ my $self = shift;
+
+ # Save current directory
+ my $cwd = cwd ();
+
+ my $tar = $self->setting (qw/OUTFILE_EXT TAR/);
+ my $verbose = $self->verbose;
+
+ for my $name (@subdirs_tar) {
+ my $dir = $self->$name;
+
+ # Ignore unless sub-directory exists
+ next unless -d $dir;
+
+ # Change to container directory
+ my $base = basename ($dir);
+ print 'cd ', dirname ($dir), "\n" if $verbose > 2;
+ chdir dirname ($dir);
+
+ # Run "tar" command
+ my $rc = &run_command (
+ [qw/tar -czf/, $base . $tar, $base],
+ PRINT => $verbose > 1, ERROR => 'warn',
+ );
+
+ # Remove sub-directory
+ &run_command ([qw/rm -rf/, $base], PRINT => $verbose > 1) if not $rc;
+ }
+
+ # Change back to "current" directory
+ print 'cd ', $cwd, "\n" if $verbose > 2;
+ chdir $cwd;
+
+ return 1;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $authority = $obj->authority();
+#
+# DESCRIPTION
+# Returns LOGNAME at MACHINE for this destination if LOGNAME is defined and not
+# the same as the user ID of the current process. Returns MACHINE if LOGNAME
+# is the same as the user ID of the current process, but MACHINE is not the
+# same as the current hostname. Returns an empty string if LOGNAME and
+# MACHINE are not defined or are the same as in the current process.
+# ------------------------------------------------------------------------------
+
+sub authority {
+ my $self = shift;
+ my $return = '';
+
+ if ($self->logname ne $self->config->user_id) {
+ $return = $self->logname . '@' . $self->machine;
+
+ } elsif ($self->machine ne &hostname()) {
+ $return = $self->machine;
+ }
+
+ return $return;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = $obj->clean([ITEM => <list>,] [MODE => 'ALL|CONTENT|EMPTY',]);
+#
+# DESCRIPTION
+# This method removes files/directories from the destination. If ITEM is set,
+# it must be a reference to a list of method names for files/directories to
+# be removed. Otherwise, the list is determined by the destination type. If
+# MODE is ALL, all directories/files created by the extract/build are
+# removed. If MODE is CONTENT, only contents within sub-directories are
+# removed. If MODE is EMPTY (default), only empty sub-directories are
+# removed.
+# ------------------------------------------------------------------------------
+
+sub clean {
+ my ($self, %args) = @_;
+ my $mode = exists $args{MODE} ? $args{MODE} : 'EMPTY';
+ my $rc = 1;
+ my @names
+ = $args{ITEM} ? @{$args{ITEM}}
+ : $self->type() eq 'ext' ? ('cachedir', @subdirs_ext)
+ : ('cachedir', @subdirs_bld, @miscfiles_bld)
+ ;
+ my @items;
+ if ($mode eq 'CONTENT') {
+ for my $name (@names) {
+ my $item = $self->$name();
+ push(@items, _directory_contents($item));
+ }
+ }
+ else {
+ for my $name (@names) {
+ my $item = $self->$name();
+ if ($mode eq 'ALL' || -d $item && !_directory_contents($item)) {
+ push(@items, $item);
+ }
+ }
+ }
+ for my $item (@items) {
+ if ($self->verbose() >= 2) {
+ printf("%s: remove\n", $item);
+ }
+ eval {rmtree($item)};
+ if ($@) {
+ w_report($@);
+ $rc = 0;
+ }
+ }
+ return $rc;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = $obj->create ([DIR => <dir-list>,]);
+#
+# DESCRIPTION
+# This method creates the directories of a destination. If DIR is set, it
+# must be a reference to a list of sub-directories to be created. Otherwise,
+# the sub-directory list is determined by the destination type. It returns
+# true if the destination is created or if it exists and is writable.
+# ------------------------------------------------------------------------------
+
+sub create {
+ my ($self, %args) = @_;
+
+ my $rc = 1;
+
+ my @dirs;
+ if (exists $args{DIR} and $args{DIR}) {
+ # Create only selected sub-directories
+ @dirs = @{ $args{DIR} };
+
+ } else {
+ # Create rootdir, cachedir and read-write sub-directories for extract/build
+ @dirs = (
+ qw/rootdir cachedir/,
+ ($self->type eq 'ext' ? @subdirs_ext : @subdirs_bld),
+ );
+ }
+
+ for my $name (@dirs) {
+ my $dir = $self->$name;
+
+ # Create directory if it does not already exist
+ if (not -d $dir) {
+ print 'Make directory: ', $dir, "\n" if $self->verbose > 1;
+ mkpath $dir;
+ }
+
+ # Check whether directory exists and is writable
+ if (!-d $dir) {
+ w_report 'ERROR: ', $dir, ': cannot create destination.';
+ $rc = 0;
+ }
+ }
+
+ return $rc;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = $obj->create_bldrunenvsh ();
+#
+# DESCRIPTION
+# This method creates the runtime environment script for the build.
+# ------------------------------------------------------------------------------
+
+sub create_bldrunenvsh {
+ my $self = shift;
+
+ # Path to executable files and directory for misc files
+ my @bin_paths = grep {_directory_contents($_)} @{$self->binpath()};
+ my $bin_dir = -d $self->bindir() ? $self->bindir() : undef;
+ my $etc_dir = _directory_contents($self->etcdir()) ? $self->etcdir() : undef;
+
+ # Create a runtime environment script if necessary
+ if (@bin_paths || $etc_dir) {
+ my $path = $self->bldrunenvsh();
+ open(my $handle, '>', $path) || croak("$path: cannot open ($!)\n");
+ printf($handle "#!%s\n", $self->setting(qw/TOOL SHELL/));
+ if (@bin_paths) {
+ printf($handle "PATH=%s:\$PATH\n", join(':', @bin_paths));
+ print($handle "export PATH\n");
+ }
+ if ($etc_dir) {
+ printf($handle "FCM_ETCDIR=%s\n", $etc_dir);
+ print($handle "export FCM_ETCDIR\n");
+ }
+ close($handle) || croak("$path: cannot close ($!)\n");
+
+ # Create symbolic links fcm_env.ksh and bin/fcm_env.ksh for backward
+ # compatibility
+ my $FCM_ENV_KSH = 'fcm_env.ksh';
+ for my $link (
+ File::Spec->catfile($self->rootdir, $FCM_ENV_KSH),
+ ($bin_dir ? File::Spec->catfile($bin_dir, $FCM_ENV_KSH) : ()),
+ ) {
+ if (-l $link && readlink($link) ne $path || -e $link) {
+ unlink($link);
+ }
+ if (!-l $link) {
+ symlink($path, $link) || croak("$link: cannot create symbolic link\n");
+ }
+ }
+ }
+ return 1;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = $obj->dearchive ();
+#
+# DESCRIPTION
+# This method extracts from TAR archives for selected sub-directories.
+# ------------------------------------------------------------------------------
+
+sub dearchive {
+ my $self = shift;
+
+ my $tar = $self->setting (qw/OUTFILE_EXT TAR/);
+ my $verbose = $self->verbose;
+
+ # Extract archives if necessary
+ for my $name (@subdirs_tar) {
+ my $tar_file = $self->$name . $tar;
+
+ # Check whether tar archive exists for the named sub-directory
+ next unless -f $tar_file;
+
+ # If so, extract the archive and remove it afterwards
+ &run_command ([qw/tar -xzf/, $tar_file], PRINT => $verbose > 1);
+ &run_command ([qw/rm -f/, $tar_file], PRINT => $verbose > 1);
+ }
+
+ return 1;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $name = $obj->get_pkgname_of_path ($path);
+#
+# DESCRIPTION
+# This method returns the package name of $path if $path is in (a relative
+# path of) $self->srcdir, or undef otherwise.
+# ------------------------------------------------------------------------------
+
+sub get_pkgname_of_path {
+ my ($self, $path) = @_;
+
+ my $relpath = File::Spec->abs2rel ($path, $self->srcdir);
+ my $name = $relpath ? [File::Spec->splitdir ($relpath)] : undef;
+
+ return $name;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# %src = $obj->get_source_files ();
+#
+# DESCRIPTION
+# This method returns a hash (keys = package names, values = file names)
+# under $self->srcdir.
+# ------------------------------------------------------------------------------
+
+sub get_source_files {
+ my $self = shift;
+
+ my %src;
+ if ($self->srcdir and -d $self->srcdir) {
+ &find (sub {
+ return if /^\./; # ignore system/hidden file
+ return if -d $File::Find::name; # ignore directory
+
+ my $name = join (
+ '__', @{ $self->get_pkgname_of_path ($File::Find::name) },
+ );
+ $src{$name} = $File::Find::name;
+ }, $self->srcdir);
+ }
+
+ return \%src;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = $obj->mirror (\@items);
+#
+# DESCRIPTION
+# This method mirrors @items (list of method names for directories or files)
+# from $dest0 (which must be an instance of FCM1::Dest for a local
+# destination) to this destination.
+# ------------------------------------------------------------------------------
+
+sub mirror {
+ my ($self, $items_ref) = @_;
+ if ($self->authority() || $self->dest0()->rootdir() ne $self->rootdir()) {
+ # Diagnostic
+ if ($self->verbose()) {
+ printf(
+ "Destination: %s\n",
+ ($self->authority() ? $self->authority() . q{:} : q{}) . $self->rootdir()
+ );
+ }
+ if ($MIRROR_IMPL_OF{$self->mirror_cmd()}) {
+ $MIRROR_IMPL_OF{$self->mirror_cmd()}->($self, $self->dest0(), $items_ref);
+ }
+ else {
+ # Unknown mirroring tool
+ w_report($self->mirror_cmd, ': unknown mirroring tool, abort.');
+ return 0;
+ }
+ }
+ return 1;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = $self->_mirror_with_rdist ($dest0, \@items);
+#
+# DESCRIPTION
+# This internal method implements $self->mirror with "rdist".
+# ------------------------------------------------------------------------------
+
+sub _mirror_with_rdist {
+ my ($self, $dest0, $items) = @_;
+
+ my $rhost = $self->authority ? $self->authority : &hostname();
+
+ # Print distfile content to temporary file
+ my @distfile = ();
+ for my $label (@$items) {
+ push @distfile, '( ' . $dest0->$label . ' ) -> ' . $rhost . "\n";
+ push @distfile, ' install ' . $self->$label . ';' . "\n";
+ }
+
+ # Set up mirroring command (use "rdist" at the moment)
+ my $command = 'rdist -R';
+ $command .= ' -q' unless $self->verbose > 1;
+ $command .= ' -f - 1>/dev/null';
+
+ # Diagnostic
+ my $croak = 'Cannot execute "' . $command . '"';
+ if ($self->verbose > 2) {
+ print timestamp_command ($command, 'Start');
+ print ' ', $_ for (@distfile);
+ }
+
+ # Execute the mirroring command
+ open COMMAND, '|-', $command or croak $croak, ' (', $!, '), abort';
+ for my $line (@distfile) {
+ print COMMAND $line;
+ }
+ close COMMAND or croak $croak, ' (', $?, '), abort';
+
+ # Diagnostic
+ print timestamp_command ($command, 'End ') if $self->verbose > 2;
+
+ return 1;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = $self->_mirror_with_rsync($dest0, \@items);
+#
+# DESCRIPTION
+# This internal method implements $self->mirror() with "rsync".
+# ------------------------------------------------------------------------------
+
+sub _mirror_with_rsync {
+ my ($self, $dest0, $items_ref) = @_;
+ my @rsh_mkdir;
+ if ($self->authority()) {
+ @rsh_mkdir = (
+ $self->rsh_mkdir_rsh(),
+ shellwords($self->rsh_mkdir_rshflags()),
+ $self->authority(),
+ $self->rsh_mkdir_mkdir(),
+ shellwords($self->rsh_mkdir_mkdirflags()),
+ );
+ }
+ my @rsync = ($self->rsync(), shellwords($self->rsyncflags()));
+ my @rsync_verbose = ($self->verbose() > 2 ? '-v' : ());
+ my $auth = $self->authority() ? $self->authority() . q{:} : q{};
+ for my $item (@{$items_ref}) {
+ # Create container directory, as rsync does not do it automatically
+ my $dir = dirname($self->$item());
+ if (@rsh_mkdir) {
+ run_command([@rsh_mkdir, $dir], TIME => $self->verbose() > 2);
+ }
+ else {
+ mkpath($dir);
+ }
+ run_command(
+ [@rsync, @rsync_verbose, $dest0->$item(), $auth . $dir],
+ TIME => $self->verbose > 2,
+ );
+ }
+ return 1;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = $obj->set_lock ();
+#
+# DESCRIPTION
+# This method sets a lock in the current destination.
+# ------------------------------------------------------------------------------
+
+sub set_lock {
+ my $self = shift;
+
+ $self->lockfile ();
+
+ if ($self->type eq 'ext' and not $self->dest0) {
+ # Only set an extract lock for the local destination
+ $self->lockfile ($self->extlock);
+
+ } elsif ($self->type eq 'bld') {
+ # Set a build lock
+ $self->lockfile ($self->bldlock);
+ }
+
+ return &touch_file ($self->lockfile) if $self->lockfile;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# @cfglines = $obj->to_cfglines ([$index]);
+#
+# DESCRIPTION
+# This method returns a list of configuration lines for the current
+# destination. If it is set, $index is the index number of the current
+# destination.
+# ------------------------------------------------------------------------------
+
+sub to_cfglines {
+ my ($self, $index) = @_;
+
+ my $PREFIX = $self->cfglabel($self->dest0() ? 'RDEST' : 'DEST');
+ my $SUFFIX = ($index ? $FCM1::Config::DELIMITER . $index : q{});
+
+ my @return = (
+ FCM1::CfgLine->new(label => $PREFIX . $SUFFIX, value => $self->rootdir()),
+ );
+ if ($self->dest0()) {
+ for my $name (qw{
+ logname
+ machine
+ mirror_cmd
+ rsh_mkdir_rsh
+ rsh_mkdir_rshflags
+ rsh_mkdir_mkdir
+ rsh_mkdir_mkdirflags
+ rsync
+ rsyncflags
+ }) {
+ if ($self->{$name} && $self->{$name} ne $PROP_OF{$name}) { # not default
+ push(
+ @return,
+ FCM1::CfgLine->new(
+ label => $PREFIX . $FCM1::Config::DELIMITER . uc($name) . $SUFFIX,
+ value => $self->{$name},
+ ),
+ );
+ }
+ }
+ }
+
+ return @return;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $string = $obj->write_rules ();
+#
+# DESCRIPTION
+# This method returns a string containing Makefile variable declarations for
+# directories and search paths in this destination.
+# ------------------------------------------------------------------------------
+
+sub write_rules {
+ my $self = shift;
+ my $return = '';
+
+ # FCM_*DIR*
+ for my $i (0 .. @{ $self->inherit }) {
+ for my $name (@paths) {
+ (my $label = $name) =~ s/path$/dir/;
+ my $dir = $name eq 'rootpath' ? $self->$name->[$i] : File::Spec->catfile (
+ '$(FCM_ROOTDIR' . ($i ? $i : '') . ')',
+ File::Spec->abs2rel ($self->$name->[$i], $self->rootpath->[$i]),
+ );
+
+ $return .= ($i ? '' : 'export ') . 'FCM_' . uc ($label) . ($i ? $i : '') .
+ ' := ' . $dir . "\n";
+ }
+ }
+
+ # FCM_*PATH
+ for my $name (@paths) {
+ (my $label = $name) =~ s/path$/dir/;
+
+ $return .= 'export FCM_' . uc ($name) . ' := ';
+ for my $i (0 .. @{ $self->$name } - 1) {
+ $return .= ($i ? ':' : '') . '$(FCM_' . uc ($label) . ($i ? $i : '') . ')';
+ }
+ $return .= "\n";
+ }
+
+ $return .= "\n";
+
+ return $return;
+}
+
+# Returns contents in directory.
+sub _directory_contents {
+ my $path = shift();
+ if (!-d $path) {
+ return;
+ }
+ opendir(my $handle, $path) || croak("$path: cannot open directory ($!)\n");
+ my @items = grep {$_ ne q{.} && $_ ne q{..}} readdir($handle);
+ closedir($handle);
+ map {File::Spec->catfile($path . $_)} @items;
+}
+
+# ------------------------------------------------------------------------------
+
+1;
+
+__END__
diff --git a/lib/FCM1/Exception.pm b/lib/FCM1/Exception.pm
new file mode 100644
index 0000000..0b0c192
--- /dev/null
+++ b/lib/FCM1/Exception.pm
@@ -0,0 +1,108 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+
+package FCM1::Exception;
+use overload (q{""} => \&as_string);
+
+use Scalar::Util qw{blessed};
+
+# ------------------------------------------------------------------------------
+# Returns true if $e is a blessed instance of this class.
+sub caught {
+ my ($class, $e) = @_;
+ return (blessed($e) && $e->isa($class));
+}
+
+# ------------------------------------------------------------------------------
+# Constructor
+sub new {
+ my ($class, $args_ref) = @_;
+ return bless(
+ {message => q{unknown problem}, ($args_ref ? %{$args_ref} : ())},
+ $class,
+ );
+}
+
+# ------------------------------------------------------------------------------
+# Returns a string representation of this exception
+sub as_string {
+ my ($self) = @_;
+ return sprintf("%s: %s\n", blessed($self), $self->get_message());
+}
+
+# ------------------------------------------------------------------------------
+# Returns the message of this exception
+sub get_message {
+ my ($self) = @_;
+ return $self->{message};
+}
+
+1;
+__END__
+
+=head1 NAME
+
+FCM1::Exception
+
+=head1 SYNOPSIS
+
+ use FCM1::Exception;
+ eval {
+ croak(FCM1::Exception->new({message => $message}));
+ };
+ if ($@) {
+ if (FCM1::Exception->caught($@)) {
+ print({STDERR} $@);
+ }
+ }
+
+=head1 DESCRIPTION
+
+This exception is raised when there is a generic problem in FCM.
+
+=head1 METHODS
+
+=over 4
+
+=item $class->caught($e)
+
+Returns true if $e is a blessed instance of this class.
+
+=item $class->new({message=E<gt>$message})
+
+Returns a new instance of this exception. Its first argument must be a
+reference to a hash containing the detailed I<message> of the exception.
+
+=item $e->as_string()
+
+Returns a string representation of this exception.
+
+=item $e->get_message()
+
+Returns the detailed message of this exception.
+
+=back
+
+=head1 COPYRIGHT
+
+E<169> Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM1/Extract.pm b/lib/FCM1/Extract.pm
new file mode 100644
index 0000000..9dbda6f
--- /dev/null
+++ b/lib/FCM1/Extract.pm
@@ -0,0 +1,1132 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# NAME
+# FCM1::Extract
+#
+# DESCRIPTION
+# This is the top level class for the FCM extract system.
+#
+# ------------------------------------------------------------------------------
+
+package FCM1::Extract;
+ at ISA = qw(FCM1::ConfigSystem);
+
+# Standard pragma
+use warnings;
+use strict;
+
+# Standard modules
+use File::Path;
+use File::Spec;
+
+# FCM component modules
+use FCM1::CfgFile;
+use FCM1::CfgLine;
+use FCM1::Config;
+use FCM1::ConfigSystem;
+use FCM1::Dest;
+use FCM1::ExtractFile;
+use FCM1::ExtractSrc;
+use FCM1::Keyword;
+use FCM1::ReposBranch;
+use FCM1::SrcDirLayer;
+use FCM1::Util;
+
+# List of scalar property methods for this class
+my @scalar_properties = (
+ 'bdeclare', # list of build declarations
+ 'branches', # list of repository branches
+ 'conflict', # conflict mode
+ 'rdest' , # remote destination information
+);
+
+# List of hash property methods for this class
+my @hash_properties = (
+ 'srcdirs' , # list of source directory extract info
+ 'files', # list of files processed key=pkgname, value=FCM1::ExtractFile
+);
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $obj = FCM1::Extract->new;
+#
+# DESCRIPTION
+# This method constructs a new instance of the FCM1::Extract class.
+# ------------------------------------------------------------------------------
+
+sub new {
+ my $this = shift;
+ my %args = @_;
+ my $class = ref $this || $this;
+
+ my $self = FCM1::ConfigSystem->new (%args);
+
+ $self->{$_} = undef for (@scalar_properties);
+
+ $self->{$_} = {} for (@hash_properties);
+
+ bless $self, $class;
+
+ # List of sub-methods for parse_cfg
+ push @{ $self->cfg_methods }, (qw/rdest bld conflict project/);
+
+ # System type
+ $self->type ('ext');
+
+ return $self;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $value = $obj->X;
+# $obj->X ($value);
+#
+# DESCRIPTION
+# Details of these properties are explained in @scalar_properties.
+# ------------------------------------------------------------------------------
+
+for my $name (@scalar_properties) {
+ no strict 'refs';
+
+ *$name = sub {
+ my $self = shift;
+
+ # Argument specified, set property to specified argument
+ if (@_) {
+ $self->{$name} = $_[0];
+ }
+
+ # Default value for property
+ if (not defined $self->{$name}) {
+ if ($name eq 'bdeclare' or $name eq 'branches') {
+ # Reference to an array
+ $self->{$name} = [];
+
+ } elsif ($name eq 'rdest') {
+ # New extract destination local/remote
+ $self->{$name} = FCM1::Dest->new (DEST0 => $self->dest(), TYPE => 'ext');
+
+ } elsif ($name eq 'conflict') {
+ # Conflict mode, default to "merge"
+ $self->{$name} = 'merge';
+ }
+ }
+
+ return $self->{$name};
+ }
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# %hash = %{ $obj->X () };
+# $obj->X (\%hash);
+#
+# $value = $obj->X ($index);
+# $obj->X ($index, $value);
+#
+# DESCRIPTION
+# Details of these properties are explained in @hash_properties.
+#
+# If no argument is set, this method returns a hash containing a list of
+# objects. If an argument is set and it is a reference to a hash, the objects
+# are replaced by the specified hash.
+#
+# If a scalar argument is specified, this method returns a reference to an
+# object, if the indexed object exists or undef if the indexed object does
+# not exist. If a second argument is set, the $index element of the hash will
+# be set to the value of the argument.
+# ------------------------------------------------------------------------------
+
+for my $name (@hash_properties) {
+ no strict 'refs';
+
+ *$name = sub {
+ my ($self, $arg1, $arg2) = @_;
+
+ # Ensure property is defined as a reference to a hash
+ $self->{$name} = {} if not defined ($self->{$name});
+
+ # Argument 1 can be a reference to a hash or a scalar index
+ my ($index, %hash);
+
+ if (defined $arg1) {
+ if (ref ($arg1) eq 'HASH') {
+ %hash = %$arg1;
+
+ } else {
+ $index = $arg1;
+ }
+ }
+
+ if (defined $index) {
+ # A scalar index is defined, set and/or return the value of an element
+ $self->{$name}{$index} = $arg2 if defined $arg2;
+
+ return (
+ exists $self->{$name}{$index} ? $self->{$name}{$index} : undef
+ );
+
+ } else {
+ # A scalar index is not defined, set and/or return the hash
+ $self->{$name} = \%hash if defined $arg1;
+ return $self->{$name};
+ }
+ }
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = $self->check_lock_is_allowed ($lock);
+#
+# DESCRIPTION
+# This method returns true if it is OK for $lock to exist in the destination.
+# ------------------------------------------------------------------------------
+
+sub check_lock_is_allowed {
+ my ($self, $lock) = @_;
+
+ # Allow existence of build lock in inherited extract
+ return ($lock eq $self->dest->bldlock and @{ $self->inherited });
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = $self->invoke_extract ();
+#
+# DESCRIPTION
+# This method invokes the extract stage of the extract system. It returns
+# true on success.
+# ------------------------------------------------------------------------------
+
+sub invoke_extract {
+ my $self = shift;
+
+ my $rc = 1;
+
+ my @methods = (
+ 'expand_cfg', # expand URL, revision keywords, relative path, etc
+ 'create_dir_stack', # analyse the branches to create an extract sequence
+ 'extract_src', # use the sequence to extract source to destination
+ 'write_cfg', # generate final configuration file
+ 'write_cfg_bld', # generate build configuration file
+ );
+
+ for my $method (@methods) {
+ $rc = $self->$method if $rc;
+ }
+
+ return $rc;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = $self->invoke_mirror ();
+#
+# DESCRIPTION
+# This method invokes the mirror stage of the extract system. It returns
+# true on success.
+# ------------------------------------------------------------------------------
+
+sub invoke_mirror {
+ my $self = shift;
+ return $self->rdest->mirror ([qw/bldcfg extcfg srcdir/]);
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = $self->invoke_system ();
+#
+# DESCRIPTION
+# This method invokes the extract system. It returns true on success.
+# ------------------------------------------------------------------------------
+
+sub invoke_system {
+ my $self = shift;
+
+ my $rc = 1;
+
+ $rc = $self->invoke_stage ('Extract', 'invoke_extract');
+ $rc = $self->invoke_stage ('Mirror', 'invoke_mirror')
+ if $rc and $self->rdest->rootdir;
+
+ return $rc;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = $self->parse_cfg_rdest(\@cfg_lines);
+#
+# DESCRIPTION
+# This method parses the remote destination settings in the @cfg_lines.
+# ------------------------------------------------------------------------------
+
+sub parse_cfg_rdest {
+ my ($self, $cfg_lines_ref) = @_;
+
+ # RDEST declarations
+ # ----------------------------------------------------------------------------
+ for my $line (grep {$_->slabel_starts_with_cfg('RDEST')} @{$cfg_lines_ref}) {
+ my ($d, $method) = map {lc($_)} $line->slabel_fields();
+ $method ||= 'rootdir';
+ if ($self->rdest()->can($method)) {
+ $self->rdest()->$method(expand_tilde($line->value()));
+ $line->parsed(1);
+ }
+ }
+
+ # MIRROR declaration, deprecated = RDEST::MIRROR_CMD
+ # ----------------------------------------------------------------------------
+ for my $line (grep {$_->slabel_starts_with_cfg('MIRROR')} @{$cfg_lines_ref}) {
+ $self->rdest()->mirror_cmd($line->value());
+ $line->parsed(1);
+ }
+
+ return 1;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = $self->parse_cfg_bld (\@cfg_lines);
+#
+# DESCRIPTION
+# This method parses the build configurations in the @cfg_lines.
+# ------------------------------------------------------------------------------
+
+sub parse_cfg_bld {
+ my ($self, $cfg_lines) = @_;
+
+ # BLD declarations
+ # ----------------------------------------------------------------------------
+ for my $line (grep {$_->slabel_starts_with_cfg ('BDECLARE')} @$cfg_lines) {
+ # Remove BLD from label
+ my @words = $line->slabel_fields;
+
+ # Check that a declaration follows BLD
+ next if @words <= 1;
+
+ push @{ $self->bdeclare }, FCM1::CfgLine->new (
+ LABEL => join ($FCM1::Config::DELIMITER, @words),
+ PREFIX => $self->cfglabel ('BDECLARE'),
+ VALUE => $line->value,
+ );
+ $line->parsed (1);
+ }
+
+ return 1;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = $self->parse_cfg_conflict (\@cfg_lines);
+#
+# DESCRIPTION
+# This method parses the conflict settings in the @cfg_lines.
+# ------------------------------------------------------------------------------
+
+sub parse_cfg_conflict {
+ my ($self, $cfg_lines) = @_;
+
+ # Deprecated: Override mode setting
+ # ----------------------------------------------------------------------------
+ for my $line (grep {$_->slabel_starts_with_cfg ('OVERRIDE')} @$cfg_lines) {
+ next if ($line->slabel_fields) > 1;
+ $self->conflict ($line->bvalue ? 'override' : 'fail');
+ $line->parsed (1);
+ $line->warning($line->slabel . ' is deprecated. Use ' .
+ $line->cfglabel('CONFLICT') . ' override|merge|fail.');
+ }
+
+ # Conflict mode setting
+ # ----------------------------------------------------------------------------
+ my @conflict_modes = qw/fail merge override/;
+ my $conflict_modes_pattern = join ('|', @conflict_modes);
+ for my $line (grep {$_->slabel_starts_with_cfg ('CONFLICT')} @$cfg_lines) {
+ if ($line->value =~ /$conflict_modes_pattern/i) {
+ $self->conflict (lc ($line->value));
+ $line->parsed (1);
+
+ } elsif ($line->value =~ /^[012]$/) {
+ $self->conflict ($conflict_modes[$line->value]);
+ $line->parsed (1);
+
+ } else {
+ $line->error ($line->value, ': invalid value');
+ }
+ }
+
+ return 1;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = $self->parse_cfg_project (\@cfg_lines);
+#
+# DESCRIPTION
+# This method parses the project settings in the @cfg_lines.
+# ------------------------------------------------------------------------------
+
+sub parse_cfg_project {
+ my ($self, $cfg_lines) = @_;
+
+ # Flag to indicate that a declared branch revision must match with its changed
+ # revision
+ # ----------------------------------------------------------------------------
+ for my $line (grep {$_->slabel_starts_with_cfg ('REVMATCH')} @$cfg_lines) {
+ next if ($line->slabel_fields) > 1;
+ $self->setting ([qw/EXT_REVMATCH/], $line->bvalue);
+ $line->parsed (1);
+ }
+
+ # Repository, revision and source directories
+ # ----------------------------------------------------------------------------
+ for my $name (qw/repos revision dirs expdirs/) {
+ my @lines = grep {
+ $_->slabel_starts_with_cfg (uc ($name)) or
+ $name eq 'revision' and $_->slabel_starts_with_cfg ('VERSION');
+ } @$cfg_lines;
+ for my $line (@lines) {
+ my @names = $line->slabel_fields;
+ shift @names;
+
+ # Detemine package and tag
+ my $tag = pop @names;
+ my $pckroot = $names[0];
+ my $pck = join ($FCM1::Config::DELIMITER, @names);
+
+ # Check that $tag and $pckroot are defined
+ next unless $tag and $pckroot;
+
+ # Check if branch already exists.
+ # If so, set $branch to point to existing branch
+ my $branch = undef;
+ for (@{ $self->branches }) {
+ next unless $_->package eq $pckroot and $_->tag eq $tag;
+
+ $branch = $_;
+ last;
+ }
+
+ # Otherwise, create a new branch
+ if (not $branch) {
+ $branch = FCM1::ReposBranch->new (PACKAGE => $pckroot, TAG => $tag,);
+
+ push @{ $self->branches }, $branch;
+ }
+
+ if ($name eq 'repos' or $name eq 'revision') {
+ # Branch location or revision
+ $branch->$name ($line->value);
+
+ } else { # $name eq 'dirs' or $name eq 'expdirs'
+ # Source directory or expandable source directory
+ if ($pck eq $pckroot and $line->value !~ m#^/#) {
+ # Sub-package name not set and source directory quoted as a relative
+ # path, determine package name from path name
+ $pck = join (
+ $FCM1::Config::DELIMITER,
+ ($pckroot, File::Spec->splitdir ($line->value)),
+ );
+ }
+
+ # A "/" is equivalent to the top (empty) package
+ my $value = ($line->value =~ m#^/+$#) ? '' : $line->value;
+ $branch->$name ($pck, $value);
+ }
+
+ $line->parsed (1);
+ }
+ }
+
+ return 1;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = $obj->expand_cfg ();
+#
+# DESCRIPTION
+# This method expands the settings of the extract configuration.
+# ------------------------------------------------------------------------------
+
+sub expand_cfg {
+ my $self = shift;
+
+ my $rc = 1;
+ for my $use (@{ $self->inherit }) {
+ $rc = $use->expand_cfg if $rc;
+ }
+
+ return $rc unless $rc;
+
+ # Establish a set of source directories from the "base repository"
+ my %base_branches = ();
+
+ # Inherit "base" set of source directories from re-used extracts
+ for my $use (@{ $self->inherit }) {
+ my @branches = @{ $use->branches };
+
+ for my $branch (@branches) {
+ my $package = $branch->package;
+ $base_branches{$package} = $branch unless exists $base_branches{$package};
+ }
+ }
+
+ for my $branch (@{ $self->branches }) {
+ # Expand URL keywords if necessary
+ if ($branch->repos) {
+ my $repos = FCM1::Util::tidy_url(FCM1::Keyword::expand($branch->repos()));
+ $branch->repos ($repos) if $repos ne $branch->repos;
+ }
+
+ # Check that repository type and revision are set
+ if ($branch->repos and &is_url ($branch->repos)) {
+ $branch->type ('svn') unless $branch->type;
+ $branch->revision ('head') unless $branch->revision;
+
+ } else {
+ $branch->type ('user') unless $branch->type;
+ $branch->revision ('user') unless $branch->revision;
+ }
+
+ $rc = $branch->expand_revision if $rc; # Get revision number from keywords
+ $rc = $branch->expand_path if $rc; # Expand relative path to full path
+ $rc = $branch->expand_all if $rc; # Search sub-directories
+ last unless $rc;
+
+ my $package = $branch->package;
+
+ if (exists $base_branches{$package}) {
+ # A base branch for this package exists
+
+ # If current branch has no source directory, use the set provided by the
+ # base branch
+ my %dirs = %{ $branch->dirs };
+ $branch->add_base_dirs ($base_branches{$package}) unless keys %dirs;
+
+ } else {
+ # This package does not yet have a base branch, set this branch as base
+ $base_branches{$package} = $branch;
+ }
+ }
+
+ return $rc;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = $obj->create_dir_stack ();
+#
+# DESCRIPTION
+# This method creates a hash of source directories to be processed. If the
+# flag INHERITED is set to true, the source directories are assumed processed
+# and extracted.
+# ------------------------------------------------------------------------------
+
+sub create_dir_stack {
+ my $self = shift;
+ my %args = @_;
+
+ # Inherit from USE ext cfg
+ for my $use (@{ $self->inherit }) {
+ $use->create_dir_stack () or return 0;
+ my %use_srcdirs = %{ $use->srcdirs };
+
+ while (my ($key, $value) = each %use_srcdirs) {
+ $self->srcdirs ($key, $value);
+
+ # Re-set destination to current destination
+ my @path = split (/$FCM1::Config::DELIMITER/, $key);
+ $self->srcdirs ($key)->{DEST} = File::Spec->catfile (
+ $self->dest->srcdir, @path,
+ );
+ }
+ }
+
+ # Build stack from current ext cfg
+ for my $branch (@{ $self->branches }) {
+ my %branch_dirs = %{ $branch->dirs };
+
+ for my $dir (keys %branch_dirs) {
+ # Check whether source directory is already in the list
+ if (not $self->srcdirs ($dir)) { # if not, create it
+ $self->srcdirs ($dir, {
+ DEST => File::Spec->catfile (
+ $self->dest->srcdir, split (/$FCM1::Config::DELIMITER/, $dir)
+ ),
+ STACK => [],
+ FILES => {},
+ });
+ }
+
+ my $stack = $self->srcdirs ($dir)->{STACK}; # copy reference
+
+ # Create a new layer in the input stack
+ my $layer = FCM1::SrcDirLayer->new (
+ NAME => $dir,
+ PACKAGE => $branch->package,
+ TAG => $branch->tag,
+ LOCATION => $branch->dirs ($dir),
+ REPOSROOT => $branch->repos,
+ REVISION => $branch->revision,
+ TYPE => $branch->type,
+ EXTRACTED => @{ $self->inherited }
+ ? $self->srcdirs ($dir)->{DEST} : undef,
+ );
+
+ # Check whether layer is already in the stack
+ my $exist = grep {
+ $_->location eq $layer->location and $_->revision eq $layer->revision;
+ } @{ $stack };
+
+ if (not $exist) {
+ # If not already exist, put layer into stack
+
+ # Note: user stack always comes last
+ if (! $layer->user and exists $stack->[-1] and $stack->[-1]->user) {
+ my $lastlayer = pop @{ $stack };
+ push @{ $stack }, $layer;
+ $layer = $lastlayer;
+ }
+
+ push @{ $stack }, $layer;
+
+ } elsif ($layer->user) {
+
+ # User layer already exists, overwrite it
+ $stack->[-1] = $layer;
+
+ }
+ }
+ }
+
+ # Use the cache to sort the source directory layer hash
+ return $self->compare_setting (METHOD_LIST => ['sort_dir_stack']);
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# ($rc, \@new_lines) = $self->sort_dir_stack ($old_lines);
+#
+# DESCRIPTION
+# This method sorts thesource directories hash to be processed.
+# ------------------------------------------------------------------------------
+
+sub sort_dir_stack {
+ my ($self, $old_lines) = @_;
+
+ my $rc = 0;
+
+ my %old = ();
+ if ($old_lines) {
+ for my $line (@$old_lines) {
+ $old{$line->label} = $line->value;
+ }
+ }
+
+ my %new;
+
+ # Compare each layer to base layer, discard unnecessary layers
+ DIR: for my $srcdir (keys %{ $self->srcdirs }) {
+ my @stack = ();
+
+ while (my $layer = shift @{ $self->srcdirs ($srcdir)->{STACK} }) {
+ if ($layer->user) {
+ # Local file system branch, check that the declared location exists
+ if (-d $layer->location) {
+ # Local file system branch always takes precedence
+ push @stack, $layer;
+
+ } else {
+ w_report 'ERROR: ', $layer->location, ': declared source directory ',
+ 'does not exists ';
+ $rc = undef;
+ last DIR;
+ }
+
+ } else {
+ my $key = join ($FCM1::Config::DELIMITER, (
+ $srcdir, $layer->location, $layer->revision
+ ));
+
+ unless ($layer->extracted and $layer->commit) {
+ # See if commit revision information is cached
+ if (keys %old and exists $old{$key}) {
+ $layer->commit ($old{$key});
+
+ } else {
+ $layer->get_commit;
+ $rc = 1;
+ }
+
+ # Check source directory for commit revision, if necessary
+ if (not $layer->commit) {
+ w_report 'Error: cannot determine the last changed revision of ',
+ $layer->location;
+ $rc = undef;
+ last DIR;
+ }
+
+ # Set cache directory for layer
+ my $tag_ver = $layer->tag . '__' . $layer->commit;
+ $layer->cachedir (File::Spec->catfile (
+ $self->dest->cachedir,
+ split (/$FCM1::Config::DELIMITER/, $srcdir),
+ $tag_ver,
+ ));
+ }
+
+ # New line in cache config file
+ $new{$key} = $layer->commit;
+
+ # Push this layer in the stack:
+ # 1. it has a different revision compared to the top layer
+ # 2. it is the top layer (base line code)
+ if (@stack > 0) {
+ push @stack, $layer if $layer->commit != $stack[0]->commit;
+
+ } else {
+ push @stack, $layer;
+ }
+
+ }
+ }
+
+ $self->srcdirs ($srcdir)->{STACK} = \@stack;
+ }
+
+ # Write "commit cache" file
+ my @new_lines;
+ if (defined ($rc)) {
+ for my $key (sort keys %new) {
+ push @new_lines, FCM1::CfgLine->new (label => $key, value => $new{$key});
+ }
+ }
+
+ return ($rc, \@new_lines);
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = $self->extract_src ();
+#
+# DESCRIPTION
+# This internal method performs the extract of the source directories and
+# files if necessary.
+# ------------------------------------------------------------------------------
+
+sub extract_src {
+ my $self = shift;
+ my $rc = 1;
+
+ # Ensure destinations exist and are directories
+ for my $srcdir (values %{ $self->srcdirs }) {
+ last if not $rc;
+ if (-f $srcdir->{DEST}) {
+ w_report $srcdir->{DEST},
+ ': destination exists and is not a directory, abort.';
+ $rc = 0;
+ }
+ }
+
+ # Retrieve previous/record current extract configuration for each file.
+ $rc = $self->compare_setting (
+ CACHEBASE => $self->setting ('CACHE_FILE_SRC'),
+ METHOD_LIST => ['compare_setting_srcfiles'],
+ ) if $rc;
+
+ return $rc;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# ($rc, \@new_lines) = $self->compare_setting_srcfiles ($old_lines);
+#
+# DESCRIPTION
+# For each file to be extracted, this method creates an instance of an
+# FCM1::ExtractFile object. It then compares its file's sources to determine
+# if they have changed. If so, it will allow the FCM1::ExtractFile to
+# "re-extract" the file to the destination. Otherwise, it will set
+# FCM1::ExtractFile->dest_status to a null string to denote an "unchanged"
+# dest_status.
+#
+# SEE ALSO
+# FCM1::ConfigSystem->compare_setting.
+# ------------------------------------------------------------------------------
+
+sub compare_setting_srcfiles {
+ my ($self, $old_lines) = @_;
+ my $rc = 1;
+
+ # Retrieve previous extract configuration for each file
+ # ----------------------------------------------------------------------------
+ my %old = ();
+ if ($old_lines) {
+ for my $line (@$old_lines) {
+ $old{$line->label} = $line->value;
+ }
+ }
+
+ # Build up a sequence using a FCM1::ExtractFile object for each file
+ # ----------------------------------------------------------------------------
+ for my $srcdir (values %{ $self->srcdirs }) {
+ my %pkgnames0; # (to be) list of package names in the base layer
+ for my $i (0 .. @{ $srcdir->{STACK} } - 1) {
+ my $layer = $srcdir->{STACK}->[$i];
+ # Update the cache for each layer of the stack if necessary
+ $layer->update_cache unless $layer->extracted or -d $layer->localdir;
+
+ # Get list of files in the cache or local directory
+ my %pkgnames;
+ for my $file (($layer->get_files)) {
+ my $pkgname = join (
+ '/', split (/$FCM1::Config::DELIMITER/, $layer->name), $file
+ );
+ $pkgnames0{$pkgname} = 1 if $i == 0; # store package name in base layer
+ $pkgnames{$pkgname} = 1; # store package name in the current layer
+ if (not $self->files ($pkgname)) {
+ $self->files ($pkgname, FCM1::ExtractFile->new (
+ conflict => $self->conflict,
+ dest => $self->dest->srcpath,
+ pkgname => $pkgname,
+ ));
+
+ # Base is empty
+ $self->files ($pkgname)->src->[0] = FCM1::ExtractSrc->new (
+ id => $layer->tag,
+ pkgname => $pkgname,
+ ) if $i > 0;
+ }
+ my $cache = File::Spec->catfile ($layer->localdir, $file);
+ push @{ $self->files ($pkgname)->src }, FCM1::ExtractSrc->new (
+ cache => $cache,
+ id => $layer->tag,
+ pkgname => $pkgname,
+ rev => ($layer->user ? (stat ($cache))[9] : $layer->commit),
+ uri => join ('/', $layer->location, $file),
+ );
+ }
+
+ # List of removed files in this layer (relative to base layer)
+ if ($i > 0) {
+ for my $pkgname (keys %pkgnames0) {
+ push @{ $self->files ($pkgname)->src }, FCM1::ExtractSrc->new (
+ id => $layer->tag,
+ pkgname => $pkgname,
+ ) if not exists $pkgnames{$pkgname}
+ }
+ }
+ }
+ }
+
+ # Compare with old settings
+ # ----------------------------------------------------------------------------
+ my %new = ();
+ for my $key (sort keys %{ $self->files }) {
+ # Set up value for cache
+ my @sources = ();
+ for my $src (@{ $self->files ($key)->src }) {
+ push @sources, (defined ($src->uri) ? ($src->uri . '@' . $src->rev) : '');
+ }
+
+ my $value = join ($FCM1::Config::DELIMITER, @sources);
+
+ # Set FCM1::ExtractFile->dest_status to "unchanged" if value is unchanged
+ if (exists($old{$key}) && $old{$key} eq $value && !grep {!$_} @sources) {
+ $self->files($key)->dest_status('');
+ }
+
+ # Write current settings
+ $new{$key} = $value;
+ }
+
+ # Delete those that exist in previous extract but not in current
+ # ----------------------------------------------------------------------------
+ for my $key (sort keys %old) {
+ next if exists $new{$key};
+ $self->files ($key, FCM1::ExtractFile->new (
+ dest => $self->dest->srcpath,
+ pkgname => $key,
+ ));
+ }
+
+ # Extract each file, if necessary
+ # ----------------------------------------------------------------------------
+ for my $key (sort keys %{ $self->files }) {
+ $rc = $self->files ($key)->run if defined ($rc);
+ last if not defined ($rc);
+ }
+
+ # Report status
+ # ----------------------------------------------------------------------------
+ if (defined ($rc) and $self->verbose) {
+ my %src_status_count = ();
+ my %dest_status_count = ();
+ for my $key (sort keys %{ $self->files }) {
+ # Report changes in destination in verbose 1 or above
+ my $dest_status = $self->files ($key)->dest_status;
+ my $src_status = $self->files ($key)->src_status;
+ next unless $self->verbose and $dest_status;
+
+ if ($dest_status and $dest_status ne '?') {
+ if (exists $dest_status_count{$dest_status}) {
+ $dest_status_count{$dest_status}++;
+
+ } else {
+ $dest_status_count{$dest_status} = 1;
+ }
+ }
+
+ if ($src_status and $src_status ne '?') {
+ if (exists $src_status_count{$src_status}) {
+ $src_status_count{$src_status}++;
+
+ } else {
+ $src_status_count{$src_status} = 1;
+ }
+ }
+
+ # Destination status in column 1, source status in column 2
+ if ($self->verbose > 1) {
+ my @srcs = @{$self->files ($key)->src_actual};
+ print ($dest_status ? $dest_status : ' ');
+ print ($src_status ? $src_status : ' ');
+ print ' ' x 5, $key;
+ print ' (', join (', ', map {$_->id} @srcs), ')' if @srcs;
+ print "\n";
+ }
+ }
+
+ # Report number of files in each dest_status category
+ if (%dest_status_count) {
+ print 'Column 1: ' if $self->verbose > 1;
+ print 'Destination status summary:', "\n";
+ for my $key (sort keys %FCM1::ExtractFile::DEST_STATUS_CODE) {
+ next unless $key;
+ next if not exists ($dest_status_count{$key});
+ print ' No of files ';
+ print '[', $key, '] ' if $self->verbose > 1;
+ print $FCM1::ExtractFile::DEST_STATUS_CODE{$key}, ': ',
+ $dest_status_count{$key}, "\n";
+ }
+ }
+
+ # Report number of files in each dest_status category
+ if (%src_status_count) {
+ print 'Column 2: ' if $self->verbose > 1;
+ print 'Source status summary:', "\n";
+ for my $key (sort keys %FCM1::ExtractFile::SRC_STATUS_CODE) {
+ next unless $key;
+ next if not exists ($src_status_count{$key});
+ print ' No of files ';
+ print '[', $key, '] ' if $self->verbose > 1;
+ print $FCM1::ExtractFile::SRC_STATUS_CODE{$key}, ': ',
+ $src_status_count{$key}, "\n";
+ }
+ }
+ }
+
+ # Record configuration of current extract for each file
+ # ----------------------------------------------------------------------------
+ my @new_lines;
+ if (defined ($rc)) {
+ for my $key (sort keys %new) {
+ push @new_lines, FCM1::CfgLine->new (label => $key, value => $new{$key});
+ }
+ }
+
+ return ($rc, \@new_lines);
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# @array = $self->sort_bdeclare ();
+#
+# DESCRIPTION
+# This method returns sorted build declarations, filtering out repeated
+# entries, where possible.
+# ------------------------------------------------------------------------------
+
+sub sort_bdeclare {
+ my $self = shift;
+
+ # Get list of build configuration labels that can be declared multiple times
+ my %cfg_keyword = map {
+ ($self->cfglabel ($_), 1)
+ } split (/$FCM1::Config::DELIMITER_LIST/, $self->setting ('CFG_KEYWORD'));
+
+ my @bdeclares = ();
+ for my $d (reverse @{ $self->bdeclare }) {
+ # Reconstruct array from bottom up
+ # * always add declarations that can be declared multiple times
+ # * otherwise add only if it is declared below
+ unshift @bdeclares, $d
+ if exists $cfg_keyword{uc (($d->slabel_fields)[0])} or
+ not grep {$_->slabel eq $d->slabel} @bdeclares;
+ }
+
+ return (sort {$a->slabel cmp $b->slabel} @bdeclares);
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# @cfglines = $obj->to_cfglines ();
+#
+# DESCRIPTION
+# See description of FCM1::ConfigSystem->to_cfglines for further information.
+# ------------------------------------------------------------------------------
+
+sub to_cfglines {
+ my ($self) = @_;
+
+ return (
+ FCM1::ConfigSystem::to_cfglines($self),
+
+ $self->rdest->to_cfglines (),
+ FCM1::CfgLine->new (),
+
+ @{ $self->bdeclare } ? (
+ FCM1::CfgLine::comment_block ('Build declarations'),
+ map {
+ FCM1::CfgLine->new (label => $_->label, value => $_->value)
+ } ($self->sort_bdeclare),
+ FCM1::CfgLine->new (),
+ ) : (),
+
+ FCM1::CfgLine::comment_block ('Project and branches'),
+ (map {($_->to_cfglines ())} @{ $self->branches }),
+
+ ($self->conflict ne 'merge') ? (
+ FCM1::CfgLine->new (
+ label => $self->cfglabel ('CONFLICT'), value => $self->conflict,
+ ),
+ FCM1::CfgLine->new (),
+ ) : (),
+ );
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# @cfglines = $obj->to_cfglines_bld ();
+#
+# DESCRIPTION
+# Returns a list of configuration lines of the current extract suitable for
+# feeding into the build system.
+# ------------------------------------------------------------------------------
+
+sub to_cfglines_bld {
+ my ($self) = @_;
+
+ my $dest = $self->rdest->rootdir ? 'rdest' : 'dest';
+ my $root = File::Spec->catfile ('$HERE', '..');
+
+ my @inherits;
+ my @no_inherits;
+ if (@{ $self->inherit }) {
+ # List of inherited builds
+ for (@{ $self->inherit }) {
+ push @inherits, FCM1::CfgLine->new (
+ label => $self->cfglabel ('USE'), value => $_->$dest->rootdir
+ );
+ }
+
+ # List of files that should not be inherited
+ for my $key (sort keys %{ $self->files }) {
+ next unless $self->files ($key)->dest_status eq 'd';
+ my $label = join ('::', (
+ $self->cfglabel ('INHERIT'),
+ $self->cfglabel ('FILE'),
+ split (m#/#, $self->files ($key)->pkgname),
+ ));
+ push @no_inherits, FCM1::CfgLine->new (label => $label, value => 'false');
+ }
+ }
+
+ return (
+ FCM1::CfgLine::comment_block ('File header'),
+ (map
+ {my ($lbl, $val) = @{$_}; FCM1::CfgLine->new(label => $lbl, value => $val)}
+ (
+ [$self->cfglabel('CFGFILE') . $FCM1::Config::DELIMITER . 'TYPE' , 'bld'],
+ [$self->cfglabel('CFGFILE') . $FCM1::Config::DELIMITER . 'VERSION', '1.0'],
+ [],
+ )
+ ),
+
+ @{ $self->inherit } ? (
+ @inherits,
+ @no_inherits,
+ FCM1::CfgLine->new (),
+ ) : (),
+
+ FCM1::CfgLine::comment_block ('Destination'),
+ FCM1::CfgLine->new (label => $self->cfglabel ('DEST'), value => $root),
+ FCM1::CfgLine->new (),
+
+ @{ $self->bdeclare } ? (
+ FCM1::CfgLine::comment_block ('Build declarations'),
+ map {
+ FCM1::CfgLine->new (label => $_->slabel, value => $_->value)
+ } ($self->sort_bdeclare),
+ FCM1::CfgLine->new (),
+ ) : (),
+ );
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = $self->write_cfg ();
+#
+# DESCRIPTION
+# This method writes the configuration file at the end of the run. It calls
+# $self->write_cfg_system ($cfg) to write any system specific settings.
+# ------------------------------------------------------------------------------
+
+sub write_cfg {
+ my $self = shift;
+
+ my $cfg = FCM1::CfgFile->new (TYPE => $self->type);
+ $cfg->lines ([$self->to_cfglines()]);
+ $cfg->print_cfg ($self->dest->extcfg);
+
+ return 1;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = $self->write_cfg_bld ();
+#
+# DESCRIPTION
+# This internal method writes the build configuration file.
+# ------------------------------------------------------------------------------
+
+sub write_cfg_bld {
+ my $self = shift;
+
+ my $cfg = FCM1::CfgFile->new (TYPE => 'bld');
+ $cfg->lines ([$self->to_cfglines_bld()]);
+ $cfg->print_cfg ($self->dest->bldcfg);
+
+ return 1;
+}
+
+# ------------------------------------------------------------------------------
+
+1;
+
+__END__
diff --git a/lib/FCM1/ExtractConfigComparator.pm b/lib/FCM1/ExtractConfigComparator.pm
new file mode 100644
index 0000000..bd4d50c
--- /dev/null
+++ b/lib/FCM1/ExtractConfigComparator.pm
@@ -0,0 +1,371 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+
+################################################################################
+# A generic reporter of the comparator's result
+{
+ package Reporter;
+
+ ############################################################################
+ # Class method: Constructor
+ sub new {
+ my ($class) = @_;
+ return bless(\do{my $annon_scalar}, $class);
+ }
+
+ ############################################################################
+ # Class method: Factory for Reporter object
+ sub get_reporter {
+ my ($self, $comparator) = @_;
+ my $class = defined($comparator->get_wiki()) ? 'WikiReporter'
+ : 'TextReporter'
+ ;
+ return $class->new();
+ }
+
+ ############################################################################
+ # Reports the results
+ sub report {
+ my ($self, $comparator) = @_;
+ if (keys(%{$comparator->get_log_of()})) {
+ print("Revisions at which extract declarations are modified:\n\n");
+ }
+ $self->report_impl($comparator);
+ }
+
+ ############################################################################
+ # Does the actual reporting
+ sub report_impl {
+ my ($self, $comparator) = @_;
+ }
+}
+
+################################################################################
+# Reports the comparator's result in Trac wiki format
+{
+ package WikiReporter;
+ our @ISA = qw{Reporter};
+
+ use FCM1::CmUrl;
+ use FCM1::Keyword;
+ use FCM1::Util qw{tidy_url};
+
+ ############################################################################
+ # Reports the comparator's result
+ sub report_impl {
+ my ($self, $comparator) = @_;
+ # Output in wiki format
+ my $wiki_url = FCM1::CmUrl->new(
+ URL => tidy_url(FCM1::Keyword::expand($comparator->get_wiki()))
+ );
+ my $base_trac
+ = $comparator->get_wiki()
+ ? FCM1::Keyword::get_browser_url($wiki_url->project_url())
+ : $wiki_url;
+ if (!$base_trac) {
+ $base_trac = $wiki_url;
+ }
+
+ for my $key (sort keys(%{$comparator->get_log_of()})) {
+ my $branch_trac = FCM1::Keyword::get_browser_url($key);
+ $branch_trac =~ s{\A $base_trac (?:/*|\z)}{source:}xms;
+ print("[$branch_trac]:\n");
+ my %branch_of = %{$comparator->get_log_of()->{$key}};
+ for my $rev (sort {$b <=> $a} keys(%branch_of)) {
+ print(
+ $branch_of{$rev}->display_svnlog($rev, $base_trac), "\n",
+ );
+ }
+ print("\n");
+ }
+ }
+}
+
+################################################################################
+# Reports the comparator's result in simple text format
+{
+ package TextReporter;
+ our @ISA = qw{Reporter};
+
+ use FCM1::Config;
+
+ my $SEPARATOR = q{-} x 80 . "\n";
+
+ ############################################################################
+ # Reports the comparator's result
+ sub report_impl {
+ my ($self, $comparator) = @_;
+ for my $key (sort keys(%{$comparator->get_log_of()})) {
+ # Output in plain text format
+ print $key, ':', "\n";
+ my %branch_of = %{$comparator->get_log_of()->{$key}};
+ if (FCM1::Config->instance()->verbose() > 1) {
+ for my $rev (sort {$b <=> $a} keys(%branch_of)) {
+ print(
+ $SEPARATOR, $branch_of{$rev}->display_svnlog($rev), "\n"
+ );
+ }
+ }
+ else {
+ print(join(q{ }, sort {$b <=> $a} keys(%branch_of)), "\n");
+ }
+ print $SEPARATOR, "\n";
+ }
+ }
+}
+
+package FCM1::ExtractConfigComparator;
+
+use FCM1::CmUrl;
+use FCM1::Extract;
+
+################################################################################
+# Class method: Constructor
+sub new {
+ my ($class, $args_ref) = @_;
+ return bless({%{$args_ref}}, $class);
+}
+
+################################################################################
+# Returns an array containing the 2 configuration files to compare
+sub get_files {
+ my ($self) = @_;
+ return (wantarray() ? @{$self->{files}} : $self->{files});
+}
+
+################################################################################
+# Returns the wiki link on wiki mode
+sub get_wiki {
+ my ($self) = @_;
+ return $self->{wiki};
+}
+
+################################################################################
+# Returns the result log
+sub get_log_of {
+ my ($self) = @_;
+ return (wantarray() ? %{$self->{log_of}} : $self->{log_of});
+}
+
+################################################################################
+# Invokes the comparator
+sub invoke {
+ my ($self) = @_;
+
+ # Reads the extract configurations
+ my (@cfg, $rc);
+ for my $i (0 .. 1) {
+ $cfg[$i] = FCM1::Extract->new();
+ $cfg[$i]->cfg()->src($self->get_files()->[$i]);
+ $cfg[$i]->parse_cfg();
+ $rc = $cfg[$i]->expand_cfg();
+ if (!$rc) {
+ e_report();
+ }
+ }
+
+ # Get list of URLs
+ # --------------------------------------------------------------------------
+ my @urls = ();
+ for my $i (0 .. 1) {
+ # List of branches in each extract configuration file
+ my @branches = @{$cfg[$i]->branches()};
+ BRANCH:
+ for my $branch (@branches) {
+ # Ignore declarations of local directories
+ if ($branch->type() eq 'user') {
+ next BRANCH;
+ }
+
+ # List of SRC declarations in each branch
+ my %dirs = %{$branch->dirs()};
+
+ for my $dir (values(%dirs)) {
+ # Set up a new instance of FCM1::CmUrl object for each SRC
+ my $cm_url = FCM1::CmUrl->new (
+ URL => $dir . (
+ $branch->revision() ? '@' . $branch->revision() : q{}
+ ),
+ );
+
+ $urls[$i]{$cm_url->branch_url()}{$dir} = $cm_url;
+ }
+ }
+ }
+
+ # Compare
+ # --------------------------------------------------------------------------
+ $self->{log_of} = {};
+ for my $i (0 .. 1) {
+ # Compare the first file with the second one and then vice versa
+ my $j = ($i == 0) ? 1 : 0;
+
+ for my $branch (sort keys(%{$urls[$i]})) {
+ if (exists($urls[$j]{$branch})) {
+ # Same REPOS declarations in both files
+ DIR:
+ for my $dir (sort keys(%{$urls[$i]{$branch}})) {
+ if (exists($urls[$j]{$branch}{$dir})) {
+ if ($i == 1) {
+ next DIR;
+ }
+
+ my $this_url = $urls[$i]{$branch}{$dir};
+ my $that_url = $urls[$j]{$branch}{$dir};
+
+ # Compare their last changed revisions
+ my $this_rev
+ = $this_url->svninfo(FLAG => 'commit:revision');
+ my $that_rev
+ = $that_url->svninfo(FLAG => 'commit:revision');
+
+ # Make sure last changed revisions differ
+ if ($this_rev eq $that_rev) {
+ next DIR;
+ }
+
+ # Not interested in the log before the minimum revision
+ my $min_rev
+ = $this_url->pegrev() > $that_url->pegrev()
+ ? $that_url->pegrev() : $this_url->pegrev();
+
+ $this_rev = $min_rev if $this_rev < $min_rev;
+ $that_rev = $min_rev if $that_rev < $min_rev;
+
+ # Get list of changed revisions using the commit log
+ my $u = ($this_rev > $that_rev) ? $this_url : $that_url;
+ my %revs = $u->svnlog(REV => [$this_rev, $that_rev]);
+
+ REV:
+ for my $rev (keys %revs) {
+ # Check if revision is already in the list
+ if (
+ exists($self->{log_of}{$branch}{$rev})
+ || $rev == $min_rev
+ ) {
+ next REV;
+ }
+
+ # Get list of changed paths. Accept this revision
+ # only if it contains changes in the current branch
+ my %paths = %{$revs{$rev}{paths}};
+
+ PATH:
+ for my $path (keys(%paths)) {
+ my $change_url
+ = FCM1::CmUrl->new(URL => $u->root() . $path);
+
+ if ($change_url->branch() eq $u->branch()) {
+ $self->{log_of}{$branch}{$rev} = $u;
+ last PATH;
+ }
+ }
+ }
+ }
+ else {
+ $self->_report_added(
+ $urls[$i]{$branch}{$dir}->url_peg(), $i, $j);
+ }
+ }
+ }
+ else {
+ $self->_report_added($branch, $i, $j);
+ }
+ }
+ }
+
+ my $reporter = Reporter->get_reporter($self);
+ $reporter->report($self);
+ return $rc;
+}
+
+################################################################################
+# Reports added/deleted declaration
+sub _report_added {
+ my ($self, $branch, $i, $j) = @_;
+ printf(
+ "%s:\n in : %s\n not in: %s\n\n",
+ $branch, $self->get_files()->[$i], $self->get_files()->[$j],
+ );
+}
+
+1;
+__END__
+
+=head1 NAME
+
+FCM1::ExtractConfigComparator
+
+=head1 SYNOPSIS
+
+ use FCM1::ExtractConfigComparator;
+ my $comparator = FCM1::ExtractConfigComparator->new({files => \@files});
+ $comparator->invoke();
+
+=head1 DESCRIPTION
+
+An object of this class represents a comparator of FCM extract configuration.
+It is used to compare the VC branch declarations in 2 FCM extract configuration
+files.
+
+=head1 METHODS
+
+=over 4
+
+=item C<new({files =E<gt> \@files, wiki =E<gt> $wiki})>
+
+Constructor.
+
+=item get_files()
+
+Returns an array containing the 2 configuration files to compare.
+
+=item get_wiki()
+
+Returns the wiki link on wiki mode.
+
+=item invoke()
+
+Invokes the comparator.
+
+=back
+
+=head1 TO DO
+
+More documentation.
+
+Improve the parser for extract configuration.
+
+Separate the comparator with the reporters.
+
+Add reporter to display HTML.
+
+More unit tests.
+
+=head1 SEE ALSO
+
+L<FCM1::Extract|FCM1::Extract>
+
+=head1 COPYRIGHT
+
+E<169> Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM1/ExtractFile.pm b/lib/FCM1/ExtractFile.pm
new file mode 100644
index 0000000..309e662
--- /dev/null
+++ b/lib/FCM1/ExtractFile.pm
@@ -0,0 +1,423 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# NAME
+# FCM1::ExtractFile
+#
+# DESCRIPTION
+# Select/combine a file in different branches and extract it to destination.
+#
+# ------------------------------------------------------------------------------
+
+use warnings;
+use strict;
+
+package FCM1::ExtractFile;
+use base qw{FCM1::Base};
+
+use FCM1::Util qw{run_command w_report};
+use File::Basename qw{dirname};
+use File::Compare qw{compare};
+use File::Copy qw{copy};
+use File::Path qw{mkpath};
+use File::Spec;
+use File::Temp qw(tempfile);
+
+# List of property methods for this class
+my @scalar_properties = (
+ 'conflict', # conflict mode
+ 'dest', # search path to destination file
+ 'dest_status', # destination status, see below
+ 'pkgname', # package name of this file
+ 'src', # list of FCM1::ExtractSrc, specified for this file
+ 'src_actual', # list of FCM1::ExtractSrc, actually used by this file
+ 'src_status', # source status, see below
+);
+
+# Status code definition for $self->dest_status
+our %DEST_STATUS_CODE = (
+ '' => 'unchanged',
+ 'M' => 'modified',
+ 'A' => 'added',
+ 'a' => 'added, overridding inherited',
+ 'D' => 'deleted',
+ 'd' => 'deleted, overridding inherited',
+ '?' => 'irrelevant',
+);
+
+# Status code definition for $self->src_status
+our %SRC_STATUS_CODE = (
+ 'A' => 'added by a branch',
+ 'B' => 'from the base',
+ 'D' => 'deleted by a branch',
+ 'M' => 'modified by a branch',
+ 'G' => 'merged from 2+ branches',
+ 'O' => 'overridden by a branch',
+ '?' => 'irrelevant',
+);
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $obj = FCM1::ExtractFile->new ();
+#
+# DESCRIPTION
+# This method constructs a new instance of the FCM1::ExtractFile class.
+# ------------------------------------------------------------------------------
+
+sub new {
+ my $this = shift;
+ my %args = @_;
+ my $class = ref $this || $this;
+
+ my $self = FCM1::Base->new (%args);
+
+ for (@scalar_properties) {
+ $self->{$_} = exists $args{$_} ? $args{$_} : undef;
+ }
+
+ bless $self, $class;
+ return $self;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $value = $obj->X;
+# $obj->X ($value);
+#
+# DESCRIPTION
+# Details of these properties are explained in @scalar_properties.
+# ------------------------------------------------------------------------------
+
+for my $name (@scalar_properties) {
+ no strict 'refs';
+
+ *$name = sub {
+ my $self = shift;
+
+ # Argument specified, set property to specified argument
+ if (@_) {
+ $self->{$name} = $_[0];
+ }
+
+ # Default value for property
+ if (not defined $self->{$name}) {
+ if ($name eq 'conflict') {
+ $self->{$name} = 'merge'; # default to "merge" mode
+
+ } elsif ($name eq 'dest' or $name eq 'src' or $name eq 'src_actual') {
+ $self->{$name} = []; # default to an empty list
+ }
+ }
+
+ return $self->{$name};
+ }
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = $obj->run();
+#
+# DESCRIPTION
+# This method runs only if $self->dest_status is not defined. It updates the
+# destination according to the source in the list and the conflict mode
+# setting. It updates the file in $self->dest as appropriate and sets
+# $self->dest_status. (See above.) This method returns true on success.
+# ------------------------------------------------------------------------------
+
+sub run {
+ my ($self) = @_;
+ my $rc = 1;
+
+ if (not defined ($self->dest_status)) {
+ # Assume file unchanged
+ $self->dest_status ('');
+
+ if (@{ $self->src }) {
+ my $used;
+ # Determine or set up a file for comparing with the destination
+ ($rc, $used) = $self->run_get_used();
+
+ # Attempt to compare the destination with $used. Update on change.
+ if ($rc) {
+ $rc = defined ($used) ? $self->run_update($used) : $self->run_delete();
+ }
+
+ } else {
+ # No source, delete file in destination
+ $self->src_status ('?');
+ $rc = $self->run_delete();
+ }
+ }
+
+ return $rc;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = $obj->run_delete();
+#
+# DESCRIPTION
+# This method is part of run(). It detects this file in the destination path.
+# If this file is in the current destination, it attempts to delete it and
+# sets the dest_status to "D". If this file is in an inherited destination,
+# it sets the dest_status to "d".
+# ------------------------------------------------------------------------------
+
+sub run_delete {
+ my ($self) = @_;
+
+ my $rc = 1;
+
+ $self->dest_status ('?');
+ for my $i (0 .. @{ $self->dest } - 1) {
+ my $dest = File::Spec->catfile ($self->dest->[$i], $self->pkgname);
+ next unless -f $dest;
+ if ($i == 0) {
+ $rc = unlink $dest;
+ $self->dest_status ('D');
+
+ } else {
+ $self->dest_status ('d');
+ last;
+ }
+ }
+
+ return $rc;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# ($rc, $used) = $obj->run_get_used();
+#
+# DESCRIPTION
+# This method is part of run(). It attempts to work out or set up the $used
+# file. ($used is undef if it is not defined in a branch for this file.)
+# ------------------------------------------------------------------------------
+
+sub run_get_used {
+ my ($self) = @_;
+ my $rc = 1;
+ my $used;
+
+ my @sources = ($self->src->[0]);
+ my $src_status = 'B';
+ if (defined ($self->src->[0]->cache)) {
+ # File exists in base branch
+ for my $i (1 .. @{ $self->src } - 1) {
+ if (defined ($self->src->[$i]->cache)) {
+ # Detect changes in this file between base branch and branch $i
+ push @sources, $self->src->[$i]
+ if &compare ($self->src->[0]->cache, $self->src->[$i]->cache);
+
+ } else {
+ # File deleted in branch $i
+ @sources = ($self->src->[$i]);
+ last unless $self->conflict eq 'override';
+ }
+ }
+
+ if ($rc) {
+ if (@sources > 2) {
+ if ($self->conflict eq 'fail') {
+ # On conflict, fail in fail mode
+ w_report 'ERROR: ', $self->pkgname,
+ ': modified in 2+ branches in fail conflict mode.';
+ $rc = undef;
+
+ } elsif ($self->conflict eq 'override') {
+ $used = $sources[-1]->cache;
+ $src_status = 'O';
+
+ } else {
+ # On conflict, attempt to merge in merge mode
+ ($rc, $used) = $self->run_get_used_by_merge (@sources);
+ $src_status = 'G' if $rc;
+ }
+
+ } else {
+ # 0 or 1 change, use last source
+ if (defined $sources[-1]->cache) {
+ $used = $sources[-1]->cache;
+ $src_status = 'M' if @sources > 1;
+
+ } else {
+ $src_status = 'D';
+ }
+ }
+ }
+
+ } else {
+ # File does not exist in base branch
+ @sources = ($self->src->[-1]);
+ $used = $self->src->[1]->cache;
+ $src_status = (defined ($used) ? 'A' : 'D');
+ if ($self->conflict ne 'override' and defined ($used)) {
+ for my $i (1 - @{ $self->src } .. -2) {
+ # Allow this only if files are the same in all branches
+ my $file = $self->src->[$i]->cache;
+ if ((not defined ($file)) or &compare ($used, $file)) {
+ w_report 'ERROR: ', $self->pkgname, ': cannot merge:',
+ ' not found in base branch,',
+ ' but differs in subsequent branches.';
+ $rc = undef;
+ last;
+
+ } else {
+ unshift @sources, $self->src->[$i];
+ }
+ }
+ }
+ }
+
+ $self->src_status ($src_status);
+ $self->src_actual (\@sources);
+
+ return ($rc, $used);
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# ($rc, $used) = $obj->run_get_used_by_merge(@soruces);
+#
+# DESCRIPTION
+# This method is part of run_get_used(). It attempts to merge the files in
+# @sources and return a temporary file $used. @sources should be an array of
+# FCM1::ExtractSrc objects. On success, $rc will be set to true.
+# ------------------------------------------------------------------------------
+
+sub run_get_used_by_merge {
+ my ($self, @sources) = @_;
+ my $rc = 1;
+
+ # Get temporary file
+ my ($fh, $used) = &tempfile ('fcm.ext.merge.XXXXXX', UNLINK => 1);
+ close $fh or die $used, ': cannot close';
+
+ for my $i (2 .. @sources - 1) {
+ # Invoke the diff3 command to merge
+ my $mine = ($i == 2 ? $sources[1]->cache : $used);
+ my $older = $sources[0]->cache;
+ my $yours = $sources[$i]->cache;
+ my @command = (
+ $self->setting (qw/TOOL DIFF3/),
+ split (/\s+/, $self->setting (qw/TOOL DIFF3FLAGS/)),
+ $mine, $older, $yours,
+ );
+ my $code;
+ my @out = &run_command (
+ \@command,
+ METHOD => 'qx',
+ ERROR => 'ignore',
+ PRINT => $self->verbose > 1,
+ RC => \$code,
+ TIME => $self->verbose > 2,
+ );
+
+ if ($code) {
+ # Failure, report and return
+ my $m = ($code == 1)
+ ? 'cannot resolve conflicts:'
+ : $self->setting (qw/TOOL DIFF3/) . 'command failed';
+ w_report 'ERROR: ', $self->pkgname, ': merge - ', $m;
+ if ($code == 1 and $self->verbose) {
+ for (0 .. $i) {
+ my $src = $sources[$_]->uri eq $sources[$_]->cache
+ ? $sources[$_]->cache
+ : ($sources[$_]->uri . '@' . $sources[$_]->rev);
+ w_report ' source[', $_, ']=', $src;
+ }
+
+ for (0 .. $i) {
+ w_report ' cache', $_, '=', $sources[$_]->cache;
+ }
+
+ w_report @out if $self->verbose > 2;
+ }
+ $rc = undef;
+ last;
+
+ } else {
+ # Success, write result to temporary file
+ open FILE, '>', $used or die $used, ': cannot open (', $!, ')';
+ print FILE @out;
+ close FILE or die $used, ': cannot close (', $!, ')';
+
+ # File permission, use most permissive combination of $mine and $yours
+ my $perm = ((stat($mine))[2] & 07777) | ((stat($yours))[2] & 07777);
+ chmod ($perm, $used);
+ }
+ }
+
+ return ($rc, $used);
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = $obj->run_update($used_file);
+#
+# DESCRIPTION
+# This method is part of run(). It compares the $used_file with the one in
+# the destination. If the file does not exist in the destination or if its
+# content is out of date, the destination is updated with the content in the
+# $used_file. Returns true on success.
+# ------------------------------------------------------------------------------
+
+sub run_update {
+ my ($self, $used_file) = @_;
+ my ($is_diff, $is_diff_in_perms, $is_in_prev, $rc) = (1, 1, undef, 1);
+
+ # Compare with the previous version if it exists
+ DEST:
+ for my $i (0 .. @{$self->dest()} - 1) {
+ my $prev_file = File::Spec->catfile($self->dest()->[$i], $self->pkgname());
+ if (-f $prev_file) {
+ $is_in_prev = $i;
+ $is_diff = compare($used_file, $prev_file);
+ $is_diff_in_perms = (stat($used_file))[2] != (stat($prev_file))[2];
+ last DEST;
+ }
+ }
+ if (!$is_diff && !$is_diff_in_perms) {
+ return $rc;
+ }
+
+ # Update destination
+ my $dest_file = File::Spec->catfile($self->dest()->[0], $self->pkgname());
+ if ($is_diff) {
+ my $dir = dirname($dest_file);
+ if (!-d $dir) {
+ mkpath($dir);
+ }
+ $rc = copy($used_file, $dest_file);
+ }
+ $rc &&= chmod((stat($used_file))[2] & oct(7777), $dest_file);
+ if ($rc) {
+ $self->dest_status(
+ $is_in_prev ? 'a'
+ : defined($is_in_prev) ? 'M'
+ : 'A'
+ );
+ }
+ return $rc;
+}
+
+# ------------------------------------------------------------------------------
+
+1;
+
+__END__
diff --git a/lib/FCM1/ExtractSrc.pm b/lib/FCM1/ExtractSrc.pm
new file mode 100644
index 0000000..0cba119
--- /dev/null
+++ b/lib/FCM1/ExtractSrc.pm
@@ -0,0 +1,100 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# NAME
+# FCM1::ExtractSrc
+#
+# DESCRIPTION
+# This class is used by the extract system to define the functionalities of a
+# source file (or directory) in a branch.
+#
+# ------------------------------------------------------------------------------
+
+package FCM1::ExtractSrc;
+ at ISA = qw(FCM1::Base);
+
+# Standard pragma
+use warnings;
+use strict;
+
+# FCM component modules
+use FCM1::Base;
+
+# List of scalar property methods for this class
+my @scalar_properties = (
+ 'cache', # location of the cache of this file in the current extract
+ 'id', # short ID of the branch where this file is from
+ 'ignore', # if set to true, ignore this file from this source
+ 'pkgname', # package name of this file
+ 'rev', # last changed revision/timestamp of this file
+ 'uri', # URL/source path of this file
+);
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $obj = FCM1::ExtractSrc->new (%args);
+#
+# DESCRIPTION
+# This method constructs a new instance of the FCM1::ExtractSrc class. See
+# @scalar_properties above for allowed list of properties in the constructor.
+# ------------------------------------------------------------------------------
+
+sub new {
+ my $this = shift;
+ my %args = @_;
+ my $class = ref $this || $this;
+
+ my $self = FCM1::Base->new (%args);
+
+ for (@scalar_properties) {
+ $self->{$_} = exists $args{$_} ? $args{$_} : undef;
+ }
+
+ bless $self, $class;
+ return $self;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $value = $obj->X;
+# $obj->X ($value);
+#
+# DESCRIPTION
+# Details of these properties are explained in @scalar_properties.
+# ------------------------------------------------------------------------------
+
+for my $name (@scalar_properties) {
+ no strict 'refs';
+
+ *$name = sub {
+ my $self = shift;
+
+ # Argument specified, set property to specified argument
+ if (@_) {
+ $self->{$name} = $_[0];
+ }
+
+ return $self->{$name};
+ }
+}
+
+# ------------------------------------------------------------------------------
+
+1;
+
+__END__
diff --git a/lib/FCM1/Interactive.pm b/lib/FCM1/Interactive.pm
new file mode 100644
index 0000000..4339b76
--- /dev/null
+++ b/lib/FCM1/Interactive.pm
@@ -0,0 +1,144 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+
+package FCM1::Interactive;
+use base qw{Exporter};
+
+our @EXPORT_OK = qw{get_input};
+
+use FCM1::Util::ClassLoader;
+
+my $DEFAULT_IMPL_CLASS = 'FCM1::Interactive::InputGetter::CLI';
+my %DEFAULT_IMPL_CLASS_OPTIONS = ();
+
+my $IMPL_CLASS = $DEFAULT_IMPL_CLASS;
+my %IMPL_CLASS_OPTIONS = %DEFAULT_IMPL_CLASS_OPTIONS;
+
+################################################################################
+# Returns the name of the current class/settings for getting input
+sub get_impl {
+ return (wantarray() ? ($IMPL_CLASS, \%IMPL_CLASS_OPTIONS) : $IMPL_CLASS);
+}
+
+################################################################################
+# Returns the name of the current class/settings for getting input
+sub get_default_impl {
+ return (
+ wantarray()
+ ? ($DEFAULT_IMPL_CLASS, \%DEFAULT_IMPL_CLASS_OPTIONS)
+ : $DEFAULT_IMPL_CLASS
+ );
+}
+
+################################################################################
+# Sets the name of the class/settings for getting input
+sub set_impl {
+ my ($impl_class, $impl_class_options_ref) = @_;
+ if ($impl_class) {
+ $IMPL_CLASS = $impl_class;
+ if ($impl_class_options_ref) {
+ %IMPL_CLASS_OPTIONS = (%{$impl_class_options_ref});
+ }
+ else {
+ %IMPL_CLASS_OPTIONS = ();
+ }
+ }
+}
+
+################################################################################
+# Gets an input from the user and returns it
+sub get_input {
+ my (%options) = @_;
+ my ($class_name, $class_options_ref) = get_impl();
+ FCM1::Util::ClassLoader::load($class_name);
+ %options = map {lc($_), $options{$_}} keys(%options);
+ return $class_name->new({%{$class_options_ref}, %options})->invoke();
+}
+
+1;
+__END__
+
+=head1 NAME
+
+FCM1::Interactive
+
+=head1 SYNOPSIS
+
+ use FCM1::Interactive;
+ FCM1::Interactive::set_impl('My::InputGetter', {option1 => 'value1', ...});
+ $answer = FCM1::Interactive::get_input(
+ title => 'My title',
+ message => 'Would you like to ...?',
+ type => 'yn',
+ default => 'n',
+ );
+
+=head1 DESCRIPTION
+
+Common interface for getting an interactive user reply. The default is to use a
+L<FCM1::Interactive::InputGetter::CLI|FCM1::Interactive::InputGetter::CLI> object
+with no extra options.
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item get_impl()
+
+Returns the class that implements the function for get_input(%options). In
+scalar context, returns the class name only. In list context, returns the class
+name and the extra hash options that would be passed to its constructor.
+
+=item get_default_impl()
+
+Returns the defaut values for get_impl().
+
+=item set_impl($impl_class,$impl_class_options_ref)
+
+Sets the class that implements the function for get_input(%options). The name
+of the class is given in $impl_class. Any extra options that should be given to
+the constructor should be set in the hash reference $impl_class_options_ref.
+
+=item get_input(%options)
+
+Calls the appropriate function to get an input string from the user, and
+returns it.
+
+Input options are: I<title>, for a short title of the prompt, I<message>, for
+the message prompt, I<type> for the prompt type, and I<default> for the default
+value of the return value.
+
+Prompt type can be YN (yes or no), YNA (yes, no or all) or input (for an input
+string).
+
+=back
+
+=head1 SEE ALSO
+
+L<FCM1::Interactive::InputGetter|FCM1::Interactive::InputGetter>,
+L<FCM1::Interactive::InputGetter::CLI|FCM1::Interactive::InputGetter::CLI>,
+L<FCM1::Interactive::InputGetter::GUI|FCM1::Interactive::InputGetter::GUI>
+
+=head1 COPYRIGHT
+
+E<169> Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM1/Interactive/InputGetter.pm b/lib/FCM1/Interactive/InputGetter.pm
new file mode 100644
index 0000000..5749133
--- /dev/null
+++ b/lib/FCM1/Interactive/InputGetter.pm
@@ -0,0 +1,135 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+
+package FCM1::Interactive::InputGetter;
+
+use Carp qw{croak};
+
+################################################################################
+# Constructor
+sub new {
+ my ($class, $args_ref) = @_;
+ return bless({%{$args_ref}}, $class);
+}
+
+################################################################################
+# Methods: get_*
+for my $key (
+ ############################################################################
+ # Returns the title of the prompt
+ 'title',
+ ############################################################################
+ # Returns the message of the prompt
+ 'message',
+ ############################################################################
+ # Returns the of the prompt
+ 'type',
+ ############################################################################
+ # Returns the default return value
+ 'default',
+) {
+ no strict qw{refs};
+ my $getter = "get_$key";
+ *$getter = sub {
+ my ($self) = @_;
+ return $self->{$key};
+ }
+}
+
+################################################################################
+# Invokes the getter
+sub invoke {
+ my ($self) = @_;
+ croak("FCM1::Interactive::InputGetter->invoke() not implemented.");
+}
+
+1;
+__END__
+
+=head1 NAME
+
+FCM1::Interactive::TxtInputGetter
+
+=head1 SYNOPSIS
+
+ use FCM1::Interactive::TxtInputGetter;
+ $answer = FCM1::Interactive::get_input(
+ title => 'My title',
+ message => 'Would you like to ...?',
+ type => 'yn',
+ default => 'n',
+ );
+
+=head1 DESCRIPTION
+
+An object of this abstract class is used by
+L<FCM1::Interactive|FCM1::Interactive> to get a user reply.
+
+=head1 METHODS
+
+=over 4
+
+=item new($args_ref)
+
+Constructor, normally invoked via L<FCM1::Interactive|FCM1::Interactive>.
+
+Input options are: I<title>, for a short title of the prompt, I<message>, for
+the message prompt, I<type> for the prompt type, and I<default> for the default
+value of the return value.
+
+Prompt type can be YN (yes or no), YNA (yes, no or all) or input (for an input
+string).
+
+=item get_title()
+
+Returns the title of the prompt.
+
+=item get_message()
+
+Returns the message of the prompt.
+
+=item get_type()
+
+Returns the type of the prompt, can be YN (yes or no), YNA (yes, no or all) or
+input (for an input string).
+
+=item get_default()
+
+Returns the default return value of invoke().
+
+=item invoke()
+
+Gets an input string from the user, and returns it. Sub-classes must override
+this method.
+
+=back
+
+=head1 SEE ALSO
+
+L<FCM1::Interactive|FCM1::Interactive>,
+L<FCM1::Interactive::TxtInputGetter|FCM1::Interactive::TxtInputGetter>,
+L<FCM1::Interactive::GUIInputGetter|FCM1::Interactive::GUIInputGetter>
+
+=head1 COPYRIGHT
+
+E<169> Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM1/Interactive/InputGetter/CLI.pm b/lib/FCM1/Interactive/InputGetter/CLI.pm
new file mode 100644
index 0000000..b28c3af
--- /dev/null
+++ b/lib/FCM1/Interactive/InputGetter/CLI.pm
@@ -0,0 +1,100 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+
+package FCM1::Interactive::InputGetter::CLI;
+use base qw{FCM1::Interactive::InputGetter};
+
+my $DEF_MSG = q{ (or just press <return> for "%s")};
+my %EXTRA_MSG_FOR = (
+ yn => qq{\nEnter "y" or "n"},
+ yna => qq{\nEnter "y", "n" or "a"},
+);
+my %CHECKER_FOR = (
+ yn => sub {$_[0] eq 'y' || $_[0] eq 'n'},
+ yna => sub {$_[0] eq 'y' || $_[0] eq 'n' || $_[0] eq 'a'},
+);
+
+sub invoke {
+ my ($self) = @_;
+ my $type = $self->get_type() ? lc($self->get_type()) : q{};
+ my $message
+ = $self->get_message()
+ . (exists($EXTRA_MSG_FOR{$type}) ? $EXTRA_MSG_FOR{$type} : q{})
+ . ($self->get_default() ? sprintf($DEF_MSG, $self->get_default()) : q{})
+ . q{: }
+ ;
+ while (1) {
+ print($message);
+ my $answer = readline(STDIN);
+ chomp($answer);
+ if (!$answer && $self->get_default()) {
+ $answer = $self->get_default();
+ }
+ if (!exists($CHECKER_FOR{$type}) || $CHECKER_FOR{$type}->($answer)) {
+ return $answer;
+ }
+ }
+ return;
+}
+
+1;
+__END__
+
+=head1 NAME
+
+FCM1::Interactive::InputGetter::CLI
+
+=head1 SYNOPSIS
+
+ use FCM1::Interactive;
+ $answer = FCM1::Interactive::get_input(
+ title => 'My title',
+ message => 'Would you like to ...?',
+ type => 'yn',
+ default => 'n',
+ );
+
+=head1 DESCRIPTION
+
+This is a solid implementation of
+L<FCM1::Interactive::InputGetter|FCM1::Interactive::InputGetter>. It gets a user
+reply from STDIN using a prompt on STDOUT.
+
+=head1 METHODS
+
+See L<FCM1::Interactive::InputGetter|FCM1::Interactive::InputGetter> for a list of
+methods.
+
+=head1 TO DO
+
+Use IO::Prompt.
+
+=head1 SEE ALSO
+
+L<FCM1::Interactive|FCM1::Interactive>,
+L<FCM1::Interactive::InputGetter|FCM1::Interactive::InputGetter>,
+L<FCM1::Interactive::InputGetter::GUI|FCM1::Interactive::InputGetter::GUI>
+
+=head1 COPYRIGHT
+
+E<169> Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM1/Interactive/InputGetter/GUI.pm b/lib/FCM1/Interactive/InputGetter/GUI.pm
new file mode 100644
index 0000000..f2dfcb9
--- /dev/null
+++ b/lib/FCM1/Interactive/InputGetter/GUI.pm
@@ -0,0 +1,261 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+
+package FCM1::Interactive::InputGetter::GUI;
+use base qw{FCM1::Interactive::InputGetter};
+
+use Tk;
+
+################################################################################
+# Returns the geometry string for the pop up message box
+sub get_geometry {
+ my ($self) = @_;
+ return $self->{geometry};
+}
+
+################################################################################
+# Invokes the getter
+sub invoke {
+ my ($self) = @_;
+ my $answer;
+ local $| = 1;
+
+ # Create a main window
+ my $mw = MainWindow->new();
+ $mw->title($self->get_title());
+
+ # Define the default which applies if the dialog box is just closed or
+ # the user selects 'cancel'
+ $answer = $self->get_default() ? $self->get_default() : q{};
+
+ if (defined($self->get_type()) && $self->get_type() =~ qr{\A yn}ixms) {
+ # Create a yes-no(-all) dialog box
+
+ # If TYPE is YNA then add a third button: 'all'
+ my $buttons = $self->get_type() =~ qr{a \z}ixms ? 3 : 2;
+
+ # Message of the dialog box
+ $mw->Label('-text' => $self->get_message())->grid(
+ '-row' => 0,
+ '-column' => 0,
+ '-columnspan' => $buttons,
+ '-padx' => 10,
+ '-pady' => 10,
+ );
+
+ # The "yes" button
+ my $y_b = $mw->Button(
+ '-text' => 'Yes',
+ '-underline' => 0,
+ '-command' => sub {$answer = 'y'; $mw->destroy()},
+ )
+ ->grid('-row' => 1, '-column' => 0, '-padx' => 5, '-pady' => 5);
+
+ # The "no" button
+ my $n_b = $mw->Button (
+ '-text' => 'No',
+ '-underline' => 0,
+ '-command' => sub {$answer = 'n'; $mw->destroy()},
+ )
+ ->grid('-row' => 1, '-column' => 1, '-padx' => 5, '-pady' => 5);
+
+ # The "all" button
+ my $a_b;
+ if ($buttons == 3) {
+ $a_b = $mw->Button(
+ '-text' => 'All',
+ '-underline' => 0,
+ '-command' => sub {$answer = 'a'; $mw->destroy()},
+ )
+ ->grid('-row' => 1, '-column' => 2, '-padx' => 5, '-pady' => 5);
+ }
+
+ # Keyboard binding
+ if ($buttons == 3) {
+ $mw->bind('<Key>' => sub {
+ my $button
+ = $Tk::event->K() eq 'Y' || $Tk::event->K() eq 'y' ? $y_b
+ : $Tk::event->K() eq 'N' || $Tk::event->K() eq 'n' ? $n_b
+ : $Tk::event->K() eq 'A' || $Tk::event->K() eq 'a' ? $a_b
+ : undef
+ ;
+ if (defined($button)) {
+ $button->invoke();
+ }
+ });
+ }
+ else {
+ $mw->bind('<Key>' => sub {
+ my $button
+ = $Tk::event->K() eq 'Y' || $Tk::event->K() eq 'y' ? $y_b
+ : $Tk::event->K() eq 'N' || $Tk::event->K() eq 'n' ? $n_b
+ : undef
+ ;
+ if (defined($button)) {
+ $button->invoke();
+ }
+ });
+ }
+
+ # Handle the situation when the user attempts to quit the window
+ $mw->protocol('WM_DELETE_WINDOW', sub {
+ if (self->get_default()) {
+ $answer = $self->get_default();
+ }
+ $mw->destroy();
+ });
+ }
+ else {
+ # Create a dialog box to obtain an input string
+ # Message of the dialog box
+ $mw->Label('-text' => $self->get_message())->grid(
+ '-row' => 0,
+ '-column' => 0,
+ '-padx' => 5,
+ '-pady' => 5,
+ );
+
+ # Entry box for the user to type in the input string
+ my $entry = $answer;
+ my $input_e = $mw->Entry(
+ '-textvariable' => \$entry,
+ '-width' => 40,
+ )
+ ->grid(
+ '-row' => 0,
+ '-column' => 1,
+ '-sticky' => 'ew',
+ '-padx' => 5,
+ '-pady' => 5,
+ );
+
+ my $b_f = $mw->Frame->grid(
+ '-row' => 1,
+ '-column' => 0,
+ '-columnspan' => 2,
+ '-sticky' => 'e',
+ );
+
+ # An OK button to accept the input string
+ my $ok_b = $b_f->Button (
+ '-text' => 'OK',
+ '-command' => sub {$answer = $entry; $mw->destroy()},
+ )
+ ->grid('-row' => 0, '-column' => 0, '-padx' => 5, '-pady' => 5);
+
+ # A Cancel button to reject the input string
+ my $cancel_b = $b_f->Button(
+ '-text' => 'Cancel',
+ '-command' => sub {$answer = undef; $mw->destroy()},
+ )
+ ->grid('-row' => 0, '-column' => 1, '-padx' => 5, '-pady' => 5);
+
+ # Keyboard binding
+ $mw->bind ('<Key>' => sub {
+ if ($Tk::event->K eq 'Return' or $Tk::event->K eq 'KP_Enter') {
+ $ok_b->invoke();
+ }
+ elsif ($Tk::event->K eq 'Escape') {
+ $cancel_b->invoke();
+ }
+ });
+
+ # Allow the entry box to expand
+ $mw->gridColumnconfigure(1, '-weight' => 1);
+
+ # Set initial focus on the entry box
+ $input_e->focus();
+ $input_e->icursor('end');
+ }
+
+ $mw->geometry($self->get_geometry());
+
+ # Switch on "always on top" property for $mw
+ $mw->property(
+ qw/set _NET_WM_STATE ATOM/,
+ 32,
+ ['_NET_WM_STATE_STAYS_ON_TOP'],
+ ($mw->toplevel()->wrapper())[0],
+ );
+
+ MainLoop();
+ return $answer;
+}
+
+1;
+__END__
+
+=head1 NAME
+
+FCM1::Interactive::InputGetter::GUI
+
+=head1 SYNOPSIS
+
+ use FCM1::Interactive;
+ $answer = FCM1::Interactive::get_input(
+ title => 'My title',
+ message => 'Would you like to ...?',
+ type => 'yn',
+ default => 'n',
+ );
+
+=head1 DESCRIPTION
+
+This is a solid implementation of
+L<FCM1::Interactive::InputGetter|FCM1::Interactive::InputGetter>. It gets a user
+reply from a TK pop up message box.
+
+=head1 METHODS
+
+See L<FCM1::Interactive::InputGetter|FCM1::Interactive::InputGetter> for a list of
+inherited methods.
+
+=over 4
+
+=item new($args_ref)
+
+As in L<FCM1::Interactive::InputGetter|FCM1::Interactive::InputGetter>, but also
+accept a I<geometry> element for setting the geometry string of the pop up
+message box.
+
+=item get_geometry()
+
+Returns the geometry string for the pop up message box.
+
+=back
+
+=head1 TO DO
+
+Tidy up the logic of invoke(). Separate the logic for YN/A box and string input
+box, probably using a strategy pattern. Factor out the logic for the display
+and the return value.
+
+=head1 SEE ALSO
+
+L<FCM1::Interactive|FCM1::Interactive>,
+L<FCM1::Interactive::InputGetter|FCM1::Interactive::InputGetter>,
+L<FCM1::Interactive::InputGetter::CLI|FCM1::Interactive::InputGetter::CLI>
+
+=head1 COPYRIGHT
+
+E<169> Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM1/Keyword.pm b/lib/FCM1/Keyword.pm
new file mode 100644
index 0000000..6ed97fc
--- /dev/null
+++ b/lib/FCM1/Keyword.pm
@@ -0,0 +1,175 @@
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+use strict;
+use warnings;
+
+package FCM1::Keyword;
+
+use FCM::Context::Locator;
+
+# Returns/Sets the FCM 2 utility functional object.
+{ my $UTIL;
+ sub get_util {
+ $UTIL;
+ }
+ sub set_util {
+ $UTIL = $_[0];
+ }
+}
+
+# Expands (the keywords in) the specfied location (and REV), and returns them
+sub expand {
+ my ($in_loc, $in_rev) = @_;
+ my $target = $in_rev ? $in_loc . '@' . $in_rev : $in_loc;
+ my $locator = FCM::Context::Locator->new($target);
+ _unparse_loc(get_util()->loc_as_normalised($locator), $in_rev);
+}
+
+# Returns the corresponding browser URL for the input VC location
+sub get_browser_url {
+ my ($in_loc, $in_rev) = @_;
+ my $target = $in_rev ? $in_loc . '@' . $in_rev : $in_loc;
+ my $locator = FCM::Context::Locator->new($target);
+ get_util()->loc_browser_url($locator);
+}
+
+# Un-expands the specfied location (and REV) to keywords, and returns them
+sub unexpand {
+ my ($in_loc, $in_rev) = @_;
+ my $target = $in_rev ? $in_loc . '@' . $in_rev : $in_loc;
+ my $locator = FCM::Context::Locator->new($target);
+ _unparse_loc(get_util()->loc_as_keyword($locator), $in_rev);
+}
+
+# If $in_rev, returns (LOC, REV). Otherwise, returns LOC at REV
+sub _unparse_loc {
+ my ($loc, $in_rev) = @_;
+ if (!$loc) {
+ return;
+ }
+ if ($in_rev) {
+ my ($l, $r) = $loc =~ qr{\A (.*?) @([^@]+) \z}msx;
+ if ($l && $r) {
+ return ($l, $r);
+ }
+ }
+ return $loc;
+}
+
+1;
+__END__
+
+=head1 NAME
+
+FCM1::Keyword
+
+=head1 SYNOPSIS
+
+ use FCM1::Keyword;
+
+ $loc = FCM1::Keyword::expand('fcm:namespace/path at rev-keyword');
+ $loc = FCM1::Keyword::unexpand('svn://host/namespace/path@1234');
+
+ ($loc, $rev) = FCM1::Keyword::expand('fcm:namespace/path', 'rev-keyword');
+ ($loc, $rev) = FCM1::Keyword::unexpand('svn://host/namespace/path', 1234);
+
+ $loc = FCM1::Keyword::get_browser_url('fcm:namespace/path');
+ $loc = FCM1::Keyword::get_browser_url('svn://host/namespace/path');
+
+ $loc = FCM1::Keyword::get_browser_url('fcm:namespace/path at 1234');
+ $loc = FCM1::Keyword::get_browser_url('svn://host/namespace/path@1234');
+
+ $loc = FCM1::Keyword::get_browser_url('fcm:namespace/path', 1234);
+ $loc = FCM1::Keyword::get_browser_url('svn://host/namespace/path', 1234);
+
+=head1 DESCRIPTION
+
+Provides a compatibility layer for code in FCM1::* name space by wrapping the
+keyword related functions in L<FCM::Util|FCM::Util>. An instance of
+L<FCM::Util|FCM::Util> must be set via the set_util($value) function before
+using the other functions.
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item expand($loc)
+
+Expands FCM keywords in $loc and returns the result.
+
+If $loc is a I<fcm> scheme URI, the leading part (before any "/" or "@"
+characters) of the URI opaque is the namespace of a FCM location keyword. This
+is expanded into the actual value. Optionally, $loc can be suffixed with a peg
+revision (an "@" followed by any characters). If a peg revision is a FCM
+revision keyword, it is expanded into the actual revision.
+
+=item expand($loc,$rev)
+
+Same as C<expand($loc)>, but $loc should not contain a peg revision. Returns a
+list containing the expanded version of $loc and $rev.
+
+=item get_browser_url($loc)
+
+Given a repository $loc in a known keyword namespace, returns the corresponding
+URL for the code browser.
+
+Optionally, $loc can be suffixed with a peg revision (an "@" followed by any
+characters).
+
+=item get_browser_url($loc,$rev)
+
+Same as get_browser_url($loc), but the revision should be specified using $rev
+but not pegged with $loc.
+
+=item get_util()
+
+Returns the L<FCM::Util|FCM::Util> instance (set by set_util($value)).
+
+=item set_util($value)
+
+Sets the L<FCM::Util|FCM::Util> instance.
+
+=item unexpand($loc)
+
+Does the opposite of expand($loc). Returns the FCM location keyword equivalence
+of $loc. If the $loc can be mapped using 2 or more namespaces, the namespace
+that results in the longest substitution is used. Optionally, $loc can be
+suffixed with a peg revision (an "@" followed by any characters). If a peg
+revision is a known revision, it is turned into its corresponding revision
+keyword.
+
+=item unexpand($loc,$rev)
+
+Same as unexpand($loc), but $loc should not contain a peg revision. Returns a
+list containing the unexpanded version of $loc and $rev
+
+=item
+
+=back
+
+=head1 SEE ALSO
+
+L<FCM::System|FCM::System>,
+L<FCM::Util|FCM::Util>
+
+=head1 COPYRIGHT
+
+(C) Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/lib/FCM1/ReposBranch.pm b/lib/FCM1/ReposBranch.pm
new file mode 100644
index 0000000..1194d64
--- /dev/null
+++ b/lib/FCM1/ReposBranch.pm
@@ -0,0 +1,528 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# NAME
+# FCM1::ReposBranch
+#
+# DESCRIPTION
+# This class contains methods for gathering information for a repository
+# branch. It currently supports Subversion repository and local user
+# directory.
+#
+# ------------------------------------------------------------------------------
+
+use warnings;
+use strict;
+
+package FCM1::ReposBranch;
+use base qw{FCM1::Base};
+
+use FCM1::CfgLine;
+use FCM1::Keyword;
+use FCM1::Util qw{expand_tilde is_url run_command w_report};
+use File::Basename qw{dirname};
+use File::Find qw{find};
+use File::Spec;
+
+# List of scalar property methods for this class
+my @scalar_properties = (
+ 'package', # package name of which this repository belongs
+ 'repos', # repository branch root URL/path
+ 'revision', # the revision of this branch
+ 'tag', # "tag" name of this branch of the repository
+ 'type', # repository type
+);
+
+# List of hash property methods for this class
+my @hash_properties = (
+ 'dirs', # list of non-recursive directories in this branch
+ 'expdirs', # list of recursive directories in this branch
+);
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $obj = FCM1::ReposBranch->new (%args);
+#
+# DESCRIPTION
+# This method constructs a new instance of the FCM1::ReposBranch class. See
+# @scalar_properties above for allowed list of properties in the constructor.
+# (KEYS should be in uppercase.)
+# ------------------------------------------------------------------------------
+
+sub new {
+ my $this = shift;
+ my %args = @_;
+ my $class = ref $this || $this;
+
+ my $self = FCM1::Base->new (%args);
+
+ for (@scalar_properties) {
+ $self->{$_} = exists $args{uc ($_)} ? $args{uc ($_)} : undef;
+ }
+
+ $self->{$_} = {} for (@hash_properties);
+
+ bless $self, $class;
+ return $self;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $value = $obj->X;
+# $obj->X ($value);
+#
+# DESCRIPTION
+# Details of these properties are explained in @scalar_properties.
+# ------------------------------------------------------------------------------
+
+for my $name (@scalar_properties) {
+ no strict 'refs';
+
+ *$name = sub {
+ my $self = shift;
+
+ # Argument specified, set property to specified argument
+ if (@_) {
+ $self->{$name} = $_[0];
+ }
+
+ return $self->{$name};
+ }
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# %hash = %{ $obj->X () };
+# $obj->X (\%hash);
+#
+# $value = $obj->X ($index);
+# $obj->X ($index, $value);
+#
+# DESCRIPTION
+# Details of these properties are explained in @hash_properties.
+#
+# If no argument is set, this method returns a hash containing a list of
+# objects. If an argument is set and it is a reference to a hash, the objects
+# are replaced by the specified hash.
+#
+# If a scalar argument is specified, this method returns a reference to an
+# object, if the indexed object exists or undef if the indexed object does
+# not exist. If a second argument is set, the $index element of the hash will
+# be set to the value of the argument.
+# ------------------------------------------------------------------------------
+
+for my $name (@hash_properties) {
+ no strict 'refs';
+
+ *$name = sub {
+ my ($self, $arg1, $arg2) = @_;
+
+ # Ensure property is defined as a reference to a hash
+ $self->{$name} = {} if not defined ($self->{$name});
+
+ # Argument 1 can be a reference to a hash or a scalar index
+ my ($index, %hash);
+
+ if (defined $arg1) {
+ if (ref ($arg1) eq 'HASH') {
+ %hash = %$arg1;
+
+ } else {
+ $index = $arg1;
+ }
+ }
+
+ if (defined $index) {
+ # A scalar index is defined, set and/or return the value of an element
+ $self->{$name}{$index} = $arg2 if defined $arg2;
+
+ return (
+ exists $self->{$name}{$index} ? $self->{$name}{$index} : undef
+ );
+
+ } else {
+ # A scalar index is not defined, set and/or return the hash
+ $self->{$name} = \%hash if defined $arg1;
+ return $self->{$name};
+ }
+ }
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = $obj->expand_revision;
+#
+# DESCRIPTION
+# This method expands the revision keywords of the current branch to a
+# revision number. It returns true on success.
+# ------------------------------------------------------------------------------
+
+sub expand_revision {
+ my $self = shift;
+
+ my $rc = 1;
+ if ($self->type eq 'svn') {
+ # Expand revision keyword
+ my $rev = (FCM1::Keyword::expand($self->repos(), $self->revision()))[1];
+
+ # Get last changed revision of the specified revision
+ my $info_ref = $self->_svn_info($self->repos(), $rev);
+ if (!defined($info_ref->{'Revision'})) {
+ my $url = $self->repos() . ($rev ? '@' . $rev : q{});
+ w_report("ERROR: $url: not a valid URL\n");
+ return 0;
+ }
+ my $lc_rev = $info_ref->{'Last Changed Rev'};
+ $rev = $info_ref->{'Revision'};
+
+ # Print info if specified revision is not the last commit revision
+ if (uc($self->revision()) ne 'HEAD' && $lc_rev != $rev) {
+ my $message = $self->repos . '@' . $rev . ': last changed at [' .
+ $lc_rev . '].';
+ if ($self->setting ('EXT_REVMATCH') and uc ($self->revision) ne 'HEAD') {
+ w_report "ERROR: specified and last changed revisions differ:\n",
+ ' ', $message, "\n";
+ $rc = 0;
+
+ } else {
+ print 'INFO: ', $message, "\n";
+ }
+ }
+
+ if ($self->verbose > 1 and uc ($self->revision) ne 'HEAD') {
+ # See if there is a later change of the branch at the HEAD
+ my $head_lc_rev = $self->_svn_info($self->repos())->{'Last Changed Rev'};
+
+ if (defined($head_lc_rev) && $head_lc_rev != $lc_rev) {
+ # Ensure that this is the same branch by checking its history
+ my @lines = &run_command (
+ [qw/svn log -q --incremental -r/, $lc_rev, $self->repos . '@HEAD'],
+ METHOD => 'qx', TIME => $self->verbose > 2, ERROR => 'ignore',
+ );
+
+ print 'INFO: ', $self->repos, '@', $rev,
+ ': newest commit at [', $head_lc_rev, '].', "\n"
+ if @lines;
+ }
+ }
+
+ $self->revision ($rev) if $rev ne $self->revision;
+
+ } elsif ($self->type eq 'user') {
+ 1; # Do nothing
+
+ } else {
+ w_report 'ERROR: ', $self->repos, ': repository type "', $self->type,
+ '" not supported.';
+ $rc = 0;
+ }
+
+ return $rc;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = $obj->expand_path;
+#
+# DESCRIPTION
+# This method expands the relative path names of sub-directories to full
+# path names. It returns true on success.
+# ------------------------------------------------------------------------------
+
+sub expand_path {
+ my $self = shift;
+
+ my $rc = 1;
+ if ($self->type eq 'svn') {
+ # SVN repository
+ # Do nothing unless there is a declared repository for this branch
+ return unless $self->repos;
+
+ # Remove trailing /
+ my $repos = $self->repos;
+ $self->repos ($repos) if $repos =~ s#/+$##;
+
+ # Consider all declared (expandable) sub-directories
+ for my $name (qw/dirs expdirs/) {
+ for my $dir (keys %{ $self->$name }) {
+ # Do nothing if declared sub-directory is quoted as a full URL
+ next if &is_url ($self->$name ($dir));
+
+ # Expand sub-directory to full URL
+ $self->$name ($dir, $self->repos . (
+ $self->$name ($dir) ? ('/' . $self->$name ($dir)) : ''
+ ));
+ }
+ }
+ # Note: "catfile" cannot be used in the above statement because it has
+ # the tendency of removing a slash from double slashes.
+
+ } elsif ($self->type eq 'user') {
+ # Local user directories
+
+ # Expand leading ~ for all declared (expandable) sub-directories
+ for my $name (qw/dirs expdirs/) {
+ for my $dir (keys %{ $self->$name }) {
+ $self->$name ($dir, expand_tilde $self->$name ($dir));
+ }
+ }
+
+ # A top directory for the source is declared
+ if ($self->repos) {
+ # Expand leading ~ for the top directory
+ $self->repos (expand_tilde $self->repos);
+
+ # Get the root directory of the file system
+ my $rootdir = File::Spec->rootdir ();
+
+ # Expand top directory to absolute path, if necessary
+ $self->repos (File::Spec->rel2abs ($self->repos))
+ if $self->repos !~ m/^$rootdir/;
+
+ # Remove trailing /
+ my $repos = $self->repos;
+ $self->repos ($repos) if $repos =~ s#/+$##;
+
+ # Consider all declared (expandable) sub-directories
+ for my $name (qw/dirs expdirs/) {
+ for my $dir (keys %{ $self->$name }) {
+ # Do nothing if declared sub-directory is quoted as a full path
+ next if $self->$name ($dir) =~ m#^$rootdir#;
+
+ # Expand sub-directory to full path
+ $self->$name (
+ $dir, $self->$name ($dir)
+ ? File::Spec->catfile ($self->repos, $self->$name ($dir))
+ : $self->repos
+ );
+ }
+ }
+ }
+
+ } else {
+ w_report 'ERROR: ', $self->repos, ': repository type "', $self->type,
+ '" not supported.';
+ $rc = 0;
+ }
+
+ return $rc;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = $obj->expand_all();
+#
+# DESCRIPTION
+# This method searches the expandable source directories recursively for
+# source directories containing regular files. The namespaces and the locators
+# of these sub-directories are then added to the source directory hash table.
+# Returns true on success.
+# ------------------------------------------------------------------------------
+
+sub expand_all {
+ my ($self) = @_;
+ my %finder_of = (
+ user => sub {
+ my ($root_locator) = @_;
+ my %ns_of;
+ my $wanted = sub {
+ my $base_name = $_;
+ my $path = $File::Find::name;
+ if (-f $path && !-l $path) {
+ my $dir_path = dirname($path);
+ my $rel_dir_path = File::Spec->abs2rel($dir_path, $root_locator);
+ if ($rel_dir_path eq q{.}) {
+ $rel_dir_path = q{};
+ }
+ if (!exists($ns_of{$dir_path})) {
+ $ns_of{$dir_path} = [File::Spec->splitdir($rel_dir_path)];
+ }
+ }
+ };
+ find($wanted, $root_locator);
+ return \%ns_of;
+ },
+ svn => sub {
+ my ($root_locator) = @_;
+ my $runner = sub {
+ map {chomp($_); $_} run_command(
+ ['svn', @_, '-R', join('@', $root_locator, $self->revision())],
+ METHOD => 'qx', TIME => $self->config()->verbose() > 2,
+ );
+ };
+ # FIXME: check for symlink switched off due to "svn pg" being very slow
+ #my %symlink_in
+ # = map {($_ =~ qr{\A(.+)\s-\s(\*)\z}xms)} ($runner->(qw{pg svn:special}));
+ #my @locators
+ # = grep {$_ !~ qr{/\z}xms && !$symlink_in{$_}} ($runner->('ls'));
+ my @locators = grep {$_ !~ qr{/\z}xms} ($runner->('ls'));
+ my %ns_of;
+ for my $locator (@locators) {
+ my ($rel_dir_locator) = $locator =~ qr{\A(.*)/[^/]+\z}xms; # dirname
+ $rel_dir_locator ||= q{};
+ my $dir_locator
+ = $rel_dir_locator ? join(q{/}, $root_locator, $rel_dir_locator)
+ : $root_locator
+ ;
+ if (!exists($ns_of{$dir_locator})) {
+ $ns_of{$dir_locator} = [split(q{/}, $rel_dir_locator)];
+ }
+ }
+ return \%ns_of;
+ },
+ );
+
+ if (!defined($finder_of{$self->type()})) {
+ w_report(sprintf(
+ qq{ERROR: %s: resource type "%s" not supported},
+ $self->repos(),
+ $self->type(),
+ ));
+ return;
+ }
+ while (my ($root_ns, $root_locator) = each(%{$self->expdirs()})) {
+ my @root_ns_list = split(qr{$FCM1::Config::DELIMITER}xms, $root_ns);
+ my $ns_hash_ref = $finder_of{$self->type()}->($root_locator);
+ while (my ($dir_path, $ns_list_ref) = each(%{$ns_hash_ref})) {
+ if (!grep {$_ =~ qr{\A\.}xms || $_ =~ qr{~\z}xms} @{$ns_list_ref}) {
+ my $ns = join($FCM1::Config::DELIMITER, @root_ns_list, @{$ns_list_ref});
+ $self->dirs($ns, $dir_path);
+ }
+ }
+ }
+ return 1;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $n = $obj->add_base_dirs ($base);
+#
+# DESCRIPTION
+# Add a list of source directories to the current branch based on the set
+# provided by $base, which must be a reference to a FCM1::ReposBranch
+# instance. It returns the total number of used sub-directories in the
+# current repositories.
+# ------------------------------------------------------------------------------
+
+sub add_base_dirs {
+ my $self = shift;
+ my $base = shift;
+
+ my %base_dirs = %{ $base->dirs };
+
+ for my $key (keys %base_dirs) {
+ # Remove repository root from base directories
+ if ($base_dirs{$key} eq $base->repos) {
+ $base_dirs{$key} = '';
+
+ } else {
+ $base_dirs{$key} = substr $base_dirs{$key}, length ($base->repos) + 1;
+ }
+
+ # Append base directories to current repository root
+ $self->dirs ($key, $base_dirs{$key});
+ }
+
+ # Expand relative path names of sub-directories
+ $self->expand_path;
+
+ return scalar keys %{ $self->dirs };
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# @cfglines = $obj->to_cfglines ();
+#
+# DESCRIPTION
+# This method returns a list of configuration lines for the current branch.
+# ------------------------------------------------------------------------------
+
+sub to_cfglines {
+ my ($self) = @_;
+ my @return = ();
+
+ my $suffix = $self->package . $FCM1::Config::DELIMITER . $self->tag;
+ push @return, FCM1::CfgLine->new (
+ label => $self->cfglabel ('REPOS') . $FCM1::Config::DELIMITER . $suffix,
+ value => $self->repos,
+ ) if $self->repos;
+
+ push @return, FCM1::CfgLine->new (
+ label => $self->cfglabel ('REVISION') . $FCM1::Config::DELIMITER . $suffix,
+ value => $self->revision,
+ ) if $self->revision;
+
+ for my $key (sort keys %{ $self->dirs }) {
+ my $value = $self->dirs ($key);
+
+ # Use relative path where possible
+ if ($self->repos) {
+ if ($value eq $self->repos) {
+ $value = '';
+
+ } elsif (index ($value, $self->repos) == 0) {
+ $value = substr ($value, length ($self->repos) + 1);
+ }
+ }
+
+ # Use top package name where possible
+ my $dsuffix = $key . $FCM1::Config::DELIMITER . $self->tag;
+ $dsuffix = $suffix if $value ne $self->dirs ($key) and $key eq join (
+ $FCM1::Config::DELIMITER, $self->package, File::Spec->splitdir ($value)
+ );
+
+ push @return, FCM1::CfgLine->new (
+ label => $self->cfglabel ('DIRS') . $FCM1::Config::DELIMITER . $dsuffix,
+ value => $value,
+ );
+ }
+
+ push @return, FCM1::CfgLine->new ();
+
+ return @return;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# my $hash_ref = $self->_svn_info($url[, $rev]);
+#
+# DESCRIPTION
+# Executes "svn info" and returns each field in a hash.
+# ------------------------------------------------------------------------------
+sub _svn_info {
+ my ($self, $url, $rev) = @_;
+ return {
+ map {
+ chomp();
+ my ($key, $value) = split(qr{\s*:\s*}xms, $_, 2);
+ $key ? ($key, $value) : ();
+ } run_command(
+ [qw{svn info}, ($rev ? ('-r', $rev, join('@', $url, $rev)) : $url)],
+ DEVNULL => 1,
+ ERROR => 'ignore',
+ METHOD => 'qx',
+ TIME => $self->verbose() > 2,
+ )
+ };
+}
+
+# ------------------------------------------------------------------------------
+
+1;
+
+__END__
diff --git a/lib/FCM1/SrcDirLayer.pm b/lib/FCM1/SrcDirLayer.pm
new file mode 100644
index 0000000..b1dda79
--- /dev/null
+++ b/lib/FCM1/SrcDirLayer.pm
@@ -0,0 +1,277 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# NAME
+# FCM1::SrcDirLayer
+#
+# DESCRIPTION
+# This class contains methods to manipulate the extract of a source
+# directory from a branch of a (Subversion) repository.
+#
+# ------------------------------------------------------------------------------
+use warnings;
+use strict;
+
+package FCM1::SrcDirLayer;
+use base qw{FCM1::Base};
+
+use FCM1::Util qw{run_command e_report w_report};
+use File::Basename qw{dirname};
+use File::Path qw{mkpath};
+use File::Spec;
+
+# List of property methods for this class
+my @scalar_properties = (
+ 'cachedir', # cache directory for this directory branch
+ 'commit', # revision at which the source directory was changed
+ 'extracted', # is this branch already extracted?
+ 'files', # list of source files in this directory branch
+ 'location', # location of the source directory in the branch
+ 'name', # sub-package name of the source directory
+ 'package', # top level package name of which the current repository belongs
+ 'reposroot', # repository root URL
+ 'revision', # revision of the repository branch
+ 'tag', # package/revision tag of the current repository branch
+ 'type', # type of the repository branch ("svn" or "user")
+);
+
+my %ERR_MESS_OF = (
+ CACHE_WRITE => '%s: cannot write to cache',
+ SYMLINK => '%s/%s: ignore symbolic link',
+ VC_TYPE => '%s: repository type not supported',
+);
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $obj = FCM1::SrcDirLayer->new (%args);
+#
+# DESCRIPTION
+# This method constructs a new instance of the FCM1::SrcDirLayer class. See
+# above for allowed list of properties. (KEYS should be in uppercase.)
+# ------------------------------------------------------------------------------
+
+sub new {
+ my $this = shift;
+ my %args = @_;
+ my $class = ref $this || $this;
+
+ my $self = FCM1::Base->new (%args);
+
+ for (@scalar_properties) {
+ $self->{$_} = exists $args{uc ($_)} ? $args{uc ($_)} : undef;
+ }
+
+ bless $self, $class;
+ return $self;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $value = $obj->X;
+# $obj->X ($value);
+#
+# DESCRIPTION
+# Details of these properties are explained in @scalar_properties.
+# ------------------------------------------------------------------------------
+
+for my $name (@scalar_properties) {
+ no strict 'refs';
+
+ *$name = sub {
+ my $self = shift;
+
+ # Argument specified, set property to specified argument
+ if (@_) {
+ $self->{$name} = $_[0];
+ }
+
+ # Default value for property
+ if (not defined $self->{$name}) {
+ if ($name eq 'files') {
+ # Reference to an array
+ $self->{$name} = [];
+ }
+ }
+
+ return $self->{$name};
+ }
+}
+
+# Handles error/warning events.
+sub _err {
+ my ($key, $args_ref, $warn_only) = @_;
+ my $reporter = $warn_only ? \&w_report : \&e_report;
+ $args_ref ||= [];
+ $reporter->(sprintf($ERR_MESS_OF{$key} . ".\n", @{$args_ref}));
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $dir = $obj->localdir;
+#
+# DESCRIPTION
+# This method returns the user or cache directory for the current revision
+# of the repository branch.
+# ------------------------------------------------------------------------------
+
+sub localdir {
+ my $self = shift;
+
+ return $self->user ? $self->location : $self->cachedir;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $user = $obj->user;
+#
+# DESCRIPTION
+# This method returns the string "user" if the current source directory
+# branch is a local directory. Otherwise, it returns "undef".
+# ------------------------------------------------------------------------------
+
+sub user {
+ my $self = shift;
+
+ return $self->type eq 'user' ? 'user' : undef;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rev = $obj->get_commit;
+#
+# DESCRIPTION
+# If the current repository type is "svn", this method attempts to obtain
+# the revision in which the branch is last committed. On a successful
+# operation, it returns this revision number. Otherwise, it returns
+# "undef".
+# ------------------------------------------------------------------------------
+
+sub get_commit {
+ my $self = shift;
+
+ if ($self->type eq 'svn') {
+ # Execute the "svn info" command
+ my @lines = &run_command (
+ [qw/svn info -r/, $self->revision, $self->location . '@' . $self->revision],
+ METHOD => 'qx', TIME => $self->config->verbose > 2,
+ );
+
+ my $rev;
+ for (@lines) {
+ if (/^Last\s+Changed\s+Rev\s*:\s*(\d+)/i) {
+ $rev = $1;
+ last;
+ }
+ }
+
+ # Commit revision of this source directory
+ $self->commit ($rev);
+
+ return $self->commit;
+
+ } elsif ($self->type eq 'user') {
+ return;
+
+ } else {
+ _err('VC_TYPE', [$self->type()]);
+ }
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = $obj->update_cache;
+#
+# DESCRIPTION
+# If the current repository type is "svn", this method attempts to extract
+# the current revision source directory from the current branch from the
+# repository, sending the output to the cache directory. It returns true on
+# a successful operation, or false if the repository is not of type "svn".
+# ------------------------------------------------------------------------------
+
+sub update_cache {
+ my $self = shift;
+
+ return unless $self->cachedir;
+
+ # Create cache extract destination, if necessary
+ my $dirname = dirname $self->cachedir;
+ mkpath($dirname);
+
+ if (!-d $dirname) {
+ _err('CACHE_WRITE', [$dirname]);
+ }
+
+ if ($self->type eq 'svn') {
+ # Set up the extract command, "svn export --force -q -N"
+ my @command = (
+ qw/svn export --force -q -N/,
+ $self->location . '@' . $self->revision,
+ $self->cachedir,
+ );
+
+ &run_command (\@command, TIME => $self->config->verbose > 2);
+
+ } elsif ($self->type eq 'user') {
+ return;
+
+ } else {
+ _err('VC_TYPE', [$self->type()]);
+ }
+
+ return 1;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# @files = $obj->get_files();
+#
+# DESCRIPTION
+# This method returns a list of file base names in the (cache of) this source
+# directory in the current branch.
+# ------------------------------------------------------------------------------
+
+sub get_files {
+ my ($self) = @_;
+ opendir(my $dir, $self->localdir())
+ || die($self->localdir(), ': cannot read directory');
+ my @base_names = ();
+ BASE_NAME:
+ while (my $base_name = readdir($dir)) {
+ if ($base_name =~ qr{\A\.}xms || $base_name =~ qr{~\z}xms) {
+ next BASE_NAME;
+ }
+ my $path = File::Spec->catfile($self->localdir(), $base_name);
+ if (-d $path) {
+ next BASE_NAME;
+ }
+ if (-l $path) {
+ _err('SYMLINK', [$self->location(), $base_name], 1);
+ next BASE_NAME;
+ }
+ push(@base_names, $base_name);
+ }
+ closedir($dir);
+ $self->files(\@base_names);
+ return @base_names;
+}
+
+# ------------------------------------------------------------------------------
+
+1;
+
+__END__
diff --git a/lib/FCM1/Timer.pm b/lib/FCM1/Timer.pm
new file mode 100644
index 0000000..26c080c
--- /dev/null
+++ b/lib/FCM1/Timer.pm
@@ -0,0 +1,85 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# NAME
+# FCM1::Timer
+#
+# DESCRIPTION
+# This is a package of timer utility used by the FCM command.
+#
+# ------------------------------------------------------------------------------
+
+package FCM1::Timer;
+
+# Standard pragma
+use warnings;
+use strict;
+
+# Exports
+our (@ISA, @EXPORT, @EXPORT_OK);
+
+sub timestamp_command;
+
+require Exporter;
+ at ISA = qw(Exporter);
+ at EXPORT = qw(timestamp_command);
+
+# ------------------------------------------------------------------------------
+
+# Module level variables
+my %cmd_start_time = (); # Command start time, (key = command, value = time)
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $string = &FCM1::Timer::timestamp_command ($command[, $status]);
+#
+# DESCRIPTION
+# This function returns a string adding to $command a prefix according the
+# value of $status. If $status is not specified or does not match the word
+# "end", the status is assumed to be "start". At "start", the prefix will
+# contain the current timestamp. If $status is the word "end", the prefix
+# will contain the total time taken since this function was called with the
+# same $command at the "start" status.
+# ------------------------------------------------------------------------------
+
+sub timestamp_command {
+ (my $command, my $status) = @_;
+
+ my $prefix;
+ if ($status and $status =~ /end/i) {
+ # Status is "end", insert time taken
+ my $lapse = time () - $cmd_start_time{$command};
+ $prefix = sprintf "# Time taken: %12d s=> ", $lapse;
+
+ } else {
+ # Status is "start", insert time stamp
+ $cmd_start_time{$command} = time;
+
+ (my $sec, my $min, my $hour, my $mday, my $mon, my $year) = localtime;
+ $prefix = sprintf "# Start: %04d-%02d-%02d %02d:%02d:%02d=> ",
+ $year + 1900, $mon + 1, $mday, $hour, $min, $sec;
+ }
+
+ return $prefix . $command . "\n";
+}
+
+# ------------------------------------------------------------------------------
+
+1;
+
+__END__
diff --git a/lib/FCM1/Util.pm b/lib/FCM1/Util.pm
new file mode 100644
index 0000000..2752b11
--- /dev/null
+++ b/lib/FCM1/Util.pm
@@ -0,0 +1,564 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# NAME
+# FCM1::Util
+#
+# DESCRIPTION
+# This is a package of misc utilities used by the FCM command.
+#
+# ------------------------------------------------------------------------------
+
+use warnings;
+use strict;
+
+package FCM1::Util;
+require Exporter;
+our @ISA = qw{Exporter};
+
+sub expand_tilde;
+sub e_report;
+sub find_file_in_path;
+sub get_command_string;
+sub get_rev_of_wc;
+sub get_url_of_wc;
+sub get_url_peg_of_wc;
+sub get_wct;
+sub is_url;
+sub is_wc;
+sub print_command;
+sub run_command;
+sub svn_date;
+sub tidy_url;
+sub touch_file;
+sub w_report;
+
+our @EXPORT = qw{
+ expand_tilde
+ e_report
+ find_file_in_path
+ get_command_string
+ get_rev_of_wc
+ get_url_of_wc
+ get_url_peg_of_wc
+ get_wct
+ is_url
+ is_wc
+ print_command
+ run_command
+ svn_date
+ tidy_url
+ touch_file
+ w_report
+};
+
+# Standard modules
+use Carp;
+use Cwd;
+use File::Basename;
+use File::Find;
+use File::Path;
+use File::Spec;
+use POSIX qw{strftime SIGINT SIGKILL SIGTERM WEXITSTATUS WIFSIGNALED WTERMSIG};
+
+# FCM component modules
+use FCM1::Timer;
+
+# ------------------------------------------------------------------------------
+
+# Module level variables
+my %svn_info = (); # "svn info" log, (key1 = path,
+ # key2 = URL, Revision, Last Changed Rev)
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# %srcdir = &FCM1::Util::find_file_in_path ($file, \@path);
+#
+# DESCRIPTION
+# Search $file in @path. Returns the full path of the $file if it is found
+# in @path. Returns "undef" if $file is not found in @path.
+# ------------------------------------------------------------------------------
+
+sub find_file_in_path {
+ my ($file, $path) = @_;
+
+ for my $dir (@$path) {
+ my $full_file = File::Spec->catfile ($dir, $file);
+ return $full_file if -e $full_file;
+ }
+
+ return undef;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $expanded_path = &FCM1::Util::expand_tilde ($path);
+#
+# DESCRIPTION
+# Returns an expanded path if $path is a path that begins with a tilde (~).
+# ------------------------------------------------------------------------------
+
+sub expand_tilde {
+ my $file = $_[0];
+
+ $file =~ s#^~([^/]*)#$1 ? (getpwnam $1)[7] : ($ENV{HOME} || $ENV{LOGDIR})#ex;
+
+ # Expand . and ..
+ while ($file =~ s#/+\.(?:/+|$)#/#g) {next}
+ while ($file =~ s#/+[^/]+/+\.\.(?:/+|$)#/#g) {next}
+
+ # Remove trailing /
+ $file =~ s#/*$##;
+
+ return $file;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $rc = &FCM1::Util::touch_file ($file);
+#
+# DESCRIPTION
+# Touch $file if it exists. Create $file if it does not exist. Return 1 for
+# success or 0 otherwise.
+# ------------------------------------------------------------------------------
+
+sub touch_file {
+ my $file = $_[0];
+ my $rc = 1;
+
+ if (-e $file) {
+ my $now = time;
+ $rc = utime $now, $now, $file;
+
+ } else {
+ mkpath dirname ($file) unless -d dirname ($file);
+
+ $rc = open FILE, '>', $file;
+ $rc = close FILE if $rc;
+ }
+
+ return $rc;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $flag = &is_wc ([$path]);
+#
+# DESCRIPTION
+# Returns true if current working directory (or $path) is a Subversion
+# working copy.
+# ------------------------------------------------------------------------------
+
+sub is_wc {
+ my $path = shift() || cwd();
+ my $path_of_dir = -f $path ? dirname($path) : $path;
+ if (-e File::Spec->catfile($path_of_dir, qw{.svn entries})) {
+ return 1;
+ }
+ my $inforc = &run_command (
+ [qw/svn info/, $path_of_dir],
+ METHOD => 'qx', DEVNULL => 1, ERROR => 'ignore'
+ );
+ return $inforc != 0;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $flag = &is_url ($url);
+#
+# DESCRIPTION
+# Returns true if $url is a URL.
+# ------------------------------------------------------------------------------
+
+sub is_url {
+ # This should handle URL beginning with svn://, http:// and svn+ssh://
+ return ($_[0] =~ m#^[\+\w]+://#);
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $url = tidy_url($url);
+#
+# DESCRIPTION
+# Returns a tidied version of $url by removing . and .. in the path.
+# ------------------------------------------------------------------------------
+
+sub tidy_url {
+ my ($url) = @_;
+ if (!is_url($url)) {
+ return $url;
+ }
+ my $DOT_PATTERN = qr{/+ \. (?:/+|(@|\z))}xms;
+ my $DOT_DOT_PATTERN = qr{/+ [^/]+ /+ \.\. (?:/+|(@|\z))}xms;
+ my $TRAILING_SLASH_PATTERN = qr{([^/]+) /* (@|\z)}xms;
+ my $RIGHT_EVAL = q{'/' . ($1 ? $1 : '')};
+ DOT:
+ while ($url =~ s{$DOT_PATTERN}{$RIGHT_EVAL}eegxms) {
+ next DOT;
+ }
+ DOT_DOT:
+ while ($url =~ s{$DOT_DOT_PATTERN}{$RIGHT_EVAL}eegxms) {
+ next DOT_DOT;
+ }
+ $url =~ s{$TRAILING_SLASH_PATTERN}{$1$2}xms;
+ return $url;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $string = &get_wct ([$dir]);
+#
+# DESCRIPTION
+# If current working directory (or $dir) is a Subversion working copy,
+# returns the top directory of this working copy; otherwise returns an empty
+# string.
+# ------------------------------------------------------------------------------
+
+sub get_wct {
+ my $dir = @_ ? $_[0] : cwd ();
+
+ return '' if not &is_wc ($dir);
+
+ my $updir = dirname $dir;
+ while (&is_wc ($updir)) {
+ $dir = $updir;
+ $updir = dirname $dir;
+ last if $updir eq $dir;
+ }
+
+ return $dir;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $string = &get_url_of_wc ([$path[, $refresh]]);
+#
+# DESCRIPTION
+# If current working directory (or $path) is a Subversion working copy,
+# returns the URL of the associated Subversion repository; otherwise returns
+# an empty string. If $refresh is specified, do not use the cached
+# information.
+# ------------------------------------------------------------------------------
+
+sub get_url_of_wc {
+ my $path = @_ ? $_[0] : cwd ();
+ my $refresh = exists $_[1] ? $_[1] : 0;
+ my $url = '';
+
+ if (&is_wc ($path)) {
+ delete $svn_info{$path} if $refresh;
+ &_invoke_svn_info (PATH => $path) unless exists $svn_info{$path};
+ $url = $svn_info{$path}{URL};
+ }
+
+ return $url;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $string = &get_url_peg_of_wc ([$path[, $refresh]]);
+#
+# DESCRIPTION
+# If current working directory (or $path) is a Subversion working copy,
+# returns the URL at REV of the associated Subversion repository; otherwise
+# returns an empty string. If $refresh is specified, do not use the cached
+# information.
+# ------------------------------------------------------------------------------
+
+sub get_url_peg_of_wc {
+ my $path = @_ ? $_[0] : cwd ();
+ my $refresh = exists $_[1] ? $_[1] : 0;
+ my $url = '';
+
+ if (&is_wc ($path)) {
+ delete $svn_info{$path} if $refresh;
+ &_invoke_svn_info (PATH => $path) unless exists $svn_info{$path};
+ $url = $svn_info{$path}{URL} . '@' . $svn_info{$path}{Revision};
+ }
+
+ return $url;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# &_invoke_svn_info (PATH => $path);
+#
+# DESCRIPTION
+# The function is internal to this module. It invokes "svn info" on $path to
+# gather information on URL, Revision and Last Changed Rev. The information
+# is stored in a hash table at the module level, so that the information can
+# be re-used.
+# ------------------------------------------------------------------------------
+
+sub _invoke_svn_info {
+ my %args = @_;
+ my $path = $args{PATH};
+ my $cfg = FCM1::Config->instance();
+
+ return if exists $svn_info{$path};
+
+ # Invoke "svn info" command
+ my @info = &run_command (
+ [qw/svn info/, $path],
+ PRINT => $cfg->verbose > 2, METHOD => 'qx', DEVNULL => 1, ERROR => 'ignore',
+ );
+ for (@info) {
+ chomp;
+
+ if (/^(URL|Revision|Last Changed Rev):\s*(.+)$/) {
+ $svn_info{$path}{$1} = $2;
+ }
+ }
+
+ return;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $string = &get_command_string ($cmd);
+# $string = &get_command_string (\@cmd);
+#
+# DESCRIPTION
+# The function returns a string by converting the list in @cmd or the scalar
+# $cmd to a form, where it can be executed as a shell command.
+# ------------------------------------------------------------------------------
+
+sub get_command_string {
+ my $cmd = $_[0];
+ my $return = '';
+
+ if (ref ($cmd) and ref ($cmd) eq 'ARRAY') {
+ # $cmd is a reference to an array
+
+ # Print each argument
+ for my $i (0 .. @{ $cmd } - 1) {
+ my $arg = $cmd->[$i];
+
+ $arg =~ s/./*/g if $i > 0 and $cmd->[$i - 1] eq '--password';
+
+ if ($arg =~ /[\s'"*?]/) {
+ # Argument contains a space, quote it
+ if (index ($arg, "'") >= 0) {
+ # Argument contains an apostrophe, quote it with double quotes
+ $return .= ($i > 0 ? ' ' : '') . '"' . $arg . '"';
+
+ } else {
+ # Otherwise, quote argument with apostrophes
+ $return .= ($i > 0 ? ' ' : '') . "'" . $arg . "'";
+ }
+
+ } else {
+ # Argument does not contain a space, just print it
+ $return .= ($i > 0 ? ' ' : '') . ($arg eq '' ? "''" : $arg);
+ }
+ }
+
+ } else {
+ # $cmd is a scalar, just print it "as is"
+ $return = $cmd;
+ }
+
+ return $return;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# &print_command ($cmd);
+# &print_command (\@cmd);
+#
+# DESCRIPTION
+# The function prints the list in @cmd or the scalar $cmd, as it would be
+# executed by the shell.
+# ------------------------------------------------------------------------------
+
+sub print_command {
+ my $cmd = $_[0];
+
+ print '=> ', &get_command_string ($cmd) , "\n";
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# @return = &run_command (\@cmd, <OPTIONS>);
+# @return = &run_command ($cmd , <OPTIONS>);
+#
+# DESCRIPTION
+# This function executes the command in the list @cmd or in the scalar $cmd.
+# The remaining are optional arguments in a hash table. Valid options are
+# listed below. If the command is run using "qx", the function returns the
+# standard output from the command. If the command is run using "system", the
+# function returns true on success. By default, the function dies on failure.
+#
+# OPTIONS
+# METHOD => $method - this can be "system", "exec" or "qx". This determines
+# how the command will be executed. If not set, the
+# default is to run the command with "system".
+# PRINT => 1 - if set, print the command before executing it.
+# ERROR => $flag - this should only be set if METHOD is set to "system"
+# or "qx". The $flag can be "die" (default), "warn" or
+# "ignore". If set to "die", the function dies on error.
+# If set to "warn", the function issues a warning on
+# error, and the function returns false. If set to
+# "ignore", the function returns false on error.
+# RC => 1 - if set, must be a reference to a scalar, which will be
+# set to the return code of the command.
+# DEVNULL => 1 - if set, re-direct STDERR to /dev/null before running
+# the command.
+# TIME => 1 - if set, print the command with a timestamp before
+# executing it, and print the time taken when it
+# completes. This option supersedes the PRINT option.
+# ------------------------------------------------------------------------------
+
+sub run_command {
+ my ($cmd, %input_opt_of) = @_;
+ my %opt_of = (
+ DEVNULL => undef,
+ ERROR => 'die',
+ METHOD => 'system',
+ PRINT => undef,
+ RC => undef,
+ TIME => undef,
+ %input_opt_of,
+ );
+ local($|) = 1; # Make sure STDOUT is flushed before running command
+
+ # Print the command before execution, if necessary
+ if ($opt_of{TIME}) {
+ print(timestamp_command(get_command_string($cmd)));
+ }
+ elsif ($opt_of{PRINT}) {
+ print_command($cmd);
+ }
+
+ # Re-direct STDERR to /dev/null if necessary
+ if ($opt_of{DEVNULL}) {
+ no warnings;
+ open(OLDERR, ">&STDERR") || croak("Cannot dup STDERR ($!), abort");
+ use warnings;
+ open(STDERR, '>', File::Spec->devnull())
+ || croak("Cannot redirect STDERR ($!), abort");
+ # Make sure the channels are unbuffered
+ my $select = select();
+ select(STDERR); local($|) = 1;
+ select($select);
+ }
+
+ my @return = ();
+ if (ref($cmd) && ref($cmd) eq 'ARRAY') {
+ # $cmd is an array
+ my @command = @{$cmd};
+ if ($opt_of{METHOD} eq 'qx') {
+ @return = qx(@command);
+ }
+ elsif ($opt_of{METHOD} eq 'exec') {
+ exec(@command);
+ }
+ else {
+ system(@command);
+ @return = $? ? () : (1);
+ }
+ }
+ else {
+ # $cmd is an scalar
+ if ($opt_of{METHOD} eq 'qx') {
+ @return = qx($cmd);
+ }
+ elsif ($opt_of{METHOD} eq 'exec') {
+ exec($cmd);
+ }
+ else {
+ system($cmd);
+ @return = $? ? () : (1);
+ }
+ }
+ my $rc = $?;
+
+ # Put STDERR back to normal, if redirected previously
+ if ($opt_of{DEVNULL}) {
+ close(STDERR);
+ open(STDERR, ">&OLDERR") || croak("Cannot dup STDERR ($!), abort");
+ }
+
+ # Print the time taken for command after execution, if necessary
+ if ($opt_of{TIME}) {
+ print(timestamp_command(get_command_string($cmd), 'end'));
+ }
+
+ # Signal and return code
+ my ($signal, $status) = (WTERMSIG($rc), WEXITSTATUS($rc));
+ if (exists($opt_of{RC})) {
+ ${$opt_of{RC}} = $status;
+ }
+ if (WIFSIGNALED($rc) && grep {$signal == $_} (SIGINT, SIGKILL, SIGTERM)) {
+ croak(sprintf('%s terminated (%d)', get_command_string($cmd), $signal));
+ }
+ if ($status && $opt_of{ERROR} ne 'ignore') {
+ my $func_ref = $opt_of{ERROR} eq 'warn' ? \&carp : \&croak;
+ $func_ref->(sprintf('%s failed (%d)', get_command_string($cmd), $status));
+ }
+ return @return;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# &e_report (@message);
+#
+# DESCRIPTION
+# The function prints @message to STDERR and aborts with a error.
+# ------------------------------------------------------------------------------
+
+sub e_report {
+ print STDERR @_, "\n" if @_;
+
+ exit 1;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# &w_report (@message);
+#
+# DESCRIPTION
+# The function prints @message to STDERR and returns.
+# ------------------------------------------------------------------------------
+
+sub w_report {
+ print STDERR @_, "\n" if @_;
+
+ return;
+}
+
+# ------------------------------------------------------------------------------
+# SYNOPSIS
+# $date = &svn_date ($time);
+#
+# DESCRIPTION
+# The function returns a date, formatted as by Subversion. The argument $time
+# is the number of seconds since epoch.
+# ------------------------------------------------------------------------------
+
+sub svn_date {
+ my $time = shift;
+
+ return strftime ('%Y-%m-%d %H:%M:%S %z (%a, %d %b %Y)', localtime ($time));
+}
+
+# ------------------------------------------------------------------------------
+
+1;
+
+__END__
diff --git a/lib/FCM1/Util/ClassLoader.pm b/lib/FCM1/Util/ClassLoader.pm
new file mode 100644
index 0000000..55a07eb
--- /dev/null
+++ b/lib/FCM1/Util/ClassLoader.pm
@@ -0,0 +1,93 @@
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+use strict;
+use warnings;
+
+package FCM1::Util::ClassLoader;
+use base qw{Exporter};
+
+our @EXPORT_OK = qw{load};
+
+use Carp qw{croak};
+use FCM1::Exception;
+
+sub load {
+ my ($class, $test_method) = @_;
+ if (!$test_method) {
+ $test_method = 'new';
+ }
+ if (!UNIVERSAL::can($class, $test_method)) {
+ eval('require ' . $class);
+ if ($@) {
+ croak(FCM1::Exception->new({message => sprintf(
+ "%s: class loading failed: %s", $class, $@,
+ )}));
+ }
+ }
+ return $class;
+}
+
+1;
+__END__
+
+=head1 NAME
+
+FCM1::ClassLoader
+
+=head1 SYNOPSIS
+
+ use FCM1::Util::ClassLoader;
+ $load_ok = FCM1::Util::ClassLoader::load($class);
+
+=head1 DESCRIPTION
+
+A wrapper for loading a class dynamically.
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item load($class,$test_method)
+
+If $class can call $test_method, returns $class. Otherwise, attempts to
+require() $class and returns it. If this fails, croak() with a
+L<FCM1::Exception|FCM1::Exception>.
+
+=item load($class)
+
+Shorthand for C<load($class, 'new')>.
+
+=back
+
+=head1 DIAGNOSTICS
+
+=over 4
+
+=item L<FCM1::Exception|FCM1::Exception>
+
+The load($class,$test_method) function croak() with this exception if it fails
+to load the specified class.
+
+=back
+
+=head1 COPYRIGHT
+
+E<169> Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/licences/Apache2 b/licences/Apache2
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/licences/Apache2
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ 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.
diff --git a/licences/GPL3 b/licences/GPL3
new file mode 120000
index 0000000..012065c
--- /dev/null
+++ b/licences/GPL3
@@ -0,0 +1 @@
+../COPYING
\ No newline at end of file
diff --git a/man/man1/fcm.1 b/man/man1/fcm.1
new file mode 100644
index 0000000..820e4c4
--- /dev/null
+++ b/man/man1/fcm.1
@@ -0,0 +1,42 @@
+.\" Process this file with
+.\" groff -man -Tascii fcm.1
+.\"
+.TH fcm 1 "" "" "User Commands"
+.SH NAME
+fcm - FCM, tools for managing and building source code.
+.SH SYNOPSIS
+.B fcm
+.I command
+[
+.I options
+] [
+.I args
+]
+.SH OVERVIEW
+.B fcm
+is a set of tools for managing and building source code.
+For full detail of the system, please refer to the FCM user guide, which you
+should receive with this distribution.
+.PP
+Run "fcm help" to access the built-in tool documentation.
+.SH AUTHOR
+FCM Team <fcm-team at metoffice.gov.uk>.
+Please feedback any bug reports or feature requests to us by e-mail.
+.SH COPYRIGHT
+\(co British Crown Copyright 2006-14 Met Office.
+.PP
+FCM 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, either version 3 of the License, or
+(at your option) any later version.
+.PP
+FCM 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.
+.PP
+You should have received a copy of the GNU General Public License
+along with FCM. If not, see <http://www.gnu.org/licenses/>.
+.SH SEE ALSO
+.BR svn (1),
+.BR perl (1)
diff --git a/sbin/fcm-add-svn-repos b/sbin/fcm-add-svn-repos
new file mode 100755
index 0000000..c4a5c60
--- /dev/null
+++ b/sbin/fcm-add-svn-repos
@@ -0,0 +1,105 @@
+#!/usr/bin/perl
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+
+use strict;
+use warnings;
+
+use FindBin;
+use lib "$FindBin::Bin/../lib";
+use FCM::Admin::System qw{add_svn_repository};
+use FCM::Admin::Util qw{option2config};
+use Getopt::Long qw{GetOptions};
+use Pod::Usage qw{pod2usage};
+
+main();
+
+sub main {
+ my %option;
+ my $result = GetOptions(
+ \%option,
+ q{help|usage|h},
+ q{svn-live-dir=s},
+ q{svn-project-suffix=s},
+ );
+ if (!$result) {
+ pod2usage(1);
+ }
+ if (exists($option{help})) {
+ pod2usage(q{-verbose} => 1);
+ }
+ option2config(\%option);
+ if (@ARGV != 1) {
+ my $message = sprintf(
+ qq{Expected exactly 1 argument, %d given.}, scalar(@ARGV),
+ );
+ pod2usage({q{-exitval} => 1, q{-message} => $message});
+ }
+ my ($project_name) = @ARGV;
+ add_svn_repository($project_name);
+}
+
+__END__
+=head1 NAME
+
+fcm-add-trac-env
+
+=head1 SYNOPSIS
+
+ fcm-add-svn-repos [OPTIONS] PROJECT
+
+=head1 OPTIONS
+
+=over 4
+
+=item --help, -h, --usage
+
+Prints help and exits.
+
+=item --svn-live-dir=DIR
+
+Specifies the root location of the live directory of Subversion repositories.
+See L<FCM::Admin::Config|FCM::Admin::Config> for the current default.
+
+=item --svn-project-suffix=NAME
+
+Specifies the suffix added to the project name for Subversion repositories. The
+default is "_svn".
+
+=back
+
+=head1 ARGUMENTS
+
+=over 4
+
+=item PROJECT
+
+Specifies the name of the project to add.
+
+=back
+
+=head1 DESCRIPTION
+
+This program adds a new Subversion repository.
+
+=head1 COPYRIGHT
+
+E<169> Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/sbin/fcm-add-svn-repos-and-trac-env b/sbin/fcm-add-svn-repos-and-trac-env
new file mode 100755
index 0000000..8d354c2
--- /dev/null
+++ b/sbin/fcm-add-svn-repos-and-trac-env
@@ -0,0 +1,119 @@
+#!/usr/bin/perl
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+
+use strict;
+use warnings;
+
+use FindBin;
+use lib "$FindBin::Bin/../lib";
+use FCM::Admin::System qw{add_svn_repository add_trac_environment};
+use FCM::Admin::Util qw{option2config};
+use Getopt::Long qw{GetOptions};
+use Pod::Usage qw{pod2usage};
+
+main();
+
+sub main {
+ my %option;
+ my $result = GetOptions(
+ \%option,
+ q{help|usage|h},
+ q{svn-live-dir=s},
+ q{svn-project-suffix=s},
+ q{trac-host-id=s},
+ q{trac-live-dir=s},
+ );
+ if (!$result) {
+ pod2usage(1);
+ }
+ if (exists($option{help})) {
+ pod2usage(q{-verbose} => 1);
+ }
+ option2config(\%option);
+ if (@ARGV != 1) {
+ my $message = sprintf(
+ qq{Expected exactly 1 argument, %d given.}, scalar(@ARGV),
+ );
+ pod2usage({q{-exitval} => 1, q{-message} => $message});
+ }
+ my ($project_name) = @ARGV;
+ add_svn_repository($project_name);
+ add_trac_environment($project_name);
+}
+
+__END__
+=head1 NAME
+
+fcm-add-trac-env
+
+=head1 SYNOPSIS
+
+ fcm-add-svn-repos-and-trac-env [OPTIONS] PROJECT
+
+=head1 OPTIONS
+
+=over 4
+
+=item --help, -h, --usage
+
+Prints help and exits.
+
+=item --svn-live-dir=DIR
+
+Specifies the root location of the live directory of Subversion repositories.
+See L<FCM::Admin::Config|FCM::Admin::Config> for the current default.
+
+=item --svn-project-suffix=NAME
+
+Specifies the suffix added to the project name for Subversion repositories. The
+default is "_svn".
+
+=item --trac-host-id=HOST
+
+Specifies the host ID of the Trac server for the new Trac environment. See
+L<FCM::Admin::Config|FCM::Admin::Config> for the current default.
+
+=item --trac-live-dir=DIR
+
+Specifies the root location of the live directory of Trac environments. See
+L<FCM::Admin::Config|FCM::Admin::Config> for the current default.
+
+=back
+
+=head1 ARGUMENTS
+
+=over 4
+
+=item PROJECT
+
+Specifies the name of the project to add.
+
+=back
+
+=head1 DESCRIPTION
+
+This program adds a new Subversion repository with an associated Trac
+environment to their live directories.
+
+=head1 COPYRIGHT
+
+E<169> Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/sbin/fcm-add-trac-env b/sbin/fcm-add-trac-env
new file mode 100755
index 0000000..4b55b08
--- /dev/null
+++ b/sbin/fcm-add-trac-env
@@ -0,0 +1,117 @@
+#!/usr/bin/perl
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+
+use strict;
+use warnings;
+
+use FindBin;
+use lib "$FindBin::Bin/../lib";
+use FCM::Admin::System qw{add_trac_environment};
+use FCM::Admin::Util qw{option2config};
+use Getopt::Long qw{GetOptions};
+use Pod::Usage qw{pod2usage};
+
+main();
+
+sub main {
+ my %option;
+ my $result = GetOptions(
+ \%option,
+ q{help|usage|h},
+ q{svn-live-dir=s},
+ q{svn-project-suffix=s},
+ q{trac-host-id=s},
+ q{trac-live-dir=s},
+ );
+ if (!$result) {
+ pod2usage(1);
+ }
+ if (exists($option{help})) {
+ pod2usage(q{-verbose} => 1);
+ }
+ option2config(\%option);
+ if (@ARGV != 1) {
+ my $message = sprintf(
+ qq{Expected exactly 1 argument, %d given.}, scalar(@ARGV),
+ );
+ pod2usage({q{-exitval} => 1, q{-message} => $message});
+ }
+ my ($project_name) = @ARGV;
+ add_trac_environment($project_name);
+}
+
+__END__
+=head1 NAME
+
+fcm-add-trac-env
+
+=head1 SYNOPSIS
+
+ fcm-add-trac-env [OPTIONS] PROJECT
+
+=head1 OPTIONS
+
+=over 4
+
+=item --help, -h, --usage
+
+Prints help and exits.
+
+=item --svn-live-dir=DIR
+
+Specifies the root location of the live directory of Subversion repositories.
+See L<FCM::Admin::Config|FCM::Admin::Config> for the current default.
+
+=item --svn-project-suffix=NAME
+
+Specifies the suffix added to the project name for Subversion repositories. The
+default is "_svn".
+
+=item --trac-host-id=HOST
+
+Specifies the host ID of the Trac server for the new Trac environment. See
+L<FCM::Admin::Config|FCM::Admin::Config> for the current default.
+
+=item --trac-live-dir=DIR
+
+Specifies the root location of the live directory of Trac environments. See
+L<FCM::Admin::Config|FCM::Admin::Config> for the current default.
+
+=back
+
+=head1 ARGUMENTS
+
+=over 4
+
+=item PROJECT
+
+Specifies the name of the project to add.
+
+=back
+
+=head1 DESCRIPTION
+
+This program adds a new Trac environment to the live directory.
+
+=head1 COPYRIGHT
+
+E<169> Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/sbin/fcm-backup-svn-repos b/sbin/fcm-backup-svn-repos
new file mode 100755
index 0000000..5568c97
--- /dev/null
+++ b/sbin/fcm-backup-svn-repos
@@ -0,0 +1,137 @@
+#!/usr/bin/perl
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+
+use strict;
+use warnings;
+
+use FindBin;
+use lib "$FindBin::Bin/../lib";
+use FCM::Admin::System qw{
+ backup_svn_repository
+ filter_projects
+ get_projects_from_svn_live
+};
+use FCM::Admin::Util qw{option2config};
+use Getopt::Long qw{GetOptions};
+use Pod::Usage qw{pod2usage};
+
+main();
+
+sub main {
+ my %option;
+ my $result = GetOptions(
+ \%option,
+ q{help|usage|h},
+ q{no-housekeep-dumps},
+ q{no-pack},
+ q{no-verify-integrity},
+ q{svn-backup-dir=s},
+ q{svn-dump-dir=s},
+ q{svn-live-dir=s},
+ q{svn-project-suffix=s},
+ );
+ if (!$result) {
+ pod2usage(1);
+ }
+ if (exists($option{help})) {
+ pod2usage(q{-verbose} => 1);
+ }
+ option2config(\%option);
+ my @projects = filter_projects([get_projects_from_svn_live()], \@ARGV);
+ for my $project (@projects) {
+ backup_svn_repository(\%option, $project);
+ }
+}
+
+__END__
+
+=head1 NAME
+
+fcm-backup-svn-repos
+
+=head1 SYNOPSIS
+
+ fcm-backup-svn-repos [OPTIONS] [PROJECT ...]
+
+=head1 OPTIONS
+
+=over 4
+
+=item --help, -h, --usage
+
+Prints help and exits.
+
+=item --no-housekeep-dumps
+
+If this option is specified, the program will not housekeep the backup revision
+dumps of each repository.
+
+=item --no-pack
+
+If this option is specified, the program will not pack each repository before
+running the backup.
+
+=item --no-verify-integrity
+
+If this option is specified, the program will not verify the integrity of a
+repository before running the backup.
+
+=item --svn-backup-dir=DIR
+
+Specifies the root location of the backup directory. See
+L<FCM::Admin::Config|FCM::Admin::Config> for the current default.
+
+=item --svn-dump-dir=DIR
+
+Specifies the root location of the directory where revision dumps are kept. See
+L<FCM::Admin::Config|FCM::Admin::Config> for the current default.
+
+=item --svn-live-dir=DIR
+
+Specifies the root location of the live directory. See
+L<FCM::Admin::Config|FCM::Admin::Config> for the current default.
+
+=item --svn-project-suffix=NAME
+
+Specifies the suffix added to the project name. The default is "_svn".
+
+=back
+
+=head1 ARGUMENTS
+
+=over 4
+
+=item PROJECT
+
+Specifies one or more project to back up. If no project is specified, the
+program searches the live directory for projects to back up.
+
+=back
+
+=head1 DESCRIPTION
+
+This program archives Subversion repositories in the live directory to the
+backup directory.
+
+=head1 COPYRIGHT
+
+E<169> Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/sbin/fcm-backup-trac-env b/sbin/fcm-backup-trac-env
new file mode 100755
index 0000000..ae1f906
--- /dev/null
+++ b/sbin/fcm-backup-trac-env
@@ -0,0 +1,118 @@
+#!/usr/bin/perl
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+
+use strict;
+use warnings;
+
+use FindBin;
+use lib "$FindBin::Bin/../lib";
+use FCM::Admin::System qw{
+ backup_trac_environment
+ backup_trac_files
+ filter_projects
+ get_projects_from_trac_live
+};
+use FCM::Admin::Util qw{option2config};
+use Getopt::Long qw{GetOptions};
+use Pod::Usage qw{pod2usage};
+
+main();
+
+sub main {
+ my %option;
+ my $result = GetOptions(
+ \%option,
+ q{help|usage|h},
+ q{no-verify-integrity},
+ q{trac-backup-dir=s},
+ q{trac-live-dir=s},
+ );
+ if (!$result) {
+ pod2usage(1);
+ }
+ if (exists($option{help})) {
+ pod2usage(q{-verbose} => 1);
+ }
+ option2config(\%option);
+ my @projects = filter_projects([get_projects_from_trac_live()], \@ARGV);
+ for my $project (@projects) {
+ backup_trac_environment(\%option, $project);
+ }
+ if (!@ARGV) {
+ backup_trac_files();
+ }
+}
+
+__END__
+
+=head1 NAME
+
+fcm-backup-trac-env
+
+=head1 SYNOPSIS
+
+ fcm-backup-trac-env [OPTIONS] [PROJECT ...]
+
+=head1 OPTIONS
+
+=over 4
+
+=item --help, -h, --usage
+
+Prints help and exits.
+
+=item --no-verify-integrity
+
+If this option is specified, the program will not verify the integrity of the
+database before running the backup.
+
+=item --trac-backup-dir=DIR
+
+Specifies the root location of the backup directory. See
+L<FCM::Admin::Config|FCM::Admin::Config> for the current default.
+
+=item --trac-live-dir=DIR
+
+Specifies the root location of the live directory. See
+L<FCM::Admin::Config|FCM::Admin::Config> for the current default.
+
+=back
+
+=head1 ARGUMENTS
+
+=over 4
+
+=item PROJECT
+
+Specifies one or more project to back up. If no project is specified, the
+program searches the live directory for projects and other files to back up.
+
+=back
+
+=head1 DESCRIPTION
+
+This program archives Trac environments, and other files in the live directory
+to the backup directory.
+
+=head1 COPYRIGHT
+
+E<169> Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/sbin/fcm-commit-update b/sbin/fcm-commit-update
new file mode 100755
index 0000000..2cd788c
--- /dev/null
+++ b/sbin/fcm-commit-update
@@ -0,0 +1,170 @@
+#!/usr/bin/perl
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+
+use strict;
+use warnings;
+
+use FindBin;
+use lib "$FindBin::Bin/../lib";
+use File::Basename qw{basename};
+use File::Spec;
+
+use FCM::Admin::Config;
+use FCM::Admin::Runner;
+use FCM::Admin::System qw{
+ distribute_wc
+ filter_projects
+ get_projects_from_svn_live
+ install_svn_hook
+};
+use FCM::Admin::Util qw{
+ run_mkpath
+ run_rmtree
+ run_svn_info
+ run_svn_update
+ write_file
+};
+use Getopt::Long qw{GetOptions};
+use Pod::Usage qw{pod2usage};
+use Text::ParseWords qw{shellwords};
+
+# ------------------------------------------------------------------------------
+my $CONFIG = FCM::Admin::Config->instance();
+my %PATTERN_OF = (
+ q{} => qr{.*}xms,
+ SRC_HOOK => qr{svn-hooks/}xms,
+);
+
+if (!caller()) {
+ main(@ARGV);
+}
+
+# ------------------------------------------------------------------------------
+# The main logic.
+sub main {
+ local(@ARGV) = @_;
+ my %option;
+ my $result = GetOptions(
+ \%option,
+ q{help|usage|h},
+ q{force},
+ );
+ if (!$result) {
+ pod2usage(1);
+ }
+ if (exists($option{help})) {
+ pod2usage(q{-verbose} => 1);
+ }
+ create_lock() || return;
+ my $RUNNER = FCM::Admin::Runner->instance();
+ my $is_force = $option{'force'};
+ UPDATE:
+ while (1) {
+ my @updates;
+ for my $source_key (shellwords($CONFIG->get_mirror_keys())) {
+ my $method = "get_$source_key";
+ push(@updates, run_svn_update($CONFIG->$method()));
+ }
+ if (!$is_force && !@updates) {
+ last UPDATE;
+ }
+ if ($is_force || grep {$_ =~ $PATTERN_OF{'SRC_HOOK'}} @updates) {
+ $RUNNER->run(
+ '(re-)installing hook scripts',
+ sub {
+ for my $project (get_projects_from_svn_live()) {
+ install_svn_hook($project);
+ }
+ return 1;
+ }
+ );
+ }
+ if ($is_force || grep {$_ =~ $PATTERN_OF{q{}}} @updates) {
+ $RUNNER->run(
+ 'distributing FCM to standard locations', \&distribute_wc);
+ }
+ $is_force = 0;
+ }
+}
+
+# ------------------------------------------------------------------------------
+# Creates a lock. Returns true on success. Removes lock when program finishes.
+our $LOCK;
+sub create_lock {
+ my $home = (getpwuid($<))[7];
+ $LOCK = File::Spec->catfile($home, sprintf(".%s.lock", basename($0)));
+ if (-e $LOCK) {
+ $LOCK = undef;
+ return;
+ }
+ return run_mkpath($LOCK);
+ END {
+ if ($LOCK) {
+ run_rmtree($LOCK);
+ }
+ }
+}
+
+__END__
+
+=head1 NAME
+
+fcm-commit-update
+
+=head1 SYNOPSIS
+
+ fcm-commit-update
+
+=head1 DESCRIPTION
+
+This program performs the post-commit update for the FCM system. It runs
+continuously until no more update is available. It prevent another copy from
+running by creating a lock. If another copy detects a lock, it exits without
+doing anything.
+
+=head1 OPTIONS
+
+=over 4
+
+=item --force
+
+Force an update.
+
+=back
+
+=head1 ARGUMENTS
+
+=over 4
+
+=item REPOS-NAME
+
+The name of the repository invoking this program.
+
+=item LOG-DIR-PATH
+
+The path to the log directory.
+
+=back
+
+=head1 COPYRIGHT
+
+E<169> Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/sbin/fcm-daily-update b/sbin/fcm-daily-update
new file mode 100755
index 0000000..4b6552e
--- /dev/null
+++ b/sbin/fcm-daily-update
@@ -0,0 +1,190 @@
+#!/usr/bin/perl
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+
+use strict;
+use warnings;
+
+use FindBin;
+use lib "$FindBin::Bin/../lib";
+use File::Basename qw{basename};
+use File::Spec::Functions qw{catfile};
+use IO::File;
+use Mail::Mailer;
+use Time::Piece qw{gmtime};
+
+use FCM::Admin::Config;
+use FCM::Admin::Runner;
+use FCM::Admin::System qw{
+ backup_svn_repository
+ backup_trac_environment
+ backup_trac_files
+ get_projects_from_svn_live
+ get_projects_from_trac_live
+ get_users
+ housekeep_svn_hook_logs
+ manage_users_in_svn_passwd
+ manage_users_in_trac_passwd
+ manage_users_in_trac_db_of
+};
+
+my $THIS = basename($0);
+my $CONFIG = FCM::Admin::Config->instance();
+my $UTIL = $FCM::Admin::Config::UTIL;
+
+if (!caller()) {
+ main(@ARGV);
+}
+
+sub main {
+ local(@ARGV) = @_;
+
+ # Redirects STDOUT and STDERR to the $out_file
+ open(my $old_out, q{>&}, \*STDOUT)
+ || die("$THIS: cannot duplicate STDOUT ($!)\n");
+ open(my $old_err, q{>&}, \*STDERR)
+ || die("$THIS: cannot duplicate STDERR ($!)\n");
+ my $log_dir = $UTIL->file_tilde_expand($CONFIG->get_log_dir());
+ my $now = gmtime();
+ my $day_of_week = lc($now->day_of_week()); # 0=sun, 1=mon, etc
+ my $day = lc($now->day()); # lower case day of week, e.g. sun, mon
+ my $out_file = catfile($log_dir, "$THIS-$day_of_week$day.log");
+ open(STDOUT, q{>}, $out_file)
+ || die("$THIS: cannot redirect STDOUT ($!)\n");
+ open(STDERR, q{>&}, \*STDOUT)
+ || die("$THIS: cannot redirect STDERR ($!)\n");
+
+ do_tasks();
+
+ # Restores STDOUT and STDERR
+ open(STDERR, q{>&}, $old_err)
+ || die("$THIS: cannot reinstate STDERR ($!)\n");
+ open(STDOUT, q{>&}, $old_out)
+ || die("$THIS: cannot reinstate STDOUT ($!)\n");
+
+ notify($out_file);
+}
+
+# ------------------------------------------------------------------------------
+# Performs the daily update tasks.
+sub do_tasks {
+ # (no argument)
+ my $RUNNER = FCM::Admin::Runner->instance();
+ my @svn_projects = get_projects_from_svn_live();
+ my @trac_projects = get_projects_from_trac_live();
+ my $user_ref = undef;
+ $RUNNER->run_continue(
+ "retrieving user accounts",
+ sub {$user_ref = get_users(); 1;},
+ );
+ if (defined($user_ref)) {
+ if ($CONFIG->get_svn_passwd_file()) {
+ $RUNNER->run_continue(
+ "updating SVN user accounts",
+ sub {manage_users_in_svn_passwd($user_ref)},
+ );
+ }
+ if ($CONFIG->get_trac_passwd_file()) {
+ $RUNNER->run_continue(
+ "updating Trac user accounts",
+ sub {manage_users_in_trac_passwd($user_ref)},
+ );
+ }
+ for my $project (@trac_projects) {
+ $RUNNER->run_continue(
+ "updating Trac accounts in $project",
+ sub {manage_users_in_trac_db_of($project, $user_ref)},
+ );
+ }
+ }
+ for my $project (@svn_projects) {
+ $RUNNER->run_continue(
+ "housekeep SVN repository logs for $project",
+ sub {housekeep_svn_hook_logs($project)},
+ );
+ $RUNNER->run_continue(
+ "backing up SVN repository for $project",
+ sub {backup_svn_repository({}, $project)},
+ );
+ }
+ for my $project (@trac_projects) {
+ $RUNNER->run_continue(
+ "backing up Trac environment for $project",
+ sub {backup_trac_environment({}, $project)},
+ );
+ }
+ $RUNNER->run_continue("backing up Trac files", \&backup_trac_files);
+}
+
+# ------------------------------------------------------------------------------
+# Notifies on completion.
+sub notify {
+ my ($out_file) = @_;
+ # Reports number of arguments in subject.
+ my @exceptions = FCM::Admin::Runner->instance()->get_exceptions();
+ my $subject
+ = sprintf(qq{$THIS finished with %d error(s)}, scalar(@exceptions));
+
+ my $mailer = Mail::Mailer->new();
+ $mailer->open({
+ From => $CONFIG->get_notification_from(),
+ To => $CONFIG->get_admin_email(),
+ Subject => $subject,
+ });
+
+ # Summarises the exceptions at the beginning of the message
+ $mailer->print(qq{$subject:\n});
+ for my $exception (@exceptions) {
+ $mailer->print(qq{ $exception});
+ }
+ $mailer->print(qq{\n});
+
+ # Prints content of the output
+ $mailer->print(q{#} x 72 . qq{\n});
+ $mailer->print(qq{# Output and error output of $THIS:\n});
+ $mailer->print(q{#} x 72 . qq{\n});
+ my $out_file_handle = IO::File->new($out_file);
+ if (defined($out_file_handle)) {
+ while (my $line = $out_file_handle->getline()) {
+ $mailer->print($line);
+ }
+ $out_file_handle->close();
+ }
+ $mailer->close();
+}
+
+__END__
+
+=head1 NAME
+
+fcm-daily-update
+
+=head1 SYNOPSIS
+
+ fcm-daily-update
+
+=head1 DESCRIPTION
+
+This program performs the daily update for the FCM system.
+
+=head1 COPYRIGHT
+
+E<169> Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/sbin/fcm-install-svn-hook b/sbin/fcm-install-svn-hook
new file mode 100755
index 0000000..bc4b07b
--- /dev/null
+++ b/sbin/fcm-install-svn-hook
@@ -0,0 +1,115 @@
+#!/usr/bin/perl
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+
+use strict;
+use warnings;
+
+use FindBin;
+use lib "$FindBin::Bin/../lib";
+use FCM::Admin::System qw{
+ filter_projects
+ get_projects_from_svn_live
+ housekeep_svn_hook_logs
+ install_svn_hook
+};
+use FCM::Admin::Util qw{option2config};
+use Getopt::Long qw{GetOptions};
+use Pod::Usage qw{pod2usage};
+
+main();
+
+sub main {
+ my %option;
+ my $result = GetOptions(
+ \%option,
+ q{clean},
+ q{help|usage|h},
+ q{svn-live-dir=s},
+ q{svn-project-suffix=s},
+ );
+ if (!$result) {
+ pod2usage(1);
+ }
+ if (exists($option{help})) {
+ pod2usage(q{-verbose} => 1);
+ }
+ option2config(\%option);
+ my @projects = filter_projects([get_projects_from_svn_live()], \@ARGV);
+ for my $project (sort {$a->get_name() cmp $b->get_name()} @projects) {
+ install_svn_hook($project, $option{clean});
+ housekeep_svn_hook_logs($project);
+ }
+}
+
+__END__
+
+=head1 NAME
+
+fcm-install-svn-hook
+
+=head1 SYNOPSIS
+
+ fcm-install-svn-hook [OPTIONS] [PROJECT ...]
+
+=head1 OPTIONS
+
+=over 4
+
+=item --clean
+
+Removes items (except logs) that are not in the install sources.
+
+=item --help, -h, --usage
+
+Prints help and exits.
+
+=item --svn-live-dir=DIR
+
+Specifies the root location of the live directory. See
+L<FCM::Admin::Config|FCM::Admin::Config> for the current default.
+
+=item --svn-project-suffix=NAME
+
+Specifies the suffix added to the project name. The default is "_svn".
+
+=back
+
+=head1 ARGUMENTS
+
+=over 4
+
+=item PROJECT
+
+Specifies one or more project requiring hooks scripts to be installed. If no
+project is specified, the program install the hook scripts to all projects in
+the live directory.
+
+=back
+
+=head1 DESCRIPTION
+
+This program install hook scripts for Subversion repositories in the live
+directory, and install/housekeep the log files for the hook scripts.
+
+=head1 COPYRIGHT
+
+E<169> Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/sbin/fcm-manage-trac-env-session b/sbin/fcm-manage-trac-env-session
new file mode 100755
index 0000000..50207cd
--- /dev/null
+++ b/sbin/fcm-manage-trac-env-session
@@ -0,0 +1,96 @@
+#!/usr/bin/perl
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+
+use strict;
+use warnings;
+
+use FindBin;
+use lib "$FindBin::Bin/../lib";
+use FCM::Admin::System qw{
+ filter_projects
+ get_projects_from_trac_live
+ get_users
+ manage_users_in_trac_db_of
+};
+use FCM::Admin::Util qw{option2config};
+use Getopt::Long qw{GetOptions};
+use Pod::Usage qw{pod2usage};
+
+if (!caller()) {
+ main(@ARGV);
+}
+
+sub main {
+ local(@ARGV) = @_;
+ my %option;
+ my $result = GetOptions(
+ \%option,
+ q{help|usage|h},
+ q{trac-live-dir=s},
+ );
+ if (!$result) {
+ pod2usage(1);
+ }
+ if (exists($option{help})) {
+ pod2usage(q{-verbose} => 1);
+ }
+ option2config(\%option);
+ my $user_ref = get_users();
+ for my $project (filter_projects([get_projects_from_trac_live()], \@ARGV)) {
+ manage_users_in_trac_db_of($project, $user_ref),
+ }
+}
+
+__END__
+
+=head1 NAME
+
+fcm-manage-trac-env-session
+
+=head1 SYNOPSIS
+
+ fcm-manage-trac-env-session [OPTIONS] [PROJECT ...]
+
+=head1 OPTIONS
+
+=over 4
+
+=item --help, -h, --usage
+
+Prints help and exits.
+
+=item --trac-live-dir=DIR
+
+Specifies the root location of the live directory of the Trac environments. See
+L<FCM::Admin::Config|FCM::Admin::Config> for the current default.
+
+=back
+
+=head1 DESCRIPTION
+
+This program manages session and session attributes for authenticated users in
+Trac environments. If no PROJECT is specified, the program acts on all trac
+environments in the live directory.
+
+=head1 COPYRIGHT
+
+E<169> Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/sbin/fcm-manage-users b/sbin/fcm-manage-users
new file mode 100755
index 0000000..d7be833
--- /dev/null
+++ b/sbin/fcm-manage-users
@@ -0,0 +1,119 @@
+#!/usr/bin/perl
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+
+use strict;
+use warnings;
+
+use FindBin;
+use lib "$FindBin::Bin/../lib";
+use FCM::Admin::System qw{
+ get_projects_from_trac_live
+ get_users
+ manage_users_in_svn_passwd
+ manage_users_in_trac_passwd
+ manage_users_in_trac_db_of
+};
+use FCM::Admin::Util qw{option2config};
+use Getopt::Long qw{GetOptions};
+use Pod::Usage qw{pod2usage};
+
+main();
+
+sub main {
+ my %option;
+ my $result = GetOptions(
+ \%option,
+ q{help|usage|h},
+ q{svn-live-dir=s},
+ q{svn-passwd-file=s},
+ q{trac-live-dir=s},
+ q{trac-passwd-file=s},
+ );
+ if (!$result) {
+ pod2usage(1);
+ }
+ if (exists($option{help})) {
+ pod2usage(q{-verbose} => 1);
+ }
+ if (@ARGV) {
+ my $message = sprintf("No argument expected, %d given", scalar(@ARGV));
+ pod2usage({q{-exitval} => 1, q{-message} => $message});
+ }
+ option2config(\%option);
+ my $user_ref = get_users();
+ manage_users_in_svn_passwd($user_ref);
+ manage_users_in_trac_passwd($user_ref);
+ my @projects = get_projects_from_trac_live();
+ for my $project (@projects) {
+ manage_users_in_trac_db_of($project, $user_ref),
+ }
+}
+
+__END__
+
+=head1 NAME
+
+fcm-manage-users
+
+=head1 SYNOPSIS
+
+ fcm-manage-users [OPTIONS]
+
+=head1 OPTIONS
+
+=over 4
+
+=item --help, -h, --usage
+
+Prints help and exits.
+
+=item --svn-live-dir=DIR
+
+Specifies the root location of the live directory of the Subversion
+repositories. See L<FCM::Admin::Config|FCM::Admin::Config> for the current
+default.
+
+=item --svn-passwd-file=FILE
+
+Specifies the base name of the Subversion password file. See
+L<FCM::Admin::Config|FCM::Admin::Config> for the current default.
+
+=item --trac-live-dir=DIR
+
+Specifies the root location of the live directory of the Trac environments. See
+L<FCM::Admin::Config|FCM::Admin::Config> for the current default.
+
+=item --trac-passwd-file=FILE
+
+Specifies the base name of the Trac password file. See
+L<FCM::Admin::Config|FCM::Admin::Config> for the current default.
+
+=back
+
+=head1 DESCRIPTION
+
+This program manages user (login) information for Subversion repositories and
+Trac environments hosted by FCM.
+
+=head1 COPYRIGHT
+
+E<169> Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/sbin/fcm-recover-svn-repos b/sbin/fcm-recover-svn-repos
new file mode 100755
index 0000000..fa13910
--- /dev/null
+++ b/sbin/fcm-recover-svn-repos
@@ -0,0 +1,136 @@
+#!/usr/bin/perl
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+
+use strict;
+use warnings;
+
+use FindBin;
+use lib "$FindBin::Bin/../lib";
+use FCM::Admin::System qw{
+ filter_projects
+ get_projects_from_svn_backup
+ recover_svn_repository
+};
+use FCM::Admin::Util qw{option2config};
+use Getopt::Long qw{GetOptions};
+use Pod::Usage qw{pod2usage};
+
+main();
+
+sub main {
+ my %option;
+ my $result = GetOptions(
+ \%option,
+ q{help|usage|h},
+ q{no-recover-dumps},
+ q{no-recover-hooks},
+ q{svn-backup-dir=s},
+ q{svn-dump-dir=s},
+ q{svn-live-dir=s},
+ q{svn-project-suffix=s},
+ );
+ if (!$result) {
+ pod2usage(1);
+ }
+ if (exists($option{help})) {
+ pod2usage(q{-verbose} => 1);
+ }
+ option2config(\%option);
+ my @projects = filter_projects([get_projects_from_svn_backup()], \@ARGV);
+ for my $project (sort {$a->get_name() cmp $b->get_name()} @projects) {
+ recover_svn_repository(
+ $project,
+ !$option{q{no-recover-dumps}},
+ !$option{q{no-recover-hooks}},
+ );
+ }
+}
+
+__END__
+
+=head1 NAME
+
+fcm-recover-svn-repos
+
+=head1 SYNOPSIS
+
+ fcm-recover-svn-repos [OPTIONS] [PROJECT ...]
+
+=head1 OPTIONS
+
+=over 4
+
+=item --help, -h, --usage
+
+Prints help and exits.
+
+=item --no-recover-dumps
+
+If this option is specified, the program will not attempt to load the revision
+dumps of each repository after it has been restored from the backup.
+
+=item --no-recover-hooks
+
+If this option is specified, the program will not attempt to reinstate the hook
+scripts for each repository after it has been restored from the backup.
+
+=item --svn-backup-dir=DIR
+
+Specifies the root location of the backup directory. See
+L<FCM::Admin::Config|FCM::Admin::Config> for the current default.
+
+=item --svn-dump-dir=DIR
+
+Specifies the root location of the directory where revision dumps are kept. See
+L<FCM::Admin::Config|FCM::Admin::Config> for the current default.
+
+=item --svn-live-dir=DIR
+
+Specifies the root location of the live directory. See
+L<FCM::Admin::Config|FCM::Admin::Config> for the current default.
+
+=item --svn-project-suffix=NAME
+
+Specifies the suffix added to the project name. See
+L<FCM::Admin::Config|FCM::Admin::Config> for the current default.
+
+=back
+
+=head1 ARGUMENTS
+
+=over 4
+
+=item PROJECT
+
+Specifies one or more project to recover. If no project is specified, the
+program searches the backup directory for projects to recover.
+
+=back
+
+=head1 DESCRIPTION
+
+This program archives Subversion repositories in the live directory to the
+backup directory.
+
+=head1 COPYRIGHT
+
+E<169> Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/sbin/fcm-recover-trac-env b/sbin/fcm-recover-trac-env
new file mode 100755
index 0000000..4d4a7fb
--- /dev/null
+++ b/sbin/fcm-recover-trac-env
@@ -0,0 +1,112 @@
+#!/usr/bin/perl
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+
+use strict;
+use warnings;
+
+use FindBin;
+use lib "$FindBin::Bin/../lib";
+use FCM::Admin::System qw{
+ filter_projects
+ get_projects_from_trac_backup
+ recover_trac_environment
+ recover_trac_files
+};
+use FCM::Admin::Util qw{option2config};
+use Getopt::Long qw{GetOptions};
+use Pod::Usage qw{pod2usage};
+
+main();
+
+sub main {
+ my %option;
+ my $result = GetOptions(
+ \%option,
+ q{help|usage|h},
+ q{trac-backup-dir=s},
+ q{trac-live-dir=s},
+ );
+ if (!$result) {
+ pod2usage(1);
+ }
+ if (exists($option{help})) {
+ pod2usage(q{-verbose} => 1);
+ }
+ option2config(\%option);
+ my @projects = filter_projects([get_projects_from_trac_backup()], \@ARGV);
+ for my $project (@projects) {
+ recover_trac_environment($project);
+ }
+ if (!@ARGV) {
+ recover_trac_files();
+ }
+}
+
+__END__
+
+=head1 NAME
+
+fcm-recover-trac-env
+
+=head1 SYNOPSIS
+
+ fcm-recover-trac-env [OPTIONS] [PROJECT ...]
+
+=head1 OPTIONS
+
+=over 4
+
+=item --help, -h, --usage
+
+Prints help and exits.
+
+=item --trac-backup-dir=DIR
+
+Specifies the root location of the backup directory. See
+L<FCM::Admin::Config|FCM::Admin::Config> for the current default.
+
+=item --trac-live-dir=DIR
+
+Specifies the root location of the live directory. See
+L<FCM::Admin::Config|FCM::Admin::Config> for the current default.
+
+=back
+
+=head1 ARGUMENTS
+
+=over 4
+
+=item PROJECT
+
+Specifies one or more project to recover. If no project is specified, the
+program searches the backup directory for projects and files to recover.
+
+=back
+
+=head1 DESCRIPTION
+
+This program recovers Trac environments and files from the backup directory to
+the live directory.
+
+=head1 COPYRIGHT
+
+E<169> Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/sbin/fcm-rpmbuild b/sbin/fcm-rpmbuild
new file mode 100755
index 0000000..25c2a27
--- /dev/null
+++ b/sbin/fcm-rpmbuild
@@ -0,0 +1,112 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+# NAME
+# fcm-rpmbuild
+#
+# SYNOPSIS
+# fcm-rpmbuild [--gui] [REV]
+# E.g.:
+# fcm rpmbuild 2014-03
+#
+# DESCRIPTION
+# Build an RPM for distributing FCM.
+# Assume that the current working directory is a local Git clone containing
+# the FCM project.
+#
+# OPTIONS
+# --gui - add GUI packages such as "perl-Tk" and "xxdiff".
+#
+# ARGUMENTS
+# REV - Revision to build. Default to HEAD.
+#-------------------------------------------------------------------------------
+
+set -eu
+THIS=$(basename $0)
+NAME='fcm'
+
+rpmdev-setuptree
+
+# Build RPM without no dependency on subversion or xxdiff?
+REQUIRES_GUI=
+if (($# > 0)) && [[ $1 == '--gui' ]]; then
+ REQUIRES_GUI='perl-Tk xxdiff'
+ shift 1
+fi
+
+# Create the source tree
+REV=${1:-HEAD}
+REV_NAME=$(git describe $REV)
+REV_BASE=$(git describe --abbrev=0 $REV)
+RELEASE=1
+if [[ $REV_NAME != $REV_BASE ]]; then
+ COMMIT_DATE=$(date -u +%Y%m%dT%H%M "--date=$(git show -s --format=%ci $REV)")
+ RELEASE=${COMMIT_DATE}git${REV_NAME#$REV_BASE-*-g}
+fi
+REV_BASE_DOT=$(sed 's/-/./g' <<<$REV_BASE)
+git archive --format=tar --prefix=$NAME-$REV_BASE_DOT/ $REV \
+ | (cd ~/rpmbuild/SOURCES/ && tar -xf -)
+SOURCE=~/rpmbuild/SOURCES/$NAME-$REV_BASE_DOT
+echo "FCM.VERSION=\"$REV_NAME\";" >$SOURCE/doc/etc/$NAME-version.js
+rm -r $SOURCE/{test,t}
+if [[ -z $REQUIRES_GUI ]]; then
+ rm $SOURCE/{bin/fcm_gui,lib/FCM1/Interactive/InputGetter/GUI.pm}
+fi
+
+# Create the rpmbuild spec file
+{
+ cat <<__SPEC__
+Name: $NAME
+Version: $REV_BASE_DOT
+Release: $RELEASE
+Summary: A modern Fortran build system + wrappers to SVN
+Group: Development/Tools
+License: GPLv3
+URL: https://github.com/metomi/$NAME/
+Source0: https://github.com/metomi/$NAME/releases/
+BuildArch: noarch
+Requires: diffutils filesystem gzip make perl-core perl-Config-IniFiles perl-MailTools perl-XML-Parser subversion subversion-perl $REQUIRES_GUI
+
+%description
+FCM: A modern Fortran build system + wrappers to Subversion for scientific
+software development
+
+%prep
+
+%build
+
+%install
+rm -fr %{buildroot}
+mkdir -p %{buildroot}/opt/ %{buildroot}/usr/bin
+cp -pr %_sourcedir/$NAME-$REV_BASE_DOT %{buildroot}/opt/$NAME
+cp -p %_sourcedir/$NAME-$REV_BASE_DOT/usr/bin/fcm %{buildroot}/usr/bin
+
+%clean
+rm -fr %{buildroot}
+
+%files
+/opt/$NAME
+/usr/bin/fcm
+__SPEC__
+} >~/rpmbuild/SPECS/$NAME.spec
+
+cd ~/rpmbuild/SPECS
+rpmbuild -ba $NAME.spec
+rm -fr ~/rpmbuild/SOURCES/$NAME-$REV_BASE_DOT
+exit
diff --git a/sbin/fcm-user-to-email b/sbin/fcm-user-to-email
new file mode 100755
index 0000000..333198e
--- /dev/null
+++ b/sbin/fcm-user-to-email
@@ -0,0 +1,66 @@
+#!/usr/bin/perl
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+
+use strict;
+use warnings;
+
+use FindBin;
+use lib "$FindBin::Bin/../lib";
+use FCM::Admin::System qw{get_users};
+use FCM::Util;
+
+my $UTIL = FCM::Util->new();
+
+if (!caller()) {
+ main(@ARGV);
+}
+
+sub main {
+ local(@ARGV) = @_;
+ if (!@ARGV) {
+ return;
+ }
+ my @names = @ARGV;
+ local($FCM::Admin::System::UTIL) = $UTIL;
+ my @emails
+ = sort grep {$_} map {$_->get_email()} values(%{get_users(@names)});
+ print(join(q{,}, @emails) . "\n");
+}
+
+1;
+__END__
+
+=head1 NAME
+
+fcm-users-to-emails
+
+=head1 SYNOPSIS
+
+ fcm-user-to-email USER ...
+
+=head1 DESCRIPTION
+
+Print email addresses (comma separated) of users in argument list.
+
+=head1 COPYRIGHT
+
+E<169> Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/sbin/fcm-vacuum-trac-env-db b/sbin/fcm-vacuum-trac-env-db
new file mode 100755
index 0000000..eda1546
--- /dev/null
+++ b/sbin/fcm-vacuum-trac-env-db
@@ -0,0 +1,101 @@
+#!/usr/bin/perl
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+
+use strict;
+use warnings;
+
+use FindBin;
+use lib "$FindBin::Bin/../lib";
+use FCM::Admin::System qw{
+ filter_projects
+ get_projects_from_trac_live
+ vacuum_trac_env_db
+};
+use FCM::Admin::Util qw{option2config};
+use Getopt::Long qw{GetOptions};
+use Pod::Usage qw{pod2usage};
+
+main();
+
+sub main {
+ my %option;
+ my $result = GetOptions(
+ \%option,
+ q{help|usage|h},
+ q{trac-live-dir=s},
+ );
+ if (!$result) {
+ pod2usage(1);
+ }
+ if (exists($option{help})) {
+ pod2usage(q{-verbose} => 1);
+ }
+ option2config(\%option);
+ my @projects = filter_projects([get_projects_from_trac_live()], \@ARGV);
+ for my $project (@projects) {
+ vacuum_trac_env_db($project);
+ }
+}
+
+__END__
+=head1 NAME
+
+fcm-vacuum-trac-env-db
+
+=head1 SYNOPSIS
+
+ fcm-vacuum-trac-env-db [OPTIONS] [PROJECT ...]
+
+=head1 OPTIONS
+
+=over 4
+
+=item --help, -h, --usage
+
+Prints help and exits.
+
+=item --trac-live-dir=DIR
+
+Specifies the root location of the live directory of Trac environments. See
+L<FCM::Admin::Config|FCM::Admin::Config> for the current default.
+
+=back
+
+=head1 ARGUMENTS
+
+=over 4
+
+=item PROJECT
+
+Specifies one or more project to vacuum. If no project is specified, the
+program searches the live directory for projects to vacuum.
+
+=back
+
+=head1 DESCRIPTION
+
+This program issues the "VACUUM" SQL command in the databases of the Trac
+environments in the live directory.
+
+=head1 COPYRIGHT
+
+E<169> Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/sbin/my-regular-update.example b/sbin/my-regular-update.example
new file mode 100755
index 0000000..3aa6c11
--- /dev/null
+++ b/sbin/my-regular-update.example
@@ -0,0 +1,43 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+set -eu
+LOCK=/path/to/lock
+WC_OF_PROJECT=/path/to/working/copy/of/project
+
+if [[ -e $LOCK ]]; then
+ exit
+fi
+
+mkdir -p $LOCK
+echo "$(whoami)@$(hostname):$$" >$LOCK/info # info on who created the lock
+
+while true; do
+ UPDATED=$(cd $WC_OF_PROJECT && svn update | sed '$d' | cut -c6-)
+ if [[ -z $UPDATED ]]; then
+ break
+ fi
+ if [[ -n $(echo "$UPDATED" | grep '^foo/bar/[^/]*\.baz$') ]]; then
+ : # Performs some update if foo/bar/*.baz has changed
+ fi
+done
+
+rm $LOCK/info
+rmdir $LOCK
+exit
diff --git a/sbin/post-commit-bg b/sbin/post-commit-bg
new file mode 100755
index 0000000..d71e0e2
--- /dev/null
+++ b/sbin/post-commit-bg
@@ -0,0 +1,160 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+# NAME
+# post-commit-bg
+#
+# SYNOPSIS
+# post-commit-bg REPOS REV
+#
+# ARGUMENTS
+# REPOS - the path to the Subversion repository
+# REV - the revision of the commit
+# TXN - the commit transaction that becomes the revision
+#
+# DESCRIPTION
+# This script performs the post-commit tasks of a Subversion repository in
+# the background.
+#
+# The script does the following:
+# 1. Creates an incremental revision dump of the current revision.
+# 2. Update corresponding Trac environment, if relevant.
+# 3. Checks the size of the revision dump. Warns if it exceeds a threshold.
+# 4. If this changeset has a change to "^/svnperms.conf", install its HEAD
+# revision at "$REPOS/hooks/", or remove it from "$REPOS/hooks/" if it is
+# removed from the HEAD.
+# 5. Runs "$REPOS/hooks/post-commit-bg-custom" and/or
+# "$REPOS/hooks/post-commit-background-custom", if available.
+# 6. E-mails the host user account on error.
+#
+# ENVIRONMENT VARIABLES
+# FCM_SVN_HOOK_COMMIT_DUMP_DIR
+# The path to dump commit deltas. Generate a commit delta if specified.
+# FCM_SVN_HOOK_TRAC_ROOT_DIR
+# The root directories of Trac environments. Update corresponding Trac
+# environment if specified.
+# FCM_SVN_HOOK_REPOS_SUFFIX
+# A suffix that should be removed from the basename of REPOS to get the
+# name of the Trac environment. (Default is "".)
+#
+# FILES
+# $REPOS/hooks/post-commit-bg-custom
+# $REPOS/hooks/post-commit-background-custom
+#-------------------------------------------------------------------------------
+set -eu
+. "$(dirname $0)/trac_hook"
+
+REPOS=$1
+REV=$2
+TXN=$3
+
+export PATH=${PATH:-'/usr/local/bin:/bin:/usr/bin'}:$(dirname $0)
+THIS=$(basename $0)
+USER=${USER:-$(whoami)}
+LOG_REV="$REPOS/log/$THIS-$REV.log"
+
+main() {
+ local RET_CODE=0
+ local NOW=$(date -u +%FT%H:%M:%SZ)
+ local AUTHOR=$(svnlook author -r "$REV" "$REPOS")
+ echo "$NOW+ $REV by $AUTHOR"
+
+ # Dump revision delta
+ if [[ -n ${FCM_SVN_HOOK_COMMIT_DUMP_DIR:-} ]]; then
+ if [[ ! -d "$FCM_SVN_HOOK_COMMIT_DUMP_DIR" ]]; then
+ mkdir -p "$FCM_SVN_HOOK_COMMIT_DUMP_DIR" || true
+ fi
+ local NAME=$(basename "$REPOS")
+ local DUMP="$FCM_SVN_HOOK_COMMIT_DUMP_DIR/$NAME-$REV.gz"
+ echo "svnadmin dump -r$REV --incremental --deltas $REPOS | gzip 1>$DUMP"
+ svnadmin dump "-r$REV" --incremental --deltas "$REPOS" \
+ | gzip 1>"$DUMP" || RET_CODE=$?
+ fi
+
+ # Resync Trac
+ trac_hook "$REPOS" "$REV" added || RET_CODE=$?
+
+ # Check size - send warning email if threshold exceeded
+ local REV_FILE=$REPOS/db/revs/$((REV / 1000))/$REV
+ local REV_FILE_SIZE_THRESHOLD=1048576 # 1MB
+ local REV_FILE_SIZE=$(du -b -s $REV_FILE | cut -f 1)
+ if (($REV_FILE_SIZE > $REV_FILE_SIZE_THRESHOLD)); then
+ echo "REV_FILE_SIZE=$REV_FILE_SIZE # EXCEED $REV_FILE_SIZE_THRESHOLD"
+ RET_CODE=1
+ else
+ echo "REV_FILE_SIZE=$REV_FILE_SIZE # within $REV_FILE_SIZE_THRESHOLD"
+ fi
+
+ # Install commit.conf and svnperms.conf, if necessary
+ local CHANGED=$(svnlook changed -r "${REV}" "${REPOS}")
+ local NAME=
+ for NAME in 'commit.conf' 'svnperms.conf'; do
+ if grep -q "^....${NAME}\$" <<<"${CHANGED}"; then
+ # Don't specify revision, so always look at latest.
+ if svnlook filesize "${REPOS}" "${NAME}" >/dev/null 2>&1; then
+ echo "svnlook cat ${REPOS} ${NAME} >${REPOS}/hooks/${NAME}"
+ svnlook cat "${REPOS}" "${NAME}" >"${REPOS}/hooks/${NAME}"
+ else
+ echo "rm -f ${REPOS}/hooks/${NAME}"
+ rm -f "${REPOS}/hooks/${NAME}"
+ fi
+ fi
+ done
+
+ # On commit to a branch, notify the branch owner if author is not him/her
+ local COMMIT_CONFIG="${REPOS}/hooks/commit.conf"
+ if ! grep -q 'no-notify-branch-owner' "$COMMIT_CONFIG" 2>/dev/null; then
+ local ADDRS=$(post-commit-bg-notify-who "$REPOS" "$REV" "$TXN")
+ if [[ -n $ADDRS ]]; then
+ SUBJECT="-s$(basename $REPOS)@$REV by $AUTHOR"
+ FROM=
+ if [[ -n ${FCM_SVN_HOOK_NOTIFICATION_FROM:-} ]]; then
+ FROM="-r${FCM_SVN_HOOK_NOTIFICATION_FROM:-}"
+ fi
+ echo -n "svn log -v -r \"$REV\" \"file://$REPOS\""
+ echo " | mail \"$FROM\" \"$SUBJECT\" \"$ADDRS\""
+ svn log -v -r "$REV" "file://$REPOS" | mail "$FROM" "$SUBJECT" "$ADDRS"
+ fi
+ fi
+
+ # Custom hook
+ local CUSTOM_HOOK=
+ for CUSTOM_HOOK in \
+ "$REPOS/hooks/$THIS-custom" \
+ "$REPOS/hooks/post-commit-background-custom"
+ do
+ if [[ -x "$CUSTOM_HOOK" ]]; then
+ echo "$CUSTOM_HOOK $REPOS $REV $TXN"
+ "$CUSTOM_HOOK" "$REPOS" "$REV" "$TXN" || RET_CODE=$?
+ fi
+ done
+
+ echo "RET_CODE=$RET_CODE"
+ return $RET_CODE
+}
+
+if ! main 1>$LOG_REV 2>&1; then
+ if [[ -n ${FCM_SVN_HOOK_ADMIN_EMAIL:-} ]]; then
+ mail -s "[$THIS] $REPOS@$REV" "$FCM_SVN_HOOK_ADMIN_EMAIL" <"$LOG_REV" \
+ || true
+ fi
+fi
+cat "$LOG_REV"
+rm -f "$LOG_REV"
+exit
diff --git a/sbin/post-commit-bg-notify-who b/sbin/post-commit-bg-notify-who
new file mode 100755
index 0000000..a557c5b
--- /dev/null
+++ b/sbin/post-commit-bg-notify-who
@@ -0,0 +1,103 @@
+#!/usr/bin/perl
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+
+use strict;
+use warnings;
+
+use FindBin;
+use lib "$FindBin::Bin/../lib";
+use FCM::Admin::System qw{get_users};
+use FCM::System::CM::SVN;
+use FCM::Util;
+
+our @IGNORES = qw{Config Rel Share};
+
+my $UTIL = FCM::Util->new();
+my $CM_SYS = FCM::System::CM::SVN->new({'util' => $UTIL});
+
+if (!caller()) {
+ main(@ARGV);
+}
+
+sub main {
+ local(@ARGV) = @_;
+ my ($repos, $rev, $txn) = @ARGV;
+
+ my %layout_config = $CM_SYS->load_layout_config('file://' . $repos);
+ if (!$layout_config{'level-owner-branch'}) {
+ return;
+ }
+
+ my ($author) = $CM_SYS->stdout(qw{svnlook author -r}, $rev, $repos);
+
+ # Get list of new paths
+ my %names = (); # {$name1 => 1, $name2 => 1, ...}
+ my @lines = $CM_SYS->stdout(qw{svnlook changed -r}, $rev, $repos);
+ LINE:
+ for my $line (@lines) {
+ my $status = substr($line, 0, 1);
+ my $path = substr($line, 4);
+ my $layout = $CM_SYS->get_layout_common(
+ $repos,
+ ($status eq 'D' ? $rev - 1 : $rev),
+ $path,
+ 1, # $is_local=1
+ );
+ my $owner = $layout->get_branch_owner();
+ if ($owner && $owner ne $author && !grep {$_ eq $owner} @IGNORES) {
+ $names{$owner} = 1;
+ }
+ }
+
+ # Get emails, if necessary
+ if (%names) {
+ local($FCM::Admin::System::UTIL) = $UTIL;
+ my @names = ($author, keys(%names));
+ my @emails
+ = sort grep {$_} map {$_->get_email()} values(%{get_users(@names)});
+ print(join(q{,}, @emails) . "\n");
+ }
+}
+
+1;
+__END__
+
+=head1 NAME
+
+post-commit-bg-notify-who
+
+=head1 SYNOPSIS
+
+ post-commit-bg-notify-who $REPOS $REV $TXN
+
+=head1 ARGUMENTS
+
+Accept the same arguments as a Subversion post-commit hook.
+
+=head1 DESCRIPTION
+
+This program prints email addresses who should be notified of the change. E.g.
+If this commit is performed by an author on someone else's branch.
+
+=head1 COPYRIGHT
+
+E<169> Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/sbin/post-revprop-change-bg b/sbin/post-revprop-change-bg
new file mode 100755
index 0000000..0bab5d4
--- /dev/null
+++ b/sbin/post-revprop-change-bg
@@ -0,0 +1,111 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+# NAME
+# post-revprop-change-bg
+#
+# SYNOPSIS
+# post-revprop-change-bg REPOS REV PROP_AUTHOR PROP_NAME ACTION
+#
+# ARGUMENTS
+# REPOS - the path to the Subversion repository
+# REV - the revision relevant for the property
+# PROP_AUTHOR - the author of this property change
+# PROP_NAME - the name of the property, should only be "svn:log"
+# ACTION - the action of the property change, should only be "M"
+#
+# DESCRIPTION
+# This script performs the post-revprop-change tasks of a Subversion
+# repository in the background.
+#
+# The script does the following:
+# 1. Write diff between old and new property.
+# 2. Update corresponding Trac environment, if relevant.
+# 3. E-mails the host user account on error.
+# 4. E-mails the changeset author if property author is not changeset author.
+#
+# ENVIRONMENT VARIABLES
+# FCM_SVN_HOOK_TRAC_ROOT_DIR
+# The root directories of Trac environments. Update corresponding Trac
+# environment if specified.
+# FCM_SVN_HOOK_REPOS_SUFFIX
+# A suffix that should be removed from the basename of REPOS to get the
+# name of the Trac environment. (Default is "".)
+#-------------------------------------------------------------------------------
+set -eu
+. "$(dirname $0)/trac_hook"
+
+REPOS=$1
+REV=$2
+PROP_AUTHOR=$3
+PROP_NAME=$4
+ACTION=$5
+
+export PATH=${PATH:-'/usr/local/bin:/bin:/usr/bin'}:$(dirname $0)
+THIS=$(basename $0)
+USER=${USER:-$(whoami)}
+LOG_TMP=$(mktemp "$REPOS/log/$THIS.log.XXXXXXXXXX")
+NAME=$(basename "$REPOS")
+
+main() {
+ local RET_CODE=0
+ local NOW=$(date -u +%FT%H:%M:%SZ)
+ echo "$NOW+ $ACTION $PROP_NAME @$REV by $PROP_AUTHOR"
+
+ # Resync Trac
+ trac_hook "$REPOS" "$REV" modified || RET_CODE=$?
+
+ # Diff old/new in log
+ local OLD_FILE=$(mktemp "$REPOS/log/$THIS.$REV.XXXXXXXXXX.old")
+ cat >"$OLD_FILE"
+ local DIFF_FILE=$(mktemp "$REPOS/log/$THIS.$REV.XXXXXXXXXX.diff")
+ {
+ echo "$NOW+ $ACTION $PROP_NAME @$REV by $PROP_AUTHOR"
+ printf '=%.0s' {1..72}
+ echo
+ } >"$DIFF_FILE"
+ svnlook pg -r "$REV" --revprop "$REPOS" "$PROP_NAME" \
+ | diff -u --label="old-value" --label="new-value" "$OLD_FILE" - \
+ | tee -a "$DIFF_FILE"
+
+ # Email to changeset author if not the same as property change author
+ REV_AUTHOR=$(svnlook author -r "$REV" "$REPOS")
+ if [[ "$REV_AUTHOR" != "$PROP_AUTHOR" ]]; then
+ SUBJECT="-s$(basename $REPOS)@$REV [$ACTION $PROP_NAME] by $PROP_AUTHOR"
+ FROM=
+ if [[ -n ${FCM_SVN_HOOK_NOTIFICATION_FROM:-} ]]; then
+ FROM="-r${FCM_SVN_HOOK_NOTIFICATION_FROM:-}"
+ fi
+ ADDRS=$(fcm-user-to-email "$REV_AUTHOR" "$PROP_AUTHOR")
+ mail "$FROM" "$SUBJECT" "$ADDRS" <"$DIFF_FILE" || true
+ fi
+ rm -f "$OLD_FILE" "$DIFF_FILE"
+
+ echo "RET_CODE=$RET_CODE"
+ return $RET_CODE
+}
+
+if ! main 1>$LOG_TMP 2>&1 && [[ -n ${FCM_SVN_HOOK_ADMIN_EMAIL:-} ]]; then
+ mail -s "[ERROR $THIS] $NAME" "$FCM_SVN_HOOK_ADMIN_EMAIL" <"$LOG_TMP" \
+ || true
+fi
+
+cat "$LOG_TMP"
+rm -f "$LOG_TMP"
+exit
diff --git a/sbin/pre-commit b/sbin/pre-commit
new file mode 100755
index 0000000..00cf366
--- /dev/null
+++ b/sbin/pre-commit
@@ -0,0 +1,110 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+# NAME
+# pre-commit
+#
+# SYNOPSIS
+# pre-commit REPOS TXN
+#
+# ARGUMENTS
+# REPOS - the path to the Subversion repository
+# TXN - the commit transaction
+#
+# DESCRIPTION
+# This script performs pre-commit check, including:
+# 1. Path-based permission check using "svnperms.py", if
+# "$REPOS/hooks/svnperms.conf" exists.
+# 2. Size check. Transaction should occupy less than 10MB, or the number of
+# MB specified in "$REPOS/hooks/pre-commit-size-threshold.conf".
+# 3. Runs "$REPOS/hooks/pre-commit-custom" if it exists.
+#
+# ENVIRONMENT VARIABLES
+# FCM_SVN_HOOK_ADMIN_EMAIL
+# The name of the admin team. (Default is "Admin".)
+#-------------------------------------------------------------------------------
+set -eu
+
+REPOS=$1
+TXN=$2
+
+export PATH=${PATH:-'/usr/local/bin:/bin:/usr/bin'}:$(dirname $0)
+THIS=$(basename $0)
+USER=${USER:-$(whoami)}
+NAME=$(basename "$REPOS")
+LOG_TXN="$REPOS/$THIS-$TXN.log"
+
+main() {
+ local NOW=$(date -u +%FT%H:%M:%SZ)
+ local AUTHOR=$(svnlook author -t "$TXN" "$REPOS")
+ echo "$NOW+ $TXN by $AUTHOR"
+ svnlook changed -t "$TXN" "$REPOS"
+ local ADMIN_EMAIL=${FCM_SVN_HOOK_ADMIN_EMAIL:-Admin}
+
+ # Check size.
+ local MB=1048576
+ local THRESHOLD=10
+ local SIZE_THRESHOLD_FILE="$REPOS/hooks/$THIS-size-threshold.conf"
+ if [[ -f "$SIZE_THRESHOLD_FILE" && -r "$SIZE_THRESHOLD_FILE" ]]; then
+ THRESHOLD=$(<"$SIZE_THRESHOLD_FILE")
+ fi
+ local TXN_FILE="$REPOS/db/txn-protorevs/$TXN.rev"
+ local SIZE=$(du -b "$TXN_FILE" | cut -f 1)
+ if ((SIZE > THRESHOLD * MB)); then
+ SIZE=$(du -h "$TXN_FILE" | cut -f 1)
+ echo "$NAME@$TXN: changeset size ${SIZE}B exceeds ${THRESHOLD}MB." >&2
+ echo "Email $ADMIN_EMAIL if you need to bypass this restriction." >&2
+ return 1
+ fi
+
+ # Check permission.
+ local PERM_CONFIG="$REPOS/hooks/svnperms.conf"
+ if [[ -r "$PERM_CONFIG" && -s "$PERM_CONFIG" ]]; then
+ svnperms.py -r "$REPOS" -t "$TXN" -f "$PERM_CONFIG" || return $?
+ elif [[ -L "$PERM_CONFIG" ]]; then
+ echo "$NAME: permission configuration file not found." >&2
+ echo "$ADMIN_EMAIL has been notified." >&2
+ return 1
+ fi
+
+ # Verify owner of any new branches, if relevant
+ local COMMIT_CONFIG="${REPOS}/hooks/commit.conf"
+ if ! grep -q 'no-verify-branch-owner' "$COMMIT_CONFIG" 2>/dev/null; then
+ pre-commit-verify-branch-owner "$REPOS" "$TXN" || return $?
+ fi
+
+ # Custom checking, if required
+ local CUSTOM_HOOK="$REPOS/hooks/$THIS-custom"
+ if [[ -x "$CUSTOM_HOOK" ]]; then
+ "$CUSTOM_HOOK" "$REPOS" "$TXN" || return $?
+ fi
+}
+
+RET_CODE=0
+if ! main 1>"$LOG_TXN" 2>&1; then
+ if [[ -n ${FCM_SVN_HOOK_ADMIN_EMAIL:-} ]]; then
+ mail -s "[$THIS] $REPOS@$TXN" "$FCM_SVN_HOOK_ADMIN_EMAIL" <"$LOG_TXN" \
+ || true
+ fi
+ cat "$LOG_TXN"
+ cat "$LOG_TXN" >&2
+ RET_CODE=1
+fi
+rm -f "$LOG_TXN"
+exit $RET_CODE
diff --git a/sbin/pre-commit-verify-branch-owner b/sbin/pre-commit-verify-branch-owner
new file mode 100755
index 0000000..536ec46
--- /dev/null
+++ b/sbin/pre-commit-verify-branch-owner
@@ -0,0 +1,100 @@
+#!/usr/bin/perl
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+
+use strict;
+use warnings;
+
+use FindBin;
+use lib "$FindBin::Bin/../lib";
+use FCM::Admin::System qw{verify_users};
+use FCM::System::CM::SVN;
+use FCM::Util;
+
+our @IGNORES = qw{Config Rel Share};
+
+my $UTIL = $FCM::Admin::System::UTIL;
+my $CM_SYS = FCM::System::CM::SVN->new({'util' => $UTIL});
+
+if (!caller()) {
+ main(@ARGV);
+}
+
+sub main {
+ local(@ARGV) = @_;
+ my ($repos, $txn) = @ARGV;
+
+ my %layout_config = $CM_SYS->load_layout_config('file://' . $repos);
+ if (!$layout_config{'level-owner-branch'}) {
+ return;
+ }
+
+ # Get list of new paths
+ my %lines_of; # $lines_of{$owner} = [$path, ...]
+ LINE:
+ for my $line ($CM_SYS->stdout(qw{svnlook changed -t}, $txn, $repos)) {
+ my $status = substr($line, 0, 1);
+ if ($status eq 'A') {
+ my $path = substr($line, 4);
+ # $is_local=1
+ my $layout = $CM_SYS->get_layout_common($repos, $txn, $path, 1);
+ my $owner = $layout->get_branch_owner();
+ if ($owner && !grep {$_ eq $owner} @IGNORES) {
+ if (!exists($lines_of{$owner})) {
+ $lines_of{$owner} = [];
+ }
+ push(@{$lines_of{$owner}}, $line);
+ }
+ }
+ }
+
+ # Verify branch owners, if necessary
+ my @bad_users = verify_users(keys(%lines_of));
+ for my $bad_user (@bad_users) {
+ for my $line (@{$lines_of{$bad_user}}) {
+ warn("[INVALID BRANCH OWNER] $line\n");
+ }
+ }
+ exit(scalar(@bad_users));
+}
+
+1;
+__END__
+
+=head1 NAME
+
+pre-commit-verify-branch-owner
+
+=head1 SYNOPSIS
+
+ pre-commit-verify-branch-owner REPOS TXN
+
+=head1 ARGUMENTS
+
+Accept the same arguments as a Subversion pre-commit hook.
+
+=head1 DESCRIPTION
+
+This program ensures that users create a branch with its owner correctly set.
+
+=head1 COPYRIGHT
+
+E<169> Crown copyright Met Office. All rights reserved.
+
+=cut
diff --git a/sbin/pre-revprop-change b/sbin/pre-revprop-change
new file mode 100755
index 0000000..0c69412
--- /dev/null
+++ b/sbin/pre-revprop-change
@@ -0,0 +1,84 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+# NAME
+# pre-revprop-change
+#
+# SYNOPSIS
+# pre-revprop-change REPOS REV PROP_AUTHOR PROP_NAME ACTION
+#
+# ARGUMENTS
+# REPOS - the path to the Subversion repository
+# REV - the revision relevant for the property
+# PROP_AUTHOR - the author of this property change
+# PROP_NAME - the name of the property, should only be "svn:log"
+# ACTION - the action of the property change, should only be "M"
+#
+# DESCRIPTION
+# This script enables users to change revision properties.
+#
+# By default, only "M svn:log" is allowed. If
+# "$REPOS/hooks/pre-revprop-change-ok.conf" exists, the contents should be a
+# list of allowed changes to revision properties. E.g.:
+#
+# M svn:author
+# M svn:log
+#
+# An empty file disables all changes.
+#
+# It e-mails the host user account whenever an action is blocked.
+#-------------------------------------------------------------------------------
+set -eu
+
+REPOS=$1
+REV=$2
+USER=$3
+PROPNAME=$4
+ACTION=$5
+
+export PATH=${PATH:-'/usr/local/bin:/bin:/usr/bin'}:$(dirname $0)
+THIS=$(basename "$0")
+USER=${USER:-(whoami)}
+NAME=$(basename "$REPOS")
+
+OK_CHANGES=$(echo 'M svn:log')
+OK_CHANGES_FILE="$REPOS/hooks/$THIS-ok.conf"
+if [[ -f $OK_CHANGES_FILE ]]; then
+ OK_CHANGES=$(<$OK_CHANGES_FILE)
+fi
+
+NOW=$(date -u +%FT%H:%M:%SZ)
+if ! grep -q "$ACTION *$PROPNAME" <<<"$OK_CHANGES"; then
+ if [[ -n "$OK_CHANGES" ]]; then
+ echo -n "[$ACTION $PROPNAME] permission denied. Can only do:" >&2
+ while read; do
+ echo -n " [$REPLY]" >&2
+ done <<<"$OK_CHANGES"
+ echo >&2
+ else
+ echo "[$ACTION $PROPNAME] permission denied." >&2
+ fi
+ if [[ -n ${FCM_SVN_HOOK_ADMIN_EMAIL:-} ]]; then
+ mail -s "$NAME:$THIS" "$FCM_SVN_HOOK_ADMIN_EMAIL" <<<"[! $NOW] $@" \
+ || true
+ fi
+ echo "[! $NOW] $@"
+ exit 1
+fi
+exit
diff --git a/sbin/svnperms.py b/sbin/svnperms.py
new file mode 100755
index 0000000..af0eb36
--- /dev/null
+++ b/sbin/svnperms.py
@@ -0,0 +1,368 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+#
+#
+
+# THIS DISTRIBUTION HAS BEEN MODIFIED.
+# Original source downloaded from r1295006 at:
+# https://svn.apache.org/viewvc/subversion/trunk/tools/hook-scripts/svnperms.py
+# This version is modified to allow custom permission message per repository.
+
+import sys, os
+import getopt
+import shlex
+
+try:
+ # Python >=3.0
+ from subprocess import getstatusoutput as subprocess_getstatusoutput
+except ImportError:
+ # Python <3.0
+ from commands import getstatusoutput as subprocess_getstatusoutput
+try:
+ my_getopt = getopt.gnu_getopt
+except AttributeError:
+ my_getopt = getopt.getopt
+import re
+
+__author__ = "Gustavo Niemeyer <gustavo at niemeyer.net>"
+
+class Error(Exception): pass
+
+SECTION = re.compile(r'\[([^]]+?)(?:\s+extends\s+([^]]+))?\]')
+OPTION = re.compile(r'(\S+)\s*=\s*(.*)$')
+
+class Config:
+ def __init__(self, filename):
+ # Options are stored in __sections_list like this:
+ # [(sectname, [(optname, optval), ...]), ...]
+ self._sections_list = []
+ self._sections_dict = {}
+ self._read(filename)
+
+ def _read(self, filename):
+ # Use the same logic as in ConfigParser.__read()
+ file = open(filename)
+ cursectdict = None
+ optname = None
+ lineno = 0
+ for line in file:
+ lineno = lineno + 1
+ if line.isspace() or line[0] == '#':
+ continue
+ if line[0].isspace() and cursectdict is not None and optname:
+ value = line.strip()
+ cursectdict[optname] = "%s %s" % (cursectdict[optname], value)
+ cursectlist[-1][1] = "%s %s" % (cursectlist[-1][1], value)
+ else:
+ m = SECTION.match(line)
+ if m:
+ sectname = m.group(1)
+ parentsectname = m.group(2)
+ if parentsectname is None:
+ # No parent section defined, so start a new section
+ cursectdict = self._sections_dict.setdefault \
+ (sectname, {})
+ cursectlist = []
+ else:
+ # Copy the parent section into the new section
+ parentsectdict = self._sections_dict.get \
+ (parentsectname, {})
+ cursectdict = self._sections_dict.setdefault \
+ (sectname, parentsectdict.copy())
+ cursectlist = self.walk(parentsectname)
+ self._sections_list.append((sectname, cursectlist))
+ optname = None
+ elif cursectdict is None:
+ raise Error("%s:%d: no section header" % \
+ (filename, lineno))
+ else:
+ m = OPTION.match(line)
+ if m:
+ optname, optval = m.groups()
+ optval = optval.strip()
+ cursectdict[optname] = optval
+ cursectlist.append([optname, optval])
+ else:
+ raise Error("%s:%d: parsing error" % \
+ (filename, lineno))
+
+ def sections(self):
+ return list(self._sections_dict.keys())
+
+ def options(self, section):
+ return list(self._sections_dict.get(section, {}).keys())
+
+ def get(self, section, option, default=None):
+ return self._sections_dict.get(option, default)
+
+ def walk(self, section, option=None):
+ ret = []
+ for sectname, options in self._sections_list:
+ if sectname == section:
+ for optname, value in options:
+ if not option or optname == option:
+ ret.append((optname, value))
+ return ret
+
+
+class Permission:
+ def __init__(self):
+ self._group = {}
+ self._permlist = []
+
+ def parse_groups(self, groupsiter):
+ for option, value in groupsiter:
+ groupusers = []
+ for token in shlex.split(value):
+ # expand nested groups in place; no forward decls
+ if token[0] == "@":
+ try:
+ groupusers.extend(self._group[token[1:]])
+ except KeyError:
+ raise Error, "group '%s' not found" % token[1:]
+ else:
+ groupusers.append(token)
+ self._group[option] = groupusers
+
+ def parse_perms(self, permsiter):
+ for option, value in permsiter:
+ # Paths never start with /, so remove it if provided
+ if option[0] == "/":
+ option = option[1:]
+ pattern = re.compile("^%s$" % option)
+ for entry in value.split():
+ openpar, closepar = entry.find("("), entry.find(")")
+ groupsusers = entry[:openpar].split(",")
+ perms = entry[openpar+1:closepar].split(",")
+ users = []
+ for groupuser in groupsusers:
+ if groupuser[0] == "@":
+ try:
+ users.extend(self._group[groupuser[1:]])
+ except KeyError:
+ raise Error("group '%s' not found" % \
+ groupuser[1:])
+ else:
+ users.append(groupuser)
+ self._permlist.append((pattern, users, perms))
+
+ def get(self, user, path):
+ ret = []
+ for pattern, users, perms in self._permlist:
+ if pattern.match(path) and (user in users or "*" in users):
+ ret = perms
+ return ret
+
+class SVNLook:
+ def __init__(self, repospath, txn=None, rev=None):
+ self.repospath = repospath
+ self.txn = txn
+ self.rev = rev
+
+ def _execcmd(self, *cmd, **kwargs):
+ cmdstr = " ".join(cmd)
+ status, output = subprocess_getstatusoutput(cmdstr)
+ if status != 0:
+ sys.stderr.write(cmdstr)
+ sys.stderr.write("\n")
+ sys.stderr.write(output)
+ raise Error("command failed: %s\n%s" % (cmdstr, output))
+ return status, output
+
+ def _execsvnlook(self, cmd, *args, **kwargs):
+ execcmd_args = ["svnlook", cmd, self.repospath]
+ self._add_txnrev(execcmd_args, kwargs)
+ execcmd_args += args
+ execcmd_kwargs = {}
+ keywords = ["show", "noerror"]
+ for key in keywords:
+ if key in kwargs:
+ execcmd_kwargs[key] = kwargs[key]
+ return self._execcmd(*execcmd_args, **execcmd_kwargs)
+
+ def _add_txnrev(self, cmd_args, received_kwargs):
+ if "txn" in received_kwargs:
+ txn = received_kwargs.get("txn")
+ if txn is not None:
+ cmd_args += ["-t", txn]
+ elif self.txn is not None:
+ cmd_args += ["-t", self.txn]
+ if "rev" in received_kwargs:
+ rev = received_kwargs.get("rev")
+ if rev is not None:
+ cmd_args += ["-r", rev]
+ elif self.rev is not None:
+ cmd_args += ["-r", self.rev]
+
+ def changed(self, **kwargs):
+ status, output = self._execsvnlook("changed", **kwargs)
+ if status != 0:
+ return None
+ changes = []
+ for line in output.splitlines():
+ line = line.rstrip()
+ if not line: continue
+ entry = [None, None, None]
+ changedata, changeprop, path = None, None, None
+ if line[0] != "_":
+ changedata = line[0]
+ if line[1] != " ":
+ changeprop = line[1]
+ path = line[4:]
+ changes.append((changedata, changeprop, path))
+ return changes
+
+ def author(self, **kwargs):
+ status, output = self._execsvnlook("author", **kwargs)
+ if status != 0:
+ return None
+ return output.strip()
+
+
+def check_perms(filename, section, repos, txn=None, rev=None, author=None):
+ svnlook = SVNLook(repos, txn=txn, rev=rev)
+ if author is None:
+ author = svnlook.author()
+ changes = svnlook.changed()
+ try:
+ config = Config(filename)
+ except IOError:
+ raise Error("can't read config file "+filename)
+ if not section in config.sections():
+ raise Error("section '%s' not found in config file" % section)
+ perm = Permission()
+ perm.parse_groups(config.walk("groups"))
+ perm.parse_groups(config.walk(section+" groups"))
+ perm.parse_perms(config.walk(section))
+ permerrors = []
+ for changedata, changeprop, path in changes:
+ pathperms = perm.get(author, path)
+ if changedata == "A" and "add" not in pathperms:
+ permerrors.append("you can't add "+path)
+ elif changedata == "U" and "update" not in pathperms:
+ permerrors.append("you can't update "+path)
+ elif changedata == "D" and "remove" not in pathperms:
+ permerrors.append("you can't remove "+path)
+ elif changeprop == "U" and "update" not in pathperms:
+ permerrors.append("you can't update properties of "+path)
+ #else:
+ # print "cdata=%s cprop=%s path=%s perms=%s" % \
+ # (str(changedata), str(changeprop), path, str(pathperms))
+ if permerrors:
+ message = config.get("message", "permerrors_prefix")
+ if message is None:
+ message = config.get(" ".join((section, "message")),
+ "permerrors_prefix")
+ if message is None:
+ message = "you don't have enough permissions for this transaction:"
+ permerrors.insert(0, message)
+ raise Error("\n".join(permerrors))
+
+
+# Command:
+
+USAGE = """\
+Usage: svnperms.py OPTIONS
+
+Options:
+ -r PATH Use repository at PATH to check transactions
+ -t TXN Query transaction TXN for commit information
+ -f PATH Use PATH as configuration file (default is repository
+ path + /conf/svnperms.conf)
+ -s NAME Use section NAME as permission section (default is
+ repository name, extracted from repository path)
+ -R REV Query revision REV for commit information (for tests)
+ -A AUTHOR Check commit as if AUTHOR had committed it (for tests)
+ -h Show this message
+"""
+
+class MissingArgumentsException(Exception):
+ "Thrown when required arguments are missing."
+ pass
+
+def parse_options():
+ try:
+ opts, args = my_getopt(sys.argv[1:], "f:s:r:t:R:A:h", ["help"])
+ except getopt.GetoptError, e:
+ raise Error(e.msg)
+ class Options: pass
+ obj = Options()
+ obj.filename = None
+ obj.section = None
+ obj.repository = None
+ obj.transaction = None
+ obj.revision = None
+ obj.author = None
+ for opt, val in opts:
+ if opt == "-f":
+ obj.filename = val
+ elif opt == "-s":
+ obj.section = val
+ elif opt == "-r":
+ obj.repository = val
+ elif opt == "-t":
+ obj.transaction = val
+ elif opt == "-R":
+ obj.revision = val
+ elif opt == "-A":
+ obj.author = val
+ elif opt in ["-h", "--help"]:
+ sys.stdout.write(USAGE)
+ sys.exit(0)
+ missingopts = []
+ if not obj.repository:
+ missingopts.append("repository")
+ if not (obj.transaction or obj.revision):
+ missingopts.append("either transaction or a revision")
+ if missingopts:
+ raise MissingArgumentsException("missing required option(s): " + ", ".join(missingopts))
+ obj.repository = os.path.abspath(obj.repository)
+ if obj.filename is None:
+ obj.filename = os.path.join(obj.repository, "conf", "svnperms.conf")
+ if obj.section is None:
+ obj.section = os.path.basename(obj.repository)
+ if not (os.path.isdir(obj.repository) and
+ os.path.isdir(os.path.join(obj.repository, "db")) and
+ os.path.isdir(os.path.join(obj.repository, "hooks")) and
+ os.path.isfile(os.path.join(obj.repository, "format"))):
+ raise Error("path '%s' doesn't look like a repository" % \
+ obj.repository)
+
+ return obj
+
+def main():
+ try:
+ opts = parse_options()
+ check_perms(opts.filename, opts.section,
+ opts.repository, opts.transaction, opts.revision,
+ opts.author)
+ except MissingArgumentsException, e:
+ sys.stderr.write("%s\n" % str(e))
+ sys.stderr.write(USAGE)
+ sys.exit(1)
+ except Error, e:
+ sys.stderr.write("error: %s\n" % str(e))
+ sys.exit(1)
+
+if __name__ == "__main__":
+ main()
+
+# vim:et:ts=4:sw=4
diff --git a/sbin/trac_hook b/sbin/trac_hook
new file mode 100644
index 0000000..224a1db
--- /dev/null
+++ b/sbin/trac_hook
@@ -0,0 +1,65 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+# NAME
+# trac_hook
+#
+# SYNOPSIS
+# . trac_hook
+# trac_hook "$REPOS" "$REV" added|modified
+#
+# DESCRIPTION
+# Provide a function called "trac_hook", which updates the corresponding Trac
+# environment, for post-commit or post-revprop-change.
+#
+# ENVIRONMENT VARIABLES
+# FCM_SVN_HOOK_TRAC_ROOT_DIR
+# The root directories of Trac environments. Update corresponding Trac
+# environment if specified.
+# FCM_SVN_HOOK_REPOS_SUFFIX
+# A suffix that should be removed from the basename of REPOS to get the
+# name of the Trac environment. (Default is "".)
+#-------------------------------------------------------------------------------
+
+trac_hook() {
+ local REPOS=$1
+ local REV=$2
+ local TRAC_ACT=$3
+ if which trac-admin 1>/dev/null 2>&1 \
+ && [[ -n ${FCM_SVN_HOOK_TRAC_ROOT_DIR:-} ]]
+ then
+ local TRAC_NAME=$(basename "$REPOS")
+ if [[ -n ${FCM_SVN_HOOK_REPOS_SUFFIX:-} ]]; then
+ TRAC_NAME=${NAME%$FCM_SVN_HOOK_REPOS_SUFFIX}
+ fi
+ local TRAC_DIR="$FCM_SVN_HOOK_TRAC_ROOT_DIR/$TRAC_NAME"
+ if [[ -d "$TRAC_DIR" ]]; then
+ if [[ $(trac-admin --version) == trac-admin\ 0.11* ]]; then
+ # N.B. "added" was automatic on access
+ if [[ "$TRAC_ACT" == 'modified' ]]; then
+ echo "trac-admin $TRAC_DIR resync $REV"
+ trac-admin "$TRAC_DIR" resync "$REV" >/dev/null
+ fi
+ else
+ echo "trac-admin $TRAC_DIR changeset $TRAC_ACT $REPOS $REV"
+ trac-admin "$TRAC_DIR" changeset "$TRAC_ACT" "$REPOS" "$REV"
+ fi
+ fi
+ fi
+}
diff --git a/t/etc/repo_files/lib/python/info/__init__.py b/t/etc/repo_files/lib/python/info/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/t/etc/repo_files/lib/python/info/poems.py b/t/etc/repo_files/lib/python/info/poems.py
new file mode 100644
index 0000000..94b1e56
--- /dev/null
+++ b/t/etc/repo_files/lib/python/info/poems.py
@@ -0,0 +1,24 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""The Python, by Hilaire Belloc
+
+A Python I should not advise,--
+It needs a doctor for its eyes,
+And has the measles yearly.
+However, if you feel inclined
+To get one (to improve your mind,
+And not from fashion merely),
+Allow no music near its cage;
+And when it flies into a rage
+Chastise it, most severely.
+I had an aunt in Yucatan
+Who bought a Python from a man
+And kept it for a pet.
+She died, because she never knew
+These simple little rules and few;--
+The Snake is living yet.
+"""
+
+import this
+
+print "\n", __doc__
diff --git a/t/etc/repo_files/module/hello_constants.f90 b/t/etc/repo_files/module/hello_constants.f90
new file mode 100644
index 0000000..b8237b9
--- /dev/null
+++ b/t/etc/repo_files/module/hello_constants.f90
@@ -0,0 +1,5 @@
+MODULE Hello_Constants
+
+INCLUDE 'hello_constants_dummy.inc'
+
+END MODULE Hello_Constants
diff --git a/t/etc/repo_files/module/hello_constants.inc b/t/etc/repo_files/module/hello_constants.inc
new file mode 100644
index 0000000..ae26a9b
--- /dev/null
+++ b/t/etc/repo_files/module/hello_constants.inc
@@ -0,0 +1 @@
+CHARACTER (LEN=80), PARAMETER :: hello_string = 'Hello Earth!'
diff --git a/t/etc/repo_files/module/hello_constants_dummy.inc b/t/etc/repo_files/module/hello_constants_dummy.inc
new file mode 100644
index 0000000..06f117b
--- /dev/null
+++ b/t/etc/repo_files/module/hello_constants_dummy.inc
@@ -0,0 +1 @@
+INCLUDE 'hello_constants.inc'
diff --git a/t/etc/repo_files/pro/hello.pro b/t/etc/repo_files/pro/hello.pro
new file mode 100644
index 0000000..bc880e8
--- /dev/null
+++ b/t/etc/repo_files/pro/hello.pro
@@ -0,0 +1,2 @@
+PRO HELLO
+END
diff --git a/t/etc/repo_files/pro/plot.pro b/t/etc/repo_files/pro/plot.pro
new file mode 100644
index 0000000..5896f2b
--- /dev/null
+++ b/t/etc/repo_files/pro/plot.pro
@@ -0,0 +1,3 @@
+PRO PLOT
+; Calls : hello.pro
+END
diff --git a/t/etc/repo_files/program/hello.F90 b/t/etc/repo_files/program/hello.F90
new file mode 100644
index 0000000..64b2cc2
--- /dev/null
+++ b/t/etc/repo_files/program/hello.F90
@@ -0,0 +1,20 @@
+PROGRAM Hello
+
+USE Hello_Constants, ONLY: hello_string
+
+IMPLICIT NONE
+
+INTEGER :: integer_arg = 1234
+
+#if defined(CALL_HELLO_SUB)
+INCLUDE 'hello_sub.interface'
+#endif
+
+CHARACTER (LEN=*), PARAMETER :: this = 'Hello'
+
+WRITE (*, '(A)') this // ': ' // TRIM (hello_string)
+#if defined(CALL_HELLO_SUB)
+CALL Hello_Sub (integer_arg)
+#endif
+
+END PROGRAM Hello
diff --git a/t/etc/repo_files/subroutine/hello_c.c b/t/etc/repo_files/subroutine/hello_c.c
new file mode 100644
index 0000000..45ca182
--- /dev/null
+++ b/t/etc/repo_files/subroutine/hello_c.c
@@ -0,0 +1,5 @@
+#include <stdio.h>
+
+void hello_c_ () {
+ printf ("%s\n", "Hello_C: Hello Earth!");
+}
diff --git a/t/etc/repo_files/subroutine/hello_sub.F90 b/t/etc/repo_files/subroutine/hello_sub.F90
new file mode 100644
index 0000000..b7d7da6
--- /dev/null
+++ b/t/etc/repo_files/subroutine/hello_sub.F90
@@ -0,0 +1,24 @@
+#if defined(HELLO_SUB)
+SUBROUTINE Hello_Sub (integer_arg)
+
+USE Hello_Constants, ONLY: hello_string
+
+IMPLICIT NONE
+
+CHARACTER (LEN=*), PARAMETER :: this = 'Hello_Sub'
+INTEGER :: integer_arg
+INTEGER :: integer_common
+COMMON /general/integer_common
+
+! DEPENDS ON: hello_c.o
+EXTERNAL Hello_C
+
+#include "hello_sub_dummy.h"
+
+WRITE (*, '(A,I0)') this // ': integer (arg): ', integer_arg
+WRITE (*, '(A,I0)') this // ': integer (common): ', integer_common
+
+CALL Hello_C ()
+
+END SUBROUTINE Hello_Sub
+#endif
diff --git a/t/etc/repo_files/subroutine/hello_sub.h b/t/etc/repo_files/subroutine/hello_sub.h
new file mode 100644
index 0000000..36fd211
--- /dev/null
+++ b/t/etc/repo_files/subroutine/hello_sub.h
@@ -0,0 +1 @@
+WRITE (*, '(A)') this // ': ' // TRIM (hello_string)
diff --git a/t/etc/repo_files/subroutine/hello_sub_dummy.h b/t/etc/repo_files/subroutine/hello_sub_dummy.h
new file mode 100644
index 0000000..591744b
--- /dev/null
+++ b/t/etc/repo_files/subroutine/hello_sub_dummy.h
@@ -0,0 +1 @@
+#include "hello_sub.h"
diff --git a/t/fcm-add-trac-env/00-basic.t b/t/fcm-add-trac-env/00-basic.t
new file mode 100755
index 0000000..974f517
--- /dev/null
+++ b/t/fcm-add-trac-env/00-basic.t
@@ -0,0 +1,83 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+# Basic tests for "fcm-add-trac-env".
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+if ! which trac-admin 1>/dev/null 2>/dev/null; then
+ skip_all 'trac-admin not available'
+fi
+tests 20
+#-------------------------------------------------------------------------------
+set -e
+mkdir -p etc srv/{svn,trac}
+# Configuration
+export FCM_CONF_PATH="$PWD/etc"
+ADMIN_USERS='holly ivy'
+cat >etc/admin.cfg <<__CONF__
+svn_live_dir=$PWD/srv/svn
+trac_admin_users=$ADMIN_USERS
+trac_live_dir=$PWD/srv/trac
+__CONF__
+# Create some Subversion repositories
+for NAME in bus lorry taxi; do
+ svnadmin create "srv/svn/$NAME"
+done
+set +e
+#-------------------------------------------------------------------------------
+for NAME in bus car lorry taxi; do
+ TEST_KEY="$TEST_KEY_BASE-$NAME"
+ # Command OK
+ run_pass "$TEST_KEY" "$FCM_HOME/sbin/fcm-add-trac-env" "$NAME"
+ # Trac environment directory exists
+ run_pass "$TEST_KEY-d" test -d "$PWD/srv/trac/$NAME"
+ # Admin users are set
+ for ADMIN_USER in $ADMIN_USERS; do
+ trac-admin "$PWD/srv/trac/$NAME" permission list "$ADMIN_USER" \
+ >"$TEST_KEY.perm.out"
+ file_grep "$TEST_KEY.perm.out" \
+ "$ADMIN_USER *TRAC_ADMIN" "$TEST_KEY.perm.out"
+ done
+ # Subversion repository paths in place
+ if [[ -d "srv/svn/$NAME" ]]; then
+ file_grep "$TEST_KEY-repository_dir" \
+ "repository_dir=$PWD/srv/svn/$NAME" \
+ "$PWD/srv/trac/$NAME/conf/trac.ini"
+ fi
+done
+
+TEST_KEY="$TEST_KEY_BASE-intertrac"
+file_cmp "$TEST_KEY" "$PWD/srv/trac/intertrac.ini" <<'__CONF__'
+[intertrac]
+bus.title=bus
+bus.url=https://localhost/trac/bus
+bus.compat=false
+car.title=car
+car.url=https://localhost/trac/car
+car.compat=false
+lorry.title=lorry
+lorry.url=https://localhost/trac/lorry
+lorry.compat=false
+taxi.title=taxi
+taxi.url=https://localhost/trac/taxi
+taxi.compat=false
+__CONF__
+#-------------------------------------------------------------------------------
+exit
diff --git a/t/fcm-add-trac-env/test_header b/t/fcm-add-trac-env/test_header
new file mode 120000
index 0000000..90bd5a3
--- /dev/null
+++ b/t/fcm-add-trac-env/test_header
@@ -0,0 +1 @@
+../lib/bash/test_header
\ No newline at end of file
diff --git a/t/fcm-add/00-simple.t b/t/fcm-add/00-simple.t
new file mode 100644
index 0000000..d133099
--- /dev/null
+++ b/t/fcm-add/00-simple.t
@@ -0,0 +1,139 @@
+#!/bin/bash
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# Basic tests for "fcm add".
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+if [[ $? -ne 0 ]]; then
+ exit 1
+fi
+#-------------------------------------------------------------------------------
+tests 24
+#-------------------------------------------------------------------------------
+setup
+init_repos
+init_branch_wc add $REPOS_URL
+mkdir $TEST_DIR/wc/added_directory1
+svn add -q $TEST_DIR/wc/added_directory1
+touch $TEST_DIR/wc/added_directory1/added_file1
+mkdir $TEST_DIR/wc/added_directory2
+touch $TEST_DIR/wc/added_directory2/added_file2
+cd $TEST_DIR/wc
+#-------------------------------------------------------------------------------
+# Tests fcm add unversioned file
+TEST_KEY=$TEST_KEY_BASE-fcm-add-file
+run_pass "$TEST_KEY" fcm add added_directory1/added_file1
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<'__OUT__'
+A added_directory1/added_file1
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm add unversioned directory
+TEST_KEY=$TEST_KEY_BASE-fcm-add-dir
+run_pass "$TEST_KEY" fcm add added_directory2
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<'__OUT__'
+A added_directory2
+A added_directory2/added_file2
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+cd $TEST_DIR
+teardown
+#-------------------------------------------------------------------------------
+init_repos
+init_branch_wc add_c $REPOS_URL
+touch $TEST_DIR/wc/unversioned_file
+mkdir $TEST_DIR/wc/unversioned_directory
+touch $TEST_DIR/wc/unversioned_directory/unversioned_file_2
+mkdir $TEST_DIR/wc/versioned_directory
+svn add -q $TEST_DIR/wc/versioned_directory
+touch $TEST_DIR/wc/versioned_directory/unversioned_file_3
+cd $TEST_DIR/wc
+#-------------------------------------------------------------------------------
+# Tests fcm add -c unversioned file
+TEST_KEY=$TEST_KEY_BASE-fcm-add-c-file
+run_pass "$TEST_KEY" fcm add -c unversioned_file <<'__EOF__'
+y
+__EOF__
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<'__OUT__'
+? unversioned_file
+Would you like to run "svn add unversioned_file"?
+Enter "y", "n" or "a" (or just press <return> for "n"): A unversioned_file
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm add -c unversioned directory
+TEST_KEY=$TEST_KEY_BASE-fcm-add-c-dir
+run_pass "$TEST_KEY" fcm add -c unversioned_directory <<'__EOF__'
+y
+__EOF__
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<'__OUT__'
+? unversioned_directory
+Would you like to run "svn add unversioned_directory"?
+Enter "y", "n" or "a" (or just press <return> for "n"): A unversioned_directory
+A unversioned_directory/unversioned_file_2
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm add -c versioned directory
+TEST_KEY=$TEST_KEY_BASE-fcm-add-c-versioned-dir
+run_pass "$TEST_KEY" fcm add -c versioned_directory <<'__EOF__'
+n
+__EOF__
+file_test "$TEST_KEY.out" "$TEST_KEY.out" -s
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm status after above tests
+TEST_KEY=$TEST_KEY_BASE-fcm-add-c-status
+run_pass "$TEST_KEY" fcm st
+sort $TEST_DIR/"$TEST_KEY.out" -o $TEST_DIR/"$TEST_KEY.out"
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<'__OUT__'
+? versioned_directory/unversioned_file_3
+A unversioned_directory
+A unversioned_directory/unversioned_file_2
+A unversioned_file
+A versioned_directory
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm add -c with no arguments
+TEST_KEY=$TEST_KEY_BASE-fcm-add-c-no-args
+fcm revert -q -R $TEST_DIR/wc/
+run_pass "$TEST_KEY" fcm add -c <<'__EOF__'
+y
+y
+y
+y
+__EOF__
+file_test "$TEST_KEY.out" "$TEST_KEY.out" -s
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm status after above tests
+TEST_KEY=$TEST_KEY_BASE-fcm-add-c-no-args-status
+run_pass "$TEST_KEY" fcm status
+sort $TEST_DIR/"$TEST_KEY.out" -o $TEST_DIR/"$TEST_KEY.out"
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<'__OUT__'
+A unversioned_directory
+A unversioned_directory/unversioned_file_2
+A unversioned_file
+A versioned_directory
+A versioned_directory/unversioned_file_3
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+teardown
+#-------------------------------------------------------------------------------
diff --git a/t/fcm-add/test_header b/t/fcm-add/test_header
new file mode 100644
index 0000000..672ce12
--- /dev/null
+++ b/t/fcm-add/test_header
@@ -0,0 +1,232 @@
+#!/bin/bash
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# Optional enviroment variables:
+# TEST_PROJECT (tests using given project name)
+# TEST_REMOTE_HOST (tests using svn+ssh repositories located on given host)
+# ------------------------------------------------------------------------------
+
+. $(dirname $0)/../lib/bash/test_header
+
+function file_cmp() {
+ local TEST_KEY=$1
+ local FILE_ACTUAL=$2
+ local FILE_EXPECT=${3:--}
+ if cmp $TEST_DIR/$FILE_ACTUAL $FILE_EXPECT; then
+ pass $TEST_KEY
+ return
+ fi
+ fail $TEST_KEY
+}
+
+function file_grep() {
+ local TEST_KEY=$1
+ local PATTERN=$2
+ local FILE=$3
+ if grep -q -e "$PATTERN" $TEST_DIR/$FILE; then
+ pass $TEST_KEY
+ return
+ fi
+ fail $TEST_KEY
+}
+
+function file_test() {
+ local TEST_KEY=$1
+ local FILE=$2
+ local OPTION=${3:--e}
+ if test $OPTION $TEST_DIR/$FILE; then
+ pass $TEST_KEY
+ else
+ fail $TEST_KEY
+ fi
+}
+
+function file_xxdiff() {
+ local TEST_KEY=$1
+ local FILE_ACTUAL=$2
+ local FILE_EXPECT=${3:--}
+ if xxdiff -D $TEST_DIR/$FILE_ACTUAL $FILE_EXPECT; then
+ pass $TEST_KEY
+ return
+ fi
+ fail $TEST_KEY
+}
+
+function init_repos() {
+ if [[ -n ${TEST_REMOTE_HOST:-} ]]; then
+ TEST_REMOTE_DIR=$(ssh $TEST_REMOTE_HOST "mktemp -d")
+ ssh $TEST_REMOTE_HOST "svnadmin create --fs-type fsfs $TEST_REMOTE_DIR"
+ REPOS_URL="svn+ssh://${TEST_REMOTE_HOST}$TEST_REMOTE_DIR"
+ else
+ svnadmin create --fs-type fsfs $TEST_DIR/test_repos
+ REPOS_URL="file://$TEST_DIR/test_repos"
+ fi
+ ROOT_URL=$REPOS_URL
+ PROJECT=
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ PROJECT=$TEST_PROJECT"/"
+ fi
+ svn import -q $TEST_SOURCE_DIR/../etc/repo_files \
+ $REPOS_URL/$PROJECT/trunk -m "initial trunk import"
+ svn mkdir -q $REPOS_URL/$PROJECT/tags -m "make tags"
+ svn mkdir -q --parents $REPOS_URL/$PROJECT/branches/dev/Share -m " "
+}
+
+function init_repos_layout_roses() {
+ if [[ -n ${TEST_REMOTE_HOST:-} ]]; then
+ TEST_REMOTE_DIR=$(ssh $TEST_REMOTE_HOST "mktemp -d")
+ ssh $TEST_REMOTE_HOST "svnadmin create --fs-type fsfs $TEST_REMOTE_DIR"
+ REPOS_URL="svn+ssh://${TEST_REMOTE_HOST}$TEST_REMOTE_DIR"
+ else
+ svnadmin create --fs-type fsfs $TEST_DIR/test_repos
+ REPOS_URL="file://$TEST_DIR/test_repos"
+ fi
+ svn mkdir -q --parents $REPOS_URL/a/a/0/0/0/trunk
+ svn import -q $TEST_SOURCE_DIR/../etc/repo_files \
+ $REPOS_URL/a/a/0/0/0/trunk -m "initial trunk import"
+ TMPFILE=$(mktemp)
+ cat >$TMPFILE <<__LAYOUT__
+depth-project = 5
+depth-branch = 1
+depth-tag = 1
+dir-trunk = trunk
+dir-branch =
+dir-tag =
+level-owner-branch =
+level-owner-tag =
+template-branch =
+template-tag =
+__LAYOUT__
+ TMPDIR=$(mktemp -d)
+ svn checkout -q $REPOS_URL $TMPDIR
+ svn propset -q --file=$TMPFILE fcm:layout $TMPDIR
+ svn commit -q -m " " $TMPDIR
+ rm -f $TMPFILE
+ rm -rf $TMPDIR
+ ROOT_URL=$REPOS_URL
+}
+
+function init_branch() {
+ local BRANCH_NAME=$1
+ local REPOS_URL=$2
+ local ROOT_URL=$REPOS_URL
+ local ROOT_PATH=
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ ROOT_PATH=$ROOT_PATH/$TEST_PROJECT
+ fi
+ MESSAGE=$(echo -e "Created $ROOT_PATH/branches/dev/Share/$BRANCH_NAME from /trunk at 1.")
+ svn copy -q -r1 $ROOT_URL/trunk $ROOT_URL/branches/dev/Share/$BRANCH_NAME \
+ -m "Made a branch $MESSAGE"
+}
+
+function init_branch_wc() {
+ local BRANCH_NAME=$1
+ local REPOS_URL=$2
+ local ROOT_URL=$REPOS_URL
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ fi
+ init_branch $BRANCH_NAME $REPOS_URL
+ svn checkout -q $ROOT_URL/branches/dev/Share/$BRANCH_NAME $TEST_DIR/wc
+}
+
+function init_merge_branches() {
+ local BRANCH_NAME=$1
+ local OTHER_BRANCH_NAME=$2
+ local REPOS_URL=$3
+ local ROOT_URL=$REPOS_URL
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ fi
+ init_branch_wc $BRANCH_NAME $REPOS_URL
+ cd $TEST_DIR/wc
+ file_list=$(find . -type f | sed "/\.svn/d" | sort | head -5)
+ other_file=$(find . -type f | sed "/\.svn/d" | sort | tail -1)
+ for file in $file_list; do
+ sed -i "s/for/FOR/g; s/fi/end if/g; s/in/IN/g;" $file
+ sed -i "/#/d; /^ *!/d" $file
+ sed -i "s/!/!!/g; s/q/\nq/g; s/[(]/(\n/g" $file
+ done
+ file_dir=$(dirname $file)
+ svn copy -q $file ./added_file
+ svn copy -q module added_directory
+ touch module/tree_conflict_file
+ svn add -q $file_dir/tree_conflict_file
+ echo "Modified a line" >>$other_file
+ svn commit -q -m "Made changes for future merge of this branch"
+ svn update -q
+ init_branch $OTHER_BRANCH_NAME $REPOS_URL
+ svn switch -q $ROOT_URL/branches/dev/Share/$OTHER_BRANCH_NAME
+ echo " " > unversioned_file
+ properties_file=$(find . -type f | sed " /\.svn/d" | sort | tail -3 | head -1)
+ svn propset -q svn:executable "executable" $properties_file
+ svn copy -q $file renamed_added_file
+ svn commit -q -m "Made changes for future merge"
+ svn update -q
+ svn switch -q $ROOT_URL/trunk
+ echo "trunk change" >>$(find . -type f | sed "/\.svn/d" | sort | head -1)
+ svn commit -q -m "Made trunk change"
+ svn update -q
+ echo "another trunk change" >>$(find . -type f | sed "/\.svn/d" | sort | head -1)
+ svn commit -q -m "Made another trunk change"
+ svn update -q
+}
+
+function run_pass() {
+ local TEST_KEY=$1
+ shift 1
+ if ! "$@" 1>$TEST_DIR/$TEST_KEY.out 2>$TEST_DIR/$TEST_KEY.err; then
+ fail $TEST_KEY
+ return
+ fi
+ pass $TEST_KEY
+}
+
+function run_fail() {
+ local TEST_KEY=$1
+ shift 1
+ if "$@" 1>$TEST_DIR/$TEST_KEY.out 2>$TEST_DIR/$TEST_KEY.err; then
+ fail $TEST_KEY
+ return
+ fi
+ pass $TEST_KEY
+}
+
+function setup() {
+ mkdir -p $TEST_DIR/.subversion
+ mkdir -p $TEST_DIR/run
+ cd $TEST_DIR/run
+}
+
+function teardown() {
+ cd $TEST_DIR
+ rm -rf $TEST_DIR/test_repos
+ rm -rf $TEST_DIR/wc
+ rm -rf $TEST_DIR/run
+ rm -rf $TEST_DIR/.subversion
+ if [[ -n ${TEST_REMOTE_HOST:-} ]]; then
+ ssh $TEST_REMOTE_HOST "rm -rf $TEST_REMOTE_DIR"
+ fi
+}
+
+REPOS_URL=
+ROOT_URL=
+PROJECT=
diff --git a/t/fcm-backup-svn-repos/00-basic.t b/t/fcm-backup-svn-repos/00-basic.t
new file mode 100755
index 0000000..bf0651b
--- /dev/null
+++ b/t/fcm-backup-svn-repos/00-basic.t
@@ -0,0 +1,103 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+# Basic tests for "fcm-backup-svn-repos".
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+if ! which svnadmin 1>/dev/null 2>/dev/null; then
+ skip_all 'svnadmin not available'
+fi
+tests 32
+#-------------------------------------------------------------------------------
+set -e
+mkdir -p etc srv/svn var/svn/{backups,cache,dumps}
+# Configuration
+export FCM_CONF_PATH="$PWD/etc"
+cat >etc/admin.cfg <<__CONF__
+svn_backup_dir=$PWD/var/svn/backups
+svn_dump_dir=$PWD/var/svn/dumps
+svn_live_dir=$PWD/srv/svn
+__CONF__
+# Create some repositories and populate them
+# Repository 1
+svnadmin create srv/svn/bar
+svn co -q file://$PWD/srv/svn/bar
+echo 'A man walks into a bar.' >bar/walk
+echo 'Barley drink.' >bar/barley
+svn add -q bar/*
+svn ci -q -m'test 1' bar
+svnadmin dump srv/svn/bar -r 1 --incremental --deltas -q \
+ | gzip >var/svn/dumps/bar-1.gz
+# Repository 2
+svnadmin create srv/svn/foo
+svn co -q file://$PWD/srv/svn/foo
+echo 'Food is yummy.' >foo/food
+svn add -q foo/*
+svn ci -q -m'test 1' foo
+svnadmin dump srv/svn/foo -r 1 --incremental --deltas -q \
+ | gzip >var/svn/dumps/foo-1.gz
+rm -fr bar foo
+set +e
+
+run_tests() {
+ local TEST_KEY=$1
+ run_pass "$TEST_KEY" "$FCM_HOME/sbin/fcm-backup-svn-repos"
+ for NAME in bar foo; do
+ file_test "$TEST_KEY-$NAME" var/svn/backups/$NAME.tgz
+ tar -xzf var/svn/backups/$NAME.tgz
+ svnlook youngest srv/svn/$NAME >"$TEST_KEY-$NAME.youngest.orig"
+ svnlook youngest $NAME >"$TEST_KEY-$NAME.youngest"
+ file_cmp "$TEST_KEY-$NAME.youngest" \
+ "$TEST_KEY-$NAME.youngest" "$TEST_KEY-$NAME.youngest.orig"
+ rm -fr $NAME
+ for REV in $(seq 1 $(<"$TEST_KEY-$NAME.youngest")); do
+ run_fail "$TEST_KEY-dumps-$NAME-$REV" ls var/svn/dumps/$NAME-$REV.gz
+ done
+ done
+}
+#-------------------------------------------------------------------------------
+run_tests "$TEST_KEY_BASE-1-1"
+run_tests "$TEST_KEY_BASE-1-2" # Re-run test
+#-------------------------------------------------------------------------------
+# Add more stuffs to repository 1
+svn co -q file://$PWD/srv/svn/bar
+for REV in {2..9}; do
+ echo "$REV men walk into a bar." >bar/walk
+ svn ci -m"test: $REV" bar/walk
+ svnadmin dump srv/svn/bar -r $REV --incremental --deltas -q \
+ | gzip >var/svn/dumps/bar-$REV.gz
+done
+# Add more stuffs to a copy of repository 1, to generate some more dumps To
+# prove that command will not housekeep dumps that are newer than the backed up
+# youngest.
+svnadmin hotcopy srv/svn/bar var/svn/cache/bar
+svn relocate file://$PWD/srv/svn/bar file://$PWD/var/svn/cache/bar bar
+for REV in {10..12}; do
+ echo "$REV men walk into a bar." >bar/walk
+ svn ci -m"test: $REV" bar/walk
+ svnadmin dump var/svn/cache/bar -r $REV --incremental --deltas -q \
+ | gzip >var/svn/dumps/bar-$REV.gz
+done
+run_tests "$TEST_KEY_BASE-2-1"
+for REV in {10..12}; do
+ file_test "$TEST_KEY_BASE-2-1-dumps-bar-$REV" var/svn/dumps/bar-$REV.gz
+done
+#-------------------------------------------------------------------------------
+exit
diff --git a/t/fcm-backup-svn-repos/test_header b/t/fcm-backup-svn-repos/test_header
new file mode 120000
index 0000000..90bd5a3
--- /dev/null
+++ b/t/fcm-backup-svn-repos/test_header
@@ -0,0 +1 @@
+../lib/bash/test_header
\ No newline at end of file
diff --git a/t/fcm-branch-create/00-simple.t b/t/fcm-branch-create/00-simple.t
new file mode 100644
index 0000000..a6789eb
--- /dev/null
+++ b/t/fcm-branch-create/00-simple.t
@@ -0,0 +1,96 @@
+#!/bin/bash
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# Basic tests for "fcm branch-create".
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+tests 12
+#-------------------------------------------------------------------------------
+setup
+init_repos
+init_branch_wc branch_test $REPOS_URL
+cd $TEST_DIR/wc
+#-------------------------------------------------------------------------------
+# Tests fcm branch-create
+TEST_KEY=$TEST_KEY_BASE-fcm-bc
+run_pass "$TEST_KEY" fcm branch-create -t SHARE --rev-flag=NONE \
+ --non-interactive \
+ my_branch_test
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+[info] Source: $ROOT_URL/trunk at 1 (4)
+Change summary:
+--------------------------------------------------------------------------------
+A $ROOT_URL/branches/dev/Share/my_branch_test
+--------------------------------------------------------------------------------
+Commit message is as follows:
+--------------------------------------------------------------------------------
+Created /${PROJECT}branches/dev/Share/my_branch_test from /${PROJECT}trunk at 1.
+--------------------------------------------------------------------------------
+
+Committed revision 5.
+[info] Created: $ROOT_URL/branches/dev/Share/my_branch_test
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests existence of branch
+TEST_KEY=$TEST_KEY_BASE-fcm-bc-branch-exists-sw
+run_pass "$TEST_KEY" svn switch \
+ $ROOT_URL/branches/dev/Share/my_branch_test
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<'__OUT__'
+At revision 5.
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+teardown
+#-------------------------------------------------------------------------------
+init_repos
+init_branch_wc branch_test $REPOS_URL
+cd $TEST_DIR/wc
+#-------------------------------------------------------------------------------
+# Tests fcm branch-create --branch-of-branch
+TEST_KEY=$TEST_KEY_BASE-fcm-bc-branch-of-branch
+run_pass "$TEST_KEY" fcm branch-create -t SHARE --rev-flag=NONE \
+ --non-interactive \
+ --branch-of-branch my_branch_test
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+[info] Source: $ROOT_URL/branches/dev/Share/branch_test at 4 (4)
+Change summary:
+--------------------------------------------------------------------------------
+A $ROOT_URL/branches/dev/Share/my_branch_test
+--------------------------------------------------------------------------------
+Commit message is as follows:
+--------------------------------------------------------------------------------
+Created /${PROJECT}branches/dev/Share/my_branch_test from /${PROJECT}branches/dev/Share/branch_test at 4.
+--------------------------------------------------------------------------------
+
+Committed revision 5.
+[info] Created: $ROOT_URL/branches/dev/Share/my_branch_test
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests existence of branch
+TEST_KEY=$TEST_KEY_BASE-fcm-bc-branch-of-branch-exists-sw
+run_pass "$TEST_KEY" svn switch \
+ $ROOT_URL/branches/dev/Share/my_branch_test
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<'__OUT__'
+At revision 5.
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+teardown
+#-------------------------------------------------------------------------------
diff --git a/t/fcm-branch-create/test_header b/t/fcm-branch-create/test_header
new file mode 100644
index 0000000..672ce12
--- /dev/null
+++ b/t/fcm-branch-create/test_header
@@ -0,0 +1,232 @@
+#!/bin/bash
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# Optional enviroment variables:
+# TEST_PROJECT (tests using given project name)
+# TEST_REMOTE_HOST (tests using svn+ssh repositories located on given host)
+# ------------------------------------------------------------------------------
+
+. $(dirname $0)/../lib/bash/test_header
+
+function file_cmp() {
+ local TEST_KEY=$1
+ local FILE_ACTUAL=$2
+ local FILE_EXPECT=${3:--}
+ if cmp $TEST_DIR/$FILE_ACTUAL $FILE_EXPECT; then
+ pass $TEST_KEY
+ return
+ fi
+ fail $TEST_KEY
+}
+
+function file_grep() {
+ local TEST_KEY=$1
+ local PATTERN=$2
+ local FILE=$3
+ if grep -q -e "$PATTERN" $TEST_DIR/$FILE; then
+ pass $TEST_KEY
+ return
+ fi
+ fail $TEST_KEY
+}
+
+function file_test() {
+ local TEST_KEY=$1
+ local FILE=$2
+ local OPTION=${3:--e}
+ if test $OPTION $TEST_DIR/$FILE; then
+ pass $TEST_KEY
+ else
+ fail $TEST_KEY
+ fi
+}
+
+function file_xxdiff() {
+ local TEST_KEY=$1
+ local FILE_ACTUAL=$2
+ local FILE_EXPECT=${3:--}
+ if xxdiff -D $TEST_DIR/$FILE_ACTUAL $FILE_EXPECT; then
+ pass $TEST_KEY
+ return
+ fi
+ fail $TEST_KEY
+}
+
+function init_repos() {
+ if [[ -n ${TEST_REMOTE_HOST:-} ]]; then
+ TEST_REMOTE_DIR=$(ssh $TEST_REMOTE_HOST "mktemp -d")
+ ssh $TEST_REMOTE_HOST "svnadmin create --fs-type fsfs $TEST_REMOTE_DIR"
+ REPOS_URL="svn+ssh://${TEST_REMOTE_HOST}$TEST_REMOTE_DIR"
+ else
+ svnadmin create --fs-type fsfs $TEST_DIR/test_repos
+ REPOS_URL="file://$TEST_DIR/test_repos"
+ fi
+ ROOT_URL=$REPOS_URL
+ PROJECT=
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ PROJECT=$TEST_PROJECT"/"
+ fi
+ svn import -q $TEST_SOURCE_DIR/../etc/repo_files \
+ $REPOS_URL/$PROJECT/trunk -m "initial trunk import"
+ svn mkdir -q $REPOS_URL/$PROJECT/tags -m "make tags"
+ svn mkdir -q --parents $REPOS_URL/$PROJECT/branches/dev/Share -m " "
+}
+
+function init_repos_layout_roses() {
+ if [[ -n ${TEST_REMOTE_HOST:-} ]]; then
+ TEST_REMOTE_DIR=$(ssh $TEST_REMOTE_HOST "mktemp -d")
+ ssh $TEST_REMOTE_HOST "svnadmin create --fs-type fsfs $TEST_REMOTE_DIR"
+ REPOS_URL="svn+ssh://${TEST_REMOTE_HOST}$TEST_REMOTE_DIR"
+ else
+ svnadmin create --fs-type fsfs $TEST_DIR/test_repos
+ REPOS_URL="file://$TEST_DIR/test_repos"
+ fi
+ svn mkdir -q --parents $REPOS_URL/a/a/0/0/0/trunk
+ svn import -q $TEST_SOURCE_DIR/../etc/repo_files \
+ $REPOS_URL/a/a/0/0/0/trunk -m "initial trunk import"
+ TMPFILE=$(mktemp)
+ cat >$TMPFILE <<__LAYOUT__
+depth-project = 5
+depth-branch = 1
+depth-tag = 1
+dir-trunk = trunk
+dir-branch =
+dir-tag =
+level-owner-branch =
+level-owner-tag =
+template-branch =
+template-tag =
+__LAYOUT__
+ TMPDIR=$(mktemp -d)
+ svn checkout -q $REPOS_URL $TMPDIR
+ svn propset -q --file=$TMPFILE fcm:layout $TMPDIR
+ svn commit -q -m " " $TMPDIR
+ rm -f $TMPFILE
+ rm -rf $TMPDIR
+ ROOT_URL=$REPOS_URL
+}
+
+function init_branch() {
+ local BRANCH_NAME=$1
+ local REPOS_URL=$2
+ local ROOT_URL=$REPOS_URL
+ local ROOT_PATH=
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ ROOT_PATH=$ROOT_PATH/$TEST_PROJECT
+ fi
+ MESSAGE=$(echo -e "Created $ROOT_PATH/branches/dev/Share/$BRANCH_NAME from /trunk at 1.")
+ svn copy -q -r1 $ROOT_URL/trunk $ROOT_URL/branches/dev/Share/$BRANCH_NAME \
+ -m "Made a branch $MESSAGE"
+}
+
+function init_branch_wc() {
+ local BRANCH_NAME=$1
+ local REPOS_URL=$2
+ local ROOT_URL=$REPOS_URL
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ fi
+ init_branch $BRANCH_NAME $REPOS_URL
+ svn checkout -q $ROOT_URL/branches/dev/Share/$BRANCH_NAME $TEST_DIR/wc
+}
+
+function init_merge_branches() {
+ local BRANCH_NAME=$1
+ local OTHER_BRANCH_NAME=$2
+ local REPOS_URL=$3
+ local ROOT_URL=$REPOS_URL
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ fi
+ init_branch_wc $BRANCH_NAME $REPOS_URL
+ cd $TEST_DIR/wc
+ file_list=$(find . -type f | sed "/\.svn/d" | sort | head -5)
+ other_file=$(find . -type f | sed "/\.svn/d" | sort | tail -1)
+ for file in $file_list; do
+ sed -i "s/for/FOR/g; s/fi/end if/g; s/in/IN/g;" $file
+ sed -i "/#/d; /^ *!/d" $file
+ sed -i "s/!/!!/g; s/q/\nq/g; s/[(]/(\n/g" $file
+ done
+ file_dir=$(dirname $file)
+ svn copy -q $file ./added_file
+ svn copy -q module added_directory
+ touch module/tree_conflict_file
+ svn add -q $file_dir/tree_conflict_file
+ echo "Modified a line" >>$other_file
+ svn commit -q -m "Made changes for future merge of this branch"
+ svn update -q
+ init_branch $OTHER_BRANCH_NAME $REPOS_URL
+ svn switch -q $ROOT_URL/branches/dev/Share/$OTHER_BRANCH_NAME
+ echo " " > unversioned_file
+ properties_file=$(find . -type f | sed " /\.svn/d" | sort | tail -3 | head -1)
+ svn propset -q svn:executable "executable" $properties_file
+ svn copy -q $file renamed_added_file
+ svn commit -q -m "Made changes for future merge"
+ svn update -q
+ svn switch -q $ROOT_URL/trunk
+ echo "trunk change" >>$(find . -type f | sed "/\.svn/d" | sort | head -1)
+ svn commit -q -m "Made trunk change"
+ svn update -q
+ echo "another trunk change" >>$(find . -type f | sed "/\.svn/d" | sort | head -1)
+ svn commit -q -m "Made another trunk change"
+ svn update -q
+}
+
+function run_pass() {
+ local TEST_KEY=$1
+ shift 1
+ if ! "$@" 1>$TEST_DIR/$TEST_KEY.out 2>$TEST_DIR/$TEST_KEY.err; then
+ fail $TEST_KEY
+ return
+ fi
+ pass $TEST_KEY
+}
+
+function run_fail() {
+ local TEST_KEY=$1
+ shift 1
+ if "$@" 1>$TEST_DIR/$TEST_KEY.out 2>$TEST_DIR/$TEST_KEY.err; then
+ fail $TEST_KEY
+ return
+ fi
+ pass $TEST_KEY
+}
+
+function setup() {
+ mkdir -p $TEST_DIR/.subversion
+ mkdir -p $TEST_DIR/run
+ cd $TEST_DIR/run
+}
+
+function teardown() {
+ cd $TEST_DIR
+ rm -rf $TEST_DIR/test_repos
+ rm -rf $TEST_DIR/wc
+ rm -rf $TEST_DIR/run
+ rm -rf $TEST_DIR/.subversion
+ if [[ -n ${TEST_REMOTE_HOST:-} ]]; then
+ ssh $TEST_REMOTE_HOST "rm -rf $TEST_REMOTE_DIR"
+ fi
+}
+
+REPOS_URL=
+ROOT_URL=
+PROJECT=
diff --git a/t/fcm-branch-delete/00-simple.t b/t/fcm-branch-delete/00-simple.t
new file mode 100644
index 0000000..e516af0
--- /dev/null
+++ b/t/fcm-branch-delete/00-simple.t
@@ -0,0 +1,65 @@
+#!/bin/bash
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# Basic tests for "fcm branch-create".
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+tests 12
+#-------------------------------------------------------------------------------
+setup
+init_repos
+init_branch branch_test $REPOS_URL
+init_branch_wc my_branch_test $REPOS_URL
+cd $TEST_DIR/wc
+#-------------------------------------------------------------------------------
+# Tests fcm branch-delete
+TEST_KEY=$TEST_KEY_BASE-delete
+run_pass "$TEST_KEY" fcm branch-delete --non-interactive $ROOT_URL/branches/dev/Share/branch_test
+file_grep "$TEST_KEY.out" "Deleting branch $ROOT_URL/branches/dev/Share/branch_test ..." \
+ "$TEST_KEY.out"
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests existence of branch
+TEST_KEY=$TEST_KEY_BASE-delete-branch-exists
+run_fail "$TEST_KEY" svn info \
+ $ROOT_URL/branches/dev/Share/branch_test
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" </dev/null
+file_test "$TEST_KEY.err" "$TEST_KEY.err" -s
+teardown
+#-------------------------------------------------------------------------------
+init_repos
+init_branch_wc branch_test $REPOS_URL
+cd $TEST_DIR/wc
+#-------------------------------------------------------------------------------
+# Tests fcm brm
+TEST_KEY=$TEST_KEY_BASE-brm
+run_pass "$TEST_KEY" fcm brm --non-interactive $ROOT_URL/branches/dev/Share/branch_test
+file_grep "$TEST_KEY.out" "Deleting branch $ROOT_URL/branches/dev/Share/branch_test ..." \
+ "$TEST_KEY.out"
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm brm disappearance of branch
+TEST_KEY=$TEST_KEY_BASE-brm-branch-exists
+run_fail "$TEST_KEY" svn info \
+ $ROOT_URL/branches/dev/Share/branch_test
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" </dev/null
+file_test "$TEST_KEY.err" "$TEST_KEY.err" -s
+teardown
+#-------------------------------------------------------------------------------
diff --git a/t/fcm-branch-delete/test_header b/t/fcm-branch-delete/test_header
new file mode 100644
index 0000000..672ce12
--- /dev/null
+++ b/t/fcm-branch-delete/test_header
@@ -0,0 +1,232 @@
+#!/bin/bash
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# Optional enviroment variables:
+# TEST_PROJECT (tests using given project name)
+# TEST_REMOTE_HOST (tests using svn+ssh repositories located on given host)
+# ------------------------------------------------------------------------------
+
+. $(dirname $0)/../lib/bash/test_header
+
+function file_cmp() {
+ local TEST_KEY=$1
+ local FILE_ACTUAL=$2
+ local FILE_EXPECT=${3:--}
+ if cmp $TEST_DIR/$FILE_ACTUAL $FILE_EXPECT; then
+ pass $TEST_KEY
+ return
+ fi
+ fail $TEST_KEY
+}
+
+function file_grep() {
+ local TEST_KEY=$1
+ local PATTERN=$2
+ local FILE=$3
+ if grep -q -e "$PATTERN" $TEST_DIR/$FILE; then
+ pass $TEST_KEY
+ return
+ fi
+ fail $TEST_KEY
+}
+
+function file_test() {
+ local TEST_KEY=$1
+ local FILE=$2
+ local OPTION=${3:--e}
+ if test $OPTION $TEST_DIR/$FILE; then
+ pass $TEST_KEY
+ else
+ fail $TEST_KEY
+ fi
+}
+
+function file_xxdiff() {
+ local TEST_KEY=$1
+ local FILE_ACTUAL=$2
+ local FILE_EXPECT=${3:--}
+ if xxdiff -D $TEST_DIR/$FILE_ACTUAL $FILE_EXPECT; then
+ pass $TEST_KEY
+ return
+ fi
+ fail $TEST_KEY
+}
+
+function init_repos() {
+ if [[ -n ${TEST_REMOTE_HOST:-} ]]; then
+ TEST_REMOTE_DIR=$(ssh $TEST_REMOTE_HOST "mktemp -d")
+ ssh $TEST_REMOTE_HOST "svnadmin create --fs-type fsfs $TEST_REMOTE_DIR"
+ REPOS_URL="svn+ssh://${TEST_REMOTE_HOST}$TEST_REMOTE_DIR"
+ else
+ svnadmin create --fs-type fsfs $TEST_DIR/test_repos
+ REPOS_URL="file://$TEST_DIR/test_repos"
+ fi
+ ROOT_URL=$REPOS_URL
+ PROJECT=
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ PROJECT=$TEST_PROJECT"/"
+ fi
+ svn import -q $TEST_SOURCE_DIR/../etc/repo_files \
+ $REPOS_URL/$PROJECT/trunk -m "initial trunk import"
+ svn mkdir -q $REPOS_URL/$PROJECT/tags -m "make tags"
+ svn mkdir -q --parents $REPOS_URL/$PROJECT/branches/dev/Share -m " "
+}
+
+function init_repos_layout_roses() {
+ if [[ -n ${TEST_REMOTE_HOST:-} ]]; then
+ TEST_REMOTE_DIR=$(ssh $TEST_REMOTE_HOST "mktemp -d")
+ ssh $TEST_REMOTE_HOST "svnadmin create --fs-type fsfs $TEST_REMOTE_DIR"
+ REPOS_URL="svn+ssh://${TEST_REMOTE_HOST}$TEST_REMOTE_DIR"
+ else
+ svnadmin create --fs-type fsfs $TEST_DIR/test_repos
+ REPOS_URL="file://$TEST_DIR/test_repos"
+ fi
+ svn mkdir -q --parents $REPOS_URL/a/a/0/0/0/trunk
+ svn import -q $TEST_SOURCE_DIR/../etc/repo_files \
+ $REPOS_URL/a/a/0/0/0/trunk -m "initial trunk import"
+ TMPFILE=$(mktemp)
+ cat >$TMPFILE <<__LAYOUT__
+depth-project = 5
+depth-branch = 1
+depth-tag = 1
+dir-trunk = trunk
+dir-branch =
+dir-tag =
+level-owner-branch =
+level-owner-tag =
+template-branch =
+template-tag =
+__LAYOUT__
+ TMPDIR=$(mktemp -d)
+ svn checkout -q $REPOS_URL $TMPDIR
+ svn propset -q --file=$TMPFILE fcm:layout $TMPDIR
+ svn commit -q -m " " $TMPDIR
+ rm -f $TMPFILE
+ rm -rf $TMPDIR
+ ROOT_URL=$REPOS_URL
+}
+
+function init_branch() {
+ local BRANCH_NAME=$1
+ local REPOS_URL=$2
+ local ROOT_URL=$REPOS_URL
+ local ROOT_PATH=
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ ROOT_PATH=$ROOT_PATH/$TEST_PROJECT
+ fi
+ MESSAGE=$(echo -e "Created $ROOT_PATH/branches/dev/Share/$BRANCH_NAME from /trunk at 1.")
+ svn copy -q -r1 $ROOT_URL/trunk $ROOT_URL/branches/dev/Share/$BRANCH_NAME \
+ -m "Made a branch $MESSAGE"
+}
+
+function init_branch_wc() {
+ local BRANCH_NAME=$1
+ local REPOS_URL=$2
+ local ROOT_URL=$REPOS_URL
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ fi
+ init_branch $BRANCH_NAME $REPOS_URL
+ svn checkout -q $ROOT_URL/branches/dev/Share/$BRANCH_NAME $TEST_DIR/wc
+}
+
+function init_merge_branches() {
+ local BRANCH_NAME=$1
+ local OTHER_BRANCH_NAME=$2
+ local REPOS_URL=$3
+ local ROOT_URL=$REPOS_URL
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ fi
+ init_branch_wc $BRANCH_NAME $REPOS_URL
+ cd $TEST_DIR/wc
+ file_list=$(find . -type f | sed "/\.svn/d" | sort | head -5)
+ other_file=$(find . -type f | sed "/\.svn/d" | sort | tail -1)
+ for file in $file_list; do
+ sed -i "s/for/FOR/g; s/fi/end if/g; s/in/IN/g;" $file
+ sed -i "/#/d; /^ *!/d" $file
+ sed -i "s/!/!!/g; s/q/\nq/g; s/[(]/(\n/g" $file
+ done
+ file_dir=$(dirname $file)
+ svn copy -q $file ./added_file
+ svn copy -q module added_directory
+ touch module/tree_conflict_file
+ svn add -q $file_dir/tree_conflict_file
+ echo "Modified a line" >>$other_file
+ svn commit -q -m "Made changes for future merge of this branch"
+ svn update -q
+ init_branch $OTHER_BRANCH_NAME $REPOS_URL
+ svn switch -q $ROOT_URL/branches/dev/Share/$OTHER_BRANCH_NAME
+ echo " " > unversioned_file
+ properties_file=$(find . -type f | sed " /\.svn/d" | sort | tail -3 | head -1)
+ svn propset -q svn:executable "executable" $properties_file
+ svn copy -q $file renamed_added_file
+ svn commit -q -m "Made changes for future merge"
+ svn update -q
+ svn switch -q $ROOT_URL/trunk
+ echo "trunk change" >>$(find . -type f | sed "/\.svn/d" | sort | head -1)
+ svn commit -q -m "Made trunk change"
+ svn update -q
+ echo "another trunk change" >>$(find . -type f | sed "/\.svn/d" | sort | head -1)
+ svn commit -q -m "Made another trunk change"
+ svn update -q
+}
+
+function run_pass() {
+ local TEST_KEY=$1
+ shift 1
+ if ! "$@" 1>$TEST_DIR/$TEST_KEY.out 2>$TEST_DIR/$TEST_KEY.err; then
+ fail $TEST_KEY
+ return
+ fi
+ pass $TEST_KEY
+}
+
+function run_fail() {
+ local TEST_KEY=$1
+ shift 1
+ if "$@" 1>$TEST_DIR/$TEST_KEY.out 2>$TEST_DIR/$TEST_KEY.err; then
+ fail $TEST_KEY
+ return
+ fi
+ pass $TEST_KEY
+}
+
+function setup() {
+ mkdir -p $TEST_DIR/.subversion
+ mkdir -p $TEST_DIR/run
+ cd $TEST_DIR/run
+}
+
+function teardown() {
+ cd $TEST_DIR
+ rm -rf $TEST_DIR/test_repos
+ rm -rf $TEST_DIR/wc
+ rm -rf $TEST_DIR/run
+ rm -rf $TEST_DIR/.subversion
+ if [[ -n ${TEST_REMOTE_HOST:-} ]]; then
+ ssh $TEST_REMOTE_HOST "rm -rf $TEST_REMOTE_DIR"
+ fi
+}
+
+REPOS_URL=
+ROOT_URL=
+PROJECT=
diff --git a/t/fcm-branch-diff/00-simple.t b/t/fcm-branch-diff/00-simple.t
new file mode 100644
index 0000000..59d2680
--- /dev/null
+++ b/t/fcm-branch-diff/00-simple.t
@@ -0,0 +1,664 @@
+#!/bin/bash
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# Basic tests for "fcm branch-diff".
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+tests 18
+#-------------------------------------------------------------------------------
+setup
+init_repos
+init_branch_wc branch_test $REPOS_URL
+cd $TEST_DIR/wc
+FILE_LIST=$(find . -type f | sed "/\.svn/d" | sort | head -5)
+for FILE in $FILE_LIST; do
+ sed -i "s/for/FOR/g; s/fi/end if/g; s/in/IN/g;" $FILE
+ sed -i "/#/d; /^ *!/d" $FILE
+ sed -i "s/!/!!/g; s/q/\nq/g; s/[(]/(\n/g" $FILE
+done
+FILE_DIR=$(dirname $FILE)
+svn copy -q $FILE added_file
+svn copy -q $FILE_DIR added_directory
+svn delete --force -q $FILE_DIR
+svn commit -q -m "make branch diff"
+svn switch -q $ROOT_URL/trunk
+TMPFILE=$(mktemp)
+for FILE in $FILE_LIST; do
+ if [[ -e $FILE ]]; then
+ tac $FILE > $TMPFILE && mv $TMPFILE $FILE
+ fi
+done
+rm -f $TMPFILE
+svn commit -q -m "make trunk diff"
+svn switch -q $ROOT_URL/branches/dev/Share/branch_test
+#-------------------------------------------------------------------------------
+# Tests fcm branch-diff
+TEST_KEY=$TEST_KEY_BASE-fcm-branch-diff
+run_pass "$TEST_KEY" fcm branch-diff
+if $SVN_VERSION_IS_16; then
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+Index: added_file
+===================================================================
+--- added_file (.../$ROOT_URL/trunk) (revision 0)
++++ added_file (revision 6)
+@@ -0,0 +1 @@
++INCLUDE 'hello_constants.INc'
+Index: added_directory/hello_constants_dummy.inc
+===================================================================
+--- added_directory/hello_constants_dummy.inc (.../$ROOT_URL/trunk) (revision 0)
++++ added_directory/hello_constants_dummy.inc (revision 6)
+@@ -0,0 +1 @@
++INCLUDE 'hello_constants.INc'
+Index: added_directory/hello_constants.inc
+===================================================================
+--- added_directory/hello_constants.inc (.../$ROOT_URL/trunk) (revision 0)
++++ added_directory/hello_constants.inc (revision 6)
+@@ -0,0 +1,2 @@
++CHARACTER (
++LEN=80), PARAMETER :: hello_strINg = 'Hello Earth!!'
+Index: added_directory/hello_constants.f90
+===================================================================
+--- added_directory/hello_constants.f90 (.../$ROOT_URL/trunk) (revision 0)
++++ added_directory/hello_constants.f90 (revision 6)
+@@ -0,0 +1,5 @@
++MODULE Hello_Constants
++
++INCLUDE 'hello_constants_dummy.INc'
++
++END MODULE Hello_Constants
+Index: module/hello_constants_dummy.inc
+===================================================================
+--- module/hello_constants_dummy.inc (.../$ROOT_URL/trunk) (revision 1)
++++ module/hello_constants_dummy.inc (working copy)
+@@ -1 +0,0 @@
+-INCLUDE 'hello_constants.inc'
+Index: module/hello_constants.inc
+===================================================================
+--- module/hello_constants.inc (.../$ROOT_URL/trunk) (revision 1)
++++ module/hello_constants.inc (working copy)
+@@ -1 +0,0 @@
+-CHARACTER (LEN=80), PARAMETER :: hello_string = 'Hello Earth!'
+Index: module/hello_constants.f90
+===================================================================
+--- module/hello_constants.f90 (.../$ROOT_URL/trunk) (revision 1)
++++ module/hello_constants.f90 (working copy)
+@@ -1,5 +0,0 @@
+-MODULE Hello_Constants
+-
+-INCLUDE 'hello_constants_dummy.inc'
+-
+-END MODULE Hello_Constants
+Index: lib/python/info/poems.py
+===================================================================
+--- lib/python/info/poems.py (.../$ROOT_URL/trunk) (revision 1)
++++ lib/python/info/poems.py (working copy)
+@@ -1,24 +1,23 @@
+-#!/usr/bin/env python
+-# -*- coding: utf-8 -*-
+ """The Python, by Hilaire Belloc
+
+ A Python I should not advise,--
+-It needs a doctor for its eyes,
++It needs a doctor FOR its eyes,
+ And has the measles yearly.
+-However, if you feel inclined
+-To get one (to improve your mind,
++However, if you feel INclINed
++To get one (
++to improve your mINd,
+ And not from fashion merely),
+ Allow no music near its cage;
+-And when it flies into a rage
++And when it flies INto a rage
+ Chastise it, most severely.
+-I had an aunt in Yucatan
++I had an aunt IN Yucatan
+ Who bought a Python from a man
+-And kept it for a pet.
++And kept it FOR a pet.
+ She died, because she never knew
+ These simple little rules and few;--
+-The Snake is living yet.
++The Snake is livINg yet.
+ """
+
+ import this
+
+-print "\n", __doc__
++prINt "\n", __doc__
+__OUT__
+else
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+Index: module/hello_constants_dummy.inc
+===================================================================
+--- module/hello_constants_dummy.inc ($ROOT_URL/trunk) (revision 1)
++++ module/hello_constants_dummy.inc (working copy)
+@@ -1 +0,0 @@
+-INCLUDE 'hello_constants.inc'
+Index: module/hello_constants.inc
+===================================================================
+--- module/hello_constants.inc ($ROOT_URL/trunk) (revision 1)
++++ module/hello_constants.inc (working copy)
+@@ -1 +0,0 @@
+-CHARACTER (LEN=80), PARAMETER :: hello_string = 'Hello Earth!'
+Index: module/hello_constants.f90
+===================================================================
+--- module/hello_constants.f90 ($ROOT_URL/trunk) (revision 1)
++++ module/hello_constants.f90 (working copy)
+@@ -1,5 +0,0 @@
+-MODULE Hello_Constants
+-
+-INCLUDE 'hello_constants_dummy.inc'
+-
+-END MODULE Hello_Constants
+Index: lib/python/info/poems.py
+===================================================================
+--- lib/python/info/poems.py ($ROOT_URL/trunk) (revision 1)
++++ lib/python/info/poems.py (working copy)
+@@ -1,24 +1,23 @@
+-#!/usr/bin/env python
+-# -*- coding: utf-8 -*-
+ """The Python, by Hilaire Belloc
+
+ A Python I should not advise,--
+-It needs a doctor for its eyes,
++It needs a doctor FOR its eyes,
+ And has the measles yearly.
+-However, if you feel inclined
+-To get one (to improve your mind,
++However, if you feel INclINed
++To get one (
++to improve your mINd,
+ And not from fashion merely),
+ Allow no music near its cage;
+-And when it flies into a rage
++And when it flies INto a rage
+ Chastise it, most severely.
+-I had an aunt in Yucatan
++I had an aunt IN Yucatan
+ Who bought a Python from a man
+-And kept it for a pet.
++And kept it FOR a pet.
+ She died, because she never knew
+ These simple little rules and few;--
+-The Snake is living yet.
++The Snake is livINg yet.
+ """
+
+ import this
+
+-print "\n", __doc__
++prINt "\n", __doc__
+Index: added_directory/hello_constants.f90
+===================================================================
+--- added_directory/hello_constants.f90 ($ROOT_URL/trunk) (revision 0)
++++ added_directory/hello_constants.f90 (revision 6)
+@@ -0,0 +1,5 @@
++MODULE Hello_Constants
++
++INCLUDE 'hello_constants_dummy.INc'
++
++END MODULE Hello_Constants
+Index: added_directory/hello_constants.inc
+===================================================================
+--- added_directory/hello_constants.inc ($ROOT_URL/trunk) (revision 0)
++++ added_directory/hello_constants.inc (revision 6)
+@@ -0,0 +1,2 @@
++CHARACTER (
++LEN=80), PARAMETER :: hello_strINg = 'Hello Earth!!'
+Index: added_directory/hello_constants_dummy.inc
+===================================================================
+--- added_directory/hello_constants_dummy.inc ($ROOT_URL/trunk) (revision 0)
++++ added_directory/hello_constants_dummy.inc (revision 6)
+@@ -0,0 +1 @@
++INCLUDE 'hello_constants.INc'
+Index: added_file
+===================================================================
+--- added_file ($ROOT_URL/trunk) (revision 0)
++++ added_file (revision 6)
+@@ -0,0 +1 @@
++INCLUDE 'hello_constants.INc'
+__OUT__
+fi
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm bdi
+TEST_KEY=$TEST_KEY_BASE-bdi
+run_pass "$TEST_KEY" fcm bdi
+if $SVN_VERSION_IS_16; then
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+Index: added_file
+===================================================================
+--- added_file (.../$ROOT_URL/trunk) (revision 0)
++++ added_file (revision 6)
+@@ -0,0 +1 @@
++INCLUDE 'hello_constants.INc'
+Index: added_directory/hello_constants_dummy.inc
+===================================================================
+--- added_directory/hello_constants_dummy.inc (.../$ROOT_URL/trunk) (revision 0)
++++ added_directory/hello_constants_dummy.inc (revision 6)
+@@ -0,0 +1 @@
++INCLUDE 'hello_constants.INc'
+Index: added_directory/hello_constants.inc
+===================================================================
+--- added_directory/hello_constants.inc (.../$ROOT_URL/trunk) (revision 0)
++++ added_directory/hello_constants.inc (revision 6)
+@@ -0,0 +1,2 @@
++CHARACTER (
++LEN=80), PARAMETER :: hello_strINg = 'Hello Earth!!'
+Index: added_directory/hello_constants.f90
+===================================================================
+--- added_directory/hello_constants.f90 (.../$ROOT_URL/trunk) (revision 0)
++++ added_directory/hello_constants.f90 (revision 6)
+@@ -0,0 +1,5 @@
++MODULE Hello_Constants
++
++INCLUDE 'hello_constants_dummy.INc'
++
++END MODULE Hello_Constants
+Index: module/hello_constants_dummy.inc
+===================================================================
+--- module/hello_constants_dummy.inc (.../$ROOT_URL/trunk) (revision 1)
++++ module/hello_constants_dummy.inc (working copy)
+@@ -1 +0,0 @@
+-INCLUDE 'hello_constants.inc'
+Index: module/hello_constants.inc
+===================================================================
+--- module/hello_constants.inc (.../$ROOT_URL/trunk) (revision 1)
++++ module/hello_constants.inc (working copy)
+@@ -1 +0,0 @@
+-CHARACTER (LEN=80), PARAMETER :: hello_string = 'Hello Earth!'
+Index: module/hello_constants.f90
+===================================================================
+--- module/hello_constants.f90 (.../$ROOT_URL/trunk) (revision 1)
++++ module/hello_constants.f90 (working copy)
+@@ -1,5 +0,0 @@
+-MODULE Hello_Constants
+-
+-INCLUDE 'hello_constants_dummy.inc'
+-
+-END MODULE Hello_Constants
+Index: lib/python/info/poems.py
+===================================================================
+--- lib/python/info/poems.py (.../$ROOT_URL/trunk) (revision 1)
++++ lib/python/info/poems.py (working copy)
+@@ -1,24 +1,23 @@
+-#!/usr/bin/env python
+-# -*- coding: utf-8 -*-
+ """The Python, by Hilaire Belloc
+
+ A Python I should not advise,--
+-It needs a doctor for its eyes,
++It needs a doctor FOR its eyes,
+ And has the measles yearly.
+-However, if you feel inclined
+-To get one (to improve your mind,
++However, if you feel INclINed
++To get one (
++to improve your mINd,
+ And not from fashion merely),
+ Allow no music near its cage;
+-And when it flies into a rage
++And when it flies INto a rage
+ Chastise it, most severely.
+-I had an aunt in Yucatan
++I had an aunt IN Yucatan
+ Who bought a Python from a man
+-And kept it for a pet.
++And kept it FOR a pet.
+ She died, because she never knew
+ These simple little rules and few;--
+-The Snake is living yet.
++The Snake is livINg yet.
+ """
+
+ import this
+
+-print "\n", __doc__
++prINt "\n", __doc__
+__OUT__
+else
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+Index: module/hello_constants_dummy.inc
+===================================================================
+--- module/hello_constants_dummy.inc ($ROOT_URL/trunk) (revision 1)
++++ module/hello_constants_dummy.inc (working copy)
+@@ -1 +0,0 @@
+-INCLUDE 'hello_constants.inc'
+Index: module/hello_constants.inc
+===================================================================
+--- module/hello_constants.inc ($ROOT_URL/trunk) (revision 1)
++++ module/hello_constants.inc (working copy)
+@@ -1 +0,0 @@
+-CHARACTER (LEN=80), PARAMETER :: hello_string = 'Hello Earth!'
+Index: module/hello_constants.f90
+===================================================================
+--- module/hello_constants.f90 ($ROOT_URL/trunk) (revision 1)
++++ module/hello_constants.f90 (working copy)
+@@ -1,5 +0,0 @@
+-MODULE Hello_Constants
+-
+-INCLUDE 'hello_constants_dummy.inc'
+-
+-END MODULE Hello_Constants
+Index: lib/python/info/poems.py
+===================================================================
+--- lib/python/info/poems.py ($ROOT_URL/trunk) (revision 1)
++++ lib/python/info/poems.py (working copy)
+@@ -1,24 +1,23 @@
+-#!/usr/bin/env python
+-# -*- coding: utf-8 -*-
+ """The Python, by Hilaire Belloc
+
+ A Python I should not advise,--
+-It needs a doctor for its eyes,
++It needs a doctor FOR its eyes,
+ And has the measles yearly.
+-However, if you feel inclined
+-To get one (to improve your mind,
++However, if you feel INclINed
++To get one (
++to improve your mINd,
+ And not from fashion merely),
+ Allow no music near its cage;
+-And when it flies into a rage
++And when it flies INto a rage
+ Chastise it, most severely.
+-I had an aunt in Yucatan
++I had an aunt IN Yucatan
+ Who bought a Python from a man
+-And kept it for a pet.
++And kept it FOR a pet.
+ She died, because she never knew
+ These simple little rules and few;--
+-The Snake is living yet.
++The Snake is livINg yet.
+ """
+
+ import this
+
+-print "\n", __doc__
++prINt "\n", __doc__
+Index: added_directory/hello_constants.f90
+===================================================================
+--- added_directory/hello_constants.f90 ($ROOT_URL/trunk) (revision 0)
++++ added_directory/hello_constants.f90 (revision 6)
+@@ -0,0 +1,5 @@
++MODULE Hello_Constants
++
++INCLUDE 'hello_constants_dummy.INc'
++
++END MODULE Hello_Constants
+Index: added_directory/hello_constants.inc
+===================================================================
+--- added_directory/hello_constants.inc ($ROOT_URL/trunk) (revision 0)
++++ added_directory/hello_constants.inc (revision 6)
+@@ -0,0 +1,2 @@
++CHARACTER (
++LEN=80), PARAMETER :: hello_strINg = 'Hello Earth!!'
+Index: added_directory/hello_constants_dummy.inc
+===================================================================
+--- added_directory/hello_constants_dummy.inc ($ROOT_URL/trunk) (revision 0)
++++ added_directory/hello_constants_dummy.inc (revision 6)
+@@ -0,0 +1 @@
++INCLUDE 'hello_constants.INc'
+Index: added_file
+===================================================================
+--- added_file ($ROOT_URL/trunk) (revision 0)
++++ added_file (revision 6)
+@@ -0,0 +1 @@
++INCLUDE 'hello_constants.INc'
+__OUT__
+fi
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm branch-diff --wiki
+TEST_KEY=$TEST_KEY_BASE-wiki
+run_pass "$TEST_KEY" fcm branch-diff --wiki
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+diff:/${PROJECT}trunk at 1///${PROJECT}branches/dev/Share/branch_test at 6
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm bdi --wiki
+TEST_KEY=$TEST_KEY_BASE-bdi-wiki
+run_pass "$TEST_KEY" fcm bdi --wiki
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+diff:/${PROJECT}trunk at 1///${PROJECT}branches/dev/Share/branch_test at 6
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm bdi on the trunk
+svn switch -q $ROOT_URL/trunk
+TEST_KEY=$TEST_KEY_BASE-bdi-trunk
+run_fail "$TEST_KEY" fcm bdi
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" </dev/null
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" <<__ERR__
+[FAIL] $ROOT_URL/trunk at 6: not a valid URL of a standard FCM branch.
+
+__ERR__
+#-------------------------------------------------------------------------------
+# Tests fcm bdi with working copy changes
+svn switch -q $ROOT_URL/branches/dev/Share/branch_test
+TEST_KEY=$TEST_KEY_BASE-bdi-wc-changes
+echo "foo" > added_directory/foo$TEST_KEY
+svn add -q added_directory/foo$TEST_KEY
+echo "bar" > added_directory/bar$TEST_KEY
+run_pass "$TEST_KEY" fcm bdi
+if $SVN_VERSION_IS_16; then
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+Index: added_file
+===================================================================
+--- added_file (.../$ROOT_URL/trunk) (revision 0)
++++ added_file (revision 6)
+@@ -0,0 +1 @@
++INCLUDE 'hello_constants.INc'
+Index: added_directory/hello_constants_dummy.inc
+===================================================================
+--- added_directory/hello_constants_dummy.inc (.../$ROOT_URL/trunk) (revision 0)
++++ added_directory/hello_constants_dummy.inc (revision 6)
+@@ -0,0 +1 @@
++INCLUDE 'hello_constants.INc'
+Index: added_directory/foo00-simple-bdi-wc-changes
+===================================================================
+--- added_directory/foo00-simple-bdi-wc-changes (.../$ROOT_URL/trunk) (revision 0)
++++ added_directory/foo00-simple-bdi-wc-changes (revision 0)
+@@ -0,0 +1 @@
++foo
+Index: added_directory/hello_constants.inc
+===================================================================
+--- added_directory/hello_constants.inc (.../$ROOT_URL/trunk) (revision 0)
++++ added_directory/hello_constants.inc (revision 6)
+@@ -0,0 +1,2 @@
++CHARACTER (
++LEN=80), PARAMETER :: hello_strINg = 'Hello Earth!!'
+Index: added_directory/hello_constants.f90
+===================================================================
+--- added_directory/hello_constants.f90 (.../$ROOT_URL/trunk) (revision 0)
++++ added_directory/hello_constants.f90 (revision 6)
+@@ -0,0 +1,5 @@
++MODULE Hello_Constants
++
++INCLUDE 'hello_constants_dummy.INc'
++
++END MODULE Hello_Constants
+Index: module/hello_constants_dummy.inc
+===================================================================
+--- module/hello_constants_dummy.inc (.../$ROOT_URL/trunk) (revision 1)
++++ module/hello_constants_dummy.inc (working copy)
+@@ -1 +0,0 @@
+-INCLUDE 'hello_constants.inc'
+Index: module/hello_constants.inc
+===================================================================
+--- module/hello_constants.inc (.../$ROOT_URL/trunk) (revision 1)
++++ module/hello_constants.inc (working copy)
+@@ -1 +0,0 @@
+-CHARACTER (LEN=80), PARAMETER :: hello_string = 'Hello Earth!'
+Index: module/hello_constants.f90
+===================================================================
+--- module/hello_constants.f90 (.../$ROOT_URL/trunk) (revision 1)
++++ module/hello_constants.f90 (working copy)
+@@ -1,5 +0,0 @@
+-MODULE Hello_Constants
+-
+-INCLUDE 'hello_constants_dummy.inc'
+-
+-END MODULE Hello_Constants
+Index: lib/python/info/poems.py
+===================================================================
+--- lib/python/info/poems.py (.../$ROOT_URL/trunk) (revision 1)
++++ lib/python/info/poems.py (working copy)
+@@ -1,24 +1,23 @@
+-#!/usr/bin/env python
+-# -*- coding: utf-8 -*-
+ """The Python, by Hilaire Belloc
+
+ A Python I should not advise,--
+-It needs a doctor for its eyes,
++It needs a doctor FOR its eyes,
+ And has the measles yearly.
+-However, if you feel inclined
+-To get one (to improve your mind,
++However, if you feel INclINed
++To get one (
++to improve your mINd,
+ And not from fashion merely),
+ Allow no music near its cage;
+-And when it flies into a rage
++And when it flies INto a rage
+ Chastise it, most severely.
+-I had an aunt in Yucatan
++I had an aunt IN Yucatan
+ Who bought a Python from a man
+-And kept it for a pet.
++And kept it FOR a pet.
+ She died, because she never knew
+ These simple little rules and few;--
+-The Snake is living yet.
++The Snake is livINg yet.
+ """
+
+ import this
+
+-print "\n", __doc__
++prINt "\n", __doc__
+__OUT__
+else
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+Index: module/hello_constants_dummy.inc
+===================================================================
+--- module/hello_constants_dummy.inc ($ROOT_URL/trunk) (revision 1)
++++ module/hello_constants_dummy.inc (working copy)
+@@ -1 +0,0 @@
+-INCLUDE 'hello_constants.inc'
+Index: module/hello_constants.inc
+===================================================================
+--- module/hello_constants.inc ($ROOT_URL/trunk) (revision 1)
++++ module/hello_constants.inc (working copy)
+@@ -1 +0,0 @@
+-CHARACTER (LEN=80), PARAMETER :: hello_string = 'Hello Earth!'
+Index: module/hello_constants.f90
+===================================================================
+--- module/hello_constants.f90 ($ROOT_URL/trunk) (revision 1)
++++ module/hello_constants.f90 (working copy)
+@@ -1,5 +0,0 @@
+-MODULE Hello_Constants
+-
+-INCLUDE 'hello_constants_dummy.inc'
+-
+-END MODULE Hello_Constants
+Index: lib/python/info/poems.py
+===================================================================
+--- lib/python/info/poems.py ($ROOT_URL/trunk) (revision 1)
++++ lib/python/info/poems.py (working copy)
+@@ -1,24 +1,23 @@
+-#!/usr/bin/env python
+-# -*- coding: utf-8 -*-
+ """The Python, by Hilaire Belloc
+
+ A Python I should not advise,--
+-It needs a doctor for its eyes,
++It needs a doctor FOR its eyes,
+ And has the measles yearly.
+-However, if you feel inclined
+-To get one (to improve your mind,
++However, if you feel INclINed
++To get one (
++to improve your mINd,
+ And not from fashion merely),
+ Allow no music near its cage;
+-And when it flies into a rage
++And when it flies INto a rage
+ Chastise it, most severely.
+-I had an aunt in Yucatan
++I had an aunt IN Yucatan
+ Who bought a Python from a man
+-And kept it for a pet.
++And kept it FOR a pet.
+ She died, because she never knew
+ These simple little rules and few;--
+-The Snake is living yet.
++The Snake is livINg yet.
+ """
+
+ import this
+
+-print "\n", __doc__
++prINt "\n", __doc__
+Index: added_directory/foo00-simple-bdi-wc-changes
+===================================================================
+--- added_directory/foo00-simple-bdi-wc-changes ($ROOT_URL/trunk) (revision 0)
++++ added_directory/foo00-simple-bdi-wc-changes (working copy)
+@@ -0,0 +1 @@
++foo
+Index: added_directory/hello_constants.f90
+===================================================================
+--- added_directory/hello_constants.f90 ($ROOT_URL/trunk) (revision 0)
++++ added_directory/hello_constants.f90 (revision 6)
+@@ -0,0 +1,5 @@
++MODULE Hello_Constants
++
++INCLUDE 'hello_constants_dummy.INc'
++
++END MODULE Hello_Constants
+Index: added_directory/hello_constants.inc
+===================================================================
+--- added_directory/hello_constants.inc ($ROOT_URL/trunk) (revision 0)
++++ added_directory/hello_constants.inc (revision 6)
+@@ -0,0 +1,2 @@
++CHARACTER (
++LEN=80), PARAMETER :: hello_strINg = 'Hello Earth!!'
+Index: added_directory/hello_constants_dummy.inc
+===================================================================
+--- added_directory/hello_constants_dummy.inc ($ROOT_URL/trunk) (revision 0)
++++ added_directory/hello_constants_dummy.inc (revision 6)
+@@ -0,0 +1 @@
++INCLUDE 'hello_constants.INc'
+Index: added_file
+===================================================================
+--- added_file ($ROOT_URL/trunk) (revision 0)
++++ added_file (revision 6)
+@@ -0,0 +1 @@
++INCLUDE 'hello_constants.INc'
+__OUT__
+fi
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+teardown
+#-------------------------------------------------------------------------------
diff --git a/t/fcm-branch-diff/test_header b/t/fcm-branch-diff/test_header
new file mode 100644
index 0000000..672ce12
--- /dev/null
+++ b/t/fcm-branch-diff/test_header
@@ -0,0 +1,232 @@
+#!/bin/bash
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# Optional enviroment variables:
+# TEST_PROJECT (tests using given project name)
+# TEST_REMOTE_HOST (tests using svn+ssh repositories located on given host)
+# ------------------------------------------------------------------------------
+
+. $(dirname $0)/../lib/bash/test_header
+
+function file_cmp() {
+ local TEST_KEY=$1
+ local FILE_ACTUAL=$2
+ local FILE_EXPECT=${3:--}
+ if cmp $TEST_DIR/$FILE_ACTUAL $FILE_EXPECT; then
+ pass $TEST_KEY
+ return
+ fi
+ fail $TEST_KEY
+}
+
+function file_grep() {
+ local TEST_KEY=$1
+ local PATTERN=$2
+ local FILE=$3
+ if grep -q -e "$PATTERN" $TEST_DIR/$FILE; then
+ pass $TEST_KEY
+ return
+ fi
+ fail $TEST_KEY
+}
+
+function file_test() {
+ local TEST_KEY=$1
+ local FILE=$2
+ local OPTION=${3:--e}
+ if test $OPTION $TEST_DIR/$FILE; then
+ pass $TEST_KEY
+ else
+ fail $TEST_KEY
+ fi
+}
+
+function file_xxdiff() {
+ local TEST_KEY=$1
+ local FILE_ACTUAL=$2
+ local FILE_EXPECT=${3:--}
+ if xxdiff -D $TEST_DIR/$FILE_ACTUAL $FILE_EXPECT; then
+ pass $TEST_KEY
+ return
+ fi
+ fail $TEST_KEY
+}
+
+function init_repos() {
+ if [[ -n ${TEST_REMOTE_HOST:-} ]]; then
+ TEST_REMOTE_DIR=$(ssh $TEST_REMOTE_HOST "mktemp -d")
+ ssh $TEST_REMOTE_HOST "svnadmin create --fs-type fsfs $TEST_REMOTE_DIR"
+ REPOS_URL="svn+ssh://${TEST_REMOTE_HOST}$TEST_REMOTE_DIR"
+ else
+ svnadmin create --fs-type fsfs $TEST_DIR/test_repos
+ REPOS_URL="file://$TEST_DIR/test_repos"
+ fi
+ ROOT_URL=$REPOS_URL
+ PROJECT=
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ PROJECT=$TEST_PROJECT"/"
+ fi
+ svn import -q $TEST_SOURCE_DIR/../etc/repo_files \
+ $REPOS_URL/$PROJECT/trunk -m "initial trunk import"
+ svn mkdir -q $REPOS_URL/$PROJECT/tags -m "make tags"
+ svn mkdir -q --parents $REPOS_URL/$PROJECT/branches/dev/Share -m " "
+}
+
+function init_repos_layout_roses() {
+ if [[ -n ${TEST_REMOTE_HOST:-} ]]; then
+ TEST_REMOTE_DIR=$(ssh $TEST_REMOTE_HOST "mktemp -d")
+ ssh $TEST_REMOTE_HOST "svnadmin create --fs-type fsfs $TEST_REMOTE_DIR"
+ REPOS_URL="svn+ssh://${TEST_REMOTE_HOST}$TEST_REMOTE_DIR"
+ else
+ svnadmin create --fs-type fsfs $TEST_DIR/test_repos
+ REPOS_URL="file://$TEST_DIR/test_repos"
+ fi
+ svn mkdir -q --parents $REPOS_URL/a/a/0/0/0/trunk
+ svn import -q $TEST_SOURCE_DIR/../etc/repo_files \
+ $REPOS_URL/a/a/0/0/0/trunk -m "initial trunk import"
+ TMPFILE=$(mktemp)
+ cat >$TMPFILE <<__LAYOUT__
+depth-project = 5
+depth-branch = 1
+depth-tag = 1
+dir-trunk = trunk
+dir-branch =
+dir-tag =
+level-owner-branch =
+level-owner-tag =
+template-branch =
+template-tag =
+__LAYOUT__
+ TMPDIR=$(mktemp -d)
+ svn checkout -q $REPOS_URL $TMPDIR
+ svn propset -q --file=$TMPFILE fcm:layout $TMPDIR
+ svn commit -q -m " " $TMPDIR
+ rm -f $TMPFILE
+ rm -rf $TMPDIR
+ ROOT_URL=$REPOS_URL
+}
+
+function init_branch() {
+ local BRANCH_NAME=$1
+ local REPOS_URL=$2
+ local ROOT_URL=$REPOS_URL
+ local ROOT_PATH=
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ ROOT_PATH=$ROOT_PATH/$TEST_PROJECT
+ fi
+ MESSAGE=$(echo -e "Created $ROOT_PATH/branches/dev/Share/$BRANCH_NAME from /trunk at 1.")
+ svn copy -q -r1 $ROOT_URL/trunk $ROOT_URL/branches/dev/Share/$BRANCH_NAME \
+ -m "Made a branch $MESSAGE"
+}
+
+function init_branch_wc() {
+ local BRANCH_NAME=$1
+ local REPOS_URL=$2
+ local ROOT_URL=$REPOS_URL
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ fi
+ init_branch $BRANCH_NAME $REPOS_URL
+ svn checkout -q $ROOT_URL/branches/dev/Share/$BRANCH_NAME $TEST_DIR/wc
+}
+
+function init_merge_branches() {
+ local BRANCH_NAME=$1
+ local OTHER_BRANCH_NAME=$2
+ local REPOS_URL=$3
+ local ROOT_URL=$REPOS_URL
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ fi
+ init_branch_wc $BRANCH_NAME $REPOS_URL
+ cd $TEST_DIR/wc
+ file_list=$(find . -type f | sed "/\.svn/d" | sort | head -5)
+ other_file=$(find . -type f | sed "/\.svn/d" | sort | tail -1)
+ for file in $file_list; do
+ sed -i "s/for/FOR/g; s/fi/end if/g; s/in/IN/g;" $file
+ sed -i "/#/d; /^ *!/d" $file
+ sed -i "s/!/!!/g; s/q/\nq/g; s/[(]/(\n/g" $file
+ done
+ file_dir=$(dirname $file)
+ svn copy -q $file ./added_file
+ svn copy -q module added_directory
+ touch module/tree_conflict_file
+ svn add -q $file_dir/tree_conflict_file
+ echo "Modified a line" >>$other_file
+ svn commit -q -m "Made changes for future merge of this branch"
+ svn update -q
+ init_branch $OTHER_BRANCH_NAME $REPOS_URL
+ svn switch -q $ROOT_URL/branches/dev/Share/$OTHER_BRANCH_NAME
+ echo " " > unversioned_file
+ properties_file=$(find . -type f | sed " /\.svn/d" | sort | tail -3 | head -1)
+ svn propset -q svn:executable "executable" $properties_file
+ svn copy -q $file renamed_added_file
+ svn commit -q -m "Made changes for future merge"
+ svn update -q
+ svn switch -q $ROOT_URL/trunk
+ echo "trunk change" >>$(find . -type f | sed "/\.svn/d" | sort | head -1)
+ svn commit -q -m "Made trunk change"
+ svn update -q
+ echo "another trunk change" >>$(find . -type f | sed "/\.svn/d" | sort | head -1)
+ svn commit -q -m "Made another trunk change"
+ svn update -q
+}
+
+function run_pass() {
+ local TEST_KEY=$1
+ shift 1
+ if ! "$@" 1>$TEST_DIR/$TEST_KEY.out 2>$TEST_DIR/$TEST_KEY.err; then
+ fail $TEST_KEY
+ return
+ fi
+ pass $TEST_KEY
+}
+
+function run_fail() {
+ local TEST_KEY=$1
+ shift 1
+ if "$@" 1>$TEST_DIR/$TEST_KEY.out 2>$TEST_DIR/$TEST_KEY.err; then
+ fail $TEST_KEY
+ return
+ fi
+ pass $TEST_KEY
+}
+
+function setup() {
+ mkdir -p $TEST_DIR/.subversion
+ mkdir -p $TEST_DIR/run
+ cd $TEST_DIR/run
+}
+
+function teardown() {
+ cd $TEST_DIR
+ rm -rf $TEST_DIR/test_repos
+ rm -rf $TEST_DIR/wc
+ rm -rf $TEST_DIR/run
+ rm -rf $TEST_DIR/.subversion
+ if [[ -n ${TEST_REMOTE_HOST:-} ]]; then
+ ssh $TEST_REMOTE_HOST "rm -rf $TEST_REMOTE_DIR"
+ fi
+}
+
+REPOS_URL=
+ROOT_URL=
+PROJECT=
diff --git a/t/fcm-branch-info/00-simple.t b/t/fcm-branch-info/00-simple.t
new file mode 100644
index 0000000..338a002
--- /dev/null
+++ b/t/fcm-branch-info/00-simple.t
@@ -0,0 +1,154 @@
+#!/bin/bash
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# Basic tests for "fcm branch-info".
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+tests 12
+#-------------------------------------------------------------------------------
+setup
+init_repos
+init_branch sibling_branch_test $REPOS_URL
+init_branch_wc branch_test $REPOS_URL
+cd $TEST_DIR/wc
+fcm branch-create -t SHARE --rev-flag=NONE \
+ --non-interactive \
+ --branch-of-branch my_branch_test >/dev/null
+svn switch -q $ROOT_URL/trunk
+FILE_LIST=$(find . -type f | sed "/\.svn/d" | sort | head -5)
+for FILE in $FILE_LIST; do
+ sed -i "s/for/FOR/g; s/fi/end if/g; s/in/IN/g;" $FILE
+ sed -i "/#/d; /^ *!/d" $FILE
+ sed -i "s/!/!!/g; s/q/\nq/g; s/[(]/(\n/g" $FILE
+done
+svn commit -q -m "add trunk commit"
+svn switch -q $ROOT_URL/branches/dev/Share/branch_test
+#-------------------------------------------------------------------------------
+# Tests fcm branch-info
+TEST_KEY=$TEST_KEY_BASE-info
+run_pass "$TEST_KEY" fcm branch-info
+sed -i "/ Date/d;" $TEST_DIR/$TEST_KEY.out
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+URL: $ROOT_URL/branches/dev/Share/branch_test
+Repository Root: $REPOS_URL
+Revision: 7
+Last Changed Author: $LOGNAME
+Last Changed Rev: 5
+--------------------------------------------------------------------------------
+Branch Create Author: $LOGNAME
+Branch Create Rev: 5
+--------------------------------------------------------------------------------
+Branch Parent: $ROOT_URL/trunk at 1
+Merges Avail From Parent: 7
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm branch-info -a
+TEST_KEY=$TEST_KEY_BASE-a
+run_pass "$TEST_KEY" fcm branch-info -a
+sed -i "/ Date/d;" $TEST_DIR/$TEST_KEY.out
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+URL: $ROOT_URL/branches/dev/Share/branch_test
+Repository Root: $REPOS_URL
+Revision: 7
+Last Changed Author: $LOGNAME
+Last Changed Rev: 5
+--------------------------------------------------------------------------------
+Branch Create Author: $LOGNAME
+Branch Create Rev: 5
+--------------------------------------------------------------------------------
+Branch Parent: $ROOT_URL/trunk at 1
+Merges Avail From Parent: 7
+--------------------------------------------------------------------------------
+Searching for siblings ... 1 sibling found.
+No merges with existing siblings.
+--------------------------------------------------------------------------------
+Searching for children ... 1 child found.
+Current children:
+ ------------------------------------------------------------------------------
+ $ROOT_URL/branches/dev/Share/my_branch_test
+ Child Create Rev: 6
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm branch-info --show-children
+TEST_KEY=$TEST_KEY_BASE-show-children
+run_pass "$TEST_KEY" fcm branch-info --show-children
+sed -i "/ Date/d;" $TEST_DIR/$TEST_KEY.out
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+URL: $ROOT_URL/branches/dev/Share/branch_test
+Repository Root: $REPOS_URL
+Revision: 7
+Last Changed Author: $LOGNAME
+Last Changed Rev: 5
+--------------------------------------------------------------------------------
+Branch Create Author: $LOGNAME
+Branch Create Rev: 5
+--------------------------------------------------------------------------------
+Branch Parent: $ROOT_URL/trunk at 1
+Merges Avail From Parent: 7
+--------------------------------------------------------------------------------
+Searching for children ... 1 child found.
+Current children:
+ ------------------------------------------------------------------------------
+ $ROOT_URL/branches/dev/Share/my_branch_test
+ Child Create Rev: 6
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm branch-info --show-siblings
+TEST_KEY=$TEST_KEY_BASE-show-siblings
+svn switch -q $ROOT_URL/branches/dev/Share/sibling_branch_test
+svn merge -q $ROOT_URL/trunk
+svn commit -q -m "Merged trunk into sibling branch"
+svn switch -q $ROOT_URL/branches/dev/Share/branch_test
+svn merge -q $ROOT_URL/branches/dev/Share/sibling_branch_test
+svn commit -q -m "Merged sibling into test branch"
+svn switch -q $ROOT_URL/branches/dev/Share/sibling_branch_test
+FILE_LIST=$(find . -type f | sed "/\.svn/d" | sort | head -5)
+TMPFILE=$(mktemp)
+for FILE in $FILE_LIST; do
+ cut -f 1 $FILE > $TMPFILE
+ mv $TMPFILE $FILE
+done
+svn commit -q -m "Add sibling commit"
+svn switch -q $ROOT_URL/branches/dev/Share/branch_test
+run_pass "$TEST_KEY" fcm branch-info --show-siblings
+sed -i "/ Date/d;" $TEST_DIR/$TEST_KEY.out
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+URL: $ROOT_URL/branches/dev/Share/branch_test
+Repository Root: $REPOS_URL
+Revision: 9
+Last Changed Author: $LOGNAME
+Last Changed Rev: 9
+--------------------------------------------------------------------------------
+Branch Create Author: $LOGNAME
+Branch Create Rev: 5
+--------------------------------------------------------------------------------
+Branch Parent: $ROOT_URL/trunk at 1
+Merges Avail From Parent: 7
+Merges Avail Into Parent: 9
+--------------------------------------------------------------------------------
+Searching for siblings ... 1 sibling found.
+No merges with existing siblings.
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+teardown
+#-------------------------------------------------------------------------------
diff --git a/t/fcm-branch-info/test_header b/t/fcm-branch-info/test_header
new file mode 100644
index 0000000..672ce12
--- /dev/null
+++ b/t/fcm-branch-info/test_header
@@ -0,0 +1,232 @@
+#!/bin/bash
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# Optional enviroment variables:
+# TEST_PROJECT (tests using given project name)
+# TEST_REMOTE_HOST (tests using svn+ssh repositories located on given host)
+# ------------------------------------------------------------------------------
+
+. $(dirname $0)/../lib/bash/test_header
+
+function file_cmp() {
+ local TEST_KEY=$1
+ local FILE_ACTUAL=$2
+ local FILE_EXPECT=${3:--}
+ if cmp $TEST_DIR/$FILE_ACTUAL $FILE_EXPECT; then
+ pass $TEST_KEY
+ return
+ fi
+ fail $TEST_KEY
+}
+
+function file_grep() {
+ local TEST_KEY=$1
+ local PATTERN=$2
+ local FILE=$3
+ if grep -q -e "$PATTERN" $TEST_DIR/$FILE; then
+ pass $TEST_KEY
+ return
+ fi
+ fail $TEST_KEY
+}
+
+function file_test() {
+ local TEST_KEY=$1
+ local FILE=$2
+ local OPTION=${3:--e}
+ if test $OPTION $TEST_DIR/$FILE; then
+ pass $TEST_KEY
+ else
+ fail $TEST_KEY
+ fi
+}
+
+function file_xxdiff() {
+ local TEST_KEY=$1
+ local FILE_ACTUAL=$2
+ local FILE_EXPECT=${3:--}
+ if xxdiff -D $TEST_DIR/$FILE_ACTUAL $FILE_EXPECT; then
+ pass $TEST_KEY
+ return
+ fi
+ fail $TEST_KEY
+}
+
+function init_repos() {
+ if [[ -n ${TEST_REMOTE_HOST:-} ]]; then
+ TEST_REMOTE_DIR=$(ssh $TEST_REMOTE_HOST "mktemp -d")
+ ssh $TEST_REMOTE_HOST "svnadmin create --fs-type fsfs $TEST_REMOTE_DIR"
+ REPOS_URL="svn+ssh://${TEST_REMOTE_HOST}$TEST_REMOTE_DIR"
+ else
+ svnadmin create --fs-type fsfs $TEST_DIR/test_repos
+ REPOS_URL="file://$TEST_DIR/test_repos"
+ fi
+ ROOT_URL=$REPOS_URL
+ PROJECT=
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ PROJECT=$TEST_PROJECT"/"
+ fi
+ svn import -q $TEST_SOURCE_DIR/../etc/repo_files \
+ $REPOS_URL/$PROJECT/trunk -m "initial trunk import"
+ svn mkdir -q $REPOS_URL/$PROJECT/tags -m "make tags"
+ svn mkdir -q --parents $REPOS_URL/$PROJECT/branches/dev/Share -m " "
+}
+
+function init_repos_layout_roses() {
+ if [[ -n ${TEST_REMOTE_HOST:-} ]]; then
+ TEST_REMOTE_DIR=$(ssh $TEST_REMOTE_HOST "mktemp -d")
+ ssh $TEST_REMOTE_HOST "svnadmin create --fs-type fsfs $TEST_REMOTE_DIR"
+ REPOS_URL="svn+ssh://${TEST_REMOTE_HOST}$TEST_REMOTE_DIR"
+ else
+ svnadmin create --fs-type fsfs $TEST_DIR/test_repos
+ REPOS_URL="file://$TEST_DIR/test_repos"
+ fi
+ svn mkdir -q --parents $REPOS_URL/a/a/0/0/0/trunk
+ svn import -q $TEST_SOURCE_DIR/../etc/repo_files \
+ $REPOS_URL/a/a/0/0/0/trunk -m "initial trunk import"
+ TMPFILE=$(mktemp)
+ cat >$TMPFILE <<__LAYOUT__
+depth-project = 5
+depth-branch = 1
+depth-tag = 1
+dir-trunk = trunk
+dir-branch =
+dir-tag =
+level-owner-branch =
+level-owner-tag =
+template-branch =
+template-tag =
+__LAYOUT__
+ TMPDIR=$(mktemp -d)
+ svn checkout -q $REPOS_URL $TMPDIR
+ svn propset -q --file=$TMPFILE fcm:layout $TMPDIR
+ svn commit -q -m " " $TMPDIR
+ rm -f $TMPFILE
+ rm -rf $TMPDIR
+ ROOT_URL=$REPOS_URL
+}
+
+function init_branch() {
+ local BRANCH_NAME=$1
+ local REPOS_URL=$2
+ local ROOT_URL=$REPOS_URL
+ local ROOT_PATH=
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ ROOT_PATH=$ROOT_PATH/$TEST_PROJECT
+ fi
+ MESSAGE=$(echo -e "Created $ROOT_PATH/branches/dev/Share/$BRANCH_NAME from /trunk at 1.")
+ svn copy -q -r1 $ROOT_URL/trunk $ROOT_URL/branches/dev/Share/$BRANCH_NAME \
+ -m "Made a branch $MESSAGE"
+}
+
+function init_branch_wc() {
+ local BRANCH_NAME=$1
+ local REPOS_URL=$2
+ local ROOT_URL=$REPOS_URL
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ fi
+ init_branch $BRANCH_NAME $REPOS_URL
+ svn checkout -q $ROOT_URL/branches/dev/Share/$BRANCH_NAME $TEST_DIR/wc
+}
+
+function init_merge_branches() {
+ local BRANCH_NAME=$1
+ local OTHER_BRANCH_NAME=$2
+ local REPOS_URL=$3
+ local ROOT_URL=$REPOS_URL
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ fi
+ init_branch_wc $BRANCH_NAME $REPOS_URL
+ cd $TEST_DIR/wc
+ file_list=$(find . -type f | sed "/\.svn/d" | sort | head -5)
+ other_file=$(find . -type f | sed "/\.svn/d" | sort | tail -1)
+ for file in $file_list; do
+ sed -i "s/for/FOR/g; s/fi/end if/g; s/in/IN/g;" $file
+ sed -i "/#/d; /^ *!/d" $file
+ sed -i "s/!/!!/g; s/q/\nq/g; s/[(]/(\n/g" $file
+ done
+ file_dir=$(dirname $file)
+ svn copy -q $file ./added_file
+ svn copy -q module added_directory
+ touch module/tree_conflict_file
+ svn add -q $file_dir/tree_conflict_file
+ echo "Modified a line" >>$other_file
+ svn commit -q -m "Made changes for future merge of this branch"
+ svn update -q
+ init_branch $OTHER_BRANCH_NAME $REPOS_URL
+ svn switch -q $ROOT_URL/branches/dev/Share/$OTHER_BRANCH_NAME
+ echo " " > unversioned_file
+ properties_file=$(find . -type f | sed " /\.svn/d" | sort | tail -3 | head -1)
+ svn propset -q svn:executable "executable" $properties_file
+ svn copy -q $file renamed_added_file
+ svn commit -q -m "Made changes for future merge"
+ svn update -q
+ svn switch -q $ROOT_URL/trunk
+ echo "trunk change" >>$(find . -type f | sed "/\.svn/d" | sort | head -1)
+ svn commit -q -m "Made trunk change"
+ svn update -q
+ echo "another trunk change" >>$(find . -type f | sed "/\.svn/d" | sort | head -1)
+ svn commit -q -m "Made another trunk change"
+ svn update -q
+}
+
+function run_pass() {
+ local TEST_KEY=$1
+ shift 1
+ if ! "$@" 1>$TEST_DIR/$TEST_KEY.out 2>$TEST_DIR/$TEST_KEY.err; then
+ fail $TEST_KEY
+ return
+ fi
+ pass $TEST_KEY
+}
+
+function run_fail() {
+ local TEST_KEY=$1
+ shift 1
+ if "$@" 1>$TEST_DIR/$TEST_KEY.out 2>$TEST_DIR/$TEST_KEY.err; then
+ fail $TEST_KEY
+ return
+ fi
+ pass $TEST_KEY
+}
+
+function setup() {
+ mkdir -p $TEST_DIR/.subversion
+ mkdir -p $TEST_DIR/run
+ cd $TEST_DIR/run
+}
+
+function teardown() {
+ cd $TEST_DIR
+ rm -rf $TEST_DIR/test_repos
+ rm -rf $TEST_DIR/wc
+ rm -rf $TEST_DIR/run
+ rm -rf $TEST_DIR/.subversion
+ if [[ -n ${TEST_REMOTE_HOST:-} ]]; then
+ ssh $TEST_REMOTE_HOST "rm -rf $TEST_REMOTE_DIR"
+ fi
+}
+
+REPOS_URL=
+ROOT_URL=
+PROJECT=
diff --git a/t/fcm-branch-list/00-simple.t b/t/fcm-branch-list/00-simple.t
new file mode 100644
index 0000000..8c5c68e
--- /dev/null
+++ b/t/fcm-branch-list/00-simple.t
@@ -0,0 +1,133 @@
+#!/bin/bash
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# Basic tests for "fcm branch-list".
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+tests 23
+#-------------------------------------------------------------------------------
+setup
+init_repos
+init_branch sibling_branch_test $REPOS_URL
+init_branch_wc branch_test $REPOS_URL
+cd $TEST_DIR/wc
+fcm branch-create --rev-flag=NONE \
+ --non-interactive \
+ --branch-of-branch my_branch_test >/dev/null
+ROOT_PATH=
+if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_PATH=/$TEST_PROJECT
+fi
+MESSAGE=$(echo -e "Created $ROOT_PATH/branches/dev/fred/donuts from /trunk at 1.")
+# Please note: if $LOGNAME is drfooeybar or Share, some tests will fail.
+svn mkdir -q -m "Dr Fooeybar branch" $ROOT_URL/branches/dev/drfooeybar/
+svn copy -q -r1 $ROOT_URL/trunk $ROOT_URL/branches/dev/drfooeybar/donuts \
+ -m "Made a branch $MESSAGE" --non-interactive
+FILE_LIST=$(find . -type f | sed "/\.svn/d" | sort | head -5)
+for FILE in $FILE_LIST; do
+ sed -i "s/for/FOR/g; s/fi/end if/g; s/in/IN/g;" $FILE
+ sed -i "/#/d; /^ *!/d" $FILE
+ sed -i "s/!/!!/g; s/q/\nq/g; s/[(]/(\n/g" $FILE
+done
+svn commit -q -m "add branch commit"
+svn update -q
+svn switch -q $ROOT_URL/branches/dev/Share/branch_test
+#-------------------------------------------------------------------------------
+# Tests fcm branch-list
+TEST_KEY=$TEST_KEY_BASE-list
+run_pass "$TEST_KEY" fcm branch-list
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+[info] $ROOT_URL at 9: 1 match(es)
+$ROOT_URL/branches/dev/$LOGNAME/my_branch_test at 9
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm branch-list -a
+TEST_KEY=$TEST_KEY_BASE-a
+run_pass "$TEST_KEY" fcm branch-list -a
+sed -i "/ Date/d;" $TEST_DIR/$TEST_KEY.out
+TMPFILE=$(mktemp)
+sort > $TMPFILE <<__OUT__
+[info] $ROOT_URL at 9: 4 match(es)
+$ROOT_URL/branches/dev/Share/branch_test at 9
+$ROOT_URL/branches/dev/Share/sibling_branch_test at 9
+$ROOT_URL/branches/dev/$LOGNAME/my_branch_test at 9
+$ROOT_URL/branches/dev/drfooeybar/donuts at 9
+__OUT__
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <$TMPFILE
+rm -f $TMPFILE
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm branch-list --user
+TEST_KEY=$TEST_KEY_BASE-a
+run_pass "$TEST_KEY" fcm branch-list --user=drfooeybar
+sed -i "/ Date/d;" $TEST_DIR/$TEST_KEY.out
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+[info] $ROOT_URL at 9: 1 match(es)
+$ROOT_URL/branches/dev/drfooeybar/donuts at 9
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm branch-list --only (1)
+TEST_KEY=$TEST_KEY_BASE-only-1
+run_pass "$TEST_KEY" fcm branch-list --only=3:donut
+sed -i "/ Date/d;" $TEST_DIR/$TEST_KEY.out
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+[info] $ROOT_URL at 9: 1 match(es)
+$ROOT_URL/branches/dev/drfooeybar/donuts at 9
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm branch-list --only (2)
+TEST_KEY=$TEST_KEY_BASE-only-2
+run_pass "$TEST_KEY" fcm branch-list --only=2:Share
+sed -i "/ Date/d;" $TEST_DIR/$TEST_KEY.out
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+[info] $ROOT_URL at 9: 2 match(es)
+$ROOT_URL/branches/dev/Share/branch_test at 9
+$ROOT_URL/branches/dev/Share/sibling_branch_test at 9
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm branch-list --only (3)
+TEST_KEY=$TEST_KEY_BASE-only-3
+run_pass "$TEST_KEY" fcm branch-list --only=2:Share --only=3:sibling
+sed -i "/ Date/d;" $TEST_DIR/$TEST_KEY.out
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+[info] $ROOT_URL at 9: 1 match(es)
+$ROOT_URL/branches/dev/Share/sibling_branch_test at 9
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm branch-list --only (4)
+TEST_KEY=$TEST_KEY_BASE-only-4
+run_pass "$TEST_KEY" fcm branch-list --only=1:something-not-right
+sed -i "/ Date/d;" $TEST_DIR/$TEST_KEY.out
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+[info] $ROOT_URL at 9: 0 match(es)
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm branch-list --only (5)
+TEST_KEY=$TEST_KEY_BASE-only-5
+run_fail "$TEST_KEY" fcm branch-list --only=1:\)
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" </dev/null
+teardown
+#-------------------------------------------------------------------------------
diff --git a/t/fcm-branch-list/test_header b/t/fcm-branch-list/test_header
new file mode 100644
index 0000000..672ce12
--- /dev/null
+++ b/t/fcm-branch-list/test_header
@@ -0,0 +1,232 @@
+#!/bin/bash
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# Optional enviroment variables:
+# TEST_PROJECT (tests using given project name)
+# TEST_REMOTE_HOST (tests using svn+ssh repositories located on given host)
+# ------------------------------------------------------------------------------
+
+. $(dirname $0)/../lib/bash/test_header
+
+function file_cmp() {
+ local TEST_KEY=$1
+ local FILE_ACTUAL=$2
+ local FILE_EXPECT=${3:--}
+ if cmp $TEST_DIR/$FILE_ACTUAL $FILE_EXPECT; then
+ pass $TEST_KEY
+ return
+ fi
+ fail $TEST_KEY
+}
+
+function file_grep() {
+ local TEST_KEY=$1
+ local PATTERN=$2
+ local FILE=$3
+ if grep -q -e "$PATTERN" $TEST_DIR/$FILE; then
+ pass $TEST_KEY
+ return
+ fi
+ fail $TEST_KEY
+}
+
+function file_test() {
+ local TEST_KEY=$1
+ local FILE=$2
+ local OPTION=${3:--e}
+ if test $OPTION $TEST_DIR/$FILE; then
+ pass $TEST_KEY
+ else
+ fail $TEST_KEY
+ fi
+}
+
+function file_xxdiff() {
+ local TEST_KEY=$1
+ local FILE_ACTUAL=$2
+ local FILE_EXPECT=${3:--}
+ if xxdiff -D $TEST_DIR/$FILE_ACTUAL $FILE_EXPECT; then
+ pass $TEST_KEY
+ return
+ fi
+ fail $TEST_KEY
+}
+
+function init_repos() {
+ if [[ -n ${TEST_REMOTE_HOST:-} ]]; then
+ TEST_REMOTE_DIR=$(ssh $TEST_REMOTE_HOST "mktemp -d")
+ ssh $TEST_REMOTE_HOST "svnadmin create --fs-type fsfs $TEST_REMOTE_DIR"
+ REPOS_URL="svn+ssh://${TEST_REMOTE_HOST}$TEST_REMOTE_DIR"
+ else
+ svnadmin create --fs-type fsfs $TEST_DIR/test_repos
+ REPOS_URL="file://$TEST_DIR/test_repos"
+ fi
+ ROOT_URL=$REPOS_URL
+ PROJECT=
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ PROJECT=$TEST_PROJECT"/"
+ fi
+ svn import -q $TEST_SOURCE_DIR/../etc/repo_files \
+ $REPOS_URL/$PROJECT/trunk -m "initial trunk import"
+ svn mkdir -q $REPOS_URL/$PROJECT/tags -m "make tags"
+ svn mkdir -q --parents $REPOS_URL/$PROJECT/branches/dev/Share -m " "
+}
+
+function init_repos_layout_roses() {
+ if [[ -n ${TEST_REMOTE_HOST:-} ]]; then
+ TEST_REMOTE_DIR=$(ssh $TEST_REMOTE_HOST "mktemp -d")
+ ssh $TEST_REMOTE_HOST "svnadmin create --fs-type fsfs $TEST_REMOTE_DIR"
+ REPOS_URL="svn+ssh://${TEST_REMOTE_HOST}$TEST_REMOTE_DIR"
+ else
+ svnadmin create --fs-type fsfs $TEST_DIR/test_repos
+ REPOS_URL="file://$TEST_DIR/test_repos"
+ fi
+ svn mkdir -q --parents $REPOS_URL/a/a/0/0/0/trunk
+ svn import -q $TEST_SOURCE_DIR/../etc/repo_files \
+ $REPOS_URL/a/a/0/0/0/trunk -m "initial trunk import"
+ TMPFILE=$(mktemp)
+ cat >$TMPFILE <<__LAYOUT__
+depth-project = 5
+depth-branch = 1
+depth-tag = 1
+dir-trunk = trunk
+dir-branch =
+dir-tag =
+level-owner-branch =
+level-owner-tag =
+template-branch =
+template-tag =
+__LAYOUT__
+ TMPDIR=$(mktemp -d)
+ svn checkout -q $REPOS_URL $TMPDIR
+ svn propset -q --file=$TMPFILE fcm:layout $TMPDIR
+ svn commit -q -m " " $TMPDIR
+ rm -f $TMPFILE
+ rm -rf $TMPDIR
+ ROOT_URL=$REPOS_URL
+}
+
+function init_branch() {
+ local BRANCH_NAME=$1
+ local REPOS_URL=$2
+ local ROOT_URL=$REPOS_URL
+ local ROOT_PATH=
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ ROOT_PATH=$ROOT_PATH/$TEST_PROJECT
+ fi
+ MESSAGE=$(echo -e "Created $ROOT_PATH/branches/dev/Share/$BRANCH_NAME from /trunk at 1.")
+ svn copy -q -r1 $ROOT_URL/trunk $ROOT_URL/branches/dev/Share/$BRANCH_NAME \
+ -m "Made a branch $MESSAGE"
+}
+
+function init_branch_wc() {
+ local BRANCH_NAME=$1
+ local REPOS_URL=$2
+ local ROOT_URL=$REPOS_URL
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ fi
+ init_branch $BRANCH_NAME $REPOS_URL
+ svn checkout -q $ROOT_URL/branches/dev/Share/$BRANCH_NAME $TEST_DIR/wc
+}
+
+function init_merge_branches() {
+ local BRANCH_NAME=$1
+ local OTHER_BRANCH_NAME=$2
+ local REPOS_URL=$3
+ local ROOT_URL=$REPOS_URL
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ fi
+ init_branch_wc $BRANCH_NAME $REPOS_URL
+ cd $TEST_DIR/wc
+ file_list=$(find . -type f | sed "/\.svn/d" | sort | head -5)
+ other_file=$(find . -type f | sed "/\.svn/d" | sort | tail -1)
+ for file in $file_list; do
+ sed -i "s/for/FOR/g; s/fi/end if/g; s/in/IN/g;" $file
+ sed -i "/#/d; /^ *!/d" $file
+ sed -i "s/!/!!/g; s/q/\nq/g; s/[(]/(\n/g" $file
+ done
+ file_dir=$(dirname $file)
+ svn copy -q $file ./added_file
+ svn copy -q module added_directory
+ touch module/tree_conflict_file
+ svn add -q $file_dir/tree_conflict_file
+ echo "Modified a line" >>$other_file
+ svn commit -q -m "Made changes for future merge of this branch"
+ svn update -q
+ init_branch $OTHER_BRANCH_NAME $REPOS_URL
+ svn switch -q $ROOT_URL/branches/dev/Share/$OTHER_BRANCH_NAME
+ echo " " > unversioned_file
+ properties_file=$(find . -type f | sed " /\.svn/d" | sort | tail -3 | head -1)
+ svn propset -q svn:executable "executable" $properties_file
+ svn copy -q $file renamed_added_file
+ svn commit -q -m "Made changes for future merge"
+ svn update -q
+ svn switch -q $ROOT_URL/trunk
+ echo "trunk change" >>$(find . -type f | sed "/\.svn/d" | sort | head -1)
+ svn commit -q -m "Made trunk change"
+ svn update -q
+ echo "another trunk change" >>$(find . -type f | sed "/\.svn/d" | sort | head -1)
+ svn commit -q -m "Made another trunk change"
+ svn update -q
+}
+
+function run_pass() {
+ local TEST_KEY=$1
+ shift 1
+ if ! "$@" 1>$TEST_DIR/$TEST_KEY.out 2>$TEST_DIR/$TEST_KEY.err; then
+ fail $TEST_KEY
+ return
+ fi
+ pass $TEST_KEY
+}
+
+function run_fail() {
+ local TEST_KEY=$1
+ shift 1
+ if "$@" 1>$TEST_DIR/$TEST_KEY.out 2>$TEST_DIR/$TEST_KEY.err; then
+ fail $TEST_KEY
+ return
+ fi
+ pass $TEST_KEY
+}
+
+function setup() {
+ mkdir -p $TEST_DIR/.subversion
+ mkdir -p $TEST_DIR/run
+ cd $TEST_DIR/run
+}
+
+function teardown() {
+ cd $TEST_DIR
+ rm -rf $TEST_DIR/test_repos
+ rm -rf $TEST_DIR/wc
+ rm -rf $TEST_DIR/run
+ rm -rf $TEST_DIR/.subversion
+ if [[ -n ${TEST_REMOTE_HOST:-} ]]; then
+ ssh $TEST_REMOTE_HOST "rm -rf $TEST_REMOTE_DIR"
+ fi
+}
+
+REPOS_URL=
+ROOT_URL=
+PROJECT=
diff --git a/t/fcm-commit/00-simple.t b/t/fcm-commit/00-simple.t
new file mode 100644
index 0000000..5033679
--- /dev/null
+++ b/t/fcm-commit/00-simple.t
@@ -0,0 +1,134 @@
+#!/bin/bash
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# Basic tests for "fcm commit".
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+tests 3
+#-------------------------------------------------------------------------------
+setup
+init_repos
+init_branch sibling_branch_test $REPOS_URL
+init_branch_wc branch_test $REPOS_URL
+cd $TEST_DIR/wc
+FILE_LIST=$(find . -type f | sed "/\.svn/d" | sort | head -5)
+for FILE in $FILE_LIST; do
+ sed -i "s/for/FOR/g; s/fi/end if/g; s/in/IN/g;" $FILE
+ sed -i "/#/d; /^ *!/d" $FILE
+ sed -i "s/!/!!/g; s/q/\nq/g; s/[(]/(\n/g" $FILE
+done
+FILE_DIR=$(dirname $FILE)
+svn copy -q $FILE added_file
+svn copy -q $FILE_DIR added_directory
+svn delete --force -q $FILE_DIR
+#-------------------------------------------------------------------------------
+# Tests fcm commit
+TEST_KEY=$TEST_KEY_BASE
+export SVN_EDITOR="sed -i 1i\foo"
+run_pass "$TEST_KEY" fcm commit --svn-non-interactive <<__IN__
+y
+__IN__
+if $SVN_VERSION_IS_16; then
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+[info] sed -i 1i\foo: starting commit message editor...
+Change summary:
+--------------------------------------------------------------------------------
+[Root : $REPOS_URL]
+[Project: ${TEST_PROJECT:-}]
+[Branch : branches/dev/Share/branch_test]
+[Sub-dir: ]
+
+A + added_file
+D module
+D module/hello_constants_dummy.inc
+D module/hello_constants.inc
+D module/hello_constants.f90
+A + added_directory
+M + added_directory/hello_constants_dummy.inc
+M + added_directory/hello_constants.inc
+M + added_directory/hello_constants.f90
+M lib/python/info/poems.py
+--------------------------------------------------------------------------------
+Commit message is as follows:
+--------------------------------------------------------------------------------
+foo
+--------------------------------------------------------------------------------
+
+*** WARNING: YOU ARE COMMITTING TO A Share BRANCH.
+*** Please ensure that you have the owner's permission.
+
+Would you like to commit this change?
+Enter "y" or "n" (or just press <return> for "n"): Adding added_directory
+Sending added_directory/hello_constants.f90
+Sending added_directory/hello_constants.inc
+Sending added_directory/hello_constants_dummy.inc
+Adding added_file
+Sending lib/python/info/poems.py
+Deleting module
+Transmitting file data .....
+Committed revision 6.
+At revision 6.
+__OUT__
+else
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+[info] sed -i 1i\foo: starting commit message editor...
+Change summary:
+--------------------------------------------------------------------------------
+[Root : $REPOS_URL]
+[Project: ${TEST_PROJECT:-}]
+[Branch : branches/dev/Share/branch_test]
+[Sub-dir: ]
+
+A + added_directory
+M + added_directory/hello_constants.f90
+M + added_directory/hello_constants.inc
+M + added_directory/hello_constants_dummy.inc
+A + added_file
+M lib/python/info/poems.py
+D module
+D module/hello_constants.f90
+D module/hello_constants.inc
+D module/hello_constants_dummy.inc
+--------------------------------------------------------------------------------
+Commit message is as follows:
+--------------------------------------------------------------------------------
+foo
+--------------------------------------------------------------------------------
+
+*** WARNING: YOU ARE COMMITTING TO A Share BRANCH.
+*** Please ensure that you have the owner's permission.
+
+Would you like to commit this change?
+Enter "y" or "n" (or just press <return> for "n"): Adding added_directory
+Sending added_directory/hello_constants.f90
+Sending added_directory/hello_constants.inc
+Sending added_directory/hello_constants_dummy.inc
+Adding added_file
+Sending lib/python/info/poems.py
+Deleting module
+Transmitting file data .....
+Committed revision 6.
+Updating '.':
+At revision 6.
+__OUT__
+fi
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+teardown
+#-------------------------------------------------------------------------------
diff --git a/t/fcm-commit/01-subtree.t b/t/fcm-commit/01-subtree.t
new file mode 100644
index 0000000..b651608
--- /dev/null
+++ b/t/fcm-commit/01-subtree.t
@@ -0,0 +1,137 @@
+#!/bin/bash
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# Basic tests for "fcm commit".
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+tests 3
+#-------------------------------------------------------------------------------
+setup
+init_repos
+init_branch sibling_branch_test $REPOS_URL
+init_branch_wc branch_test $REPOS_URL
+cd $TEST_DIR/wc
+FILE_LIST=$(find . -type f | sed "/\.svn/d" | sort | head -5)
+for FILE in $FILE_LIST; do
+ sed -i "s/for/FOR/g; s/fi/end if/g; s/in/IN/g;" $FILE
+ sed -i "/#/d; /^ *!/d" $FILE
+ sed -i "s/!/!!/g; s/q/\nq/g; s/[(]/(\n/g" $FILE
+done
+FILE_DIR=$(dirname $FILE)
+svn copy -q $FILE added_file
+svn copy -q $FILE_DIR added_directory
+svn delete --force -q $FILE_DIR
+#-------------------------------------------------------------------------------
+# Tests fcm commit
+TEST_KEY=$TEST_KEY_BASE
+export SVN_EDITOR="sed -i 1i\foo"
+cd program
+run_pass "$TEST_KEY" fcm commit --svn-non-interactive <<__IN__
+y
+__IN__
+if $SVN_VERSION_IS_16; then
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+$TEST_DIR/wc: working directory changed to top of working copy.
+[info] sed -i 1i\foo: starting commit message editor...
+Change summary:
+--------------------------------------------------------------------------------
+[Root : $REPOS_URL]
+[Project: ${TEST_PROJECT:-}]
+[Branch : branches/dev/Share/branch_test]
+[Sub-dir: ]
+
+A + added_file
+D module
+D module/hello_constants_dummy.inc
+D module/hello_constants.inc
+D module/hello_constants.f90
+A + added_directory
+M + added_directory/hello_constants_dummy.inc
+M + added_directory/hello_constants.inc
+M + added_directory/hello_constants.f90
+M lib/python/info/poems.py
+--------------------------------------------------------------------------------
+Commit message is as follows:
+--------------------------------------------------------------------------------
+foo
+--------------------------------------------------------------------------------
+
+*** WARNING: YOU ARE COMMITTING TO A Share BRANCH.
+*** Please ensure that you have the owner's permission.
+
+Would you like to commit this change?
+Enter "y" or "n" (or just press <return> for "n"): Adding added_directory
+Sending added_directory/hello_constants.f90
+Sending added_directory/hello_constants.inc
+Sending added_directory/hello_constants_dummy.inc
+Adding added_file
+Sending lib/python/info/poems.py
+Deleting module
+Transmitting file data .....
+Committed revision 6.
+At revision 6.
+__OUT__
+else
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+$TEST_DIR/wc: working directory changed to top of working copy.
+[info] sed -i 1i\foo: starting commit message editor...
+Change summary:
+--------------------------------------------------------------------------------
+[Root : $REPOS_URL]
+[Project: ${TEST_PROJECT:-}]
+[Branch : branches/dev/Share/branch_test]
+[Sub-dir: ]
+
+A + added_directory
+M + added_directory/hello_constants.f90
+M + added_directory/hello_constants.inc
+M + added_directory/hello_constants_dummy.inc
+A + added_file
+M lib/python/info/poems.py
+D module
+D module/hello_constants.f90
+D module/hello_constants.inc
+D module/hello_constants_dummy.inc
+--------------------------------------------------------------------------------
+Commit message is as follows:
+--------------------------------------------------------------------------------
+foo
+--------------------------------------------------------------------------------
+
+*** WARNING: YOU ARE COMMITTING TO A Share BRANCH.
+*** Please ensure that you have the owner's permission.
+
+Would you like to commit this change?
+Enter "y" or "n" (or just press <return> for "n"): Adding added_directory
+Sending added_directory/hello_constants.f90
+Sending added_directory/hello_constants.inc
+Sending added_directory/hello_constants_dummy.inc
+Adding added_file
+Sending lib/python/info/poems.py
+Deleting module
+Transmitting file data .....
+Committed revision 6.
+Updating '.':
+At revision 6.
+__OUT__
+fi
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+teardown
+#-------------------------------------------------------------------------------
diff --git a/t/fcm-commit/02-bad.t b/t/fcm-commit/02-bad.t
new file mode 100644
index 0000000..51c85d7
--- /dev/null
+++ b/t/fcm-commit/02-bad.t
@@ -0,0 +1,48 @@
+#!/bin/bash
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# Bad-behaviour tests for "fcm commit".
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+tests 3
+#-------------------------------------------------------------------------------
+setup
+init_repos
+init_branch sibling_branch_test $REPOS_URL
+init_branch_wc branch_test $REPOS_URL
+cd $TEST_DIR/wc
+svn copy -q pro/hello.pro copied_file
+svn copy -q module copied_directory
+svn delete -q --force lib
+rm -rf program/hello.F90
+#-------------------------------------------------------------------------------
+# Tests fcm commit
+TEST_KEY=$TEST_KEY_BASE
+export SVN_EDITOR="sed -i 1i\foo"
+run_fail "$TEST_KEY" fcm commit --svn-non-interactive
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" </dev/null
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" <<__ERR__
+[ERROR] File(s) missing:
+! 5 program/hello.F90
+[FAIL] FCM1::Cm::Abort: abort
+
+__ERR__
+teardown
+#-------------------------------------------------------------------------------
diff --git a/t/fcm-commit/03-message-file.t b/t/fcm-commit/03-message-file.t
new file mode 100644
index 0000000..12de033
--- /dev/null
+++ b/t/fcm-commit/03-message-file.t
@@ -0,0 +1,58 @@
+#!/bin/bash
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# Tests for "fcm commit", attempt to add commit message file.
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+tests 6
+#-------------------------------------------------------------------------------
+svnadmin create foo
+svn co -q file://$PWD/foo 'test-work'
+touch 'test-work/#commit_message#'
+svn add 'test-work/#commit_message#'
+export SVN_EDITOR='cat'
+#-------------------------------------------------------------------------------
+# Tests fcm commit, bad commit file 1
+TEST_KEY="$TEST_KEY_BASE-1"
+run_fail "$TEST_KEY" fcm commit --svn-non-interactive 'test-work'
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+$PWD/test-work: working directory changed to top of working copy.
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" <<'__ERR__'
+[ERROR] Attempt to add commit message file:
+A #commit_message#
+[FAIL] FCM1::Cm::Abort: abort
+
+__ERR__
+#-------------------------------------------------------------------------------
+# Tests fcm commit, bad commit file 2
+TEST_KEY="$TEST_KEY_BASE-2"
+cd 'test-work'
+run_fail "$TEST_KEY" fcm commit --svn-non-interactive
+cd ..
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" </dev/null
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" <<'__ERR__'
+[ERROR] Attempt to add commit message file:
+A #commit_message#
+[FAIL] FCM1::Cm::Abort: abort
+
+__ERR__
+#-------------------------------------------------------------------------------
+exit
diff --git a/t/fcm-commit/test_header b/t/fcm-commit/test_header
new file mode 100644
index 0000000..672ce12
--- /dev/null
+++ b/t/fcm-commit/test_header
@@ -0,0 +1,232 @@
+#!/bin/bash
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# Optional enviroment variables:
+# TEST_PROJECT (tests using given project name)
+# TEST_REMOTE_HOST (tests using svn+ssh repositories located on given host)
+# ------------------------------------------------------------------------------
+
+. $(dirname $0)/../lib/bash/test_header
+
+function file_cmp() {
+ local TEST_KEY=$1
+ local FILE_ACTUAL=$2
+ local FILE_EXPECT=${3:--}
+ if cmp $TEST_DIR/$FILE_ACTUAL $FILE_EXPECT; then
+ pass $TEST_KEY
+ return
+ fi
+ fail $TEST_KEY
+}
+
+function file_grep() {
+ local TEST_KEY=$1
+ local PATTERN=$2
+ local FILE=$3
+ if grep -q -e "$PATTERN" $TEST_DIR/$FILE; then
+ pass $TEST_KEY
+ return
+ fi
+ fail $TEST_KEY
+}
+
+function file_test() {
+ local TEST_KEY=$1
+ local FILE=$2
+ local OPTION=${3:--e}
+ if test $OPTION $TEST_DIR/$FILE; then
+ pass $TEST_KEY
+ else
+ fail $TEST_KEY
+ fi
+}
+
+function file_xxdiff() {
+ local TEST_KEY=$1
+ local FILE_ACTUAL=$2
+ local FILE_EXPECT=${3:--}
+ if xxdiff -D $TEST_DIR/$FILE_ACTUAL $FILE_EXPECT; then
+ pass $TEST_KEY
+ return
+ fi
+ fail $TEST_KEY
+}
+
+function init_repos() {
+ if [[ -n ${TEST_REMOTE_HOST:-} ]]; then
+ TEST_REMOTE_DIR=$(ssh $TEST_REMOTE_HOST "mktemp -d")
+ ssh $TEST_REMOTE_HOST "svnadmin create --fs-type fsfs $TEST_REMOTE_DIR"
+ REPOS_URL="svn+ssh://${TEST_REMOTE_HOST}$TEST_REMOTE_DIR"
+ else
+ svnadmin create --fs-type fsfs $TEST_DIR/test_repos
+ REPOS_URL="file://$TEST_DIR/test_repos"
+ fi
+ ROOT_URL=$REPOS_URL
+ PROJECT=
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ PROJECT=$TEST_PROJECT"/"
+ fi
+ svn import -q $TEST_SOURCE_DIR/../etc/repo_files \
+ $REPOS_URL/$PROJECT/trunk -m "initial trunk import"
+ svn mkdir -q $REPOS_URL/$PROJECT/tags -m "make tags"
+ svn mkdir -q --parents $REPOS_URL/$PROJECT/branches/dev/Share -m " "
+}
+
+function init_repos_layout_roses() {
+ if [[ -n ${TEST_REMOTE_HOST:-} ]]; then
+ TEST_REMOTE_DIR=$(ssh $TEST_REMOTE_HOST "mktemp -d")
+ ssh $TEST_REMOTE_HOST "svnadmin create --fs-type fsfs $TEST_REMOTE_DIR"
+ REPOS_URL="svn+ssh://${TEST_REMOTE_HOST}$TEST_REMOTE_DIR"
+ else
+ svnadmin create --fs-type fsfs $TEST_DIR/test_repos
+ REPOS_URL="file://$TEST_DIR/test_repos"
+ fi
+ svn mkdir -q --parents $REPOS_URL/a/a/0/0/0/trunk
+ svn import -q $TEST_SOURCE_DIR/../etc/repo_files \
+ $REPOS_URL/a/a/0/0/0/trunk -m "initial trunk import"
+ TMPFILE=$(mktemp)
+ cat >$TMPFILE <<__LAYOUT__
+depth-project = 5
+depth-branch = 1
+depth-tag = 1
+dir-trunk = trunk
+dir-branch =
+dir-tag =
+level-owner-branch =
+level-owner-tag =
+template-branch =
+template-tag =
+__LAYOUT__
+ TMPDIR=$(mktemp -d)
+ svn checkout -q $REPOS_URL $TMPDIR
+ svn propset -q --file=$TMPFILE fcm:layout $TMPDIR
+ svn commit -q -m " " $TMPDIR
+ rm -f $TMPFILE
+ rm -rf $TMPDIR
+ ROOT_URL=$REPOS_URL
+}
+
+function init_branch() {
+ local BRANCH_NAME=$1
+ local REPOS_URL=$2
+ local ROOT_URL=$REPOS_URL
+ local ROOT_PATH=
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ ROOT_PATH=$ROOT_PATH/$TEST_PROJECT
+ fi
+ MESSAGE=$(echo -e "Created $ROOT_PATH/branches/dev/Share/$BRANCH_NAME from /trunk at 1.")
+ svn copy -q -r1 $ROOT_URL/trunk $ROOT_URL/branches/dev/Share/$BRANCH_NAME \
+ -m "Made a branch $MESSAGE"
+}
+
+function init_branch_wc() {
+ local BRANCH_NAME=$1
+ local REPOS_URL=$2
+ local ROOT_URL=$REPOS_URL
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ fi
+ init_branch $BRANCH_NAME $REPOS_URL
+ svn checkout -q $ROOT_URL/branches/dev/Share/$BRANCH_NAME $TEST_DIR/wc
+}
+
+function init_merge_branches() {
+ local BRANCH_NAME=$1
+ local OTHER_BRANCH_NAME=$2
+ local REPOS_URL=$3
+ local ROOT_URL=$REPOS_URL
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ fi
+ init_branch_wc $BRANCH_NAME $REPOS_URL
+ cd $TEST_DIR/wc
+ file_list=$(find . -type f | sed "/\.svn/d" | sort | head -5)
+ other_file=$(find . -type f | sed "/\.svn/d" | sort | tail -1)
+ for file in $file_list; do
+ sed -i "s/for/FOR/g; s/fi/end if/g; s/in/IN/g;" $file
+ sed -i "/#/d; /^ *!/d" $file
+ sed -i "s/!/!!/g; s/q/\nq/g; s/[(]/(\n/g" $file
+ done
+ file_dir=$(dirname $file)
+ svn copy -q $file ./added_file
+ svn copy -q module added_directory
+ touch module/tree_conflict_file
+ svn add -q $file_dir/tree_conflict_file
+ echo "Modified a line" >>$other_file
+ svn commit -q -m "Made changes for future merge of this branch"
+ svn update -q
+ init_branch $OTHER_BRANCH_NAME $REPOS_URL
+ svn switch -q $ROOT_URL/branches/dev/Share/$OTHER_BRANCH_NAME
+ echo " " > unversioned_file
+ properties_file=$(find . -type f | sed " /\.svn/d" | sort | tail -3 | head -1)
+ svn propset -q svn:executable "executable" $properties_file
+ svn copy -q $file renamed_added_file
+ svn commit -q -m "Made changes for future merge"
+ svn update -q
+ svn switch -q $ROOT_URL/trunk
+ echo "trunk change" >>$(find . -type f | sed "/\.svn/d" | sort | head -1)
+ svn commit -q -m "Made trunk change"
+ svn update -q
+ echo "another trunk change" >>$(find . -type f | sed "/\.svn/d" | sort | head -1)
+ svn commit -q -m "Made another trunk change"
+ svn update -q
+}
+
+function run_pass() {
+ local TEST_KEY=$1
+ shift 1
+ if ! "$@" 1>$TEST_DIR/$TEST_KEY.out 2>$TEST_DIR/$TEST_KEY.err; then
+ fail $TEST_KEY
+ return
+ fi
+ pass $TEST_KEY
+}
+
+function run_fail() {
+ local TEST_KEY=$1
+ shift 1
+ if "$@" 1>$TEST_DIR/$TEST_KEY.out 2>$TEST_DIR/$TEST_KEY.err; then
+ fail $TEST_KEY
+ return
+ fi
+ pass $TEST_KEY
+}
+
+function setup() {
+ mkdir -p $TEST_DIR/.subversion
+ mkdir -p $TEST_DIR/run
+ cd $TEST_DIR/run
+}
+
+function teardown() {
+ cd $TEST_DIR
+ rm -rf $TEST_DIR/test_repos
+ rm -rf $TEST_DIR/wc
+ rm -rf $TEST_DIR/run
+ rm -rf $TEST_DIR/.subversion
+ if [[ -n ${TEST_REMOTE_HOST:-} ]]; then
+ ssh $TEST_REMOTE_HOST "rm -rf $TEST_REMOTE_DIR"
+ fi
+}
+
+REPOS_URL=
+ROOT_URL=
+PROJECT=
diff --git a/t/fcm-conflicts/00-tree-add-add.t b/t/fcm-conflicts/00-tree-add-add.t
new file mode 100644
index 0000000..ea1207f
--- /dev/null
+++ b/t/fcm-conflicts/00-tree-add-add.t
@@ -0,0 +1,154 @@
+#!/bin/bash
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# Basic tests for "fcm conflicts" (tree conflict mode).
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+tests 24
+#-------------------------------------------------------------------------------
+setup
+init_repos
+init_branch ctrl $REPOS_URL
+init_branch_wc add_add $REPOS_URL
+cd $TEST_DIR/wc
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: add, add, discard local
+TEST_KEY=$TEST_KEY_BASE-discard
+svn switch -q $ROOT_URL/branches/dev/Share/ctrl
+echo "Local contents (1)" > new_file
+svn add -q new_file
+svn commit -q -m "Added duplicate-name copy of conflict file"
+svn update -q
+svn switch -q $ROOT_URL/branches/dev/Share/add_add
+echo "Merge contents (1)" >new_file
+echo "Merge contents (2)" >>new_file
+svn add -q new_file
+svn commit -q -m "Added duplicated-name copy of conflict file"
+svn update -q
+svn switch -q $ROOT_URL/branches/dev/Share/ctrl
+fcm merge --non-interactive $ROOT_URL/branches/dev/Share/add_add >/dev/null
+run_pass "$TEST_KEY" fcm conflicts <<__IN__
+n
+__IN__
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+[info] new_file: in tree conflict.
+Locally: added.
+Externally: added.
+Answer (y) to keep the local file filename.
+Answer (n) to keep the external file filename.
+Keep the local version?
+Enter "y" or "n" (or just press <return> for "n") Resolved conflicted state of 'new_file'
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: add, add, discard local (status)
+TEST_KEY=$TEST_KEY_BASE-discard-status
+run_pass "$TEST_KEY" svn status --config-dir=$TEST_DIR/.subversion/
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+ M .
+M new_file
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: add, add, discard local (info)
+TEST_KEY=$TEST_KEY_BASE-discard-info
+run_pass "$TEST_KEY" svn info new_file
+sed -i "/Date:\|Updated:\|UUID:\|Checksum\|Relative URL:\|Working Copy Root Path:/d" $TEST_DIR/"$TEST_KEY.out"
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+Path: new_file
+Name: new_file
+URL: $ROOT_URL/branches/dev/Share/ctrl/new_file
+Repository Root: $REPOS_URL
+Revision: 7
+Node Kind: file
+Schedule: normal
+Last Changed Author: $LOGNAME
+Last Changed Rev: 6
+
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: add, add, discard local (cat)
+TEST_KEY=$TEST_KEY_BASE-discard-cat
+run_pass "$TEST_KEY" cat new_file
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+Merge contents (1)
+Merge contents (2)
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+cd $TEST_DIR
+rm -rf $TEST_DIR/wc
+mkdir $TEST_DIR/wc
+svn checkout -q $ROOT_URL/branches/dev/Share/ctrl $TEST_DIR/wc
+cd $TEST_DIR/wc
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: add, add, keep local
+TEST_KEY=$TEST_KEY_BASE-keep
+fcm merge --non-interactive $ROOT_URL/branches/dev/Share/add_add >/dev/null
+run_pass "$TEST_KEY" fcm conflicts <<__IN__
+y
+__IN__
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+[info] new_file: in tree conflict.
+Locally: added.
+Externally: added.
+Answer (y) to keep the local file filename.
+Answer (n) to keep the external file filename.
+Keep the local version?
+Enter "y" or "n" (or just press <return> for "n") Resolved conflicted state of 'new_file'
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: add, add, keep local (status)
+TEST_KEY=$TEST_KEY_BASE-keep-status
+run_pass "$TEST_KEY" svn status --config-dir=$TEST_DIR/.subversion/
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+ M .
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: add, add, keep local (info)
+TEST_KEY=$TEST_KEY_BASE-keep-info
+run_pass "$TEST_KEY" svn info new_file
+sed -i "/Date:\|Updated:\|UUID:\|Checksum\|Relative URL:\|Working Copy Root Path:/d" $TEST_DIR/"$TEST_KEY.out"
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+Path: new_file
+Name: new_file
+URL: $ROOT_URL/branches/dev/Share/ctrl/new_file
+Repository Root: $REPOS_URL
+Revision: 7
+Node Kind: file
+Schedule: normal
+Last Changed Author: $LOGNAME
+Last Changed Rev: 6
+
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: add, add, keep local (cat)
+TEST_KEY=$TEST_KEY_BASE-keep-cat
+run_pass "$TEST_KEY" cat new_file
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+Local contents (1)
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+teardown
+#-------------------------------------------------------------------------------
diff --git a/t/fcm-conflicts/01-tree-delete-delete.t b/t/fcm-conflicts/01-tree-delete-delete.t
new file mode 100644
index 0000000..57140c6
--- /dev/null
+++ b/t/fcm-conflicts/01-tree-delete-delete.t
@@ -0,0 +1,97 @@
+#!/bin/bash
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# Basic tests for "fcm conflicts" (tree conflict mode).
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+tests 12
+#-------------------------------------------------------------------------------
+setup
+init_repos
+init_branch ctrl $REPOS_URL
+init_branch_wc del_del $REPOS_URL
+cd $TEST_DIR/wc
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: delete, delete, discard local
+TEST_KEY=$TEST_KEY_BASE-discard
+svn switch -q $ROOT_URL/branches/dev/Share/ctrl
+svn delete -q pro/hello.pro
+svn commit -q -m "Deleted conflict file (local)"
+svn update -q
+svn switch -q $ROOT_URL/branches/dev/Share/del_del
+svn delete -q pro/hello.pro
+svn commit -q -m "Deleted conflict file (merge)"
+svn update -q
+svn switch -q $ROOT_URL/branches/dev/Share/ctrl
+fcm merge --non-interactive $ROOT_URL/branches/dev/Share/del_del >/dev/null
+run_pass "$TEST_KEY" fcm conflicts <<__IN__
+n
+__IN__
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+[info] pro/hello.pro: in tree conflict.
+Locally: deleted.
+Externally: deleted.
+Answer (y) to accept the local delete.
+Answer (n) to accept the external delete.
+Keep the local version?
+Enter "y" or "n" (or just press <return> for "n") Resolved conflicted state of 'pro/hello.pro'
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: delete, delete, discard local (status)
+TEST_KEY=$TEST_KEY_BASE-discard-status
+run_pass "$TEST_KEY" svn status --config-dir=$TEST_DIR/.subversion/
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+ M .
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+cd $TEST_DIR
+rm -rf $TEST_DIR/wc
+mkdir $TEST_DIR/wc
+svn checkout -q $ROOT_URL/branches/dev/Share/ctrl $TEST_DIR/wc
+cd $TEST_DIR/wc
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: delete, delete, keep local
+TEST_KEY=$TEST_KEY_BASE-keep
+fcm merge --non-interactive $ROOT_URL/branches/dev/Share/del_del >/dev/null
+run_pass "$TEST_KEY" fcm conflicts <<__IN__
+y
+__IN__
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+[info] pro/hello.pro: in tree conflict.
+Locally: deleted.
+Externally: deleted.
+Answer (y) to accept the local delete.
+Answer (n) to accept the external delete.
+Keep the local version?
+Enter "y" or "n" (or just press <return> for "n") Resolved conflicted state of 'pro/hello.pro'
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: delete, delete, keep local (status)
+TEST_KEY=$TEST_KEY_BASE-keep-status
+run_pass "$TEST_KEY" svn status --config-dir=$TEST_DIR/.subversion/
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+ M .
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+teardown
+#-------------------------------------------------------------------------------
diff --git a/t/fcm-conflicts/02-tree-delete-edit.t b/t/fcm-conflicts/02-tree-delete-edit.t
new file mode 100644
index 0000000..02f71ce
--- /dev/null
+++ b/t/fcm-conflicts/02-tree-delete-edit.t
@@ -0,0 +1,132 @@
+#!/bin/bash
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# Basic tests for "fcm conflicts" (tree conflict mode).
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+tests 18
+#-------------------------------------------------------------------------------
+setup
+init_repos
+init_branch ctrl $REPOS_URL
+init_branch_wc del_ed $REPOS_URL
+export SVN_EDITOR="sed -i 1i\foo"
+# Set a special (null) fcm-graphic-merge diff editor.
+export FCM_GRAPHIC_MERGE=fcm-dummy-diff
+cd $TEST_DIR/wc
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: delete, edit, discard local
+TEST_KEY=$TEST_KEY_BASE-discard
+svn switch -q $ROOT_URL/branches/dev/Share/ctrl
+svn delete -q pro/hello.pro
+svn commit -q -m "Deleted local copy of conflict file"
+svn update -q
+svn switch -q $ROOT_URL/branches/dev/Share/del_ed
+echo "Merge contents (1)" >>pro/hello.pro
+svn commit -q -m "Modified merge copy of conflict file"
+svn update -q
+svn switch -q $ROOT_URL/branches/dev/Share/ctrl
+fcm merge --non-interactive $ROOT_URL/branches/dev/Share/del_ed >/dev/null
+run_pass "$TEST_KEY" fcm conflicts <<__IN__
+n
+__IN__
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+[info] pro/hello.pro: in tree conflict.
+Locally: deleted.
+Externally: edited.
+Answer (y) to accept the local delete.
+Answer (n) to keep the file.
+Keep the local version?
+Enter "y" or "n" (or just press <return> for "n") A pro/hello.pro
+Resolved conflicted state of 'pro/hello.pro'
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: delete, edit, discard local (status)
+TEST_KEY=$TEST_KEY_BASE-discard-status
+run_pass "$TEST_KEY" svn status --config-dir=$TEST_DIR/.subversion/
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+ M .
+A + pro/hello.pro
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: delete, edit, discard local (info)
+TEST_KEY=$TEST_KEY_BASE-discard-info
+run_pass "$TEST_KEY" svn info pro/hello.pro
+sed -i "/Date:\|Updated:\|UUID:\|Checksum\|Relative URL:\|Working Copy Root Path:/d" $TEST_DIR/"$TEST_KEY.out"
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+Path: pro/hello.pro
+Name: hello.pro
+URL: $ROOT_URL/branches/dev/Share/ctrl/pro/hello.pro
+Repository Root: $REPOS_URL
+Revision: 7
+Node Kind: file
+Schedule: add
+Copied From URL: $ROOT_URL/branches/dev/Share/del_ed/pro/hello.pro
+Copied From Rev: 7
+Last Changed Author: $LOGNAME
+Last Changed Rev: 7
+
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: delete, edit, discard local (cat)
+TEST_KEY=$TEST_KEY_BASE-discard-cat
+run_pass "$TEST_KEY" cat pro/hello.pro
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+PRO HELLO
+END
+Merge contents (1)
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+cd $TEST_DIR
+rm -rf $TEST_DIR/wc
+mkdir $TEST_DIR/wc
+svn checkout -q $ROOT_URL/branches/dev/Share/ctrl $TEST_DIR/wc
+cd $TEST_DIR/wc
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: delete, edit, keep local
+TEST_KEY=$TEST_KEY_BASE-keep
+fcm merge --non-interactive $ROOT_URL/branches/dev/Share/del_ed >/dev/null
+run_pass "$TEST_KEY" fcm conflicts <<__IN__
+y
+__IN__
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+[info] pro/hello.pro: in tree conflict.
+Locally: deleted.
+Externally: edited.
+Answer (y) to accept the local delete.
+Answer (n) to keep the file.
+Keep the local version?
+Enter "y" or "n" (or just press <return> for "n") Resolved conflicted state of 'pro/hello.pro'
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: delete, edit, keep local (status)
+TEST_KEY=$TEST_KEY_BASE-keep-status
+run_pass "$TEST_KEY" svn status --config-dir=$TEST_DIR/.subversion/
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+ M .
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+teardown
+#-------------------------------------------------------------------------------
diff --git a/t/fcm-conflicts/03-tree-delete-rename.t b/t/fcm-conflicts/03-tree-delete-rename.t
new file mode 100644
index 0000000..56b4bfd
--- /dev/null
+++ b/t/fcm-conflicts/03-tree-delete-rename.t
@@ -0,0 +1,111 @@
+#!/bin/bash
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# Basic tests for "fcm conflicts" (tree conflict mode).
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+tests 15
+#-------------------------------------------------------------------------------
+setup
+init_repos
+init_branch ctrl $REPOS_URL
+init_branch_wc del_ren $REPOS_URL
+cd $TEST_DIR/wc
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: delete, rename, discard local
+TEST_KEY=$TEST_KEY_BASE-discard
+svn switch -q $ROOT_URL/branches/dev/Share/ctrl
+svn delete -q pro/hello.pro
+svn commit -q -m "Deleted conflict file (local)"
+svn update -q
+svn switch -q $ROOT_URL/branches/dev/Share/del_ren
+svn rename -q pro/hello.pro pro/hello.pro.renamed
+svn commit -q -m "Renamed conflict file (merge)"
+svn update -q
+echo "Merge changes (1)" >>pro/hello.pro.renamed
+svn commit -q -m "Modified conflict file (merge)"
+svn update -q
+svn switch -q $ROOT_URL/branches/dev/Share/ctrl
+fcm merge --non-interactive $ROOT_URL/branches/dev/Share/del_ren >/dev/null
+run_pass "$TEST_KEY" fcm conflicts <<__IN__
+n
+__IN__
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+[info] pro/hello.pro: in tree conflict.
+Locally: deleted.
+Externally: renamed to pro/hello.pro.renamed.
+Answer (y) to accept the local delete.
+Answer (n) to accept the external rename.
+Keep the local version?
+Enter "y" or "n" (or just press <return> for "n") Resolved conflicted state of 'pro/hello.pro'
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: delete, rename, discard local (status)
+TEST_KEY=$TEST_KEY_BASE-discard-status
+run_pass "$TEST_KEY" svn status --config-dir=$TEST_DIR/.subversion/
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+ M .
+A + pro/hello.pro.renamed
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: delete, rename, discard local (cat)
+TEST_KEY=$TEST_KEY_BASE-discard-cat
+run_pass "$TEST_KEY" cat pro/hello.pro.renamed
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+PRO HELLO
+END
+Merge changes (1)
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+cd $TEST_DIR
+rm -rf $TEST_DIR/wc
+mkdir $TEST_DIR/wc
+svn checkout -q $ROOT_URL/branches/dev/Share/ctrl $TEST_DIR/wc
+cd $TEST_DIR/wc
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: delete, rename, keep local
+TEST_KEY=$TEST_KEY_BASE-keep
+fcm merge --non-interactive $ROOT_URL/branches/dev/Share/del_ren >/dev/null
+run_pass "$TEST_KEY" fcm conflicts <<__IN__
+y
+__IN__
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+[info] pro/hello.pro: in tree conflict.
+Locally: deleted.
+Externally: renamed to pro/hello.pro.renamed.
+Answer (y) to accept the local delete.
+Answer (n) to accept the external rename.
+Keep the local version?
+Enter "y" or "n" (or just press <return> for "n") Reverted 'pro/hello.pro.renamed'
+Resolved conflicted state of 'pro/hello.pro'
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: delete, rename, keep local (status)
+TEST_KEY=$TEST_KEY_BASE-keep-status
+run_pass "$TEST_KEY" svn status --config-dir=$TEST_DIR/.subversion/
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+ M .
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
diff --git a/t/fcm-conflicts/04-tree-edit-delete.t b/t/fcm-conflicts/04-tree-edit-delete.t
new file mode 100644
index 0000000..a1ac908
--- /dev/null
+++ b/t/fcm-conflicts/04-tree-edit-delete.t
@@ -0,0 +1,130 @@
+#!/bin/bash
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# Basic tests for "fcm conflicts" (tree conflict mode).
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+tests 18
+#-------------------------------------------------------------------------------
+setup
+init_repos
+init_branch ctrl $REPOS_URL
+init_branch_wc ed_del $REPOS_URL
+export SVN_EDITOR="sed -i 1i\foo"
+# Set a special (null) fcm-graphic-merge diff editor.
+export FCM_GRAPHIC_MERGE=fcm-dummy-diff
+cd $TEST_DIR/wc
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: edit, delete, discard local
+TEST_KEY=$TEST_KEY_BASE-discard
+svn switch -q $ROOT_URL/branches/dev/Share/ctrl
+echo "Local contents (1)" >>pro/hello.pro
+svn commit -q -m "Modified local copy of conflict file"
+svn update -q
+svn switch -q $ROOT_URL/branches/dev/Share/ed_del
+svn delete -q pro/hello.pro
+svn commit -q -m "Deleted merge copy of conflict file"
+svn update -q
+svn switch -q $ROOT_URL/branches/dev/Share/ctrl
+fcm merge --non-interactive $ROOT_URL/branches/dev/Share/ed_del >/dev/null
+run_pass "$TEST_KEY" fcm conflicts <<__IN__
+n
+__IN__
+sed -i "/^Resolved conflicted state of 'pro\/hello.pro'$/d" $TEST_DIR/"$TEST_KEY.out"
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+[info] pro/hello.pro: in tree conflict.
+Locally: edited.
+Externally: deleted.
+Answer (y) to keep the file.
+Answer (n) to accept the external delete.
+Keep the local version?
+Enter "y" or "n" (or just press <return> for "n") D pro/hello.pro
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: edit, delete, discard local (status)
+TEST_KEY=$TEST_KEY_BASE-discard-status
+run_pass "$TEST_KEY" svn status --config-dir=$TEST_DIR/.subversion/
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+ M .
+D pro/hello.pro
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: edit, delete, discard local (info)
+TEST_KEY=$TEST_KEY_BASE-discard-info
+run_pass "$TEST_KEY" svn info pro/hello.pro
+sed -i "/Date:\|Updated:\|UUID:\|Checksum\|Relative URL:\|Working Copy Root Path:/d" $TEST_DIR/$TEST_KEY.out
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+Path: pro/hello.pro
+Name: hello.pro
+URL: $ROOT_URL/branches/dev/Share/ctrl/pro/hello.pro
+Repository Root: $REPOS_URL
+Revision: 7
+Node Kind: file
+Schedule: delete
+Last Changed Author: $LOGNAME
+Last Changed Rev: 6
+
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+cd $TEST_DIR
+rm -rf $TEST_DIR/wc
+mkdir $TEST_DIR/wc
+svn checkout -q $ROOT_URL/branches/dev/Share/ctrl $TEST_DIR/wc
+cd $TEST_DIR/wc
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: edit, delete, keep local
+TEST_KEY=$TEST_KEY_BASE-keep
+fcm merge --non-interactive $ROOT_URL/branches/dev/Share/ed_del >/dev/null
+run_pass "$TEST_KEY" fcm conflicts <<__IN__
+y
+__IN__
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+[info] pro/hello.pro: in tree conflict.
+Locally: edited.
+Externally: deleted.
+Answer (y) to keep the file.
+Answer (n) to accept the external delete.
+Keep the local version?
+Enter "y" or "n" (or just press <return> for "n") Resolved conflicted state of 'pro/hello.pro'
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: edit, delete, keep local (status)
+TEST_KEY=$TEST_KEY_BASE-keep-status
+run_pass "$TEST_KEY" svn status --config-dir=$TEST_DIR/.subversion/
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+ M .
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: edit, delete, keep local (cat)
+TEST_KEY=$TEST_KEY_BASE-keep-cat
+run_pass "$TEST_KEY" cat pro/hello.pro
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+PRO HELLO
+END
+Local contents (1)
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+teardown
+#-------------------------------------------------------------------------------
diff --git a/t/fcm-conflicts/05-tree-edit-rename.t b/t/fcm-conflicts/05-tree-edit-rename.t
new file mode 100644
index 0000000..7e2fc01
--- /dev/null
+++ b/t/fcm-conflicts/05-tree-edit-rename.t
@@ -0,0 +1,185 @@
+#!/bin/bash
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# Basic tests for "fcm conflicts" (tree conflict mode).
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+tests 24
+#-------------------------------------------------------------------------------
+setup
+init_repos
+init_branch ctrl $REPOS_URL
+init_branch_wc ed_ren $REPOS_URL
+export SVN_EDITOR="sed -i 1i\foo"
+# Set a special (null) fcm-graphic-merge diff editor.
+export FCM_GRAPHIC_MERGE=fcm-dummy-diff
+cd $TEST_DIR/wc
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: edit, rename, discard local
+TEST_KEY=$TEST_KEY_BASE-discard
+svn switch -q $ROOT_URL/branches/dev/Share/ctrl
+echo "Local contents (1)" >>pro/hello.pro
+svn commit -q -m "Modified local copy of conflict file"
+svn update -q
+svn switch -q $ROOT_URL/branches/dev/Share/ed_ren
+echo "Merge contents (1)" >>pro/hello.pro
+svn rename -q pro/hello.pro pro/hello.pro.renamed
+svn commit -q -m "Modified and renamed merge copy of conflict file"
+svn update -q
+echo "Merge contents (2)" >>pro/hello.pro.renamed
+svn commit -q -m "Modified the merge copy of renamed conflict file"
+svn update -q
+svn switch -q $ROOT_URL/branches/dev/Share/ctrl
+fcm merge --non-interactive $ROOT_URL/branches/dev/Share/ed_ren >/dev/null
+run_pass "$TEST_KEY" fcm conflicts <<__IN__
+n
+__IN__
+sed -i "/^Resolved conflicted state of 'pro\/hello.pro'$/d" $TEST_DIR/"$TEST_KEY.out"
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+[info] pro/hello.pro: in tree conflict.
+Locally: edited.
+Externally: renamed to pro/hello.pro.renamed.
+Answer (y) to keep the file.
+Answer (n) to accept the external rename.
+You can then merge in changes.
+Keep the local version?
+Enter "y" or "n" (or just press <return> for "n") diff3 pro/hello.pro.renamed.working pro/hello.pro.renamed.merge-left.r1 pro/hello.pro.renamed.merge-right.r8
+====
+1:3c
+ Local contents (1)
+2:2a
+3:3,4c
+ Merge contents (1)
+ Merge contents (2)
+D pro/hello.pro
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: edit, rename, discard local (status)
+TEST_KEY=$TEST_KEY_BASE-discard-status
+run_pass "$TEST_KEY" svn status --config-dir=$TEST_DIR/.subversion/
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+ M .
+D pro/hello.pro
+A + pro/hello.pro.renamed
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: edit, rename, discard local (info)
+TEST_KEY=$TEST_KEY_BASE-discard-info
+run_pass "$TEST_KEY" svn info pro/hello.pro.renamed
+sed -i "/Date:\|Updated:\|UUID:\|Checksum\|Relative URL:\|Working Copy Root Path:/d" $TEST_DIR/"$TEST_KEY.out"
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+Path: pro/hello.pro.renamed
+Name: hello.pro.renamed
+URL: $ROOT_URL/branches/dev/Share/ctrl/pro/hello.pro.renamed
+Repository Root: $REPOS_URL
+Revision: 8
+Node Kind: file
+Schedule: add
+Copied From URL: $ROOT_URL/branches/dev/Share/ed_ren/pro/hello.pro.renamed
+Copied From Rev: 8
+Last Changed Author: $LOGNAME
+Last Changed Rev: 8
+
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: delete, rename, discard local (cat)
+TEST_KEY=$TEST_KEY_BASE-discard-cat
+run_pass "$TEST_KEY" cat pro/hello.pro.renamed
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+PRO HELLO
+END
+Merge contents (1)
+Merge contents (2)
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+cd $TEST_DIR
+rm -rf $TEST_DIR/wc
+mkdir $TEST_DIR/wc
+svn checkout -q $ROOT_URL/branches/dev/Share/ctrl $TEST_DIR/wc
+cd $TEST_DIR/wc
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: edit, rename, keep local
+TEST_KEY=$TEST_KEY_BASE-keep
+fcm merge --non-interactive $ROOT_URL/branches/dev/Share/ed_ren >/dev/null
+run_pass "$TEST_KEY" fcm conflicts <<__IN__
+y
+__IN__
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+[info] pro/hello.pro: in tree conflict.
+Locally: edited.
+Externally: renamed to pro/hello.pro.renamed.
+Answer (y) to keep the file.
+Answer (n) to accept the external rename.
+You can then merge in changes.
+Keep the local version?
+Enter "y" or "n" (or just press <return> for "n") diff3 pro/hello.pro.working pro/hello.pro.merge-left.r1 pro/hello.pro.merge-right.r8
+====
+1:3c
+ Local contents (1)
+2:2a
+3:3,4c
+ Merge contents (1)
+ Merge contents (2)
+Reverted 'pro/hello.pro.renamed'
+Resolved conflicted state of 'pro/hello.pro'
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: edit, rename, keep local (status)
+TEST_KEY=$TEST_KEY_BASE-keep-status
+run_pass "$TEST_KEY" svn status --config-dir=$TEST_DIR/.subversion/
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+ M .
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: edit, rename, keep local (info)
+TEST_KEY=$TEST_KEY_BASE-keep-info
+run_pass "$TEST_KEY" svn info pro/hello.pro
+sed -i "/Date:\|Updated:\|UUID:\|Checksum\|Relative URL:\|Working Copy Root Path:/d" $TEST_DIR/"$TEST_KEY.out"
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+Path: pro/hello.pro
+Name: hello.pro
+URL: $ROOT_URL/branches/dev/Share/ctrl/pro/hello.pro
+Repository Root: $REPOS_URL
+Revision: 8
+Node Kind: file
+Schedule: normal
+Last Changed Author: $LOGNAME
+Last Changed Rev: 6
+
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: delete, rename, keep local (cat)
+TEST_KEY=$TEST_KEY_BASE-keep-cat
+run_pass "$TEST_KEY" cat pro/hello.pro
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+PRO HELLO
+END
+Local contents (1)
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+teardown
+#-------------------------------------------------------------------------------
diff --git a/t/fcm-conflicts/06-tree-rename-delete.t b/t/fcm-conflicts/06-tree-rename-delete.t
new file mode 100644
index 0000000..be05fbb
--- /dev/null
+++ b/t/fcm-conflicts/06-tree-rename-delete.t
@@ -0,0 +1,111 @@
+#!/bin/bash
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# Basic tests for "fcm conflicts" (tree conflict mode).
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+tests 15
+#-------------------------------------------------------------------------------
+setup
+init_repos
+init_branch ctrl $REPOS_URL
+init_branch_wc ren_del $REPOS_URL
+cd $TEST_DIR/wc
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: rename, delete, discard local
+TEST_KEY=$TEST_KEY_BASE-discard
+svn switch -q $ROOT_URL/branches/dev/Share/ctrl
+svn rename -q pro/hello.pro pro/hello.pro.renamed
+svn commit -q -m "Renamed conflict file (local)"
+svn update -q
+echo "Local contents (1)" >>pro/hello.pro.renamed
+svn commit -q -m "Modified conflict file (local)"
+svn update -q
+svn switch -q $ROOT_URL/branches/dev/Share/ren_del
+svn delete -q pro/hello.pro
+svn commit -q -m "Deleted conflict file (merge)"
+svn update -q
+svn switch -q $ROOT_URL/branches/dev/Share/ctrl
+fcm merge --non-interactive $ROOT_URL/branches/dev/Share/ren_del >/dev/null
+run_pass "$TEST_KEY" fcm conflicts <<__IN__
+n
+__IN__
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+[info] pro/hello.pro: in tree conflict.
+Locally: renamed to pro/hello.pro.renamed.
+Externally: deleted.
+Answer (y) to accept the local rename.
+Answer (n) to accept the external delete.
+Keep the local version?
+Enter "y" or "n" (or just press <return> for "n") D pro/hello.pro.renamed
+Resolved conflicted state of 'pro/hello.pro'
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: rename, delete, discard local (status)
+TEST_KEY=$TEST_KEY_BASE-discard-status
+run_pass "$TEST_KEY" svn status --config-dir=$TEST_DIR/.subversion/
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+ M .
+D pro/hello.pro.renamed
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+cd $TEST_DIR
+rm -rf $TEST_DIR/wc
+mkdir $TEST_DIR/wc
+svn checkout -q $ROOT_URL/branches/dev/Share/ctrl $TEST_DIR/wc
+cd $TEST_DIR/wc
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: rename, delete, keep local
+TEST_KEY=$TEST_KEY_BASE-keep
+fcm merge --non-interactive $ROOT_URL/branches/dev/Share/ren_del >/dev/null
+run_pass "$TEST_KEY" fcm conflicts <<__IN__
+y
+__IN__
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+[info] pro/hello.pro: in tree conflict.
+Locally: renamed to pro/hello.pro.renamed.
+Externally: deleted.
+Answer (y) to accept the local rename.
+Answer (n) to accept the external delete.
+Keep the local version?
+Enter "y" or "n" (or just press <return> for "n") Resolved conflicted state of 'pro/hello.pro'
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: rename, delete, keep local (status)
+TEST_KEY=$TEST_KEY_BASE-keep-status
+run_pass "$TEST_KEY" svn status --config-dir=$TEST_DIR/.subversion/
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+ M .
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: rename, delete, keep local (cat)
+TEST_KEY=$TEST_KEY_BASE-keep-cat
+run_pass "$TEST_KEY" cat pro/hello.pro.renamed
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+PRO HELLO
+END
+Local contents (1)
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+teardown
diff --git a/t/fcm-conflicts/07-tree-rename-edit.t b/t/fcm-conflicts/07-tree-rename-edit.t
new file mode 100644
index 0000000..28d74ea
--- /dev/null
+++ b/t/fcm-conflicts/07-tree-rename-edit.t
@@ -0,0 +1,142 @@
+#!/bin/bash
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# Basic tests for "fcm conflicts" (tree conflict mode).
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+tests 18
+#-------------------------------------------------------------------------------
+setup
+init_repos
+init_branch ctrl $REPOS_URL
+init_branch_wc ren_ed $REPOS_URL
+# Set a special (null) fcm-graphic-merge diff editor.
+export FCM_GRAPHIC_MERGE=fcm-dummy-diff
+cd $TEST_DIR/wc
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: rename, edit, discard local
+TEST_KEY=$TEST_KEY_BASE-discard
+svn switch -q $ROOT_URL/branches/dev/Share/ctrl
+svn rename -q pro/hello.pro pro/hello.pro.renamed
+svn commit -q -m "Renamed conflict file (local)"
+svn update -q
+echo "Local contents (1)" >>pro/hello.pro.renamed
+svn commit -q -m "Modified conflict file (local)"
+svn update -q
+svn switch -q $ROOT_URL/branches/dev/Share/ren_ed
+echo "Merge contents (1)" >>pro/hello.pro
+svn commit -q -m "Modified conflict file (merge)"
+svn update -q
+svn switch -q $ROOT_URL/branches/dev/Share/ctrl
+fcm merge --non-interactive $ROOT_URL/branches/dev/Share/ren_ed >/dev/null
+run_pass "$TEST_KEY" fcm conflicts <<__IN__
+n
+__IN__
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+[info] pro/hello.pro: in tree conflict.
+Locally: renamed to pro/hello.pro.renamed.
+Externally: edited.
+Answer (y) to accept the local rename.
+Answer (n) to keep the file.
+You can then merge in changes.
+Keep the local version?
+Enter "y" or "n" (or just press <return> for "n") diff3 pro/hello.pro.renamed.working pro/hello.pro.renamed.merge-left.r1 pro/hello.pro.renamed.merge-right.r8
+====
+1:3c
+ Local contents (1)
+2:2a
+3:3c
+ Merge contents (1)
+A pro/hello.pro
+D pro/hello.pro.renamed
+Resolved conflicted state of 'pro/hello.pro'
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: rename, edit, discard local (status)
+TEST_KEY=$TEST_KEY_BASE-discard-status
+run_pass "$TEST_KEY" svn status --config-dir=$TEST_DIR/.subversion/
+sed -i "/^ \{8\}> moved /d" $TEST_DIR/"$TEST_KEY.out"
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+ M .
+A + pro/hello.pro
+D pro/hello.pro.renamed
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: rename, edit, discard local (cat)
+TEST_KEY=$TEST_KEY_BASE-discard-cat
+run_pass "$TEST_KEY" cat pro/hello.pro
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+PRO HELLO
+END
+Local contents (1)
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+cd $TEST_DIR
+rm -rf $TEST_DIR/wc
+mkdir $TEST_DIR/wc
+svn checkout -q $ROOT_URL/branches/dev/Share/ctrl $TEST_DIR/wc
+cd $TEST_DIR/wc
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: rename, edit, keep local
+TEST_KEY=$TEST_KEY_BASE-keep
+fcm merge --non-interactive $ROOT_URL/branches/dev/Share/ren_ed >/dev/null
+run_pass "$TEST_KEY" fcm conflicts <<__IN__
+y
+__IN__
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+[info] pro/hello.pro: in tree conflict.
+Locally: renamed to pro/hello.pro.renamed.
+Externally: edited.
+Answer (y) to accept the local rename.
+Answer (n) to keep the file.
+You can then merge in changes.
+Keep the local version?
+Enter "y" or "n" (or just press <return> for "n") diff3 pro/hello.pro.renamed.working pro/hello.pro.renamed.merge-left.r1 pro/hello.pro.renamed.merge-right.r8
+====
+1:3c
+ Local contents (1)
+2:2a
+3:3c
+ Merge contents (1)
+Resolved conflicted state of 'pro/hello.pro'
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: rename, edit, keep local (status)
+TEST_KEY=$TEST_KEY_BASE-keep-status
+run_pass "$TEST_KEY" svn status --config-dir=$TEST_DIR/.subversion/
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+ M .
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: rename, edit, keep local (cat)
+TEST_KEY=$TEST_KEY_BASE-keep-cat
+run_pass "$TEST_KEY" cat pro/hello.pro.renamed
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+PRO HELLO
+END
+Local contents (1)
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+teardown
diff --git a/t/fcm-conflicts/08-tree-rename-rename-diff.t b/t/fcm-conflicts/08-tree-rename-rename-diff.t
new file mode 100644
index 0000000..fd44a7a
--- /dev/null
+++ b/t/fcm-conflicts/08-tree-rename-rename-diff.t
@@ -0,0 +1,144 @@
+#!/bin/bash
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# Basic tests for "fcm conflicts" (tree conflict mode).
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+tests 18
+#-------------------------------------------------------------------------------
+setup
+init_repos
+init_branch ctrl $REPOS_URL
+init_branch_wc ren_ren $REPOS_URL
+# Set a special (null) fcm-graphic-merge diff editor.
+export FCM_GRAPHIC_MERGE=fcm-dummy-diff
+cd $TEST_DIR/wc
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: rename, rename, diff rename, discard local
+TEST_KEY=$TEST_KEY_BASE-discard
+svn switch -q $ROOT_URL/branches/dev/Share/ctrl
+svn rename -q pro/hello.pro pro/hello.pro.renamed-local
+svn commit -q -m "Renamed conflict file (local)"
+svn update -q
+echo "Local contents (1)" >>pro/hello.pro.renamed-local
+svn commit -q -m "Modified conflict file (local)"
+svn update -q
+svn switch -q $ROOT_URL/branches/dev/Share/ren_ren
+echo "Merge contents (1)" >>pro/hello.pro
+svn commit -q -m "Modified conflict file (merge)"
+svn update -q
+svn rename -q pro/hello.pro pro/hello.pro.renamed-merge
+svn commit -q -m "Renamed conflict file (merge)"
+svn update -q
+svn switch -q $ROOT_URL/branches/dev/Share/ctrl
+fcm merge --non-interactive $ROOT_URL/branches/dev/Share/ren_ren >/dev/null
+run_pass "$TEST_KEY" fcm conflicts <<__IN__
+n
+__IN__
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+[info] pro/hello.pro: in tree conflict.
+Locally: renamed to pro/hello.pro.renamed-local.
+Externally: renamed to pro/hello.pro.renamed-merge.
+Answer (y) to accept the local rename.
+Answer (n) to accept the external rename.
+You can then merge in changes.
+Keep the local version?
+Enter "y" or "n" (or just press <return> for "n") diff3 pro/hello.pro.renamed-merge.working pro/hello.pro.renamed-merge.merge-left.r1 pro/hello.pro.renamed-merge.merge-right.r9
+====
+1:3c
+ Local contents (1)
+2:2a
+3:3c
+ Merge contents (1)
+D pro/hello.pro.renamed-local
+Resolved conflicted state of 'pro/hello.pro'
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: rename, rename, diff rename, discard local (status)
+TEST_KEY=$TEST_KEY_BASE-discard-status
+run_pass "$TEST_KEY" svn status --config-dir=$TEST_DIR/.subversion/
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+ M .
+D pro/hello.pro.renamed-local
+A + pro/hello.pro.renamed-merge
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: rename, rename, diff rename, discard local (cat)
+TEST_KEY=$TEST_KEY_BASE-discard-cat
+run_pass "$TEST_KEY" cat pro/hello.pro.renamed-merge
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+PRO HELLO
+END
+Merge contents (1)
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+cd $TEST_DIR
+rm -rf $TEST_DIR/wc
+mkdir $TEST_DIR/wc
+svn checkout -q $ROOT_URL/branches/dev/Share/ctrl $TEST_DIR/wc
+cd $TEST_DIR/wc
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: rename, rename, diff rename, keep local
+TEST_KEY=$TEST_KEY_BASE-keep
+fcm merge --non-interactive $ROOT_URL/branches/dev/Share/ren_ren >/dev/null
+run_pass "$TEST_KEY" fcm conflicts <<__IN__
+y
+__IN__
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+[info] pro/hello.pro: in tree conflict.
+Locally: renamed to pro/hello.pro.renamed-local.
+Externally: renamed to pro/hello.pro.renamed-merge.
+Answer (y) to accept the local rename.
+Answer (n) to accept the external rename.
+You can then merge in changes.
+Keep the local version?
+Enter "y" or "n" (or just press <return> for "n") diff3 pro/hello.pro.renamed-local.working pro/hello.pro.renamed-local.merge-left.r1 pro/hello.pro.renamed-local.merge-right.r9
+====
+1:3c
+ Local contents (1)
+2:2a
+3:3c
+ Merge contents (1)
+Reverted 'pro/hello.pro.renamed-merge'
+Resolved conflicted state of 'pro/hello.pro'
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: rename, rename, diff rename, keep local (status)
+TEST_KEY=$TEST_KEY_BASE-keep-status
+run_pass "$TEST_KEY" svn status --config-dir=$TEST_DIR/.subversion/
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+ M .
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: rename, rename, diff rename, keep local (cat)
+TEST_KEY=$TEST_KEY_BASE-keep-cat
+run_pass "$TEST_KEY" cat pro/hello.pro.renamed-local
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+PRO HELLO
+END
+Local contents (1)
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+teardown
diff --git a/t/fcm-conflicts/09-tree-rename-rename-same.t b/t/fcm-conflicts/09-tree-rename-rename-same.t
new file mode 100644
index 0000000..4fb2d91
--- /dev/null
+++ b/t/fcm-conflicts/09-tree-rename-rename-same.t
@@ -0,0 +1,220 @@
+#!/bin/bash
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# Basic tests for "fcm conflicts" (tree conflict mode).
+# TODO: The behaviour exhibited by fcm conflicts in this file is wrong.
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+tests 33
+#-------------------------------------------------------------------------------
+setup
+init_repos
+init_branch ctrl $REPOS_URL
+init_branch_wc ren_ren $REPOS_URL
+# Set a special (null) fcm-graphic-merge diff editor.
+export FCM_GRAPHIC_MERGE=fcm-dummy-diff
+cd $TEST_DIR/wc
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: rename, rename, diff rename, discard local, discard local
+TEST_KEY=$TEST_KEY_BASE-discard-discard
+svn switch -q $ROOT_URL/branches/dev/Share/ctrl
+svn rename -q pro/hello.pro pro/hello.pro.renamed
+svn commit -q -m "Renamed conflict file (local)"
+svn update -q
+echo "Local contents (1)" >>pro/hello.pro.renamed
+svn commit -q -m "Modified conflict file (local)"
+svn update -q
+svn switch -q $ROOT_URL/branches/dev/Share/ren_ren
+echo "Merge contents (1)" >>pro/hello.pro
+svn commit -q -m "Modified conflict file (merge)"
+svn update -q
+svn rename -q pro/hello.pro pro/hello.pro.renamed
+svn commit -q -m "Renamed conflict file (merge)"
+svn update -q
+svn switch -q $ROOT_URL/branches/dev/Share/ctrl
+fcm merge --non-interactive $ROOT_URL/branches/dev/Share/ren_ren >/dev/null
+run_pass "$TEST_KEY" fcm conflicts <<__IN__
+n
+n
+__IN__
+sed -i -n "1,8p" $TEST_DIR/"$TEST_KEY.out"
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+[info] pro/hello.pro: in tree conflict.
+Locally: renamed to pro/hello.pro.renamed.
+Externally: deleted.
+Answer (y) to accept the local rename.
+Answer (n) to accept the external delete.
+Keep the local version?
+Enter "y" or "n" (or just press <return> for "n") D pro/hello.pro.renamed
+Resolved conflicted state of 'pro/hello.pro'
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: rename, rename, diff rename, discard local, discard local (status)
+TEST_KEY=$TEST_KEY_BASE-discard-discard-status
+run_pass "$TEST_KEY" svn status --config-dir=$TEST_DIR/.subversion/
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+ M .
+D pro/hello.pro.renamed
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+cd $TEST_DIR
+rm -rf $TEST_DIR/wc
+mkdir $TEST_DIR/wc
+svn checkout -q $ROOT_URL/branches/dev/Share/ctrl $TEST_DIR/wc
+cd $TEST_DIR/wc
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: rename, rename, diff rename, discard local, keep local
+TEST_KEY=$TEST_KEY_BASE-discard-keep
+fcm merge --non-interactive $ROOT_URL/branches/dev/Share/ren_ren >/dev/null
+run_pass "$TEST_KEY" fcm conflicts <<__IN__
+n
+y
+__IN__
+sed -i -n "1,8p" $TEST_DIR/"$TEST_KEY.out"
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+[info] pro/hello.pro: in tree conflict.
+Locally: renamed to pro/hello.pro.renamed.
+Externally: deleted.
+Answer (y) to accept the local rename.
+Answer (n) to accept the external delete.
+Keep the local version?
+Enter "y" or "n" (or just press <return> for "n") D pro/hello.pro.renamed
+Resolved conflicted state of 'pro/hello.pro'
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: rename, rename, diff rename, discard local, keep local (status)
+TEST_KEY=$TEST_KEY_BASE-discard-keep-status
+run_pass "$TEST_KEY" svn status --config-dir=$TEST_DIR/.subversion/
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+ M .
+D pro/hello.pro.renamed
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: rename, rename, diff rename, discard local, keep local (cat)
+TEST_KEY=$TEST_KEY_BASE-discard-keep-cat
+run_fail "$TEST_KEY" cat pro/hello.pro.renamed
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" </dev/null
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" <<__ERR__
+cat: pro/hello.pro.renamed: No such file or directory
+__ERR__
+#-------------------------------------------------------------------------------
+cd $TEST_DIR
+rm -rf $TEST_DIR/wc
+mkdir $TEST_DIR/wc
+svn checkout -q $ROOT_URL/branches/dev/Share/ctrl $TEST_DIR/wc
+cd $TEST_DIR/wc
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: rename, rename, diff rename, keep local, discard local
+TEST_KEY=$TEST_KEY_BASE-keep-discard
+fcm merge --non-interactive $ROOT_URL/branches/dev/Share/ren_ren >/dev/null
+run_pass "$TEST_KEY" fcm conflicts <<__IN__
+y
+n
+__IN__
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+[info] pro/hello.pro: in tree conflict.
+Locally: renamed to pro/hello.pro.renamed.
+Externally: deleted.
+Answer (y) to accept the local rename.
+Answer (n) to accept the external delete.
+Keep the local version?
+Enter "y" or "n" (or just press <return> for "n") Resolved conflicted state of 'pro/hello.pro'
+[info] pro/hello.pro.renamed: in tree conflict.
+Locally: added.
+Externally: added.
+Answer (y) to keep the local file filename.
+Answer (n) to keep the external file filename.
+Keep the local version?
+Enter "y" or "n" (or just press <return> for "n") Resolved conflicted state of 'pro/hello.pro.renamed'
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: rename, rename, diff rename, keep local (status)
+TEST_KEY=$TEST_KEY_BASE-keep-discard-status
+run_pass "$TEST_KEY" svn status --config-dir=$TEST_DIR/.subversion/
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+ M .
+M pro/hello.pro.renamed
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: rename, rename, diff rename, keep local (cat)
+TEST_KEY=$TEST_KEY_BASE-keep-discard-cat
+run_pass "$TEST_KEY" cat pro/hello.pro.renamed
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+PRO HELLO
+END
+Merge contents (1)
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+cd $TEST_DIR
+rm -rf $TEST_DIR/wc
+mkdir $TEST_DIR/wc
+svn checkout -q $ROOT_URL/branches/dev/Share/ctrl $TEST_DIR/wc
+cd $TEST_DIR/wc
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: rename, rename, diff rename, keep local, keep local
+TEST_KEY=$TEST_KEY_BASE-keep-keep
+fcm merge --non-interactive $ROOT_URL/branches/dev/Share/ren_ren >/dev/null
+run_pass "$TEST_KEY" fcm conflicts <<__IN__
+y
+y
+__IN__
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+[info] pro/hello.pro: in tree conflict.
+Locally: renamed to pro/hello.pro.renamed.
+Externally: deleted.
+Answer (y) to accept the local rename.
+Answer (n) to accept the external delete.
+Keep the local version?
+Enter "y" or "n" (or just press <return> for "n") Resolved conflicted state of 'pro/hello.pro'
+[info] pro/hello.pro.renamed: in tree conflict.
+Locally: added.
+Externally: added.
+Answer (y) to keep the local file filename.
+Answer (n) to keep the external file filename.
+Keep the local version?
+Enter "y" or "n" (or just press <return> for "n") Resolved conflicted state of 'pro/hello.pro.renamed'
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: rename, rename, diff rename, keep local, keep local (status)
+TEST_KEY=$TEST_KEY_BASE-keep-keep-status
+run_pass "$TEST_KEY" svn status --config-dir=$TEST_DIR/.subversion/
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+ M .
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm conflicts: rename, rename, diff rename, keep local, keep local (cat)
+TEST_KEY=$TEST_KEY_BASE-keep-keep-cat
+run_pass "$TEST_KEY" cat pro/hello.pro.renamed
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+PRO HELLO
+END
+Local contents (1)
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+teardown
diff --git a/t/fcm-conflicts/10-text.t b/t/fcm-conflicts/10-text.t
new file mode 100644
index 0000000..de7c53f
--- /dev/null
+++ b/t/fcm-conflicts/10-text.t
@@ -0,0 +1,180 @@
+#!/bin/bash
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# Basic tests for "fcm conflicts" (text conflict following merge).
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+tests 11
+#-------------------------------------------------------------------------------
+setup
+init_repos
+init_merge_branches merge1 merge2 $REPOS_URL
+cd $TEST_DIR/wc
+#-------------------------------------------------------------------------------
+TEST_KEY=$TEST_KEY_BASE-merge
+export SVN_EDITOR="sed -i 1i\foo"
+echo "The End" >> lib/python/info/poems.py
+svn commit -m "Finish off the poem" -q
+svn update -q
+run_pass "$TEST_KEY" fcm merge --non-interactive $ROOT_URL/branches/dev/Share/merge1
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+TEST_KEY=$TEST_KEY_BASE-merge-status
+run_pass "$TEST_KEY" svn status --config-dir=$TEST_DIR/.subversion/
+if $SVN_VERSION_IS_16; then
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<'__OUT__'
+ M .
+? unversioned_file
+M subroutine/hello_sub_dummy.h
+A + added_file
+A + module/tree_conflict_file
+M module/hello_constants_dummy.inc
+M module/hello_constants.inc
+M module/hello_constants.f90
+A + added_directory
+A + added_directory/hello_constants_dummy.inc
+A + added_directory/hello_constants.inc
+A + added_directory/hello_constants.f90
+? lib/python/info/poems.py.merge-left.r1
+? lib/python/info/poems.py.merge-right.r5
+? lib/python/info/poems.py.working
+C lib/python/info/poems.py
+__OUT__
+else
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<'__OUT__'
+ M .
+A + added_directory
+A + added_file
+C lib/python/info/poems.py
+? lib/python/info/poems.py.merge-left.r1
+? lib/python/info/poems.py.merge-right.r5
+? lib/python/info/poems.py.working
+M module/hello_constants.f90
+M module/hello_constants.inc
+M module/hello_constants_dummy.inc
+A + module/tree_conflict_file
+M subroutine/hello_sub_dummy.h
+? unversioned_file
+Summary of conflicts:
+ Text conflicts: 1
+__OUT__
+fi
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+TEST_KEY=$TEST_KEY_BASE-merge-conflicts
+export FCM_GRAPHIC_MERGE=fcm-dummy-diff
+run_pass "$TEST_KEY" fcm conflicts <<'__IN__'
+y
+y
+__IN__
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+[info] lib/python/info/poems.py: in text conflict.
+diff3 $PWD/lib/python/info/poems.py.working $PWD/lib/python/info/poems.py.merge-left.r1 $PWD/lib/python/info/poems.py.merge-right.r5
+====3
+1:1,2c
+2:1,2c
+ #!/usr/bin/env python
+ # -*- coding: utf-8 -*-
+3:0a
+====3
+1:6c
+2:6c
+ It needs a doctor for its eyes,
+3:4c
+ It needs a doctor FOR its eyes,
+====3
+1:8,9c
+2:8,9c
+ However, if you feel inclined
+ To get one (to improve your mind,
+3:6,8c
+ However, if you feel INclINed
+ To get one (
+ to improve your mINd,
+====3
+1:12c
+2:12c
+ And when it flies into a rage
+3:11c
+ And when it flies INto a rage
+====3
+1:14c
+2:14c
+ I had an aunt in Yucatan
+3:13c
+ I had an aunt IN Yucatan
+====3
+1:16c
+2:16c
+ And kept it for a pet.
+3:15c
+ And kept it FOR a pet.
+====3
+1:19c
+2:19c
+ The Snake is living yet.
+3:18c
+ The Snake is livINg yet.
+====
+1:24,25c
+ print "\n", __doc__
+ The End
+2:24c
+ print "\n", __doc__
+3:23c
+ prINt "\n", __doc__
+Run "svn resolve --accept working lib/python/info/poems.py"?
+Enter "y" or "n" (or just press <return> for "n") Resolved conflicted state of 'lib/python/info/poems.py'
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+TEST_KEY=$TEST_KEY_BASE-merge-conflicts-status
+run_pass "$TEST_KEY" svn status --config-dir=$TEST_DIR/.subversion/
+if $SVN_VERSION_IS_16; then
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<'__OUT__'
+ M .
+? unversioned_file
+M subroutine/hello_sub_dummy.h
+A + added_file
+A + module/tree_conflict_file
+M module/hello_constants_dummy.inc
+M module/hello_constants.inc
+M module/hello_constants.f90
+A + added_directory
+A + added_directory/hello_constants_dummy.inc
+A + added_directory/hello_constants.inc
+A + added_directory/hello_constants.f90
+M lib/python/info/poems.py
+__OUT__
+else
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<'__OUT__'
+ M .
+A + added_directory
+A + added_file
+M lib/python/info/poems.py
+M module/hello_constants.f90
+M module/hello_constants.inc
+M module/hello_constants_dummy.inc
+A + module/tree_conflict_file
+M subroutine/hello_sub_dummy.h
+? unversioned_file
+__OUT__
+fi
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
diff --git a/t/fcm-conflicts/test_header b/t/fcm-conflicts/test_header
new file mode 100644
index 0000000..672ce12
--- /dev/null
+++ b/t/fcm-conflicts/test_header
@@ -0,0 +1,232 @@
+#!/bin/bash
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# Optional enviroment variables:
+# TEST_PROJECT (tests using given project name)
+# TEST_REMOTE_HOST (tests using svn+ssh repositories located on given host)
+# ------------------------------------------------------------------------------
+
+. $(dirname $0)/../lib/bash/test_header
+
+function file_cmp() {
+ local TEST_KEY=$1
+ local FILE_ACTUAL=$2
+ local FILE_EXPECT=${3:--}
+ if cmp $TEST_DIR/$FILE_ACTUAL $FILE_EXPECT; then
+ pass $TEST_KEY
+ return
+ fi
+ fail $TEST_KEY
+}
+
+function file_grep() {
+ local TEST_KEY=$1
+ local PATTERN=$2
+ local FILE=$3
+ if grep -q -e "$PATTERN" $TEST_DIR/$FILE; then
+ pass $TEST_KEY
+ return
+ fi
+ fail $TEST_KEY
+}
+
+function file_test() {
+ local TEST_KEY=$1
+ local FILE=$2
+ local OPTION=${3:--e}
+ if test $OPTION $TEST_DIR/$FILE; then
+ pass $TEST_KEY
+ else
+ fail $TEST_KEY
+ fi
+}
+
+function file_xxdiff() {
+ local TEST_KEY=$1
+ local FILE_ACTUAL=$2
+ local FILE_EXPECT=${3:--}
+ if xxdiff -D $TEST_DIR/$FILE_ACTUAL $FILE_EXPECT; then
+ pass $TEST_KEY
+ return
+ fi
+ fail $TEST_KEY
+}
+
+function init_repos() {
+ if [[ -n ${TEST_REMOTE_HOST:-} ]]; then
+ TEST_REMOTE_DIR=$(ssh $TEST_REMOTE_HOST "mktemp -d")
+ ssh $TEST_REMOTE_HOST "svnadmin create --fs-type fsfs $TEST_REMOTE_DIR"
+ REPOS_URL="svn+ssh://${TEST_REMOTE_HOST}$TEST_REMOTE_DIR"
+ else
+ svnadmin create --fs-type fsfs $TEST_DIR/test_repos
+ REPOS_URL="file://$TEST_DIR/test_repos"
+ fi
+ ROOT_URL=$REPOS_URL
+ PROJECT=
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ PROJECT=$TEST_PROJECT"/"
+ fi
+ svn import -q $TEST_SOURCE_DIR/../etc/repo_files \
+ $REPOS_URL/$PROJECT/trunk -m "initial trunk import"
+ svn mkdir -q $REPOS_URL/$PROJECT/tags -m "make tags"
+ svn mkdir -q --parents $REPOS_URL/$PROJECT/branches/dev/Share -m " "
+}
+
+function init_repos_layout_roses() {
+ if [[ -n ${TEST_REMOTE_HOST:-} ]]; then
+ TEST_REMOTE_DIR=$(ssh $TEST_REMOTE_HOST "mktemp -d")
+ ssh $TEST_REMOTE_HOST "svnadmin create --fs-type fsfs $TEST_REMOTE_DIR"
+ REPOS_URL="svn+ssh://${TEST_REMOTE_HOST}$TEST_REMOTE_DIR"
+ else
+ svnadmin create --fs-type fsfs $TEST_DIR/test_repos
+ REPOS_URL="file://$TEST_DIR/test_repos"
+ fi
+ svn mkdir -q --parents $REPOS_URL/a/a/0/0/0/trunk
+ svn import -q $TEST_SOURCE_DIR/../etc/repo_files \
+ $REPOS_URL/a/a/0/0/0/trunk -m "initial trunk import"
+ TMPFILE=$(mktemp)
+ cat >$TMPFILE <<__LAYOUT__
+depth-project = 5
+depth-branch = 1
+depth-tag = 1
+dir-trunk = trunk
+dir-branch =
+dir-tag =
+level-owner-branch =
+level-owner-tag =
+template-branch =
+template-tag =
+__LAYOUT__
+ TMPDIR=$(mktemp -d)
+ svn checkout -q $REPOS_URL $TMPDIR
+ svn propset -q --file=$TMPFILE fcm:layout $TMPDIR
+ svn commit -q -m " " $TMPDIR
+ rm -f $TMPFILE
+ rm -rf $TMPDIR
+ ROOT_URL=$REPOS_URL
+}
+
+function init_branch() {
+ local BRANCH_NAME=$1
+ local REPOS_URL=$2
+ local ROOT_URL=$REPOS_URL
+ local ROOT_PATH=
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ ROOT_PATH=$ROOT_PATH/$TEST_PROJECT
+ fi
+ MESSAGE=$(echo -e "Created $ROOT_PATH/branches/dev/Share/$BRANCH_NAME from /trunk at 1.")
+ svn copy -q -r1 $ROOT_URL/trunk $ROOT_URL/branches/dev/Share/$BRANCH_NAME \
+ -m "Made a branch $MESSAGE"
+}
+
+function init_branch_wc() {
+ local BRANCH_NAME=$1
+ local REPOS_URL=$2
+ local ROOT_URL=$REPOS_URL
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ fi
+ init_branch $BRANCH_NAME $REPOS_URL
+ svn checkout -q $ROOT_URL/branches/dev/Share/$BRANCH_NAME $TEST_DIR/wc
+}
+
+function init_merge_branches() {
+ local BRANCH_NAME=$1
+ local OTHER_BRANCH_NAME=$2
+ local REPOS_URL=$3
+ local ROOT_URL=$REPOS_URL
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ fi
+ init_branch_wc $BRANCH_NAME $REPOS_URL
+ cd $TEST_DIR/wc
+ file_list=$(find . -type f | sed "/\.svn/d" | sort | head -5)
+ other_file=$(find . -type f | sed "/\.svn/d" | sort | tail -1)
+ for file in $file_list; do
+ sed -i "s/for/FOR/g; s/fi/end if/g; s/in/IN/g;" $file
+ sed -i "/#/d; /^ *!/d" $file
+ sed -i "s/!/!!/g; s/q/\nq/g; s/[(]/(\n/g" $file
+ done
+ file_dir=$(dirname $file)
+ svn copy -q $file ./added_file
+ svn copy -q module added_directory
+ touch module/tree_conflict_file
+ svn add -q $file_dir/tree_conflict_file
+ echo "Modified a line" >>$other_file
+ svn commit -q -m "Made changes for future merge of this branch"
+ svn update -q
+ init_branch $OTHER_BRANCH_NAME $REPOS_URL
+ svn switch -q $ROOT_URL/branches/dev/Share/$OTHER_BRANCH_NAME
+ echo " " > unversioned_file
+ properties_file=$(find . -type f | sed " /\.svn/d" | sort | tail -3 | head -1)
+ svn propset -q svn:executable "executable" $properties_file
+ svn copy -q $file renamed_added_file
+ svn commit -q -m "Made changes for future merge"
+ svn update -q
+ svn switch -q $ROOT_URL/trunk
+ echo "trunk change" >>$(find . -type f | sed "/\.svn/d" | sort | head -1)
+ svn commit -q -m "Made trunk change"
+ svn update -q
+ echo "another trunk change" >>$(find . -type f | sed "/\.svn/d" | sort | head -1)
+ svn commit -q -m "Made another trunk change"
+ svn update -q
+}
+
+function run_pass() {
+ local TEST_KEY=$1
+ shift 1
+ if ! "$@" 1>$TEST_DIR/$TEST_KEY.out 2>$TEST_DIR/$TEST_KEY.err; then
+ fail $TEST_KEY
+ return
+ fi
+ pass $TEST_KEY
+}
+
+function run_fail() {
+ local TEST_KEY=$1
+ shift 1
+ if "$@" 1>$TEST_DIR/$TEST_KEY.out 2>$TEST_DIR/$TEST_KEY.err; then
+ fail $TEST_KEY
+ return
+ fi
+ pass $TEST_KEY
+}
+
+function setup() {
+ mkdir -p $TEST_DIR/.subversion
+ mkdir -p $TEST_DIR/run
+ cd $TEST_DIR/run
+}
+
+function teardown() {
+ cd $TEST_DIR
+ rm -rf $TEST_DIR/test_repos
+ rm -rf $TEST_DIR/wc
+ rm -rf $TEST_DIR/run
+ rm -rf $TEST_DIR/.subversion
+ if [[ -n ${TEST_REMOTE_HOST:-} ]]; then
+ ssh $TEST_REMOTE_HOST "rm -rf $TEST_REMOTE_DIR"
+ fi
+}
+
+REPOS_URL=
+ROOT_URL=
+PROJECT=
diff --git a/t/fcm-diff/00-simple.t b/t/fcm-diff/00-simple.t
new file mode 100644
index 0000000..b0cdf42
--- /dev/null
+++ b/t/fcm-diff/00-simple.t
@@ -0,0 +1,239 @@
+#!/bin/bash
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# Basic tests for "fcm diff".
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+tests 3
+#-------------------------------------------------------------------------------
+setup
+init_repos
+init_branch_wc branch_test $REPOS_URL
+cd $TEST_DIR/wc
+FILE_LIST=$(find . -type f | sed "/\.svn/d" | sort | head -5)
+for FILE in $FILE_LIST; do
+ sed -i "s/for/FOR/g; s/fi/end if/g; s/in/IN/g;" $FILE
+ sed -i "/#/d; /^ *!/d" $FILE
+ sed -i "s/!/!!/g; s/q/\nq/g; s/[(]/(\n/g" $FILE
+done
+FILE_DIR=$(dirname $FILE)
+svn copy -q $FILE added_file
+svn copy -q $FILE_DIR added_directory
+svn delete --force -q $FILE_DIR
+#-------------------------------------------------------------------------------
+# Tests fcm branch-diff
+TEST_KEY=$TEST_KEY_BASE-fcm-diff
+run_pass "$TEST_KEY" fcm diff
+if $SVN_VERSION_IS_16; then
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+Index: added_file
+===================================================================
+--- added_file (revision 4)
++++ added_file (working copy)
+@@ -1 +1 @@
+-INCLUDE 'hello_constants.inc'
++INCLUDE 'hello_constants.INc'
+Index: module/hello_constants_dummy.inc
+===================================================================
+--- module/hello_constants_dummy.inc (revision 4)
++++ module/hello_constants_dummy.inc (working copy)
+@@ -1 +0,0 @@
+-INCLUDE 'hello_constants.inc'
+Index: module/hello_constants.inc
+===================================================================
+--- module/hello_constants.inc (revision 4)
++++ module/hello_constants.inc (working copy)
+@@ -1 +0,0 @@
+-CHARACTER (LEN=80), PARAMETER :: hello_string = 'Hello Earth!'
+Index: module/hello_constants.f90
+===================================================================
+--- module/hello_constants.f90 (revision 4)
++++ module/hello_constants.f90 (working copy)
+@@ -1,5 +0,0 @@
+-MODULE Hello_Constants
+-
+-INCLUDE 'hello_constants_dummy.inc'
+-
+-END MODULE Hello_Constants
+Index: added_directory/hello_constants_dummy.inc
+===================================================================
+--- added_directory/hello_constants_dummy.inc (revision 4)
++++ added_directory/hello_constants_dummy.inc (working copy)
+@@ -1 +1 @@
+-INCLUDE 'hello_constants.inc'
++INCLUDE 'hello_constants.INc'
+Index: added_directory/hello_constants.inc
+===================================================================
+--- added_directory/hello_constants.inc (revision 4)
++++ added_directory/hello_constants.inc (working copy)
+@@ -1 +1,2 @@
+-CHARACTER (LEN=80), PARAMETER :: hello_string = 'Hello Earth!'
++CHARACTER (
++LEN=80), PARAMETER :: hello_strINg = 'Hello Earth!!'
+Index: added_directory/hello_constants.f90
+===================================================================
+--- added_directory/hello_constants.f90 (revision 4)
++++ added_directory/hello_constants.f90 (working copy)
+@@ -1,5 +1,5 @@
+ MODULE Hello_Constants
+
+-INCLUDE 'hello_constants_dummy.inc'
++INCLUDE 'hello_constants_dummy.INc'
+
+ END MODULE Hello_Constants
+Index: lib/python/info/poems.py
+===================================================================
+--- lib/python/info/poems.py (revision 4)
++++ lib/python/info/poems.py (working copy)
+@@ -1,24 +1,23 @@
+-#!/usr/bin/env python
+-# -*- coding: utf-8 -*-
+ """The Python, by Hilaire Belloc
+
+ A Python I should not advise,--
+-It needs a doctor for its eyes,
++It needs a doctor FOR its eyes,
+ And has the measles yearly.
+-However, if you feel inclined
+-To get one (to improve your mind,
++However, if you feel INclINed
++To get one (
++to improve your mINd,
+ And not from fashion merely),
+ Allow no music near its cage;
+-And when it flies into a rage
++And when it flies INto a rage
+ Chastise it, most severely.
+-I had an aunt in Yucatan
++I had an aunt IN Yucatan
+ Who bought a Python from a man
+-And kept it for a pet.
++And kept it FOR a pet.
+ She died, because she never knew
+ These simple little rules and few;--
+-The Snake is living yet.
++The Snake is livINg yet.
+ """
+
+ import this
+
+-print "\n", __doc__
++prINt "\n", __doc__
+__OUT__
+else
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+Index: added_directory/hello_constants.f90
+===================================================================
+--- added_directory/hello_constants.f90 (revision 4)
++++ added_directory/hello_constants.f90 (working copy)
+@@ -1,5 +1,5 @@
+ MODULE Hello_Constants
+
+-INCLUDE 'hello_constants_dummy.inc'
++INCLUDE 'hello_constants_dummy.INc'
+
+ END MODULE Hello_Constants
+Index: added_directory/hello_constants.inc
+===================================================================
+--- added_directory/hello_constants.inc (revision 4)
++++ added_directory/hello_constants.inc (working copy)
+@@ -1 +1,2 @@
+-CHARACTER (LEN=80), PARAMETER :: hello_string = 'Hello Earth!'
++CHARACTER (
++LEN=80), PARAMETER :: hello_strINg = 'Hello Earth!!'
+Index: added_directory/hello_constants_dummy.inc
+===================================================================
+--- added_directory/hello_constants_dummy.inc (revision 4)
++++ added_directory/hello_constants_dummy.inc (working copy)
+@@ -1 +1 @@
+-INCLUDE 'hello_constants.inc'
++INCLUDE 'hello_constants.INc'
+Index: added_file
+===================================================================
+--- added_file (revision 4)
++++ added_file (working copy)
+@@ -1 +1 @@
+-INCLUDE 'hello_constants.inc'
++INCLUDE 'hello_constants.INc'
+Index: lib/python/info/poems.py
+===================================================================
+--- lib/python/info/poems.py (revision 4)
++++ lib/python/info/poems.py (working copy)
+@@ -1,24 +1,23 @@
+-#!/usr/bin/env python
+-# -*- coding: utf-8 -*-
+ """The Python, by Hilaire Belloc
+
+ A Python I should not advise,--
+-It needs a doctor for its eyes,
++It needs a doctor FOR its eyes,
+ And has the measles yearly.
+-However, if you feel inclined
+-To get one (to improve your mind,
++However, if you feel INclINed
++To get one (
++to improve your mINd,
+ And not from fashion merely),
+ Allow no music near its cage;
+-And when it flies into a rage
++And when it flies INto a rage
+ Chastise it, most severely.
+-I had an aunt in Yucatan
++I had an aunt IN Yucatan
+ Who bought a Python from a man
+-And kept it for a pet.
++And kept it FOR a pet.
+ She died, because she never knew
+ These simple little rules and few;--
+-The Snake is living yet.
++The Snake is livINg yet.
+ """
+
+ import this
+
+-print "\n", __doc__
++prINt "\n", __doc__
+Index: module/hello_constants.f90
+===================================================================
+--- module/hello_constants.f90 (revision 4)
++++ module/hello_constants.f90 (working copy)
+@@ -1,5 +0,0 @@
+-MODULE Hello_Constants
+-
+-INCLUDE 'hello_constants_dummy.inc'
+-
+-END MODULE Hello_Constants
+Index: module/hello_constants.inc
+===================================================================
+--- module/hello_constants.inc (revision 4)
++++ module/hello_constants.inc (working copy)
+@@ -1 +0,0 @@
+-CHARACTER (LEN=80), PARAMETER :: hello_string = 'Hello Earth!'
+Index: module/hello_constants_dummy.inc
+===================================================================
+--- module/hello_constants_dummy.inc (revision 4)
++++ module/hello_constants_dummy.inc (working copy)
+@@ -1 +0,0 @@
+-INCLUDE 'hello_constants.inc'
+__OUT__
+fi
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+teardown
+#-------------------------------------------------------------------------------
diff --git a/t/fcm-diff/test_header b/t/fcm-diff/test_header
new file mode 100644
index 0000000..672ce12
--- /dev/null
+++ b/t/fcm-diff/test_header
@@ -0,0 +1,232 @@
+#!/bin/bash
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# Optional enviroment variables:
+# TEST_PROJECT (tests using given project name)
+# TEST_REMOTE_HOST (tests using svn+ssh repositories located on given host)
+# ------------------------------------------------------------------------------
+
+. $(dirname $0)/../lib/bash/test_header
+
+function file_cmp() {
+ local TEST_KEY=$1
+ local FILE_ACTUAL=$2
+ local FILE_EXPECT=${3:--}
+ if cmp $TEST_DIR/$FILE_ACTUAL $FILE_EXPECT; then
+ pass $TEST_KEY
+ return
+ fi
+ fail $TEST_KEY
+}
+
+function file_grep() {
+ local TEST_KEY=$1
+ local PATTERN=$2
+ local FILE=$3
+ if grep -q -e "$PATTERN" $TEST_DIR/$FILE; then
+ pass $TEST_KEY
+ return
+ fi
+ fail $TEST_KEY
+}
+
+function file_test() {
+ local TEST_KEY=$1
+ local FILE=$2
+ local OPTION=${3:--e}
+ if test $OPTION $TEST_DIR/$FILE; then
+ pass $TEST_KEY
+ else
+ fail $TEST_KEY
+ fi
+}
+
+function file_xxdiff() {
+ local TEST_KEY=$1
+ local FILE_ACTUAL=$2
+ local FILE_EXPECT=${3:--}
+ if xxdiff -D $TEST_DIR/$FILE_ACTUAL $FILE_EXPECT; then
+ pass $TEST_KEY
+ return
+ fi
+ fail $TEST_KEY
+}
+
+function init_repos() {
+ if [[ -n ${TEST_REMOTE_HOST:-} ]]; then
+ TEST_REMOTE_DIR=$(ssh $TEST_REMOTE_HOST "mktemp -d")
+ ssh $TEST_REMOTE_HOST "svnadmin create --fs-type fsfs $TEST_REMOTE_DIR"
+ REPOS_URL="svn+ssh://${TEST_REMOTE_HOST}$TEST_REMOTE_DIR"
+ else
+ svnadmin create --fs-type fsfs $TEST_DIR/test_repos
+ REPOS_URL="file://$TEST_DIR/test_repos"
+ fi
+ ROOT_URL=$REPOS_URL
+ PROJECT=
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ PROJECT=$TEST_PROJECT"/"
+ fi
+ svn import -q $TEST_SOURCE_DIR/../etc/repo_files \
+ $REPOS_URL/$PROJECT/trunk -m "initial trunk import"
+ svn mkdir -q $REPOS_URL/$PROJECT/tags -m "make tags"
+ svn mkdir -q --parents $REPOS_URL/$PROJECT/branches/dev/Share -m " "
+}
+
+function init_repos_layout_roses() {
+ if [[ -n ${TEST_REMOTE_HOST:-} ]]; then
+ TEST_REMOTE_DIR=$(ssh $TEST_REMOTE_HOST "mktemp -d")
+ ssh $TEST_REMOTE_HOST "svnadmin create --fs-type fsfs $TEST_REMOTE_DIR"
+ REPOS_URL="svn+ssh://${TEST_REMOTE_HOST}$TEST_REMOTE_DIR"
+ else
+ svnadmin create --fs-type fsfs $TEST_DIR/test_repos
+ REPOS_URL="file://$TEST_DIR/test_repos"
+ fi
+ svn mkdir -q --parents $REPOS_URL/a/a/0/0/0/trunk
+ svn import -q $TEST_SOURCE_DIR/../etc/repo_files \
+ $REPOS_URL/a/a/0/0/0/trunk -m "initial trunk import"
+ TMPFILE=$(mktemp)
+ cat >$TMPFILE <<__LAYOUT__
+depth-project = 5
+depth-branch = 1
+depth-tag = 1
+dir-trunk = trunk
+dir-branch =
+dir-tag =
+level-owner-branch =
+level-owner-tag =
+template-branch =
+template-tag =
+__LAYOUT__
+ TMPDIR=$(mktemp -d)
+ svn checkout -q $REPOS_URL $TMPDIR
+ svn propset -q --file=$TMPFILE fcm:layout $TMPDIR
+ svn commit -q -m " " $TMPDIR
+ rm -f $TMPFILE
+ rm -rf $TMPDIR
+ ROOT_URL=$REPOS_URL
+}
+
+function init_branch() {
+ local BRANCH_NAME=$1
+ local REPOS_URL=$2
+ local ROOT_URL=$REPOS_URL
+ local ROOT_PATH=
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ ROOT_PATH=$ROOT_PATH/$TEST_PROJECT
+ fi
+ MESSAGE=$(echo -e "Created $ROOT_PATH/branches/dev/Share/$BRANCH_NAME from /trunk at 1.")
+ svn copy -q -r1 $ROOT_URL/trunk $ROOT_URL/branches/dev/Share/$BRANCH_NAME \
+ -m "Made a branch $MESSAGE"
+}
+
+function init_branch_wc() {
+ local BRANCH_NAME=$1
+ local REPOS_URL=$2
+ local ROOT_URL=$REPOS_URL
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ fi
+ init_branch $BRANCH_NAME $REPOS_URL
+ svn checkout -q $ROOT_URL/branches/dev/Share/$BRANCH_NAME $TEST_DIR/wc
+}
+
+function init_merge_branches() {
+ local BRANCH_NAME=$1
+ local OTHER_BRANCH_NAME=$2
+ local REPOS_URL=$3
+ local ROOT_URL=$REPOS_URL
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ fi
+ init_branch_wc $BRANCH_NAME $REPOS_URL
+ cd $TEST_DIR/wc
+ file_list=$(find . -type f | sed "/\.svn/d" | sort | head -5)
+ other_file=$(find . -type f | sed "/\.svn/d" | sort | tail -1)
+ for file in $file_list; do
+ sed -i "s/for/FOR/g; s/fi/end if/g; s/in/IN/g;" $file
+ sed -i "/#/d; /^ *!/d" $file
+ sed -i "s/!/!!/g; s/q/\nq/g; s/[(]/(\n/g" $file
+ done
+ file_dir=$(dirname $file)
+ svn copy -q $file ./added_file
+ svn copy -q module added_directory
+ touch module/tree_conflict_file
+ svn add -q $file_dir/tree_conflict_file
+ echo "Modified a line" >>$other_file
+ svn commit -q -m "Made changes for future merge of this branch"
+ svn update -q
+ init_branch $OTHER_BRANCH_NAME $REPOS_URL
+ svn switch -q $ROOT_URL/branches/dev/Share/$OTHER_BRANCH_NAME
+ echo " " > unversioned_file
+ properties_file=$(find . -type f | sed " /\.svn/d" | sort | tail -3 | head -1)
+ svn propset -q svn:executable "executable" $properties_file
+ svn copy -q $file renamed_added_file
+ svn commit -q -m "Made changes for future merge"
+ svn update -q
+ svn switch -q $ROOT_URL/trunk
+ echo "trunk change" >>$(find . -type f | sed "/\.svn/d" | sort | head -1)
+ svn commit -q -m "Made trunk change"
+ svn update -q
+ echo "another trunk change" >>$(find . -type f | sed "/\.svn/d" | sort | head -1)
+ svn commit -q -m "Made another trunk change"
+ svn update -q
+}
+
+function run_pass() {
+ local TEST_KEY=$1
+ shift 1
+ if ! "$@" 1>$TEST_DIR/$TEST_KEY.out 2>$TEST_DIR/$TEST_KEY.err; then
+ fail $TEST_KEY
+ return
+ fi
+ pass $TEST_KEY
+}
+
+function run_fail() {
+ local TEST_KEY=$1
+ shift 1
+ if "$@" 1>$TEST_DIR/$TEST_KEY.out 2>$TEST_DIR/$TEST_KEY.err; then
+ fail $TEST_KEY
+ return
+ fi
+ pass $TEST_KEY
+}
+
+function setup() {
+ mkdir -p $TEST_DIR/.subversion
+ mkdir -p $TEST_DIR/run
+ cd $TEST_DIR/run
+}
+
+function teardown() {
+ cd $TEST_DIR
+ rm -rf $TEST_DIR/test_repos
+ rm -rf $TEST_DIR/wc
+ rm -rf $TEST_DIR/run
+ rm -rf $TEST_DIR/.subversion
+ if [[ -n ${TEST_REMOTE_HOST:-} ]]; then
+ ssh $TEST_REMOTE_HOST "rm -rf $TEST_REMOTE_DIR"
+ fi
+}
+
+REPOS_URL=
+ROOT_URL=
+PROJECT=
diff --git a/t/fcm-install-svn-hook/00-basic.t b/t/fcm-install-svn-hook/00-basic.t
new file mode 100755
index 0000000..d3947a2
--- /dev/null
+++ b/t/fcm-install-svn-hook/00-basic.t
@@ -0,0 +1,144 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+# Basic tests for "fcm-install-svn-hook".
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+. $TEST_SOURCE_DIR/test_header_more
+#-------------------------------------------------------------------------------
+if ! which svnadmin 1>/dev/null 2>/dev/null; then
+ skip_all 'svnadmin not available'
+fi
+tests 149
+#-------------------------------------------------------------------------------
+FCM_REAL_HOME=$(readlink -f "$FCM_HOME")
+TODAY=$(date -u +%Y%m%d)
+mkdir -p conf/
+export FCM_CONF_PATH="$PWD/conf"
+cat >conf/admin.cfg <<__CONF__
+svn_group=
+svn_live_dir=$PWD/svn-repos
+svn_project_suffix=
+__CONF__
+cat >hooks-env <<__CONF__
+[default]
+FCM_HOME=$FCM_REAL_HOME
+FCM_SVN_HOOK_ADMIN_EMAIL=$USER
+FCM_SVN_HOOK_COMMIT_DUMP_DIR=/var/svn/dumps
+FCM_SVN_HOOK_TRAC_ROOT_DIR=/srv/trac
+TZ=UTC
+__CONF__
+#-------------------------------------------------------------------------------
+# Live directory does not exist
+TEST_KEY="$TEST_KEY_BASE-no-live-dir"
+run_pass "$TEST_KEY" "$FCM_HOME/sbin/fcm-install-svn-hook"
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" /dev/null
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" /dev/null
+#-------------------------------------------------------------------------------
+# Project does not exist
+TEST_KEY="$TEST_KEY_BASE-no-project"
+run_fail "$TEST_KEY" "$FCM_HOME/sbin/fcm-install-svn-hook" foo
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" /dev/null
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" <<'__ERR__'
+foo: not found
+__ERR__
+#-------------------------------------------------------------------------------
+# Live directory is empty
+TEST_KEY="$TEST_KEY_BASE-empty"
+mkdir svn-repos
+run_pass "$TEST_KEY" "$FCM_HOME/sbin/fcm-install-svn-hook"
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" /dev/null
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" /dev/null
+#-------------------------------------------------------------------------------
+run_tests() {
+ # Create repository and add content if necessary
+ rm -fr svn-repos/foo
+ svnadmin create svn-repos/foo
+ if [[ -d svn-import ]]; then
+ svn import -q -m't' svn-import file://$PWD/svn-repos/foo
+ fi
+ # Hooks before
+ local HOOK_TMPLS=$(ls svn-repos/foo/hooks/*)
+ # Install
+ run_pass "$TEST_KEY" "$FCM_HOME/sbin/fcm-install-svn-hook" "$@"
+ # Hooks env
+ file_cmp "$TEST_KEY-hooks-env" svn-repos/foo/conf/hooks-env hooks-env
+ # Make sure all hooks are installed
+ local FILE=
+ for FILE in $(cd "$FCM_HOME/etc/svn-hooks" && ls); do
+ file_cmp "$TEST_KEY-$FILE" \
+ "$FCM_HOME/etc/svn-hooks/$FILE" "svn-repos/foo/hooks/$FILE"
+ file_test "$TEST_KEY-$FILE-chmod" "svn-repos/foo/hooks/$FILE" -x
+ file_test "$TEST_KEY-$FILE.log.$TODAY" \
+ "svn-repos/foo/log/$FILE.log.$TODAY"
+ readlink "svn-repos/foo/log/$FILE.log" >"$TEST_KEY-$FILE.log.link"
+ file_cmp "$TEST_KEY-$FILE.log" \
+ "$TEST_KEY-$FILE.log.link" <<<"$FILE.log.$TODAY"
+ done
+ # Hooks after
+ if [[ "$@" == *--clean* ]]; then
+ run_fail "$TEST_KEY-ls-tmpl" ls $HOOK_TMPLS
+ else
+ run_pass "$TEST_KEY-ls-tmpl" ls $HOOK_TMPLS
+ fi
+ # STDOUT and STDERR
+ date2datefmt "$TEST_KEY.out" >"$TEST_KEY.out.parsed"
+ m4 -DFCM_REAL_HOME=$FCM_REAL_HOME -DPWD=$PWD -DTODAY=$TODAY \
+ "$TEST_SOURCE_DIR/$TEST_KEY_BASE/$NAME.out" >"$TEST_KEY.out.exp"
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out.parsed" "$TEST_KEY.out.exp"
+ file_cmp "$TEST_KEY.err" "$TEST_KEY.err" /dev/null
+ # Run command a second time, should no longer install logs
+ run_pass "$TEST_KEY-2" "$FCM_HOME/sbin/fcm-install-svn-hook" "$@"
+ date2datefmt "$TEST_KEY-2.out" >"$TEST_KEY-2.out.parsed"
+ m4 -DFCM_REAL_HOME=$FCM_REAL_HOME -DPWD=$PWD \
+ "$TEST_SOURCE_DIR/$TEST_KEY_BASE/$NAME-2.out" >"$TEST_KEY-2.out.exp"
+ file_cmp "$TEST_KEY-2.out" "$TEST_KEY-2.out.parsed" "$TEST_KEY-2.out.exp"
+}
+
+# New install, single repository
+TEST_KEY="$TEST_KEY_BASE-new"
+NAME=new run_tests
+TEST_KEY="$TEST_KEY_BASE-new-foo"
+NAME=new run_tests foo
+
+# Clean install, single repository
+TEST_KEY="$TEST_KEY_BASE-clean"
+NAME=clean run_tests --clean
+TEST_KEY="$TEST_KEY_BASE-clean-foo"
+NAME=clean run_tests --clean foo
+
+# New install, single repository, with svnperms.conf
+TEST_KEY="$TEST_KEY_BASE-svnperms.conf"
+mkdir -p 'svn-import'
+echo '[foo]' >'svn-import/svnperms.conf'
+NAME='svnperms-conf' run_tests
+file_cmp "$TEST_KEY-ls-svnperms.conf" \
+ 'svn-repos/foo/hooks/svnperms.conf' 'svn-import/svnperms.conf'
+
+# New install, single repository, with commit.conf
+TEST_KEY="$TEST_KEY_BASE-commit.conf"
+{
+ echo 'no-notify-branch-owner'
+ echo 'no-verify-branch-owner'
+} >'svn-import/commit.conf'
+NAME='commit-conf' run_tests
+file_cmp "$TEST_KEY-ls-svnperms.conf" \
+ 'svn-repos/foo/hooks/commit.conf' 'svn-import/commit.conf'
+#-------------------------------------------------------------------------------
+exit
diff --git a/t/fcm-install-svn-hook/00-basic/clean-2.out b/t/fcm-install-svn-hook/00-basic/clean-2.out
new file mode 120000
index 0000000..f0138ab
--- /dev/null
+++ b/t/fcm-install-svn-hook/00-basic/clean-2.out
@@ -0,0 +1 @@
+new-2.out
\ No newline at end of file
diff --git a/t/fcm-install-svn-hook/00-basic/clean.out b/t/fcm-install-svn-hook/00-basic/clean.out
new file mode 100644
index 0000000..6222b8a
--- /dev/null
+++ b/t/fcm-install-svn-hook/00-basic/clean.out
@@ -0,0 +1,17 @@
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_REAL_HOME/etc/svn-hooks/post-commit to PWD/svn-repos/foo/hooks/post-commit
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_REAL_HOME/etc/svn-hooks/post-revprop-change to PWD/svn-repos/foo/hooks/post-revprop-change
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_REAL_HOME/etc/svn-hooks/pre-commit to PWD/svn-repos/foo/hooks/pre-commit
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_REAL_HOME/etc/svn-hooks/pre-revprop-change to PWD/svn-repos/foo/hooks/pre-revprop-change
+YYYY-mm-ddTHH:MM:SSZ: removing PWD/svn-repos/foo/hooks/post-commit.tmpl
+YYYY-mm-ddTHH:MM:SSZ: removing PWD/svn-repos/foo/hooks/post-lock.tmpl
+YYYY-mm-ddTHH:MM:SSZ: removing PWD/svn-repos/foo/hooks/post-revprop-change.tmpl
+YYYY-mm-ddTHH:MM:SSZ: removing PWD/svn-repos/foo/hooks/post-unlock.tmpl
+YYYY-mm-ddTHH:MM:SSZ: removing PWD/svn-repos/foo/hooks/pre-commit.tmpl
+YYYY-mm-ddTHH:MM:SSZ: removing PWD/svn-repos/foo/hooks/pre-lock.tmpl
+YYYY-mm-ddTHH:MM:SSZ: removing PWD/svn-repos/foo/hooks/pre-revprop-change.tmpl
+YYYY-mm-ddTHH:MM:SSZ: removing PWD/svn-repos/foo/hooks/pre-unlock.tmpl
+YYYY-mm-ddTHH:MM:SSZ: removing PWD/svn-repos/foo/hooks/start-commit.tmpl
+YYYY-mm-ddTHH:MM:SSZ: creating symlink: post-commit.log.TODAY -> PWD/svn-repos/foo/log/post-commit.log
+YYYY-mm-ddTHH:MM:SSZ: creating symlink: post-revprop-change.log.TODAY -> PWD/svn-repos/foo/log/post-revprop-change.log
+YYYY-mm-ddTHH:MM:SSZ: creating symlink: pre-commit.log.TODAY -> PWD/svn-repos/foo/log/pre-commit.log
+YYYY-mm-ddTHH:MM:SSZ: creating symlink: pre-revprop-change.log.TODAY -> PWD/svn-repos/foo/log/pre-revprop-change.log
diff --git a/t/fcm-install-svn-hook/00-basic/commit-conf-2.out b/t/fcm-install-svn-hook/00-basic/commit-conf-2.out
new file mode 100644
index 0000000..351fbf7
--- /dev/null
+++ b/t/fcm-install-svn-hook/00-basic/commit-conf-2.out
@@ -0,0 +1,6 @@
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_REAL_HOME/etc/svn-hooks/post-commit to PWD/svn-repos/foo/hooks/post-commit
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_REAL_HOME/etc/svn-hooks/post-revprop-change to PWD/svn-repos/foo/hooks/post-revprop-change
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_REAL_HOME/etc/svn-hooks/pre-commit to PWD/svn-repos/foo/hooks/pre-commit
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_REAL_HOME/etc/svn-hooks/pre-revprop-change to PWD/svn-repos/foo/hooks/pre-revprop-change
+YYYY-mm-ddTHH:MM:SSZ: install PWD/svn-repos/foo/hooks/commit.conf <- ^/commit.conf
+YYYY-mm-ddTHH:MM:SSZ: install PWD/svn-repos/foo/hooks/svnperms.conf <- ^/svnperms.conf
diff --git a/t/fcm-install-svn-hook/00-basic/commit-conf.out b/t/fcm-install-svn-hook/00-basic/commit-conf.out
new file mode 100644
index 0000000..04c55a1
--- /dev/null
+++ b/t/fcm-install-svn-hook/00-basic/commit-conf.out
@@ -0,0 +1,10 @@
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_REAL_HOME/etc/svn-hooks/post-commit to PWD/svn-repos/foo/hooks/post-commit
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_REAL_HOME/etc/svn-hooks/post-revprop-change to PWD/svn-repos/foo/hooks/post-revprop-change
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_REAL_HOME/etc/svn-hooks/pre-commit to PWD/svn-repos/foo/hooks/pre-commit
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_REAL_HOME/etc/svn-hooks/pre-revprop-change to PWD/svn-repos/foo/hooks/pre-revprop-change
+YYYY-mm-ddTHH:MM:SSZ: install PWD/svn-repos/foo/hooks/commit.conf <- ^/commit.conf
+YYYY-mm-ddTHH:MM:SSZ: install PWD/svn-repos/foo/hooks/svnperms.conf <- ^/svnperms.conf
+YYYY-mm-ddTHH:MM:SSZ: creating symlink: post-commit.log.TODAY -> PWD/svn-repos/foo/log/post-commit.log
+YYYY-mm-ddTHH:MM:SSZ: creating symlink: post-revprop-change.log.TODAY -> PWD/svn-repos/foo/log/post-revprop-change.log
+YYYY-mm-ddTHH:MM:SSZ: creating symlink: pre-commit.log.TODAY -> PWD/svn-repos/foo/log/pre-commit.log
+YYYY-mm-ddTHH:MM:SSZ: creating symlink: pre-revprop-change.log.TODAY -> PWD/svn-repos/foo/log/pre-revprop-change.log
diff --git a/t/fcm-install-svn-hook/00-basic/new-2.out b/t/fcm-install-svn-hook/00-basic/new-2.out
new file mode 100644
index 0000000..2f91761
--- /dev/null
+++ b/t/fcm-install-svn-hook/00-basic/new-2.out
@@ -0,0 +1,4 @@
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_REAL_HOME/etc/svn-hooks/post-commit to PWD/svn-repos/foo/hooks/post-commit
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_REAL_HOME/etc/svn-hooks/post-revprop-change to PWD/svn-repos/foo/hooks/post-revprop-change
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_REAL_HOME/etc/svn-hooks/pre-commit to PWD/svn-repos/foo/hooks/pre-commit
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_REAL_HOME/etc/svn-hooks/pre-revprop-change to PWD/svn-repos/foo/hooks/pre-revprop-change
diff --git a/t/fcm-install-svn-hook/00-basic/new.out b/t/fcm-install-svn-hook/00-basic/new.out
new file mode 100644
index 0000000..d631893
--- /dev/null
+++ b/t/fcm-install-svn-hook/00-basic/new.out
@@ -0,0 +1,8 @@
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_REAL_HOME/etc/svn-hooks/post-commit to PWD/svn-repos/foo/hooks/post-commit
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_REAL_HOME/etc/svn-hooks/post-revprop-change to PWD/svn-repos/foo/hooks/post-revprop-change
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_REAL_HOME/etc/svn-hooks/pre-commit to PWD/svn-repos/foo/hooks/pre-commit
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_REAL_HOME/etc/svn-hooks/pre-revprop-change to PWD/svn-repos/foo/hooks/pre-revprop-change
+YYYY-mm-ddTHH:MM:SSZ: creating symlink: post-commit.log.TODAY -> PWD/svn-repos/foo/log/post-commit.log
+YYYY-mm-ddTHH:MM:SSZ: creating symlink: post-revprop-change.log.TODAY -> PWD/svn-repos/foo/log/post-revprop-change.log
+YYYY-mm-ddTHH:MM:SSZ: creating symlink: pre-commit.log.TODAY -> PWD/svn-repos/foo/log/pre-commit.log
+YYYY-mm-ddTHH:MM:SSZ: creating symlink: pre-revprop-change.log.TODAY -> PWD/svn-repos/foo/log/pre-revprop-change.log
diff --git a/t/fcm-install-svn-hook/00-basic/svnperms-conf-2.out b/t/fcm-install-svn-hook/00-basic/svnperms-conf-2.out
new file mode 100644
index 0000000..9671ede
--- /dev/null
+++ b/t/fcm-install-svn-hook/00-basic/svnperms-conf-2.out
@@ -0,0 +1,5 @@
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_REAL_HOME/etc/svn-hooks/post-commit to PWD/svn-repos/foo/hooks/post-commit
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_REAL_HOME/etc/svn-hooks/post-revprop-change to PWD/svn-repos/foo/hooks/post-revprop-change
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_REAL_HOME/etc/svn-hooks/pre-commit to PWD/svn-repos/foo/hooks/pre-commit
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_REAL_HOME/etc/svn-hooks/pre-revprop-change to PWD/svn-repos/foo/hooks/pre-revprop-change
+YYYY-mm-ddTHH:MM:SSZ: install PWD/svn-repos/foo/hooks/svnperms.conf <- ^/svnperms.conf
diff --git a/t/fcm-install-svn-hook/00-basic/svnperms-conf.out b/t/fcm-install-svn-hook/00-basic/svnperms-conf.out
new file mode 100644
index 0000000..4c4d89a
--- /dev/null
+++ b/t/fcm-install-svn-hook/00-basic/svnperms-conf.out
@@ -0,0 +1,9 @@
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_REAL_HOME/etc/svn-hooks/post-commit to PWD/svn-repos/foo/hooks/post-commit
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_REAL_HOME/etc/svn-hooks/post-revprop-change to PWD/svn-repos/foo/hooks/post-revprop-change
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_REAL_HOME/etc/svn-hooks/pre-commit to PWD/svn-repos/foo/hooks/pre-commit
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_REAL_HOME/etc/svn-hooks/pre-revprop-change to PWD/svn-repos/foo/hooks/pre-revprop-change
+YYYY-mm-ddTHH:MM:SSZ: install PWD/svn-repos/foo/hooks/svnperms.conf <- ^/svnperms.conf
+YYYY-mm-ddTHH:MM:SSZ: creating symlink: post-commit.log.TODAY -> PWD/svn-repos/foo/log/post-commit.log
+YYYY-mm-ddTHH:MM:SSZ: creating symlink: post-revprop-change.log.TODAY -> PWD/svn-repos/foo/log/post-revprop-change.log
+YYYY-mm-ddTHH:MM:SSZ: creating symlink: pre-commit.log.TODAY -> PWD/svn-repos/foo/log/pre-commit.log
+YYYY-mm-ddTHH:MM:SSZ: creating symlink: pre-revprop-change.log.TODAY -> PWD/svn-repos/foo/log/pre-revprop-change.log
diff --git a/t/fcm-install-svn-hook/01-housekeep-log.t b/t/fcm-install-svn-hook/01-housekeep-log.t
new file mode 100755
index 0000000..f377876
--- /dev/null
+++ b/t/fcm-install-svn-hook/01-housekeep-log.t
@@ -0,0 +1,192 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+# Tests housekeep hook logs functionalities provided by "fcm-install-svn-hook".
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+. $TEST_SOURCE_DIR/test_header_more
+#-------------------------------------------------------------------------------
+if ! which svnadmin 1>/dev/null 2>/dev/null; then
+ skip_all 'svnadmin not available'
+fi
+tests 14
+#-------------------------------------------------------------------------------
+FCM_REAL_HOME=$(readlink -f "$FCM_HOME")
+TODAY=$(date -u +%Y%m%d)
+mkdir -p conf/ svn-repos/
+export FCM_CONF_PATH="$PWD/conf"
+cat >conf/admin.cfg <<__CONF__
+svn_group=
+svn_live_dir=$PWD/svn-repos
+svn_project_suffix=
+__CONF__
+#-------------------------------------------------------------------------------
+# Newly created logs
+svnadmin create svn-repos/bar
+svnadmin create svn-repos/foo
+
+# 1st run, create logs
+KEY="0-cmd0"
+TEST_KEY="$TEST_KEY_BASE-$KEY"
+run_pass "$TEST_KEY" "$FCM_HOME/sbin/fcm-install-svn-hook"
+date2datefmt "$TEST_KEY.out" >"$TEST_KEY.out.parsed"
+m4 -DFCM_HOME="$FCM_REAL_HOME" -DPWD="$PWD" -DTODAY="$TODAY" \
+ "$TEST_SOURCE_DIR/$TEST_KEY_BASE/$KEY.out" >"$TEST_KEY.out.exp"
+diff -u "$TEST_KEY.out.parsed" "$TEST_KEY.out.exp" >&2
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out.parsed" "$TEST_KEY.out.exp"
+
+# Add something to logs between runs
+for FILE in svn-repos/{foo,bar}/log/*.log; do
+ echo "$FILE: time passes, and contents were written to me." >"$FILE"
+done
+sha1sum svn-repos/{foo,bar}/log/*.log >logs.shalsum
+#-------------------------------------------------------------------------------
+# 2nd run on same day, should leave logs alone
+KEY="0-cmd1"
+TEST_KEY="$TEST_KEY_BASE-$KEY"
+run_pass "$TEST_KEY" "$FCM_HOME/sbin/fcm-install-svn-hook"
+date2datefmt "$TEST_KEY.out" >"$TEST_KEY.out.parsed"
+m4 -DFCM_HOME="$FCM_REAL_HOME" -DPWD="$PWD" -DTODAY="$TODAY" \
+ "$TEST_SOURCE_DIR/$TEST_KEY_BASE/$KEY.out" >"$TEST_KEY.out.exp"
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out.parsed" "$TEST_KEY.out.exp"
+# Logs should not be modified
+run_pass "$TEST_KEY.sha1sum" sha1sum -c logs.shalsum
+#-------------------------------------------------------------------------------
+# Pretend that logs were created 3 days ago
+TEST_KEY="$TEST_KEY_BASE-3"
+DATE_P3D=$(date --date='3 days ago' +%Y%m%d)
+
+# Add something to logs
+# Pretend that they were created 3 days ago
+for FILE in svn-repos/{foo,bar}/log/*.log; do
+ echo "$FILE: time flies in the world of testing." >>"$FILE"
+ NAME=$(readlink "$FILE")
+ mv "$(dirname $FILE)/$NAME" "$FILE.$DATE_P3D"
+ ln -f -s "$(basename $FILE).$DATE_P3D" "$FILE"
+done
+sha1sum svn-repos/{foo,bar}/log/*.log >logs.shalsum
+
+# Run with logs created 3 days ago
+# STDOUT should be identical to "0-cmd1".
+run_pass "$TEST_KEY" "$FCM_HOME/sbin/fcm-install-svn-hook"
+date2datefmt "$TEST_KEY.out" >"$TEST_KEY.out.parsed"
+m4 -DFCM_HOME="$FCM_REAL_HOME" -DPWD="$PWD" -DTODAY="$TODAY" \
+ "$TEST_SOURCE_DIR/$TEST_KEY_BASE/0-cmd1.out" >"$TEST_KEY.out.exp"
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out.parsed" "$TEST_KEY.out.exp"
+
+# Logs should not be modified
+run_pass "$TEST_KEY.sha1sum" sha1sum -c logs.shalsum
+#-------------------------------------------------------------------------------
+# Pretend that logs were created 7 days ago
+TEST_KEY="$TEST_KEY_BASE-7"
+DATE_P7D=$(date --date='7 days ago' +%Y%m%d)
+
+# Add something to logs
+# Pretend that they were created 7 days ago
+for FILE in svn-repos/{foo,bar}/log/*.log; do
+ echo "$FILE: time continues to fly in the world of testing." >>"$FILE"
+ NAME=$(readlink "$FILE")
+ mv "$(dirname $FILE)/$NAME" "$FILE.$DATE_P7D"
+ ln -f -s "$(basename $FILE).$DATE_P7D" "$FILE"
+done
+sha1sum svn-repos/{foo,bar}/log/*.log >logs.shalsum
+
+# Run with logs created 7 days ago, should gzip old logs and create new ones
+run_pass "$TEST_KEY" "$FCM_HOME/sbin/fcm-install-svn-hook"
+date2datefmt "$TEST_KEY.out" >"$TEST_KEY.out.parsed"
+m4 -DFCM_HOME="$FCM_REAL_HOME" -DPWD="$PWD" -DTODAY="$TODAY" \
+ -DDATE_P7D="$DATE_P7D" \
+ "$TEST_SOURCE_DIR/$TEST_KEY_BASE/7-cmd.out" >"$TEST_KEY.out.exp"
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out.parsed" "$TEST_KEY.out.exp"
+
+# Unzip old logs and check that their contents are unchanged
+mkdir -p old-logs/svn-repos/{foo,bar}/log
+for FILE in svn-repos/{foo,bar}/log/*.log.*.gz; do
+ gunzip -c "$FILE" >old-logs/${FILE%.$DATE_P7D.gz}
+done
+cd old-logs
+run_pass "$TEST_KEY.sha1sum" sha1sum -c ../logs.shalsum
+cd "$OLDPWD"
+#-------------------------------------------------------------------------------
+# Pretend that logs were created between 7 to 28 days ago
+TEST_KEY="$TEST_KEY_BASE-28"
+DATE_P14D=$(date --date='14 days ago' +%Y%m%d)
+DATE_P21D=$(date --date='21 days ago' +%Y%m%d)
+DATE_P28D=$(date --date='28 days ago' +%Y%m%d)
+
+# Create fake logs
+rm -f svn-repos/{foo,bar}/log/*.log*
+for FILE in svn-repos/{foo,bar}/log/{pre,post}-commit.log; do
+ for DATE in $DATE_P14D $DATE_P21D $DATE_P28D; do
+ echo "$FILE $DATE whatever" >"$FILE.$DATE"
+ gzip "$FILE.$DATE"
+ done
+ echo "$FILE $DATE_P7D whatever" >"$FILE.$DATE_P7D"
+ ln -s $(basename "$FILE.$DATE_P7D") "$FILE"
+done
+for FILE in svn-repos/{foo,bar}/log/{pre,post}-revprop-change.log; do
+ echo "$FILE $DATE_P7D whatever" >"$FILE.$DATE_P7D"
+ ln -s $(basename "$FILE.$DATE_P7D") "$FILE"
+done
+
+# Run with logs created 7 to 28 days ago.
+# Should remove oldest and empty ones, gzip old ones and create new ones
+run_pass "$TEST_KEY" "$FCM_HOME/sbin/fcm-install-svn-hook"
+date2datefmt "$TEST_KEY.out" >"$TEST_KEY.out.parsed"
+m4 -DFCM_HOME="$FCM_REAL_HOME" -DPWD="$PWD" -DTODAY="$TODAY" \
+ -DDATE_P7D="$DATE_P7D" -DDATE_P28D="$DATE_P28D" \
+ "$TEST_SOURCE_DIR/$TEST_KEY_BASE/28-cmd.out" >"$TEST_KEY.out.exp"
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out.parsed" "$TEST_KEY.out.exp"
+ls svn-repos/{foo,bar}/log/*.log* | sort >"$TEST_KEY.ls.out"
+file_cmp "$TEST_KEY.ls" "$TEST_KEY.ls.out" <<__LIST__
+svn-repos/bar/log/post-commit.log
+svn-repos/bar/log/post-commit.log.$DATE_P21D.gz
+svn-repos/bar/log/post-commit.log.$DATE_P14D.gz
+svn-repos/bar/log/post-commit.log.$DATE_P7D.gz
+svn-repos/bar/log/post-commit.log.$TODAY
+svn-repos/bar/log/post-revprop-change.log
+svn-repos/bar/log/post-revprop-change.log.$DATE_P7D.gz
+svn-repos/bar/log/post-revprop-change.log.$TODAY
+svn-repos/bar/log/pre-commit.log
+svn-repos/bar/log/pre-commit.log.$DATE_P21D.gz
+svn-repos/bar/log/pre-commit.log.$DATE_P14D.gz
+svn-repos/bar/log/pre-commit.log.$DATE_P7D.gz
+svn-repos/bar/log/pre-commit.log.$TODAY
+svn-repos/bar/log/pre-revprop-change.log
+svn-repos/bar/log/pre-revprop-change.log.$DATE_P7D.gz
+svn-repos/bar/log/pre-revprop-change.log.$TODAY
+svn-repos/foo/log/post-commit.log
+svn-repos/foo/log/post-commit.log.$DATE_P21D.gz
+svn-repos/foo/log/post-commit.log.$DATE_P14D.gz
+svn-repos/foo/log/post-commit.log.$DATE_P7D.gz
+svn-repos/foo/log/post-commit.log.$TODAY
+svn-repos/foo/log/post-revprop-change.log
+svn-repos/foo/log/post-revprop-change.log.$DATE_P7D.gz
+svn-repos/foo/log/post-revprop-change.log.$TODAY
+svn-repos/foo/log/pre-commit.log
+svn-repos/foo/log/pre-commit.log.$DATE_P21D.gz
+svn-repos/foo/log/pre-commit.log.$DATE_P14D.gz
+svn-repos/foo/log/pre-commit.log.$DATE_P7D.gz
+svn-repos/foo/log/pre-commit.log.$TODAY
+svn-repos/foo/log/pre-revprop-change.log
+svn-repos/foo/log/pre-revprop-change.log.$DATE_P7D.gz
+svn-repos/foo/log/pre-revprop-change.log.$TODAY
+__LIST__
+#-------------------------------------------------------------------------------
+exit
diff --git a/t/fcm-install-svn-hook/01-housekeep-log/0-cmd0.out b/t/fcm-install-svn-hook/01-housekeep-log/0-cmd0.out
new file mode 100644
index 0000000..3dd1556
--- /dev/null
+++ b/t/fcm-install-svn-hook/01-housekeep-log/0-cmd0.out
@@ -0,0 +1,16 @@
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_HOME/etc/svn-hooks/post-commit to PWD/svn-repos/bar/hooks/post-commit
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_HOME/etc/svn-hooks/post-revprop-change to PWD/svn-repos/bar/hooks/post-revprop-change
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_HOME/etc/svn-hooks/pre-commit to PWD/svn-repos/bar/hooks/pre-commit
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_HOME/etc/svn-hooks/pre-revprop-change to PWD/svn-repos/bar/hooks/pre-revprop-change
+YYYY-mm-ddTHH:MM:SSZ: creating symlink: post-commit.log.TODAY -> PWD/svn-repos/bar/log/post-commit.log
+YYYY-mm-ddTHH:MM:SSZ: creating symlink: post-revprop-change.log.TODAY -> PWD/svn-repos/bar/log/post-revprop-change.log
+YYYY-mm-ddTHH:MM:SSZ: creating symlink: pre-commit.log.TODAY -> PWD/svn-repos/bar/log/pre-commit.log
+YYYY-mm-ddTHH:MM:SSZ: creating symlink: pre-revprop-change.log.TODAY -> PWD/svn-repos/bar/log/pre-revprop-change.log
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_HOME/etc/svn-hooks/post-commit to PWD/svn-repos/foo/hooks/post-commit
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_HOME/etc/svn-hooks/post-revprop-change to PWD/svn-repos/foo/hooks/post-revprop-change
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_HOME/etc/svn-hooks/pre-commit to PWD/svn-repos/foo/hooks/pre-commit
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_HOME/etc/svn-hooks/pre-revprop-change to PWD/svn-repos/foo/hooks/pre-revprop-change
+YYYY-mm-ddTHH:MM:SSZ: creating symlink: post-commit.log.TODAY -> PWD/svn-repos/foo/log/post-commit.log
+YYYY-mm-ddTHH:MM:SSZ: creating symlink: post-revprop-change.log.TODAY -> PWD/svn-repos/foo/log/post-revprop-change.log
+YYYY-mm-ddTHH:MM:SSZ: creating symlink: pre-commit.log.TODAY -> PWD/svn-repos/foo/log/pre-commit.log
+YYYY-mm-ddTHH:MM:SSZ: creating symlink: pre-revprop-change.log.TODAY -> PWD/svn-repos/foo/log/pre-revprop-change.log
diff --git a/t/fcm-install-svn-hook/01-housekeep-log/0-cmd1.out b/t/fcm-install-svn-hook/01-housekeep-log/0-cmd1.out
new file mode 100644
index 0000000..5979d54
--- /dev/null
+++ b/t/fcm-install-svn-hook/01-housekeep-log/0-cmd1.out
@@ -0,0 +1,8 @@
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_HOME/etc/svn-hooks/post-commit to PWD/svn-repos/bar/hooks/post-commit
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_HOME/etc/svn-hooks/post-revprop-change to PWD/svn-repos/bar/hooks/post-revprop-change
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_HOME/etc/svn-hooks/pre-commit to PWD/svn-repos/bar/hooks/pre-commit
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_HOME/etc/svn-hooks/pre-revprop-change to PWD/svn-repos/bar/hooks/pre-revprop-change
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_HOME/etc/svn-hooks/post-commit to PWD/svn-repos/foo/hooks/post-commit
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_HOME/etc/svn-hooks/post-revprop-change to PWD/svn-repos/foo/hooks/post-revprop-change
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_HOME/etc/svn-hooks/pre-commit to PWD/svn-repos/foo/hooks/pre-commit
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_HOME/etc/svn-hooks/pre-revprop-change to PWD/svn-repos/foo/hooks/pre-revprop-change
diff --git a/t/fcm-install-svn-hook/01-housekeep-log/28-cmd.out b/t/fcm-install-svn-hook/01-housekeep-log/28-cmd.out
new file mode 100644
index 0000000..4395d49
--- /dev/null
+++ b/t/fcm-install-svn-hook/01-housekeep-log/28-cmd.out
@@ -0,0 +1,36 @@
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_HOME/etc/svn-hooks/post-commit to PWD/svn-repos/bar/hooks/post-commit
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_HOME/etc/svn-hooks/post-revprop-change to PWD/svn-repos/bar/hooks/post-revprop-change
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_HOME/etc/svn-hooks/pre-commit to PWD/svn-repos/bar/hooks/pre-commit
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_HOME/etc/svn-hooks/pre-revprop-change to PWD/svn-repos/bar/hooks/pre-revprop-change
+YYYY-mm-ddTHH:MM:SSZ: removing PWD/svn-repos/bar/log/post-commit.log
+YYYY-mm-ddTHH:MM:SSZ: creating symlink: post-commit.log.TODAY -> PWD/svn-repos/bar/log/post-commit.log
+YYYY-mm-ddTHH:MM:SSZ: removing PWD/svn-repos/bar/log/post-commit.log.DATE_P28D.gz
+YYYY-mm-ddTHH:MM:SSZ: gzip PWD/svn-repos/bar/log/post-commit.log.DATE_P7D
+YYYY-mm-ddTHH:MM:SSZ: removing PWD/svn-repos/bar/log/post-revprop-change.log
+YYYY-mm-ddTHH:MM:SSZ: creating symlink: post-revprop-change.log.TODAY -> PWD/svn-repos/bar/log/post-revprop-change.log
+YYYY-mm-ddTHH:MM:SSZ: gzip PWD/svn-repos/bar/log/post-revprop-change.log.DATE_P7D
+YYYY-mm-ddTHH:MM:SSZ: removing PWD/svn-repos/bar/log/pre-commit.log
+YYYY-mm-ddTHH:MM:SSZ: creating symlink: pre-commit.log.TODAY -> PWD/svn-repos/bar/log/pre-commit.log
+YYYY-mm-ddTHH:MM:SSZ: removing PWD/svn-repos/bar/log/pre-commit.log.DATE_P28D.gz
+YYYY-mm-ddTHH:MM:SSZ: gzip PWD/svn-repos/bar/log/pre-commit.log.DATE_P7D
+YYYY-mm-ddTHH:MM:SSZ: removing PWD/svn-repos/bar/log/pre-revprop-change.log
+YYYY-mm-ddTHH:MM:SSZ: creating symlink: pre-revprop-change.log.TODAY -> PWD/svn-repos/bar/log/pre-revprop-change.log
+YYYY-mm-ddTHH:MM:SSZ: gzip PWD/svn-repos/bar/log/pre-revprop-change.log.DATE_P7D
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_HOME/etc/svn-hooks/post-commit to PWD/svn-repos/foo/hooks/post-commit
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_HOME/etc/svn-hooks/post-revprop-change to PWD/svn-repos/foo/hooks/post-revprop-change
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_HOME/etc/svn-hooks/pre-commit to PWD/svn-repos/foo/hooks/pre-commit
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_HOME/etc/svn-hooks/pre-revprop-change to PWD/svn-repos/foo/hooks/pre-revprop-change
+YYYY-mm-ddTHH:MM:SSZ: removing PWD/svn-repos/foo/log/post-commit.log
+YYYY-mm-ddTHH:MM:SSZ: creating symlink: post-commit.log.TODAY -> PWD/svn-repos/foo/log/post-commit.log
+YYYY-mm-ddTHH:MM:SSZ: removing PWD/svn-repos/foo/log/post-commit.log.DATE_P28D.gz
+YYYY-mm-ddTHH:MM:SSZ: gzip PWD/svn-repos/foo/log/post-commit.log.DATE_P7D
+YYYY-mm-ddTHH:MM:SSZ: removing PWD/svn-repos/foo/log/post-revprop-change.log
+YYYY-mm-ddTHH:MM:SSZ: creating symlink: post-revprop-change.log.TODAY -> PWD/svn-repos/foo/log/post-revprop-change.log
+YYYY-mm-ddTHH:MM:SSZ: gzip PWD/svn-repos/foo/log/post-revprop-change.log.DATE_P7D
+YYYY-mm-ddTHH:MM:SSZ: removing PWD/svn-repos/foo/log/pre-commit.log
+YYYY-mm-ddTHH:MM:SSZ: creating symlink: pre-commit.log.TODAY -> PWD/svn-repos/foo/log/pre-commit.log
+YYYY-mm-ddTHH:MM:SSZ: removing PWD/svn-repos/foo/log/pre-commit.log.DATE_P28D.gz
+YYYY-mm-ddTHH:MM:SSZ: gzip PWD/svn-repos/foo/log/pre-commit.log.DATE_P7D
+YYYY-mm-ddTHH:MM:SSZ: removing PWD/svn-repos/foo/log/pre-revprop-change.log
+YYYY-mm-ddTHH:MM:SSZ: creating symlink: pre-revprop-change.log.TODAY -> PWD/svn-repos/foo/log/pre-revprop-change.log
+YYYY-mm-ddTHH:MM:SSZ: gzip PWD/svn-repos/foo/log/pre-revprop-change.log.DATE_P7D
diff --git a/t/fcm-install-svn-hook/01-housekeep-log/7-cmd.out b/t/fcm-install-svn-hook/01-housekeep-log/7-cmd.out
new file mode 100644
index 0000000..6152c5e
--- /dev/null
+++ b/t/fcm-install-svn-hook/01-housekeep-log/7-cmd.out
@@ -0,0 +1,32 @@
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_HOME/etc/svn-hooks/post-commit to PWD/svn-repos/bar/hooks/post-commit
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_HOME/etc/svn-hooks/post-revprop-change to PWD/svn-repos/bar/hooks/post-revprop-change
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_HOME/etc/svn-hooks/pre-commit to PWD/svn-repos/bar/hooks/pre-commit
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_HOME/etc/svn-hooks/pre-revprop-change to PWD/svn-repos/bar/hooks/pre-revprop-change
+YYYY-mm-ddTHH:MM:SSZ: removing PWD/svn-repos/bar/log/post-commit.log
+YYYY-mm-ddTHH:MM:SSZ: creating symlink: post-commit.log.TODAY -> PWD/svn-repos/bar/log/post-commit.log
+YYYY-mm-ddTHH:MM:SSZ: gzip PWD/svn-repos/bar/log/post-commit.log.DATE_P7D
+YYYY-mm-ddTHH:MM:SSZ: removing PWD/svn-repos/bar/log/post-revprop-change.log
+YYYY-mm-ddTHH:MM:SSZ: creating symlink: post-revprop-change.log.TODAY -> PWD/svn-repos/bar/log/post-revprop-change.log
+YYYY-mm-ddTHH:MM:SSZ: gzip PWD/svn-repos/bar/log/post-revprop-change.log.DATE_P7D
+YYYY-mm-ddTHH:MM:SSZ: removing PWD/svn-repos/bar/log/pre-commit.log
+YYYY-mm-ddTHH:MM:SSZ: creating symlink: pre-commit.log.TODAY -> PWD/svn-repos/bar/log/pre-commit.log
+YYYY-mm-ddTHH:MM:SSZ: gzip PWD/svn-repos/bar/log/pre-commit.log.DATE_P7D
+YYYY-mm-ddTHH:MM:SSZ: removing PWD/svn-repos/bar/log/pre-revprop-change.log
+YYYY-mm-ddTHH:MM:SSZ: creating symlink: pre-revprop-change.log.TODAY -> PWD/svn-repos/bar/log/pre-revprop-change.log
+YYYY-mm-ddTHH:MM:SSZ: gzip PWD/svn-repos/bar/log/pre-revprop-change.log.DATE_P7D
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_HOME/etc/svn-hooks/post-commit to PWD/svn-repos/foo/hooks/post-commit
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_HOME/etc/svn-hooks/post-revprop-change to PWD/svn-repos/foo/hooks/post-revprop-change
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_HOME/etc/svn-hooks/pre-commit to PWD/svn-repos/foo/hooks/pre-commit
+YYYY-mm-ddTHH:MM:SSZ: copy FCM_HOME/etc/svn-hooks/pre-revprop-change to PWD/svn-repos/foo/hooks/pre-revprop-change
+YYYY-mm-ddTHH:MM:SSZ: removing PWD/svn-repos/foo/log/post-commit.log
+YYYY-mm-ddTHH:MM:SSZ: creating symlink: post-commit.log.TODAY -> PWD/svn-repos/foo/log/post-commit.log
+YYYY-mm-ddTHH:MM:SSZ: gzip PWD/svn-repos/foo/log/post-commit.log.DATE_P7D
+YYYY-mm-ddTHH:MM:SSZ: removing PWD/svn-repos/foo/log/post-revprop-change.log
+YYYY-mm-ddTHH:MM:SSZ: creating symlink: post-revprop-change.log.TODAY -> PWD/svn-repos/foo/log/post-revprop-change.log
+YYYY-mm-ddTHH:MM:SSZ: gzip PWD/svn-repos/foo/log/post-revprop-change.log.DATE_P7D
+YYYY-mm-ddTHH:MM:SSZ: removing PWD/svn-repos/foo/log/pre-commit.log
+YYYY-mm-ddTHH:MM:SSZ: creating symlink: pre-commit.log.TODAY -> PWD/svn-repos/foo/log/pre-commit.log
+YYYY-mm-ddTHH:MM:SSZ: gzip PWD/svn-repos/foo/log/pre-commit.log.DATE_P7D
+YYYY-mm-ddTHH:MM:SSZ: removing PWD/svn-repos/foo/log/pre-revprop-change.log
+YYYY-mm-ddTHH:MM:SSZ: creating symlink: pre-revprop-change.log.TODAY -> PWD/svn-repos/foo/log/pre-revprop-change.log
+YYYY-mm-ddTHH:MM:SSZ: gzip PWD/svn-repos/foo/log/pre-revprop-change.log.DATE_P7D
diff --git a/t/fcm-install-svn-hook/02-env.t b/t/fcm-install-svn-hook/02-env.t
new file mode 100644
index 0000000..b3f26a4
--- /dev/null
+++ b/t/fcm-install-svn-hook/02-env.t
@@ -0,0 +1,59 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+# Test "fcm-install-svn-hook", "hooks-env" installation.
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+. $TEST_SOURCE_DIR/test_header_more
+#-------------------------------------------------------------------------------
+if ! which svnadmin 1>/dev/null 2>/dev/null; then
+ skip_all 'svnadmin not available'
+fi
+tests 2
+FCM_REAL_HOME=$(readlink -f "$FCM_HOME")
+mkdir conf svn-repos
+export FCM_CONF_PATH="$PWD/conf"
+cat >conf/admin.cfg <<__CONF__
+admin_email=robert.fitzroy at metoffice.gov.uk
+notification_from=notifications at localhost
+svn_dump_dir=$PWD/svn/dumps
+svn_group=
+svn_hook_path_env=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
+svn_live_dir=$PWD/svn-repos
+svn_project_suffix=.svn
+trac_live_dir=$PWD/trac
+__CONF__
+svnadmin create svn-repos/foo.svn
+cat >hooks-env <<__CONF__
+[default]
+FCM_HOME=$FCM_REAL_HOME
+FCM_SVN_HOOK_ADMIN_EMAIL=robert.fitzroy at metoffice.gov.uk
+FCM_SVN_HOOK_COMMIT_DUMP_DIR=$PWD/svn/dumps
+FCM_SVN_HOOK_NOTIFICATION_FROM=notifications at localhost
+FCM_SVN_HOOK_REPOS_SUFFIX=.svn
+FCM_SVN_HOOK_TRAC_ROOT_DIR=$PWD/trac
+PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
+TZ=UTC
+__CONF__
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE"
+run_pass "$TEST_KEY" "$FCM_HOME/sbin/fcm-install-svn-hook"
+file_cmp "$TEST_KEY.foo.hooks-env" svn-repos/foo.svn/conf/hooks-env hooks-env
+#-------------------------------------------------------------------------------
+exit
diff --git a/t/fcm-install-svn-hook/test_header b/t/fcm-install-svn-hook/test_header
new file mode 120000
index 0000000..90bd5a3
--- /dev/null
+++ b/t/fcm-install-svn-hook/test_header
@@ -0,0 +1 @@
+../lib/bash/test_header
\ No newline at end of file
diff --git a/t/fcm-install-svn-hook/test_header_more b/t/fcm-install-svn-hook/test_header_more
new file mode 100644
index 0000000..2418b64
--- /dev/null
+++ b/t/fcm-install-svn-hook/test_header_more
@@ -0,0 +1,25 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+
+# Convert a string like "2014-05-01T09:30:45Z" to "YYYY-mm-ddTHH:MM:SSZ".
+# This allows output files to compare.
+date2datefmt() {
+ perl -p -e 's/\d+-\d\d-\d\dT\d\d:\d\d:\d\dZ/YYYY-mm-ddTHH:MM:SSZ/' "$@"
+}
diff --git a/t/fcm-keyword-print/00-simple.t b/t/fcm-keyword-print/00-simple.t
new file mode 100755
index 0000000..f22fd83
--- /dev/null
+++ b/t/fcm-keyword-print/00-simple.t
@@ -0,0 +1,67 @@
+#!/bin/bash
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# Basic tests for "fcm keyword-print".
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+svnadmin create plants
+URL="file://$PWD/plants"
+svn mkdir --parents -q -m 'test' $URL/{daisy,ivy,holly}/trunk
+mkdir -p conf
+cat >conf/keyword.cfg <<__CFG__
+location{primary}[daisy]=$URL/daisy
+location{primary}[ivy]=$URL/ivy
+location{primary}[holly]=$URL/holly
+__CFG__
+export FCM_CONF_PATH=$PWD/conf
+#-------------------------------------------------------------------------------
+tests 21
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE" # no argument
+run_pass "$TEST_KEY" fcm kp
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+location{primary}[daisy] = $URL/daisy
+location{primary}[holly] = $URL/holly
+location{primary}[ivy] = $URL/ivy
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+for NS in daisy ivy holly; do
+ TEST_KEY="$TEST_KEY_BASE-$NS" # normal mode
+ run_pass "$TEST_KEY" fcm kp fcm:$NS
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+location{primary}[$NS] = $URL/$NS
+__OUT__
+ file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+
+ TEST_KEY="$TEST_KEY_BASE-v-$NS" # verbose mode
+ run_pass "$TEST_KEY" fcm kp -v fcm:$NS
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+location{primary}[$NS] = $URL/$NS
+location[${NS}-br] = $URL/$NS/branches
+location[${NS}-tg] = $URL/$NS/tags
+location[${NS}-tr] = $URL/$NS/trunk
+location[${NS}_br] = $URL/$NS/branches
+location[${NS}_tg] = $URL/$NS/tags
+location[${NS}_tr] = $URL/$NS/trunk
+__OUT__
+ file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+done
+#-------------------------------------------------------------------------------
+exit
diff --git a/t/fcm-keyword-print/test_header b/t/fcm-keyword-print/test_header
new file mode 120000
index 0000000..90bd5a3
--- /dev/null
+++ b/t/fcm-keyword-print/test_header
@@ -0,0 +1 @@
+../lib/bash/test_header
\ No newline at end of file
diff --git a/t/fcm-loc-layout/00-simple.t b/t/fcm-loc-layout/00-simple.t
new file mode 100644
index 0000000..55e902c
--- /dev/null
+++ b/t/fcm-loc-layout/00-simple.t
@@ -0,0 +1,163 @@
+#!/bin/bash
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# Basic tests for "fcm status".
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+tests 24
+#-------------------------------------------------------------------------------
+setup
+unset TEST_PROJECT
+init_repos
+init_merge_branches merge1 merge2 $REPOS_URL
+export SVN_EDITOR="sed -i 1i\foo"
+cd $TEST_DIR/wc
+#-------------------------------------------------------------------------------
+# Tests fcm loc-layout, no project, default setup
+TEST_KEY=$TEST_KEY_BASE-no-project-default
+run_pass "$TEST_KEY" fcm loc-layout
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+target: .
+url: $ROOT_URL/trunk at 9
+root: $REPOS_URL
+path: /trunk
+peg_rev: 9
+project:
+branch: trunk
+branch_category: trunk
+sub_tree:
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm loc-layout, no project, default setup, cd to subdirectory
+TEST_KEY=$TEST_KEY_BASE-no-project-subtree
+cd module
+run_pass "$TEST_KEY" fcm loc-layout
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+target: .
+url: $ROOT_URL/trunk/module at 9
+root: $REPOS_URL
+path: /trunk/module
+peg_rev: 9
+project:
+branch: trunk
+branch_category: trunk
+sub_tree: module
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+cd ..
+#-------------------------------------------------------------------------------
+# Tests fcm loc-layout, no project, default setup, target subdirectory
+TEST_KEY=$TEST_KEY_BASE-no-project-target-subtree
+run_pass "$TEST_KEY" fcm loc-layout module
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+target: module
+url: $ROOT_URL/trunk/module at 9
+root: $REPOS_URL
+path: /trunk/module
+peg_rev: 9
+project:
+branch: trunk
+branch_category: trunk
+sub_tree: module
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm loc-layout, no project, default setup, target subdirectory
+TEST_KEY=$TEST_KEY_BASE-no-project-target-repos
+run_pass "$TEST_KEY" fcm loc-layout $REPOS_URL
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+target: $REPOS_URL
+url: $ROOT_URL at 9
+root: $REPOS_URL
+path:
+peg_rev: 9
+project:
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+teardown
+#-------------------------------------------------------------------------------
+setup
+init_repos_layout_roses
+svn checkout -q $ROOT_URL/a/a/0/0/0/trunk $TEST_DIR/wc
+cd $TEST_DIR/wc
+#-------------------------------------------------------------------------------
+# Tests fcm loc-layout, 'roses' 5-level project
+TEST_KEY=$TEST_KEY_BASE-roses-default
+run_pass "$TEST_KEY" fcm loc-layout
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+target: .
+url: $ROOT_URL/a/a/0/0/0/trunk at 3
+root: $REPOS_URL
+path: /a/a/0/0/0/trunk
+peg_rev: 3
+project: a/a/0/0/0
+branch: trunk
+branch_category: trunk
+sub_tree:
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm loc-layout, 'roses' 5-level project
+TEST_KEY=$TEST_KEY_BASE-roses-subtree
+cd module
+run_pass "$TEST_KEY" fcm loc-layout
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+target: .
+url: $ROOT_URL/a/a/0/0/0/trunk/module at 3
+root: $REPOS_URL
+path: /a/a/0/0/0/trunk/module
+peg_rev: 3
+project: a/a/0/0/0
+branch: trunk
+branch_category: trunk
+sub_tree: module
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+cd ..
+#-------------------------------------------------------------------------------
+# Tests fcm loc-layout, 'roses' 5-level project
+TEST_KEY=$TEST_KEY_BASE-roses-target-subtree
+run_pass "$TEST_KEY" fcm loc-layout module
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+target: module
+url: $ROOT_URL/a/a/0/0/0/trunk/module at 3
+root: $REPOS_URL
+path: /a/a/0/0/0/trunk/module
+peg_rev: 3
+project: a/a/0/0/0
+branch: trunk
+branch_category: trunk
+sub_tree: module
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm loc-layout, no project, default setup, target subdirectory
+TEST_KEY=$TEST_KEY_BASE-roses-target-repos
+run_pass "$TEST_KEY" fcm loc-layout $REPOS_URL
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+target: $REPOS_URL
+url: $ROOT_URL at 3
+root: $REPOS_URL
+path:
+peg_rev: 3
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+teardown
diff --git a/t/fcm-loc-layout/test_header b/t/fcm-loc-layout/test_header
new file mode 100644
index 0000000..6c38759
--- /dev/null
+++ b/t/fcm-loc-layout/test_header
@@ -0,0 +1,231 @@
+#!/bin/bash
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# Optional enviroment variables:
+# TEST_REMOTE_HOST (tests using svn+ssh repositories located on given host)
+# ------------------------------------------------------------------------------
+
+. $(dirname $0)/../lib/bash/test_header
+
+function file_cmp() {
+ local TEST_KEY=$1
+ local FILE_ACTUAL=$2
+ local FILE_EXPECT=${3:--}
+ if cmp $TEST_DIR/$FILE_ACTUAL $FILE_EXPECT; then
+ pass $TEST_KEY
+ return
+ fi
+ fail $TEST_KEY
+}
+
+function file_grep() {
+ local TEST_KEY=$1
+ local PATTERN=$2
+ local FILE=$3
+ if grep -q -e "$PATTERN" $TEST_DIR/$FILE; then
+ pass $TEST_KEY
+ return
+ fi
+ fail $TEST_KEY
+}
+
+function file_test() {
+ local TEST_KEY=$1
+ local FILE=$2
+ local OPTION=${3:--e}
+ if test $OPTION $TEST_DIR/$FILE; then
+ pass $TEST_KEY
+ else
+ fail $TEST_KEY
+ fi
+}
+
+function file_xxdiff() {
+ local TEST_KEY=$1
+ local FILE_ACTUAL=$2
+ local FILE_EXPECT=${3:--}
+ if xxdiff -D $TEST_DIR/$FILE_ACTUAL $FILE_EXPECT; then
+ pass $TEST_KEY
+ return
+ fi
+ fail $TEST_KEY
+}
+
+function init_repos() {
+ if [[ -n ${TEST_REMOTE_HOST:-} ]]; then
+ TEST_REMOTE_DIR=$(ssh $TEST_REMOTE_HOST "mktemp -d")
+ ssh $TEST_REMOTE_HOST "svnadmin create --fs-type fsfs $TEST_REMOTE_DIR"
+ REPOS_URL="svn+ssh://${TEST_REMOTE_HOST}$TEST_REMOTE_DIR"
+ else
+ svnadmin create --fs-type fsfs $TEST_DIR/test_repos
+ REPOS_URL="file://$TEST_DIR/test_repos"
+ fi
+ ROOT_URL=$REPOS_URL
+ PROJECT=
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ PROJECT=$TEST_PROJECT"/"
+ fi
+ svn import -q $TEST_SOURCE_DIR/../etc/repo_files \
+ $REPOS_URL/$PROJECT/trunk -m "initial trunk import"
+ svn mkdir -q $REPOS_URL/$PROJECT/tags -m "make tags"
+ svn mkdir -q --parents $REPOS_URL/$PROJECT/branches/dev/Share -m " "
+}
+
+function init_repos_layout_roses() {
+ if [[ -n ${TEST_REMOTE_HOST:-} ]]; then
+ TEST_REMOTE_DIR=$(ssh $TEST_REMOTE_HOST "mktemp -d")
+ ssh $TEST_REMOTE_HOST "svnadmin create --fs-type fsfs $TEST_REMOTE_DIR"
+ REPOS_URL="svn+ssh://${TEST_REMOTE_HOST}$TEST_REMOTE_DIR"
+ else
+ svnadmin create --fs-type fsfs $TEST_DIR/test_repos
+ REPOS_URL="file://$TEST_DIR/test_repos"
+ fi
+ svn mkdir -q --parents $REPOS_URL/a/a/0/0/0/trunk
+ svn import -q $TEST_SOURCE_DIR/../etc/repo_files \
+ $REPOS_URL/a/a/0/0/0/trunk -m "initial trunk import"
+ TMPFILE=$(mktemp)
+ cat >$TMPFILE <<__LAYOUT__
+depth-project = 5
+depth-branch = 1
+depth-tag = 1
+dir-trunk = trunk
+dir-branch =
+dir-tag =
+level-owner-branch =
+level-owner-tag =
+template-branch =
+template-tag =
+__LAYOUT__
+ TMPDIR=$(mktemp -d)
+ svn checkout -q $REPOS_URL $TMPDIR
+ svn propset -q --file=$TMPFILE fcm:layout $TMPDIR
+ svn commit -q -m " " $TMPDIR
+ rm -f $TMPFILE
+ rm -rf $TMPDIR
+ ROOT_URL=$REPOS_URL
+}
+
+function init_branch() {
+ local BRANCH_NAME=$1
+ local REPOS_URL=$2
+ local ROOT_URL=$REPOS_URL
+ local ROOT_PATH=
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ ROOT_PATH=$ROOT_PATH/$TEST_PROJECT
+ fi
+ MESSAGE=$(echo -e "Created $ROOT_PATH/branches/dev/Share/$BRANCH_NAME from /trunk at 1.")
+ svn copy -q -r1 $ROOT_URL/trunk $ROOT_URL/branches/dev/Share/$BRANCH_NAME \
+ -m "Made a branch $MESSAGE"
+}
+
+function init_branch_wc() {
+ local BRANCH_NAME=$1
+ local REPOS_URL=$2
+ local ROOT_URL=$REPOS_URL
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ fi
+ init_branch $BRANCH_NAME $REPOS_URL
+ svn checkout -q $ROOT_URL/branches/dev/Share/$BRANCH_NAME $TEST_DIR/wc
+}
+
+function init_merge_branches() {
+ local BRANCH_NAME=$1
+ local OTHER_BRANCH_NAME=$2
+ local REPOS_URL=$3
+ local ROOT_URL=$REPOS_URL
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ fi
+ init_branch_wc $BRANCH_NAME $REPOS_URL
+ cd $TEST_DIR/wc
+ file_list=$(find . -type f | sed "/\.svn/d" | sort | head -5)
+ other_file=$(find . -type f | sed "/\.svn/d" | sort | tail -1)
+ for file in $file_list; do
+ sed -i "s/for/FOR/g; s/fi/end if/g; s/in/IN/g;" $file
+ sed -i "/#/d; /^ *!/d" $file
+ sed -i "s/!/!!/g; s/q/\nq/g; s/[(]/(\n/g" $file
+ done
+ file_dir=$(dirname $file)
+ svn copy -q $file ./added_file
+ svn copy -q module added_directory
+ touch module/tree_conflict_file
+ svn add -q $file_dir/tree_conflict_file
+ echo "Modified a line" >>$other_file
+ svn commit -q -m "Made changes for future merge of this branch"
+ svn update -q
+ init_branch $OTHER_BRANCH_NAME $REPOS_URL
+ svn switch -q $ROOT_URL/branches/dev/Share/$OTHER_BRANCH_NAME
+ echo " " > unversioned_file
+ properties_file=$(find . -type f | sed " /\.svn/d" | sort | tail -3 | head -1)
+ svn propset -q svn:executable "executable" $properties_file
+ svn copy -q $file renamed_added_file
+ svn commit -q -m "Made changes for future merge"
+ svn update -q
+ svn switch -q $ROOT_URL/trunk
+ echo "trunk change" >>$(find . -type f | sed "/\.svn/d" | sort | head -1)
+ svn commit -q -m "Made trunk change"
+ svn update -q
+ echo "another trunk change" >>$(find . -type f | sed "/\.svn/d" | sort | head -1)
+ svn commit -q -m "Made another trunk change"
+ svn update -q
+}
+
+function run_pass() {
+ local TEST_KEY=$1
+ shift 1
+ if ! "$@" 1>$TEST_DIR/$TEST_KEY.out 2>$TEST_DIR/$TEST_KEY.err; then
+ fail $TEST_KEY
+ return
+ fi
+ pass $TEST_KEY
+}
+
+function run_fail() {
+ local TEST_KEY=$1
+ shift 1
+ if "$@" 1>$TEST_DIR/$TEST_KEY.out 2>$TEST_DIR/$TEST_KEY.err; then
+ fail $TEST_KEY
+ return
+ fi
+ pass $TEST_KEY
+}
+
+function setup() {
+ mkdir -p $TEST_DIR/.subversion
+ mkdir -p $TEST_DIR/run
+ cd $TEST_DIR/run
+}
+
+function teardown() {
+ cd $TEST_DIR
+ rm -rf $TEST_DIR/test_repos
+ rm -rf $TEST_DIR/wc
+ rm -rf $TEST_DIR/run
+ rm -rf $TEST_DIR/.subversion
+ if [[ -n ${TEST_REMOTE_HOST:-} ]]; then
+ ssh $TEST_REMOTE_HOST "rm -rf $TEST_REMOTE_DIR"
+ fi
+}
+
+REPOS_URL=
+ROOT_URL=
+PROJECT=
diff --git a/t/fcm-make/00-build-basic.t b/t/fcm-make/00-build-basic.t
new file mode 100755
index 0000000..6b0c9ba
--- /dev/null
+++ b/t/fcm-make/00-build-basic.t
@@ -0,0 +1,85 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+# Basic tests for "fcm make".
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+tests 18
+cp -r $TEST_SOURCE_DIR/$TEST_KEY_BASE/* .
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE"
+run_pass "$TEST_KEY" fcm make
+find .fcm-make build -type f | sed 's/^\(\.fcm-make\/log\).*$/\1/' \
+ | sort >"$TEST_KEY.find"
+file_cmp "$TEST_KEY.find" "$TEST_KEY.find" <<'__OUT__'
+.fcm-make/config-as-parsed.cfg
+.fcm-make/config-on-success.cfg
+.fcm-make/ctx.gz
+.fcm-make/log
+build/bin/hello.exe
+build/include/world.mod
+build/o/hello.o
+build/o/world.o
+__OUT__
+file_test "$TEST_KEY.log" fcm-make.log
+file_test "$TEST_KEY.fcm-make-as-parsed.cfg" fcm-make-as-parsed.cfg
+file_test "$TEST_KEY.fcm-make-on-success.cfg" fcm-make-on-success.cfg
+readlink fcm-make.log >"$TEST_KEY.log.readlink"
+file_cmp "$TEST_KEY.log.readlink" "$TEST_KEY.log.readlink" <<<'.fcm-make/log'
+sed '/^\[info\] \(source->target\|target\|required-target\) /!d' \
+ .fcm-make/log >"$TEST_KEY.log.sed"
+file_cmp "$TEST_KEY.log.sed" "$TEST_KEY.log.sed" <<'__LOG__'
+[info] source->target / -> (archive) lib/ libo.a
+[info] source->target hello.f90 -> (link) bin/ hello.exe
+[info] source->target hello.f90 -> (install) include/ hello.f90
+[info] source->target hello.f90 -> (compile) o/ hello.o
+[info] source->target world.f90 -> (install) include/ world.f90
+[info] source->target world.f90 -> (compile+) include/ world.mod
+[info] source->target world.f90 -> (compile) o/ world.o
+[info] target hello.exe
+[info] target - hello.o
+[info] target - - world.mod
+[info] target - - - world.o
+[info] target - world.o
+__LOG__
+file_test "$TEST_KEY-as-parsed.cfg" fcm-make-as-parsed.cfg
+readlink fcm-make-as-parsed.cfg >"$TEST_KEY-as-parsed.cfg.out"
+file_cmp "$TEST_KEY-as-parsed.cfg.out" "$TEST_KEY-as-parsed.cfg.out" \
+ <<<'.fcm-make/config-as-parsed.cfg'
+file_test "$TEST_KEY-on-success.cfg" fcm-make-on-success.cfg
+readlink fcm-make-on-success.cfg >"$TEST_KEY-on-success.cfg.out"
+file_cmp "$TEST_KEY-on-success.cfg.out" "$TEST_KEY-on-success.cfg.out" \
+ <<<'.fcm-make/config-on-success.cfg'
+run_pass "$TEST_KEY.exe" $PWD/build/bin/hello.exe
+file_cmp "$TEST_KEY.exe.out" "$TEST_KEY.exe.out" <<<'Hello Earth'
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-incr"
+find build -type f -exec stat -c'%Y %n' {} \; | sort >"$TEST_KEY.mtime.old"
+run_pass "$TEST_KEY" fcm make
+find build -type f -exec stat -c'%Y %n' {} \; | sort >"$TEST_KEY.mtime"
+file_cmp "$TEST_KEY.mtime" "$TEST_KEY.mtime.old" "$TEST_KEY.mtime"
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-incr-fail"
+echo 'build.target=foo' >>fcm-make.cfg
+run_fail "$TEST_KEY" fcm make
+run_fail "$TEST_KEY.config-on-success" test -e .fcm-make/config-on-success
+run_fail "$TEST_KEY.fcm-make-on-success.cfg" test -e fcm-make-on-success.cfg
+#-------------------------------------------------------------------------------
+exit 0
diff --git a/t/fcm-make/00-build-basic/bin/my-ld b/t/fcm-make/00-build-basic/bin/my-ld
new file mode 100755
index 0000000..aff7d0e
--- /dev/null
+++ b/t/fcm-make/00-build-basic/bin/my-ld
@@ -0,0 +1,21 @@
+#!/usr/bin/env bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+echo "$@" >$(basename $0).out
+exec ${FCM_TEST_FC:-gfortran} "$@"
diff --git a/t/fcm-make/00-build-basic/fcm-make.cfg b/t/fcm-make/00-build-basic/fcm-make.cfg
new file mode 100644
index 0000000..81f4f9b
--- /dev/null
+++ b/t/fcm-make/00-build-basic/fcm-make.cfg
@@ -0,0 +1,5 @@
+steps = build
+build.source = $HERE/src
+build.target{task} = link
+#build.prop{keep-lib-o} = true
+#build.prop{ld} = ld
diff --git a/t/fcm-make/00-build-basic/src/hello.f90 b/t/fcm-make/00-build-basic/src/hello.f90
new file mode 100644
index 0000000..ab9aaea
--- /dev/null
+++ b/t/fcm-make/00-build-basic/src/hello.f90
@@ -0,0 +1,4 @@
+program hello
+use world, only: get_world
+WRITE(*, '(A,A)') 'Hello ', trim(get_world())
+end program hello
diff --git a/t/fcm-make/00-build-basic/src/world.f90 b/t/fcm-make/00-build-basic/src/world.f90
new file mode 100644
index 0000000..7c08240
--- /dev/null
+++ b/t/fcm-make/00-build-basic/src/world.f90
@@ -0,0 +1,8 @@
+module world
+character(*), parameter :: world1 = 'Earth'
+contains
+elemental function get_world() result(w)
+character(len=len(world1)) :: w
+w = world1
+end function get_world
+end module world
diff --git a/t/fcm-make/01-build-link-opts b/t/fcm-make/01-build-link-opts
new file mode 120000
index 0000000..8bddc38
--- /dev/null
+++ b/t/fcm-make/01-build-link-opts
@@ -0,0 +1 @@
+00-build-basic
\ No newline at end of file
diff --git a/t/fcm-make/01-build-link-opts.t b/t/fcm-make/01-build-link-opts.t
new file mode 100755
index 0000000..beb7396
--- /dev/null
+++ b/t/fcm-make/01-build-link-opts.t
@@ -0,0 +1,80 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+# Tests some linker options for "fcm make".
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+tests 11
+cp -r $TEST_SOURCE_DIR/$TEST_KEY_BASE/* .
+PATH=$PWD/bin:$PATH
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-keep-lib-o-incr"
+fcm make -q
+echo 'build.prop{keep-lib-o} = true' >>fcm-make.cfg
+find build -type f -exec stat -c'%Y %n' {} \; | sort >"$TEST_KEY.mtime.old"
+run_pass "$TEST_KEY" fcm make
+find build -type f -exec stat -c'%Y %n' {} \; | sort >"$TEST_KEY.mtime"
+if cmp -s "$TEST_KEY.mtime.old" "$TEST_KEY.mtime"; then
+ fail "$TEST_KEY.mtime"
+else
+ pass "$TEST_KEY.mtime"
+fi
+file_grep "$TEST_KEY.mtime.grep" 'lib/libhello[.]a' "$TEST_KEY.mtime"
+sed -i '/hello[.]exe/d' "$TEST_KEY.mtime.old"
+sed -i '/libhello[.]a/d; /hello[.]exe/d' "$TEST_KEY.mtime"
+file_cmp "$TEST_KEY.mtime.old" "$TEST_KEY.mtime.old" "$TEST_KEY.mtime"
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-keep-lib-o-new"
+# echo 'build.prop{keep-lib-o} = true' >>fcm-make.cfg # already done above
+run_pass "$TEST_KEY" fcm make --new
+find build -type f | sort >"$TEST_KEY.find"
+file_cmp "$TEST_KEY.find" "$TEST_KEY.find" <<'__OUT__'
+build/bin/hello.exe
+build/include/world.mod
+build/lib/libhello.a
+build/o/hello.o
+build/o/world.o
+__OUT__
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-ld-incr"
+cp -r $TEST_SOURCE_DIR/$TEST_KEY_BASE/fcm-make.cfg .
+fcm make -q --new
+find build -type f -exec stat -c'%Y %n' {} \; | sort >"$TEST_KEY.mtime.old"
+echo 'build.prop{ld} = my-ld' >>fcm-make.cfg
+run_pass "$TEST_KEY" fcm make
+find build -type f -exec stat -c'%Y %n' {} \; | sort >"$TEST_KEY.mtime"
+file_grep "$TEST_KEY.mtime.grep" 'build/my-ld[.]out' "$TEST_KEY.mtime"
+sed -i '/hello[.]exe/d' "$TEST_KEY.mtime.old"
+sed -i '/hello[.]exe/d; /my-ld[.]out/d' "$TEST_KEY.mtime"
+file_cmp "$TEST_KEY.mtime.old" "$TEST_KEY.mtime.old" "$TEST_KEY.mtime"
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-ld-new"
+# echo 'build.prop{ld} = my-ld' >>fcm-make.cfg # already done above
+run_pass "$TEST_KEY" fcm make --new
+find build -type f | sort >"$TEST_KEY.find"
+file_cmp "$TEST_KEY.find" "$TEST_KEY.find" <<'__OUT__'
+build/bin/hello.exe
+build/include/world.mod
+build/my-ld.out
+build/o/hello.o
+build/o/world.o
+__OUT__
+#-------------------------------------------------------------------------------
+exit 0
diff --git a/t/fcm-make/02-build-ext-iface.t b/t/fcm-make/02-build-ext-iface.t
new file mode 100755
index 0000000..4c06975
--- /dev/null
+++ b/t/fcm-make/02-build-ext-iface.t
@@ -0,0 +1,46 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+# Tests build ext-iface for "fcm make".
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+tests 4
+cp -r $TEST_SOURCE_DIR/$TEST_KEY_BASE/* .
+#-------------------------------------------------------------------------------
+# Normal operation. Lots of examples in a single source file.
+TEST_KEY="$TEST_KEY_BASE-t1"
+TARGETS=t1.interface run_pass "$TEST_KEY" fcm make --new
+file_cmp "$TEST_KEY.interface" build/include/t1.interface expected/t1.interface
+#-------------------------------------------------------------------------------
+# Bad syntax 1: missing close bracket ) in a local declaration statement.
+# Hang at FCM-2-3-1.
+# We can ignore this problem, as it does not add to the interface
+TEST_KEY="$TEST_KEY_BASE-t2"
+TARGETS=t2.interface run_fail "$TEST_KEY" fcm make --new
+# Time may not be 0.0 on a very very slow computer
+sed -i '2s/ [0-9][0-9]*\.[0-9][0-9]* / ?.? /' "$TEST_KEY.err"
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" <<__ERR__
+[FAIL] $PWD/src/t2.f90(2): syntax error
+[FAIL] ext-iface ?.? ! t2.interface <- t2.f90
+[FAIL] ! t2.interface : update task failed
+
+__ERR__
+#-------------------------------------------------------------------------------
+exit 0
diff --git a/t/fcm-make/02-build-ext-iface/expected/t1.interface b/t/fcm-make/02-build-ext-iface/expected/t1.interface
new file mode 100644
index 0000000..0ca0006
--- /dev/null
+++ b/t/fcm-make/02-build-ext-iface/expected/t1.interface
@@ -0,0 +1,70 @@
+interface
+logical function func_simple()
+end function func_simple
+logical function func_simple_1()
+end function
+logical function func_simple_2()
+end
+pure logical function func_simple_pure()
+end function func_simple_pure
+recursive pure integer function func_simple_recursive_pure(i)
+integer, intent(in) :: i
+end function func_simple_recursive_pure
+elemental logical function func_simple_elemental()
+end function func_simple_elemental
+integer(selected_int_kind(0)) function func_with_use_and_args(egg, ham)
+use foo
+use bar, only:&
+ & i_am_dim
+integer, intent(in) :: egg(i_am_dim)
+integer, intent(in) :: ham(i_am_dim, 2)
+end function func_with_use_and_args
+character(20) function func_with_parameters(egg, ham)
+character*(*), parameter :: x_param = '01234567890'
+character(*), parameter :: &
+ y_param &
+ = '!&!&!&!&!&!'
+character(len(x_param)), intent(in) :: egg
+character(len(y_param)), intent(in) :: ham
+end function func_with_parameters
+function func_with_parameters_1(egg, ham) result(r)
+integer, parameter :: x_param = 10
+integer z_param
+parameter(z_param = 2)
+real, intent(in), dimension(x_param) :: egg
+integer, intent(in) :: ham
+logical :: r(z_param)
+end function func_with_parameters_1
+character(10) function func_with_contains(mushroom, tomoato)
+character(5) mushroom
+character(5) tomoato
+end function func_with_contains
+Function func_mix_local_and_result(egg, ham, bacon) Result(Breakfast)
+Integer, Intent(in) :: egg, ham
+Real, Intent(in) :: bacon
+Real :: tomato, breakfast
+End Function func_mix_local_and_result
+subroutine sub_simple()
+end subroutine sub_simple
+subroutine sub_simple_1()
+end subroutine
+subroutine sub_simple_2()
+end
+subroutine sub_simple_3()
+end sub&
+&routine&
+& sub_simple_3
+subroutine sub_with_contains(foo)
+character*(len('!"&''&"!')) &
+ foo
+end subroutine sub_with_contains
+subroutine sub_with_renamed_import(i_am_dim)
+integer, parameter :: d = 2
+complex :: i_am_dim(d)
+end subroutine sub_with_renamed_import
+subroutine sub_with_external(proc)
+external proc
+end subroutine sub_with_external
+subroutine sub_with_end()
+end subroutine sub_with_end
+end interface
diff --git a/t/fcm-make/02-build-ext-iface/expected/t2.interface b/t/fcm-make/02-build-ext-iface/expected/t2.interface
new file mode 100644
index 0000000..97a772e
--- /dev/null
+++ b/t/fcm-make/02-build-ext-iface/expected/t2.interface
@@ -0,0 +1,4 @@
+interface
+subroutine t2()
+end subroutine t2
+end interface
diff --git a/t/fcm-make/02-build-ext-iface/fcm-make.cfg b/t/fcm-make/02-build-ext-iface/fcm-make.cfg
new file mode 100644
index 0000000..5e71b6b
--- /dev/null
+++ b/t/fcm-make/02-build-ext-iface/fcm-make.cfg
@@ -0,0 +1,4 @@
+steps=build
+build.source=$HERE/src
+$TARGETS{?}=t1.interface
+build.target=$TARGETS
diff --git a/t/fcm-make/02-build-ext-iface/src/m1.f90 b/t/fcm-make/02-build-ext-iface/src/m1.f90
new file mode 100644
index 0000000..f5c5e63
--- /dev/null
+++ b/t/fcm-make/02-build-ext-iface/src/m1.f90
@@ -0,0 +1,26 @@
+! A module with nonsense
+module bar
+type food
+integer :: cooking_method
+end type food
+type organic
+integer :: growing_method
+end type organic
+integer, parameter :: i_am_dim = 10
+end module bar
+
+! A module with more nonsense
+module foo
+use bar, only: FOOD
+integer :: foo_int
+contains
+subroutine foo_sub(egg)
+integer, parameter :: egg_dim = 10
+type(Food), intent(in) :: egg
+write(*, *) egg
+end subroutine foo_sub
+elemental function foo_func() result(f)
+integer :: f
+f = 0
+end function
+end module foo
diff --git a/t/fcm-make/02-build-ext-iface/src/t1.f90 b/t/fcm-make/02-build-ext-iface/src/t1.f90
new file mode 100644
index 0000000..c7ce2f2
--- /dev/null
+++ b/t/fcm-make/02-build-ext-iface/src/t1.f90
@@ -0,0 +1,163 @@
+! A simple function
+logical function func_simple()
+func_simple = .true.
+end function func_simple
+
+! A simple function, but with less friendly end
+logical function func_simple_1()
+func_simple_1 = .true.
+end function
+
+! A simple function, but with even less friendly end
+logical function func_simple_2()
+func_simple_2 = .true.
+end
+
+! A pure simple function
+pure logical function func_simple_pure()
+func_simple_pure = .true.
+end function func_simple_pure
+
+! A pure recursive function
+recursive pure integer function func_simple_recursive_pure(i)
+integer, intent(in) :: i
+if (i <= 0) then
+ func_simple_recursive_pure = i
+else
+ func_simple_recursive_pure = i + func_simple_recursive_pure(i - 1)
+end if
+end function func_simple_recursive_pure
+
+! An elemental simple function
+elemental logical function func_simple_elemental()
+func_simple_elemental = .true.
+end function func_simple_elemental
+
+! An function with arguments and module imports
+integer(selected_int_kind(0)) function func_with_use_and_args(egg, ham)
+use foo
+! Deliberate trailing spaces in next line
+use bar, only : organic, i_am_dim
+implicit none
+integer, intent(in) :: egg(i_am_dim)
+integer, intent(in) :: ham(i_am_dim, 2)
+real bacon
+! Deliberate trailing spaces in next line
+type( organic ) :: tomato
+func_with_use_and_args = egg(1) + ham(1, 1)
+end function func_with_use_and_args
+
+! A function with some parameters
+character(20) function func_with_parameters(egg, ham)
+implicit none
+character*(*), parameter :: x_param = '01234567890'
+character(*), parameter :: & ! throw in some comments
+ y_param &
+ = '!&!&!&!&!&!' ! how to make life interesting
+integer, parameter :: z = 20
+character(len(x_param)), intent(in) :: egg
+character(len(y_param)), intent(in) :: ham
+func_with_parameters = egg // ham
+end function func_with_parameters
+
+! A function with some parameters, with a result
+function func_with_parameters_1(egg, ham) result(r)
+implicit none
+integer, parameter :: x_param = 10
+integer z_param
+parameter(z_param = 2)
+real, intent(in), dimension(x_param) :: egg
+integer, intent(in) :: ham
+logical :: r(z_param)
+r(1) = int(egg(1)) + ham > 0
+r(2) = .false.
+end function func_with_parameters_1
+
+! A function with a contains
+character(10) function func_with_contains(mushroom, tomoato)
+character(5) mushroom
+character(5) tomoato
+func_with_contains = func_with_contains_1()
+contains
+character(10) function func_with_contains_1()
+func_with_contains_1 = mushroom // tomoato
+end function func_with_contains_1
+end function func_with_contains
+
+! A function with its result declared after a local in the same statement
+Function func_mix_local_and_result(egg, ham, bacon) Result(Breakfast)
+Integer, Intent(in) :: egg, ham
+Real, Intent(in) :: bacon
+Real :: tomato, breakfast
+Breakfast = real(egg) + real(ham) + bacon
+End Function func_mix_local_and_result
+
+! A simple subroutine
+subroutine sub_simple()
+end subroutine sub_simple
+
+! A simple subroutine, with not so friendly end
+subroutine sub_simple_1()
+end subroutine
+
+! A simple subroutine, with even less friendly end
+subroutine sub_simple_2()
+end
+
+! A simple subroutine, with funny continuation
+subroutine sub_simple_3()
+end sub&
+&routine&
+& sub_simple_3
+
+! A subroutine with a few contains
+subroutine sub_with_contains(foo) ! " &
+! Deliberate trailing spaces in next line
+use Bar, only: i_am_dim
+character*(len('!"&''&"!')) & ! what a mess!
+ foo
+call sub_with_contains_first()
+call sub_with_contains_second()
+call sub_with_contains_third()
+print*, foo
+contains
+subroutine sub_with_contains_first()
+interface
+integer function x()
+end function x
+end interface
+end subroutine sub_with_contains_first
+subroutine sub_with_contains_second()
+end subroutine
+subroutine sub_with_contains_third()
+end subroutine
+end subroutine sub_with_contains
+
+! A subroutine with a renamed module import
+subroutine sub_with_renamed_import(i_am_dim)
+use bar, only: i_am_not_dim => i_am_dim
+integer, parameter :: d = 2
+complex :: i_am_dim(d)
+print*, i_am_dim
+end subroutine sub_with_renamed_import
+
+! A subroutine with an external argument
+subroutine sub_with_external(proc)
+external proc
+call proc()
+end subroutine sub_with_external
+
+! A subroutine with a variable named "end"
+subroutine sub_with_end()
+integer :: end
+end = 0
+end subroutine sub_with_end
+
+! A module with nonsense
+module stuff
+character(*), parameter :: stuffing = 'yummy'
+contains
+subroutine matter()
+print*, 'Matter is not dark'
+end subroutine matter
+end module stuff
diff --git a/t/fcm-make/02-build-ext-iface/src/t2.f90 b/t/fcm-make/02-build-ext-iface/src/t2.f90
new file mode 100644
index 0000000..66ce982
--- /dev/null
+++ b/t/fcm-make/02-build-ext-iface/src/t2.f90
@@ -0,0 +1,3 @@
+subroutine t2()
+integer :: c( ! bad syntax
+end subroutine t2
diff --git a/t/fcm-make/03-build-include-paths.t b/t/fcm-make/03-build-include-paths.t
new file mode 100755
index 0000000..5d865f9
--- /dev/null
+++ b/t/fcm-make/03-build-include-paths.t
@@ -0,0 +1,69 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+# Tests for "fcm make", "build.prop{fc.include-paths}".
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+
+function get_compiler_log() {
+ sed '/^\[info\] shell(0.*) gfortran/!d;
+ /hello\.exe/d;
+ s/^\[info\] shell(0.*) //' .fcm-make/log
+}
+#-------------------------------------------------------------------------------
+tests 11
+cp -r $TEST_SOURCE_DIR/$TEST_KEY_BASE/* .
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-control"
+run_fail "$TEST_KEY" fcm make
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE"
+FCM_TEST_FC_INCLUDE_PATHS="$PWD/include/world1 $PWD/include/world2" \
+ run_pass "$TEST_KEY" fcm make
+$PWD/build/bin/hello.exe >"$TEST_KEY.command.out"
+file_cmp "$TEST_KEY.command.out" "$TEST_KEY.command.out" <<<'Hello Earth'
+get_compiler_log >"$TEST_KEY.gfortran.log"
+file_cmp "$TEST_KEY.gfortran.log" "$TEST_KEY.gfortran.log" <<__LOG__
+gfortran -oo/world.o -c -I./include -I$PWD/include/world1 -I$PWD/include/world2 $PWD/src/world.f90
+gfortran -oo/hello.o -c -I./include -I$PWD/include/world1 -I$PWD/include/world2 $PWD/src/hello.f90
+__LOG__
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-incr0"
+find build -type f -exec stat -c'%Y %n' {} \; | sort >"$TEST_KEY.mtime.old"
+FCM_TEST_FC_INCLUDE_PATHS="$PWD/include/world1 $PWD/include/world2" \
+ run_pass "$TEST_KEY" fcm make
+find build -type f -exec stat -c'%Y %n' {} \; | sort >"$TEST_KEY.mtime"
+file_cmp "$TEST_KEY.mtime" "$TEST_KEY.mtime.old" "$TEST_KEY.mtime"
+$PWD/build/bin/hello.exe >"$TEST_KEY.command.out"
+file_cmp "$TEST_KEY.command.out" "$TEST_KEY.command.out" <<<'Hello Earth'
+get_compiler_log >"$TEST_KEY.gfortran.log"
+file_cmp "$TEST_KEY.gfortran.log" "$TEST_KEY.gfortran.log" </dev/null
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-incr1"
+FCM_TEST_FC_INCLUDE_PATHS="$PWD/include/world2 $PWD/include/world1" \
+ run_pass "$TEST_KEY" fcm make
+$PWD/build/bin/hello.exe >"$TEST_KEY.command.out"
+file_cmp "$TEST_KEY.command.out" "$TEST_KEY.command.out" <<<'Hello Moon'
+get_compiler_log >"$TEST_KEY.gfortran.log"
+file_cmp "$TEST_KEY.gfortran.log" "$TEST_KEY.gfortran.log" <<__LOG__
+gfortran -oo/world.o -c -I./include -I$PWD/include/world2 -I$PWD/include/world1 $PWD/src/world.f90
+gfortran -oo/hello.o -c -I./include -I$PWD/include/world2 -I$PWD/include/world1 $PWD/src/hello.f90
+__LOG__
+#-------------------------------------------------------------------------------
+exit 0
diff --git a/t/fcm-make/03-build-include-paths/fcm-make.cfg b/t/fcm-make/03-build-include-paths/fcm-make.cfg
new file mode 100644
index 0000000..6ae690f
--- /dev/null
+++ b/t/fcm-make/03-build-include-paths/fcm-make.cfg
@@ -0,0 +1,6 @@
+steps=build
+build.source=$HERE/src
+build.target{task}=link
+build.prop{no-dep.include}=worldx.f90
+$FCM_TEST_FC_INCLUDE_PATHS{?}=
+build.prop{fc.include-paths}=$FCM_TEST_FC_INCLUDE_PATHS
diff --git a/t/fcm-make/03-build-include-paths/include/world1/worldx.f90 b/t/fcm-make/03-build-include-paths/include/world1/worldx.f90
new file mode 100644
index 0000000..9daa8f8
--- /dev/null
+++ b/t/fcm-make/03-build-include-paths/include/world1/worldx.f90
@@ -0,0 +1 @@
+character(*), parameter :: world1 = 'Earth'
diff --git a/t/fcm-make/03-build-include-paths/include/world2/worldx.f90 b/t/fcm-make/03-build-include-paths/include/world2/worldx.f90
new file mode 100644
index 0000000..9c54a71
--- /dev/null
+++ b/t/fcm-make/03-build-include-paths/include/world2/worldx.f90
@@ -0,0 +1 @@
+character(*), parameter :: world1 = 'Moon'
diff --git a/t/fcm-make/03-build-include-paths/src/hello.f90 b/t/fcm-make/03-build-include-paths/src/hello.f90
new file mode 100644
index 0000000..ab9aaea
--- /dev/null
+++ b/t/fcm-make/03-build-include-paths/src/hello.f90
@@ -0,0 +1,4 @@
+program hello
+use world, only: get_world
+WRITE(*, '(A,A)') 'Hello ', trim(get_world())
+end program hello
diff --git a/t/fcm-make/03-build-include-paths/src/world.f90 b/t/fcm-make/03-build-include-paths/src/world.f90
new file mode 100644
index 0000000..8ba26cb
--- /dev/null
+++ b/t/fcm-make/03-build-include-paths/src/world.f90
@@ -0,0 +1,8 @@
+module world
+include 'worldx.f90'
+contains
+elemental function get_world() result(w)
+character(len=len(world1)) :: w
+w = world1
+end function get_world
+end module world
diff --git a/t/fcm-make/04-build-libs.t b/t/fcm-make/04-build-libs.t
new file mode 100755
index 0000000..fce71cb
--- /dev/null
+++ b/t/fcm-make/04-build-libs.t
@@ -0,0 +1,71 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+# Tests for "fcm make", "build.prop{fc.lib-paths}" and "build.prop{fc.libs}".
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+
+function get_linker_log() {
+ sed '/^\[info\] shell(0.*) gfortran/!d;
+ s/^\[info\] shell(0.*) //' .fcm-make/log
+}
+#-------------------------------------------------------------------------------
+tests 11
+set -e
+cp -r $TEST_SOURCE_DIR/$TEST_KEY_BASE/* .
+gfortran -c src-lib/*
+mkdir -p greet/lib
+ar rs greet/lib/libgreet.a greet.o 2>/dev/null
+ar rs greet/lib/libearth.a earth.o 2>/dev/null
+ar rs greet/lib/libmoon.a moon.o 2>/dev/null
+rm *.o
+set +e
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-control"
+run_fail "$TEST_KEY" fcm make
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE"
+FCM_TEST_FC_LIBS='greet earth' run_pass "$TEST_KEY" fcm make
+$PWD/build/bin/hello.exe >"$TEST_KEY.command.out"
+file_cmp "$TEST_KEY.command.out" "$TEST_KEY.command.out" <<<'Hello Earth'
+get_linker_log >"$TEST_KEY.gfortran.log"
+file_cmp "$TEST_KEY.gfortran.log" "$TEST_KEY.gfortran.log" <<__LOG__
+gfortran -obin/hello.exe o/hello.o -L$PWD/greet/lib -lgreet -learth
+__LOG__
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-incr0"
+find build -type f -exec stat -c'%Y %n' {} \; | sort >"$TEST_KEY.mtime.old"
+FCM_TEST_FC_LIBS='greet earth' run_pass "$TEST_KEY" fcm make
+find build -type f -exec stat -c'%Y %n' {} \; | sort >"$TEST_KEY.mtime"
+file_cmp "$TEST_KEY.mtime" "$TEST_KEY.mtime.old" "$TEST_KEY.mtime"
+$PWD/build/bin/hello.exe >"$TEST_KEY.command.out"
+file_cmp "$TEST_KEY.command.out" "$TEST_KEY.command.out" <<<'Hello Earth'
+get_linker_log >"$TEST_KEY.gfortran.log"
+file_cmp "$TEST_KEY.gfortran.log" "$TEST_KEY.gfortran.log" </dev/null
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-incr1"
+FCM_TEST_FC_LIBS='greet moon' run_pass "$TEST_KEY" fcm make
+$PWD/build/bin/hello.exe >"$TEST_KEY.command.out"
+file_cmp "$TEST_KEY.command.out" "$TEST_KEY.command.out" <<<'Hello Moon'
+get_linker_log >"$TEST_KEY.gfortran.log"
+file_cmp "$TEST_KEY.gfortran.log" "$TEST_KEY.gfortran.log" <<__LOG__
+gfortran -obin/hello.exe o/hello.o -L$PWD/greet/lib -lgreet -lmoon
+__LOG__
+#-------------------------------------------------------------------------------
+exit 0
diff --git a/t/fcm-make/04-build-libs/fcm-make.cfg b/t/fcm-make/04-build-libs/fcm-make.cfg
new file mode 100644
index 0000000..c41a586
--- /dev/null
+++ b/t/fcm-make/04-build-libs/fcm-make.cfg
@@ -0,0 +1,6 @@
+steps=build
+build.source=$HERE/src
+build.target{task}=link
+$FCM_TEST_FC_LIBS{?}=
+build.prop{fc.lib-paths}=$HERE/greet/lib
+build.prop{fc.libs}=$FCM_TEST_FC_LIBS
diff --git a/t/fcm-make/04-build-libs/src-lib/earth.f90 b/t/fcm-make/04-build-libs/src-lib/earth.f90
new file mode 100644
index 0000000..c0c050d
--- /dev/null
+++ b/t/fcm-make/04-build-libs/src-lib/earth.f90
@@ -0,0 +1,4 @@
+subroutine world(w)
+character(*), intent(out) :: w
+w = 'Earth'
+end subroutine world
diff --git a/t/fcm-make/04-build-libs/src-lib/greet.f90 b/t/fcm-make/04-build-libs/src-lib/greet.f90
new file mode 100644
index 0000000..1a52957
--- /dev/null
+++ b/t/fcm-make/04-build-libs/src-lib/greet.f90
@@ -0,0 +1,4 @@
+subroutine greet(hello, world)
+character(*), intent(in) :: hello, world
+write(*, '(A,1X,A)') trim(hello), trim(world)
+end subroutine greet
diff --git a/t/fcm-make/04-build-libs/src-lib/moon.f90 b/t/fcm-make/04-build-libs/src-lib/moon.f90
new file mode 100644
index 0000000..e5bacb9
--- /dev/null
+++ b/t/fcm-make/04-build-libs/src-lib/moon.f90
@@ -0,0 +1,4 @@
+subroutine world(w)
+character(*), intent(out) :: w
+w = 'Moon'
+end subroutine world
diff --git a/t/fcm-make/04-build-libs/src/hello.f90 b/t/fcm-make/04-build-libs/src/hello.f90
new file mode 100644
index 0000000..f6d31c7
--- /dev/null
+++ b/t/fcm-make/04-build-libs/src/hello.f90
@@ -0,0 +1,13 @@
+program hello
+character(5) :: w
+interface
+subroutine greet(hi, world)
+character(*), intent(in) :: hi, world
+end subroutine greet
+subroutine world(w)
+character(*), intent(out) :: w
+end subroutine world
+end interface
+call world(w)
+call greet('Hello', w)
+end program hello
diff --git a/t/fcm-make/05-build-c-cxx-basic.t b/t/fcm-make/05-build-c-cxx-basic.t
new file mode 100755
index 0000000..c3ec951
--- /dev/null
+++ b/t/fcm-make/05-build-c-cxx-basic.t
@@ -0,0 +1,84 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+# Basic tests for "fcm make" C and C++ source.
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+
+function get_compiler_log() {
+ sed '/^\[info\] shell(0.*) gcc\|g++/!d;
+ s/^\[info\] shell(0.*) //' .fcm-make/log
+}
+#-------------------------------------------------------------------------------
+tests 14
+cp -r $TEST_SOURCE_DIR/$TEST_KEY_BASE/* .
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE"
+run_pass "$TEST_KEY" fcm make
+find .fcm-make build -type f | sed 's/^\(\.fcm-make\/log\).*$/\1/' \
+ | sort >"$TEST_KEY.find"
+file_cmp "$TEST_KEY.find" "$TEST_KEY.find" <<'__OUT__'
+.fcm-make/config-as-parsed.cfg
+.fcm-make/config-on-success.cfg
+.fcm-make/ctx.gz
+.fcm-make/log
+build/bin/chello
+build/bin/cxxhello
+build/o/chello.o
+build/o/cxxhello.o
+__OUT__
+run_pass "$TEST_KEY.chello" $PWD/build/bin/chello
+file_cmp "$TEST_KEY.chello.out" "$TEST_KEY.chello.out" <<<'Hello C'
+run_pass "$TEST_KEY.cxxhello" $PWD/build/bin/cxxhello
+file_cmp "$TEST_KEY.cxxhello.out" "$TEST_KEY.cxxhello.out" <<<'Hello C++'
+get_compiler_log >"$TEST_KEY.compiler.log"
+file_cmp "$TEST_KEY.compiler.log" "$TEST_KEY.compiler.log" <<__LOG__
+gcc -oo/chello.o -c -I./include $PWD/src/chello.c
+gcc -obin/chello o/chello.o
+g++ -oo/cxxhello.o -c -I./include $PWD/src/cxxhello.cxx
+g++ -obin/cxxhello o/cxxhello.o
+__LOG__
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-incr-0"
+find build -type f -exec stat -c'%Y %n' {} \; | sort >"$TEST_KEY.mtime.old"
+run_pass "$TEST_KEY" fcm make
+find build -type f -exec stat -c'%Y %n' {} \; | sort >"$TEST_KEY.mtime"
+file_cmp "$TEST_KEY.mtime" "$TEST_KEY.mtime.old" "$TEST_KEY.mtime"
+get_compiler_log >"$TEST_KEY.compiler.log"
+file_cmp "$TEST_KEY.compiler.log" "$TEST_KEY.compiler.log" </dev/null
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-incr-1"
+export CCFLAGS=-O2
+run_pass "$TEST_KEY" fcm make
+get_compiler_log >"$TEST_KEY.compiler.log"
+file_cmp "$TEST_KEY.compiler.log" "$TEST_KEY.compiler.log" <<__LOG__
+gcc -oo/chello.o -c -I./include -O2 $PWD/src/chello.c
+gcc -obin/chello o/chello.o
+__LOG__
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-incr-2"
+export CXXFLAGS=-O3
+run_pass "$TEST_KEY" fcm make
+get_compiler_log >"$TEST_KEY.compiler.log"
+file_cmp "$TEST_KEY.compiler.log" "$TEST_KEY.compiler.log" <<__LOG__
+g++ -oo/cxxhello.o -c -I./include -O3 $PWD/src/cxxhello.cxx
+g++ -obin/cxxhello o/cxxhello.o
+__LOG__
+#-------------------------------------------------------------------------------
+exit 0
diff --git a/t/fcm-make/05-build-c-cxx-basic/fcm-make.cfg b/t/fcm-make/05-build-c-cxx-basic/fcm-make.cfg
new file mode 100644
index 0000000..88e5d80
--- /dev/null
+++ b/t/fcm-make/05-build-c-cxx-basic/fcm-make.cfg
@@ -0,0 +1,8 @@
+steps=build
+build.source=$HERE/src
+build.target{task}=link
+build.prop{file-ext.bin}=
+$CCFLAGS{?}=
+build.prop{cc.flags}=$CCFLAGS
+$CXXFLAGS{?}=
+build.prop{cxx.flags}=$CXXFLAGS
diff --git a/t/fcm-make/05-build-c-cxx-basic/src/chello.c b/t/fcm-make/05-build-c-cxx-basic/src/chello.c
new file mode 100644
index 0000000..4809c80
--- /dev/null
+++ b/t/fcm-make/05-build-c-cxx-basic/src/chello.c
@@ -0,0 +1,6 @@
+#include <stdio.h>
+
+int main() {
+ printf("Hello C\n");
+ return 0;
+}
diff --git a/t/fcm-make/05-build-c-cxx-basic/src/cxxhello.cxx b/t/fcm-make/05-build-c-cxx-basic/src/cxxhello.cxx
new file mode 100644
index 0000000..04aa598
--- /dev/null
+++ b/t/fcm-make/05-build-c-cxx-basic/src/cxxhello.cxx
@@ -0,0 +1,6 @@
+#include <iostream>
+
+int main() {
+ std::cout << "Hello C++\n";
+ return 0;
+}
diff --git a/t/fcm-make/06-extract-ssh.t b/t/fcm-make/06-extract-ssh.t
new file mode 100755
index 0000000..994aef1
--- /dev/null
+++ b/t/fcm-make/06-extract-ssh.t
@@ -0,0 +1,108 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+# Tests for "fcm make", "extract" from SSH location.
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+N_TESTS=6
+tests $N_TESTS
+#-------------------------------------------------------------------------------
+# Get a remote host for testing
+T_HOST=
+for FILE in $HOME/.metomi/fcm/t.cfg $FCM_HOME/etc/fcm/t.cfg; do
+ if [[ ! -f $FILE || ! -r $FILE ]]; then
+ continue
+ fi
+ T_HOST=$(fcm cfg $FILE | sed '/^ *host *=/!d; s/^ *host *= *//' | tail -1)
+ if [[ -n $T_HOST ]]; then
+ break
+ fi
+done
+if [[ -z $T_HOST ]]; then
+ skip $N_TESTS 'fcm/t.cfg: "host" not defined'
+ exit 0
+fi
+#-------------------------------------------------------------------------------
+# Create a source tree on the remote host
+mkdir -p hello/{greet,hello,hi,.secret}
+for NAME in mercury venus earth mars; do
+ echo "Greet $NAME" >hello/greet/greet_${NAME}.txt
+ echo "Hello $NAME" >hello/hello/hello_${NAME}.txt
+ echo "[Alien-speak] $NAME" >hello/.secret/hello_${NAME}.txt
+ echo "Hi $NAME" >hello/hi/hi_${NAME}.txt
+done
+T_HOST_WORK_DIR=$(ssh -oBatchMode=yes $T_HOST mktemp -d)
+rsync -a hello $T_HOST:$T_HOST_WORK_DIR
+rm -r hello
+#-------------------------------------------------------------------------------
+# Create a fcm-make.cfg
+cat >fcm-make.cfg <<__FCM_MAKE_CFG__
+steps=extract
+extract.ns=hello
+extract.location[hello]=$T_HOST:$T_HOST_WORK_DIR/hello
+__FCM_MAKE_CFG__
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE"
+run_pass "$TEST_KEY" fcm make
+grep -e '\[info\] location hello: 0' -e '\[info\] AU hello:0' fcm-make.log \
+ >"$TEST_KEY.log"
+file_cmp "$TEST_KEY.log" "$TEST_KEY.log" <<__LOG__
+[info] location hello: 0: $T_HOST:$T_HOST_WORK_DIR/hello
+[info] AU hello:0 hi/hi_mars.txt
+[info] AU hello:0 greet/greet_venus.txt
+[info] AU hello:0 hello/hello_mercury.txt
+[info] AU hello:0 hello/hello_venus.txt
+[info] AU hello:0 greet/greet_mars.txt
+[info] AU hello:0 hi/hi_mercury.txt
+[info] AU hello:0 greet/greet_earth.txt
+[info] AU hello:0 greet/greet_mercury.txt
+[info] AU hello:0 hi/hi_earth.txt
+[info] AU hello:0 hello/hello_earth.txt
+[info] AU hello:0 hello/hello_mars.txt
+[info] AU hello:0 hi/hi_venus.txt
+__LOG__
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-incr0"
+run_pass "$TEST_KEY" fcm make
+grep -e '\[info\] dest:' -e '\[info\] source:' fcm-make.log \
+ >"$TEST_KEY.log"
+file_cmp "$TEST_KEY.log" "$TEST_KEY.log" <<__LOG__
+[info] dest: 12 [U unchanged]
+[info] source: 12 [U from base]
+__LOG__
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-incr1"
+echo 'Hello Martians' \
+ | ssh -oBatchMode=yes $T_HOST "cat >$T_HOST_WORK_DIR/hello/hello/hello_mars.txt"
+run_pass "$TEST_KEY" fcm make
+grep \
+ -e '\[info\] dest:' \
+ -e '\[info\] source:' \
+ -e '\[info\] MU hello:0' \
+ fcm-make.log >"$TEST_KEY.log"
+file_cmp "$TEST_KEY.log" "$TEST_KEY.log" <<__LOG__
+[info] MU hello:0 hello/hello_mars.txt
+[info] dest: 1 [M modified]
+[info] dest: 11 [U unchanged]
+[info] source: 12 [U from base]
+__LOG__
+#-------------------------------------------------------------------------------
+ssh -oBatchMode=yes $T_HOST rm -r $T_HOST_WORK_DIR
+exit 0
diff --git a/t/fcm-make/07-build-ns-dep.t b/t/fcm-make/07-build-ns-dep.t
new file mode 100755
index 0000000..b938aa8
--- /dev/null
+++ b/t/fcm-make/07-build-ns-dep.t
@@ -0,0 +1,68 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+# Tests for "fcm make", "build.prop{ns-dep.o}".
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+tests 6
+set -e
+cp -r $TEST_SOURCE_DIR/$TEST_KEY_BASE/* .
+set +e
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-bad-1"
+run_fail "$TEST_KEY" fcm make
+tail -2 .fcm-make/log >"$TEST_KEY.log" 2>/dev/null
+file_cmp "$TEST_KEY.log" "$TEST_KEY.log" <<'__LOG__'
+[FAIL] foo: bad or missing dependency (type=ns-dep.o)
+[FAIL] required by: hello.exe
+__LOG__
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-bad-2"
+mkdir src/foo # An empty directory
+run_fail "$TEST_KEY" fcm make
+tail -2 .fcm-make/log >"$TEST_KEY.log" 2>/dev/null
+file_cmp "$TEST_KEY.log" "$TEST_KEY.log" <<'__LOG__'
+[FAIL] foo: bad or missing dependency (type=ns-dep.o)
+[FAIL] required by: hello.exe
+__LOG__
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE"
+FCM_TEST_NS_DEP_O=lib run_pass "$TEST_KEY" fcm make
+sed '/^\[info\] \(source->target\|target\) /!d' .fcm-make/log >"$TEST_KEY.log"
+file_cmp "$TEST_KEY.log" "$TEST_KEY.log" <<'__LOG__'
+[info] source->target / -> (archive) lib/ libo.a
+[info] source->target lib -> (archive) lib/ lib/libo.a
+[info] source->target lib/earth.f90 -> (install) include/ earth.f90
+[info] source->target lib/earth.f90 -> (ext-iface) include/ earth.interface
+[info] source->target lib/earth.f90 -> (compile) o/ world.o
+[info] source->target lib/greet.f90 -> (install) include/ greet.f90
+[info] source->target lib/greet.f90 -> (ext-iface) include/ greet.interface
+[info] source->target lib/greet.f90 -> (compile) o/ greet.o
+[info] source->target main -> (archive) lib/ main/libo.a
+[info] source->target main/hello.f90 -> (link) bin/ hello.exe
+[info] source->target main/hello.f90 -> (install) include/ hello.f90
+[info] source->target main/hello.f90 -> (compile) o/ hello.o
+[info] target hello.exe
+[info] target - hello.o
+[info] target - world.o
+[info] target - greet.o
+__LOG__
+#-------------------------------------------------------------------------------
+exit 0
diff --git a/t/fcm-make/07-build-ns-dep/fcm-make.cfg b/t/fcm-make/07-build-ns-dep/fcm-make.cfg
new file mode 100644
index 0000000..f631302
--- /dev/null
+++ b/t/fcm-make/07-build-ns-dep/fcm-make.cfg
@@ -0,0 +1,5 @@
+steps=build
+build.source=$HERE/src
+build.target{task}=link
+$FCM_TEST_NS_DEP_O{?}=foo
+build.prop{ns-dep.o}=$FCM_TEST_NS_DEP_O
diff --git a/t/fcm-make/07-build-ns-dep/src/lib/earth.f90 b/t/fcm-make/07-build-ns-dep/src/lib/earth.f90
new file mode 100644
index 0000000..c0c050d
--- /dev/null
+++ b/t/fcm-make/07-build-ns-dep/src/lib/earth.f90
@@ -0,0 +1,4 @@
+subroutine world(w)
+character(*), intent(out) :: w
+w = 'Earth'
+end subroutine world
diff --git a/t/fcm-make/07-build-ns-dep/src/lib/greet.f90 b/t/fcm-make/07-build-ns-dep/src/lib/greet.f90
new file mode 100644
index 0000000..1a52957
--- /dev/null
+++ b/t/fcm-make/07-build-ns-dep/src/lib/greet.f90
@@ -0,0 +1,4 @@
+subroutine greet(hello, world)
+character(*), intent(in) :: hello, world
+write(*, '(A,1X,A)') trim(hello), trim(world)
+end subroutine greet
diff --git a/t/fcm-make/07-build-ns-dep/src/main/hello.f90 b/t/fcm-make/07-build-ns-dep/src/main/hello.f90
new file mode 100644
index 0000000..c11c8c6
--- /dev/null
+++ b/t/fcm-make/07-build-ns-dep/src/main/hello.f90
@@ -0,0 +1,13 @@
+program hello
+character(5) :: w
+interface
+subroutine greet(hello, world)
+character(*), intent(in) :: hello, world
+end subroutine greet
+subroutine world(w)
+character(*), intent(out) :: w
+end subroutine world
+end interface
+call world(w)
+call greet('Hello', w)
+end program hello
diff --git a/t/fcm-make/08-build-dup-dep.t b/t/fcm-make/08-build-dup-dep.t
new file mode 100755
index 0000000..8c51581
--- /dev/null
+++ b/t/fcm-make/08-build-dup-dep.t
@@ -0,0 +1,60 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+# Tests for "fcm make", "build.prop{ns-dep.o}".
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+tests 4
+set -e
+cp -r $TEST_SOURCE_DIR/$TEST_KEY_BASE/* .
+set +e
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-bad"
+run_fail "$TEST_KEY" fcm make
+tail -2 .fcm-make/log >"$TEST_KEY.log" 2>/dev/null
+file_cmp "$TEST_KEY.log" "$TEST_KEY.log" <<'__LOG__'
+[FAIL] world.o: same target from [lib/earth.f90, lib/moon.f90]
+[FAIL] required by: hello.exe
+__LOG__
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE"
+rm src/lib/moon.f90
+run_pass "$TEST_KEY" fcm make
+sed '/^\[info\] \(source->target\|target\) /!d' .fcm-make/log >"$TEST_KEY.log"
+file_cmp "$TEST_KEY.log" "$TEST_KEY.log" <<'__LOG__'
+[info] source->target / -> (archive) lib/ libo.a
+[info] source->target lib -> (archive) lib/ lib/libo.a
+[info] source->target lib/earth.f90 -> (install) include/ earth.f90
+[info] source->target lib/earth.f90 -> (ext-iface) include/ earth.interface
+[info] source->target lib/earth.f90 -> (compile) o/ world.o
+[info] source->target lib/greet.f90 -> (install) include/ greet.f90
+[info] source->target lib/greet.f90 -> (ext-iface) include/ greet.interface
+[info] source->target lib/greet.f90 -> (compile) o/ greet.o
+[info] source->target main -> (archive) lib/ main/libo.a
+[info] source->target main/hello.f90 -> (link) bin/ hello.exe
+[info] source->target main/hello.f90 -> (install) include/ hello.f90
+[info] source->target main/hello.f90 -> (compile) o/ hello.o
+[info] target hello.exe
+[info] target - hello.o
+[info] target - world.o
+[info] target - greet.o
+__LOG__
+#-------------------------------------------------------------------------------
+exit 0
diff --git a/t/fcm-make/08-build-dup-dep/fcm-make.cfg b/t/fcm-make/08-build-dup-dep/fcm-make.cfg
new file mode 100644
index 0000000..1a9229e
--- /dev/null
+++ b/t/fcm-make/08-build-dup-dep/fcm-make.cfg
@@ -0,0 +1,4 @@
+steps=build
+build.source=$HERE/src
+build.target{task}=link
+build.prop{ns-dep.o}=lib
diff --git a/t/fcm-make/08-build-dup-dep/src/lib/earth.f90 b/t/fcm-make/08-build-dup-dep/src/lib/earth.f90
new file mode 100644
index 0000000..c0c050d
--- /dev/null
+++ b/t/fcm-make/08-build-dup-dep/src/lib/earth.f90
@@ -0,0 +1,4 @@
+subroutine world(w)
+character(*), intent(out) :: w
+w = 'Earth'
+end subroutine world
diff --git a/t/fcm-make/08-build-dup-dep/src/lib/greet.f90 b/t/fcm-make/08-build-dup-dep/src/lib/greet.f90
new file mode 100644
index 0000000..1a52957
--- /dev/null
+++ b/t/fcm-make/08-build-dup-dep/src/lib/greet.f90
@@ -0,0 +1,4 @@
+subroutine greet(hello, world)
+character(*), intent(in) :: hello, world
+write(*, '(A,1X,A)') trim(hello), trim(world)
+end subroutine greet
diff --git a/t/fcm-make/08-build-dup-dep/src/lib/moon.f90 b/t/fcm-make/08-build-dup-dep/src/lib/moon.f90
new file mode 100644
index 0000000..e5bacb9
--- /dev/null
+++ b/t/fcm-make/08-build-dup-dep/src/lib/moon.f90
@@ -0,0 +1,4 @@
+subroutine world(w)
+character(*), intent(out) :: w
+w = 'Moon'
+end subroutine world
diff --git a/t/fcm-make/08-build-dup-dep/src/main/hello.f90 b/t/fcm-make/08-build-dup-dep/src/main/hello.f90
new file mode 100644
index 0000000..c11c8c6
--- /dev/null
+++ b/t/fcm-make/08-build-dup-dep/src/main/hello.f90
@@ -0,0 +1,13 @@
+program hello
+character(5) :: w
+interface
+subroutine greet(hello, world)
+character(*), intent(in) :: hello, world
+end subroutine greet
+subroutine world(w)
+character(*), intent(out) :: w
+end subroutine world
+end interface
+call world(w)
+call greet('Hello', w)
+end program hello
diff --git a/t/fcm-make/09-build-dep-o.t b/t/fcm-make/09-build-dep-o.t
new file mode 100755
index 0000000..39b505d
--- /dev/null
+++ b/t/fcm-make/09-build-dep-o.t
@@ -0,0 +1,64 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+# Tests for "fcm make", "build.prop{dep.o}" top namespace.
+# (Cyclic dependency bug in 2013-09.)
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+tests 2
+cp -r $TEST_SOURCE_DIR/$TEST_KEY_BASE/* .
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE"
+run_pass "$TEST_KEY" fcm make
+sed '/^\[info\] \(source->target\|target\) /!d' .fcm-make/log >"$TEST_KEY.log"
+file_cmp "$TEST_KEY.log" "$TEST_KEY.log" <<'__LOG__'
+[info] source->target / -> (archive) lib/ libo.a
+[info] source->target lib -> (archive) lib/ lib/libo.a
+[info] source->target lib/earth.f90 -> (install) include/ earth.f90
+[info] source->target lib/earth.f90 -> (ext-iface) include/ earth.interface
+[info] source->target lib/earth.f90 -> (compile) o/ world.o
+[info] source->target lib/greet.f90 -> (install) include/ greet.f90
+[info] source->target lib/greet.f90 -> (ext-iface) include/ greet.interface
+[info] source->target lib/greet.f90 -> (compile) o/ greet.o
+[info] source->target lib/greet_fmt_mod.f90 -> (install) include/ greet_fmt_mod.f90
+[info] source->target lib/greet_fmt_mod.f90 -> (compile+) include/ greet_fmt_mod.mod
+[info] source->target lib/greet_fmt_mod.f90 -> (compile) o/ greet_fmt_mod.o
+[info] source->target main -> (archive) lib/ main/libo.a
+[info] source->target main/hello.f90 -> (link) bin/ hello.exe
+[info] source->target main/hello.f90 -> (install) include/ hello.f90
+[info] source->target main/hello.f90 -> (compile) o/ hello.o
+[info] source->target main/hi.f90 -> (link) bin/ hi.exe
+[info] source->target main/hi.f90 -> (install) include/ hi.f90
+[info] source->target main/hi.f90 -> (compile) o/ hi.o
+[info] target hi.exe
+[info] target - hi.o
+[info] target - world.o
+[info] target - greet.o
+[info] target - - greet_fmt_mod.mod
+[info] target - - - greet_fmt_mod.o
+[info] target - greet_fmt_mod.o
+[info] target hello.exe
+[info] target - hello.o
+[info] target - world.o
+[info] target - greet.o (n-deps=1)
+[info] target - greet_fmt_mod.o
+__LOG__
+#-------------------------------------------------------------------------------
+exit 0
diff --git a/t/fcm-make/09-build-dep-o/fcm-make.cfg b/t/fcm-make/09-build-dep-o/fcm-make.cfg
new file mode 100644
index 0000000..63daa09
--- /dev/null
+++ b/t/fcm-make/09-build-dep-o/fcm-make.cfg
@@ -0,0 +1,4 @@
+steps=build
+build.source=$HERE/src
+build.target{task}=link
+build.prop{dep.o}=world.o greet.o
diff --git a/t/fcm-make/09-build-dep-o/src/lib/earth.f90 b/t/fcm-make/09-build-dep-o/src/lib/earth.f90
new file mode 100644
index 0000000..c0c050d
--- /dev/null
+++ b/t/fcm-make/09-build-dep-o/src/lib/earth.f90
@@ -0,0 +1,4 @@
+subroutine world(w)
+character(*), intent(out) :: w
+w = 'Earth'
+end subroutine world
diff --git a/t/fcm-make/09-build-dep-o/src/lib/greet.f90 b/t/fcm-make/09-build-dep-o/src/lib/greet.f90
new file mode 100644
index 0000000..ddeea6f
--- /dev/null
+++ b/t/fcm-make/09-build-dep-o/src/lib/greet.f90
@@ -0,0 +1,5 @@
+subroutine greet(hello, world)
+use greet_fmt_mod, only: greet_fmt
+character(*), intent(in) :: hello, world
+write(*, greet_fmt) trim(hello), trim(world)
+end subroutine greet
diff --git a/t/fcm-make/09-build-dep-o/src/lib/greet_fmt_mod.f90 b/t/fcm-make/09-build-dep-o/src/lib/greet_fmt_mod.f90
new file mode 100644
index 0000000..16f11cc
--- /dev/null
+++ b/t/fcm-make/09-build-dep-o/src/lib/greet_fmt_mod.f90
@@ -0,0 +1,3 @@
+MODULE greet_fmt_mod
+CHARACTER(*), PARAMETER :: greet_fmt='(A,1X,A)'
+END MODULE greet_fmt_mod
diff --git a/t/fcm-make/09-build-dep-o/src/main/hello.f90 b/t/fcm-make/09-build-dep-o/src/main/hello.f90
new file mode 100644
index 0000000..f6d31c7
--- /dev/null
+++ b/t/fcm-make/09-build-dep-o/src/main/hello.f90
@@ -0,0 +1,13 @@
+program hello
+character(5) :: w
+interface
+subroutine greet(hi, world)
+character(*), intent(in) :: hi, world
+end subroutine greet
+subroutine world(w)
+character(*), intent(out) :: w
+end subroutine world
+end interface
+call world(w)
+call greet('Hello', w)
+end program hello
diff --git a/t/fcm-make/09-build-dep-o/src/main/hi.f90 b/t/fcm-make/09-build-dep-o/src/main/hi.f90
new file mode 100644
index 0000000..0e30aa4
--- /dev/null
+++ b/t/fcm-make/09-build-dep-o/src/main/hi.f90
@@ -0,0 +1,13 @@
+program hi
+character(5) :: w
+interface
+subroutine greet(hello, world)
+character(*), intent(in) :: hello, world
+end subroutine greet
+subroutine world(w)
+character(*), intent(out) :: w
+end subroutine world
+end interface
+call world(w)
+call greet('Hello', w)
+end program hi
diff --git a/t/fcm-make/10-log b/t/fcm-make/10-log
new file mode 120000
index 0000000..8bddc38
--- /dev/null
+++ b/t/fcm-make/10-log
@@ -0,0 +1 @@
+00-build-basic
\ No newline at end of file
diff --git a/t/fcm-make/10-log.t b/t/fcm-make/10-log.t
new file mode 100755
index 0000000..cc03bda
--- /dev/null
+++ b/t/fcm-make/10-log.t
@@ -0,0 +1,54 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+# Basic tests for "fcm make".
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+tests 7
+cp -r $TEST_SOURCE_DIR/$TEST_KEY_BASE/* .
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE"
+run_pass "$TEST_KEY" fcm make
+if [[ -d $FCM_HOME/.git ]]; then
+ VERSION="FCM $(git --git-dir=$FCM_HOME/.git describe)"
+else
+ VERSION=$(sed '/FCM\.VERSION/!d; s/^.*="\(.*\)";$/\1/' \
+ $FCM_HOME/doc/etc/fcm-version.js)
+ VERSION="FCM $VERSION"
+fi
+file_grep "$TEST_KEY.log.version" "\\[info\\] $VERSION" .fcm-make/log
+file_grep "$TEST_KEY.log.mode" '\[info\] mode=new' .fcm-make/log
+if [[ $(ls .fcm-make/log-* | wc -l) == 1 ]]; then
+ pass "$TEST_KEY-n-logs"
+else
+ fail "$TEST_KEY-n-logs"
+fi
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-incr"
+sleep 1
+run_pass "$TEST_KEY" fcm make
+file_grep "$TEST_KEY.log.mode" '\[info\] mode=incremental' .fcm-make/log
+if [[ $(ls .fcm-make/log-* | wc -l) == 2 ]]; then
+ pass "$TEST_KEY-n-logs"
+else
+ fail "$TEST_KEY-n-logs"
+fi
+#-------------------------------------------------------------------------------
+exit 0
diff --git a/t/fcm-make/11-preprocess-include-path.t b/t/fcm-make/11-preprocess-include-path.t
new file mode 100755
index 0000000..b3679f0
--- /dev/null
+++ b/t/fcm-make/11-preprocess-include-path.t
@@ -0,0 +1,59 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+# Tests for "fcm make", "preprocess.prop{fpp.include-paths}".
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+
+function get_cpp_log() {
+ sed '/^\[info\] shell(0.*) cpp/!d; s/^\[info\] shell(0.*) //' .fcm-make/log
+}
+#-------------------------------------------------------------------------------
+tests 11
+cp -r $TEST_SOURCE_DIR/$TEST_KEY_BASE/* .
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-control"
+run_fail "$TEST_KEY" fcm make
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE"
+FCM_TEST_FPP_INCLUDE_PATHS="$PWD/include/world1" run_pass "$TEST_KEY" fcm make
+file_grep "$TEST_KEY.world.F90" "world1 = 'Earth'" $PWD/preprocess/src/world.F90
+get_cpp_log >"$TEST_KEY.cpp.log"
+file_cmp "$TEST_KEY.cpp.log" "$TEST_KEY.cpp.log" <<__LOG__
+cpp -P -traditional -I./include -I$PWD/include/world1 $PWD/src/world.F90
+__LOG__
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-incr0"
+find preprocess -type f -exec stat -c'%Y %n' {} \; | sort >"$TEST_KEY.mtime.old"
+FCM_TEST_FPP_INCLUDE_PATHS="$PWD/include/world1" run_pass "$TEST_KEY" fcm make
+find preprocess -type f -exec stat -c'%Y %n' {} \; | sort >"$TEST_KEY.mtime"
+file_cmp "$TEST_KEY.mtime" "$TEST_KEY.mtime.old" "$TEST_KEY.mtime"
+file_grep "$TEST_KEY.world.F90" "world1 = 'Earth'" $PWD/preprocess/src/world.F90
+get_cpp_log >"$TEST_KEY.cpp.log"
+file_cmp "$TEST_KEY.cpp.log" "$TEST_KEY.cpp.log" </dev/null
+##-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-incr1"
+FCM_TEST_FPP_INCLUDE_PATHS="$PWD/include/world2" run_pass "$TEST_KEY" fcm make
+file_grep "$TEST_KEY.world.F90" "world1 = 'Moon'" $PWD/preprocess/src/world.F90
+get_cpp_log >"$TEST_KEY.cpp.log"
+file_cmp "$TEST_KEY.cpp.log" "$TEST_KEY.cpp.log" <<__LOG__
+cpp -P -traditional -I./include -I$PWD/include/world2 $PWD/src/world.F90
+__LOG__
+#-------------------------------------------------------------------------------
+exit 0
diff --git a/t/fcm-make/11-preprocess-include-path/fcm-make.cfg b/t/fcm-make/11-preprocess-include-path/fcm-make.cfg
new file mode 100644
index 0000000..ed71513
--- /dev/null
+++ b/t/fcm-make/11-preprocess-include-path/fcm-make.cfg
@@ -0,0 +1,4 @@
+steps=preprocess
+preprocess.source=$HERE/src
+$FCM_TEST_FPP_INCLUDE_PATHS{?}=
+preprocess.prop{fpp.include-paths}=$FCM_TEST_FPP_INCLUDE_PATHS
diff --git a/t/fcm-make/11-preprocess-include-path/include/world1/worldx.h b/t/fcm-make/11-preprocess-include-path/include/world1/worldx.h
new file mode 100644
index 0000000..9daa8f8
--- /dev/null
+++ b/t/fcm-make/11-preprocess-include-path/include/world1/worldx.h
@@ -0,0 +1 @@
+character(*), parameter :: world1 = 'Earth'
diff --git a/t/fcm-make/11-preprocess-include-path/include/world2/worldx.h b/t/fcm-make/11-preprocess-include-path/include/world2/worldx.h
new file mode 100644
index 0000000..9c54a71
--- /dev/null
+++ b/t/fcm-make/11-preprocess-include-path/include/world2/worldx.h
@@ -0,0 +1 @@
+character(*), parameter :: world1 = 'Moon'
diff --git a/t/fcm-make/11-preprocess-include-path/src/world.F90 b/t/fcm-make/11-preprocess-include-path/src/world.F90
new file mode 100644
index 0000000..b13078e
--- /dev/null
+++ b/t/fcm-make/11-preprocess-include-path/src/world.F90
@@ -0,0 +1,8 @@
+module world
+#include <worldx.h>
+contains
+elemental function get_world() result(w)
+character(len=len(world1)) :: w
+w = world1
+end function get_world
+end module world
diff --git a/t/fcm-make/12-build-class-prop.t b/t/fcm-make/12-build-class-prop.t
new file mode 100755
index 0000000..0436c69
--- /dev/null
+++ b/t/fcm-make/12-build-class-prop.t
@@ -0,0 +1,68 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+# Tests for "fcm make", *.prop{class,*}.
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+tests 6
+cp -r $TEST_SOURCE_DIR/$TEST_KEY_BASE/* .
+PATH=$PWD/bin:$PATH
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE"
+run_pass "$TEST_KEY" fcm make
+find build* -type f | sort >"$TEST_KEY.find"
+file_cmp "$TEST_KEY.find" "$TEST_KEY.find" <<'__FIND__'
+build/bin/hello.bin
+build/o/hello.o
+build_house/bin/hello_house
+build_house/o/hello_house.o
+build_office/bin/hello_office
+build_office/o/hello_office.o
+build_road/bin/hello_road
+build_road/o/hello_road.o
+__FIND__
+sed '/^\[info\] shell(0.*) \(my-fc\|gfortran\)/!d; s/^\[info\] shell(0.*) //' \
+ .fcm-make/log >"$TEST_KEY.log"
+file_cmp "$TEST_KEY.log" "$TEST_KEY.log" <<__LOG__
+my-fc -oo/hello.o -c -I./include $PWD/src/hello.f90
+my-fc -obin/hello.bin o/hello.o
+my-fc -oo/hello_house.o -c -I./include $PWD/src/hello_house.f90
+my-fc -obin/hello_house o/hello_house.o
+my-fc -oo/hello_office.o -c -I./include $PWD/src/hello_office.f90
+my-fc -obin/hello_office o/hello_office.o
+gfortran -oo/hello_road.o -c -I./include $PWD/src/hello_road.f90
+gfortran -obin/hello_road o/hello_road.o
+__LOG__
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-incr"
+find build* -type f -exec stat -c'%Y %n' {} \; | sort >"$TEST_KEY.mtime.old"
+run_pass "$TEST_KEY" fcm make
+find build* -type f -exec stat -c'%Y %n' {} \; | sort >"$TEST_KEY.mtime"
+file_cmp "$TEST_KEY.mtime" "$TEST_KEY.mtime.old" "$TEST_KEY.mtime"
+sed '/^\[info\] \(compile\|link\) targets:/!d; s/total-time=.*$//' \
+ .fcm-make/log >"$TEST_KEY.log"
+file_cmp "$TEST_KEY.log" "$TEST_KEY.log" <<'__LOG__'
+[info] compile targets: modified=0, unchanged=1, failed=0,
+[info] compile targets: modified=0, unchanged=1, failed=0,
+[info] compile targets: modified=0, unchanged=1, failed=0,
+[info] compile targets: modified=0, unchanged=1, failed=0,
+__LOG__
+#-------------------------------------------------------------------------------
+exit 0
diff --git a/t/fcm-make/12-build-class-prop/bin/my-fc b/t/fcm-make/12-build-class-prop/bin/my-fc
new file mode 100755
index 0000000..d3762bc
--- /dev/null
+++ b/t/fcm-make/12-build-class-prop/bin/my-fc
@@ -0,0 +1,2 @@
+#!/bin/bash
+exec gfortran "$@"
diff --git a/t/fcm-make/12-build-class-prop/fcm-make.cfg b/t/fcm-make/12-build-class-prop/fcm-make.cfg
new file mode 100644
index 0000000..ab8fd1b
--- /dev/null
+++ b/t/fcm-make/12-build-class-prop/fcm-make.cfg
@@ -0,0 +1,14 @@
+step.class[build_house build_office build_road]=build
+steps=build build_house build_office build_road
+build.prop{class,fc}=my-fc
+build.prop{class,file-ext.bin}=
+build.source=$HERE/src
+build.target=hello.bin
+build.prop{file-ext.bin}=.bin
+build_house.source=$HERE/src
+build_house.target=hello_house
+build_office.source=$HERE/src
+build_office.target=hello_office
+build_road.source=$HERE/src
+build_road.target=hello_road
+build_road.prop{fc}=gfortran
diff --git a/t/fcm-make/12-build-class-prop/src/hello.f90 b/t/fcm-make/12-build-class-prop/src/hello.f90
new file mode 100644
index 0000000..f66011a
--- /dev/null
+++ b/t/fcm-make/12-build-class-prop/src/hello.f90
@@ -0,0 +1,3 @@
+program hello
+WRITE(*, '(A,1X,A)') 'Hello', 'World'
+end program hello
diff --git a/t/fcm-make/12-build-class-prop/src/hello_house.f90 b/t/fcm-make/12-build-class-prop/src/hello_house.f90
new file mode 100644
index 0000000..41b2365
--- /dev/null
+++ b/t/fcm-make/12-build-class-prop/src/hello_house.f90
@@ -0,0 +1,3 @@
+program hello_house
+WRITE(*, '(A,1X,A)') 'Hello', 'House'
+end program hello_house
diff --git a/t/fcm-make/12-build-class-prop/src/hello_office.f90 b/t/fcm-make/12-build-class-prop/src/hello_office.f90
new file mode 100644
index 0000000..abc33eb
--- /dev/null
+++ b/t/fcm-make/12-build-class-prop/src/hello_office.f90
@@ -0,0 +1,3 @@
+program hello_office
+WRITE(*, '(A,1X,A)') 'Hello', 'Office'
+end program hello_office
diff --git a/t/fcm-make/12-build-class-prop/src/hello_road.f90 b/t/fcm-make/12-build-class-prop/src/hello_road.f90
new file mode 100644
index 0000000..73bbff0
--- /dev/null
+++ b/t/fcm-make/12-build-class-prop/src/hello_road.f90
@@ -0,0 +1,3 @@
+program hello_road
+WRITE(*, '(A,1X,A)') 'Hello', 'Road'
+end program hello_road
diff --git a/t/fcm-make/13-build-target-prop.t b/t/fcm-make/13-build-target-prop.t
new file mode 100755
index 0000000..3af64be
--- /dev/null
+++ b/t/fcm-make/13-build-target-prop.t
@@ -0,0 +1,53 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+# Test "fcm make", build.prop{*}[target].
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+tests 5
+cp -r $TEST_SOURCE_DIR/$TEST_KEY_BASE/* .
+PATH=$PWD/bin:$PATH
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE"
+run_pass "$TEST_KEY" fcm make
+find build -type f | sort >"$TEST_KEY.find"
+file_cmp "$TEST_KEY.find" "$TEST_KEY.find" <<'__OUT__'
+build/bin/hello.exe
+build/include/world.mod
+build/o/hello.o
+build/o/world.o
+__OUT__
+sed '
+ /\[info\] shell(0.*) \(gfortran\|my-fc\)/!d;
+ /hello\.exe/d;
+ s/^\[info\] shell(0.*) //
+' .fcm-make/log >"$TEST_KEY.log"
+file_cmp "$TEST_KEY.log" "$TEST_KEY.log" <<__LOG__
+my-fc -oo/world.o -c -I./include $PWD/src/world.f90
+gfortran -oo/hello.o -c -I./include $PWD/src/hello.f90
+__LOG__
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-incr"
+find build -type f -exec stat -c'%Y %n' {} \; | sort >"$TEST_KEY.mtime.old"
+run_pass "$TEST_KEY" fcm make
+find build -type f -exec stat -c'%Y %n' {} \; | sort >"$TEST_KEY.mtime"
+file_cmp "$TEST_KEY.mtime" "$TEST_KEY.mtime.old" "$TEST_KEY.mtime"
+#-------------------------------------------------------------------------------
+exit 0
diff --git a/t/fcm-make/13-build-target-prop/bin/my-fc b/t/fcm-make/13-build-target-prop/bin/my-fc
new file mode 100755
index 0000000..a5afb0e
--- /dev/null
+++ b/t/fcm-make/13-build-target-prop/bin/my-fc
@@ -0,0 +1,20 @@
+#!/usr/bin/env bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+exec gfortran "$@"
diff --git a/t/fcm-make/13-build-target-prop/fcm-make.cfg b/t/fcm-make/13-build-target-prop/fcm-make.cfg
new file mode 100644
index 0000000..3c7731c
--- /dev/null
+++ b/t/fcm-make/13-build-target-prop/fcm-make.cfg
@@ -0,0 +1,4 @@
+steps = build
+build.source = $HERE/src
+build.target{task} = link
+build.prop{fc}[world.o] = my-fc
diff --git a/t/fcm-make/13-build-target-prop/src/hello.f90 b/t/fcm-make/13-build-target-prop/src/hello.f90
new file mode 100644
index 0000000..ab9aaea
--- /dev/null
+++ b/t/fcm-make/13-build-target-prop/src/hello.f90
@@ -0,0 +1,4 @@
+program hello
+use world, only: get_world
+WRITE(*, '(A,A)') 'Hello ', trim(get_world())
+end program hello
diff --git a/t/fcm-make/13-build-target-prop/src/world.f90 b/t/fcm-make/13-build-target-prop/src/world.f90
new file mode 100644
index 0000000..7c08240
--- /dev/null
+++ b/t/fcm-make/13-build-target-prop/src/world.f90
@@ -0,0 +1,8 @@
+module world
+character(*), parameter :: world1 = 'Earth'
+contains
+elemental function get_world() result(w)
+character(len=len(world1)) :: w
+w = world1
+end function get_world
+end module world
diff --git a/t/fcm-make/14-build-etc.t b/t/fcm-make/14-build-etc.t
new file mode 100755
index 0000000..a1bfd07
--- /dev/null
+++ b/t/fcm-make/14-build-etc.t
@@ -0,0 +1,60 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+# Test "fcm make", build etc files, broken at 2013-11 due to:
+# build.prop{class,file-she.script} = #!
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+tests 5
+cp -r $TEST_SOURCE_DIR/$TEST_KEY_BASE/* .
+PATH=$PWD/bin:$PATH
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE"
+run_pass "$TEST_KEY" fcm make
+find build -type f | sort >"$TEST_KEY.find"
+file_cmp "$TEST_KEY.find" "$TEST_KEY.find" <<'__OUT__'
+build/bin/foo
+build/etc/.etc
+build/etc/hello.txt
+build/etc/hi/.etc
+build/etc/hi/hi-earth.txt
+build/etc/hi/hi-mars.txt
+__OUT__
+sed '
+ /\[info\] install/!d;
+ /\[info\] install *targets:/d;
+ s/^\[info\] install *[^ ]* M //
+' .fcm-make/log >"$TEST_KEY.log"
+file_cmp "$TEST_KEY.log" "$TEST_KEY.log" <<'__LOG__'
+foo <- foo
+hello.txt <- hello.txt
+hi/hi-earth.txt <- hi/hi-earth.txt
+hi/hi-mars.txt <- hi/hi-mars.txt
+.etc <-
+hi/.etc <- hi
+__LOG__
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-incr"
+find build -type f -exec stat -c'%Y %n' {} \; | sort >"$TEST_KEY.mtime.old"
+run_pass "$TEST_KEY" fcm make
+find build -type f -exec stat -c'%Y %n' {} \; | sort >"$TEST_KEY.mtime"
+file_cmp "$TEST_KEY.mtime" "$TEST_KEY.mtime.old" "$TEST_KEY.mtime"
+#-------------------------------------------------------------------------------
+exit 0
diff --git a/t/fcm-make/14-build-etc/fcm-make.cfg b/t/fcm-make/14-build-etc/fcm-make.cfg
new file mode 100644
index 0000000..5b5828a
--- /dev/null
+++ b/t/fcm-make/14-build-etc/fcm-make.cfg
@@ -0,0 +1,4 @@
+steps=build
+build.source=$HERE/src
+build.target{task}=install
+build.prop{class,file-she.script} = #!
diff --git a/t/fcm-make/14-build-etc/src/foo b/t/fcm-make/14-build-etc/src/foo
new file mode 100755
index 0000000..dd58ffb
--- /dev/null
+++ b/t/fcm-make/14-build-etc/src/foo
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+echo FOO
diff --git a/t/fcm-make/14-build-etc/src/hello.txt b/t/fcm-make/14-build-etc/src/hello.txt
new file mode 100644
index 0000000..557db03
--- /dev/null
+++ b/t/fcm-make/14-build-etc/src/hello.txt
@@ -0,0 +1 @@
+Hello World
diff --git a/t/fcm-make/14-build-etc/src/hi/hi-earth.txt b/t/fcm-make/14-build-etc/src/hi/hi-earth.txt
new file mode 100644
index 0000000..e6e0932
--- /dev/null
+++ b/t/fcm-make/14-build-etc/src/hi/hi-earth.txt
@@ -0,0 +1 @@
+Hi Earth
diff --git a/t/fcm-make/14-build-etc/src/hi/hi-mars.txt b/t/fcm-make/14-build-etc/src/hi/hi-mars.txt
new file mode 100644
index 0000000..ec2891b
--- /dev/null
+++ b/t/fcm-make/14-build-etc/src/hi/hi-mars.txt
@@ -0,0 +1 @@
+Hi Mars
diff --git a/t/fcm-make/15-extract-loc-reset.t b/t/fcm-make/15-extract-loc-reset.t
new file mode 100755
index 0000000..f124dc2
--- /dev/null
+++ b/t/fcm-make/15-extract-loc-reset.t
@@ -0,0 +1,182 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+# Tests for "fcm make", "extract", location reset and base eq diff cases.
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+N_TESTS=34
+tests $N_TESTS
+#-------------------------------------------------------------------------------
+svnadmin create repos
+T_REPOS=file://$PWD/repos
+mkdir t
+echo "Hello World" >t/hello.txt
+echo "Hi World" >t/hi.txt
+svn import -q -m'Test' t/hello.txt $T_REPOS/trunk/hello.txt
+svn import -q -m'Test' t $T_REPOS/branch
+rm t/hello.txt t/hi.txt
+rmdir t
+#-------------------------------------------------------------------------------
+cat >fcm-make.cfg.0 <<__FCM_MAKE_CFG__
+steps=extract
+extract.ns=t
+extract.location{primary}[t]=$T_REPOS
+__FCM_MAKE_CFG__
+#-------------------------------------------------------------------------------
+base_tests() {
+ run_pass "$TEST_KEY" fcm make --new
+ find extract -type f >"$TEST_KEY.find"
+ file_cmp "$TEST_KEY.find" "$TEST_KEY.find" <<<'extract/t/hello.txt'
+ sed '/^\[info\] location t: /!d; s/^\[info\] location t: //' \
+ .fcm-make/log > "$TEST_KEY.log"
+ file_cmp "$TEST_KEY.log" "$TEST_KEY.log" <<__LOG__
+0: $T_REPOS/trunk at 2 (1)
+__LOG__
+}
+#-------------------------------------------------------------------------------
+diff_tests() {
+ run_pass "$TEST_KEY" fcm make --new
+ find extract -type f | sort >"$TEST_KEY.find"
+ file_cmp "$TEST_KEY.find" "$TEST_KEY.find" <<'__FIND__'
+extract/t/hello.txt
+extract/t/hi.txt
+__FIND__
+ sed '/^\[info\] location t: /!d; s/^\[info\] location t: //' \
+ .fcm-make/log > "$TEST_KEY.log"
+file_cmp "$TEST_KEY.log" "$TEST_KEY.log" <<__LOG__
+0: $T_REPOS/trunk at 2 (1)
+1: $T_REPOS/branch at 2 (2)
+__LOG__
+}
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-primary"
+{
+ cat fcm-make.cfg.0
+ echo "extract.location{primary}[t]="
+ echo "extract.location[t]=$T_REPOS/trunk"
+} >fcm-make.cfg
+base_tests
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-base-0"
+{
+ cat fcm-make.cfg.0
+ echo "extract.location[t]="
+} >fcm-make.cfg
+base_tests
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-base-0-with-eq-diff"
+{
+ cat fcm-make.cfg.0
+ echo "extract.location{diff}[t]=$T_REPOS/trunk"
+} >fcm-make.cfg
+base_tests
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-base"
+{
+ cat fcm-make.cfg.0
+ echo "extract.location[t]=$T_REPOS/trunk"
+ echo "extract.location[t]="
+} >fcm-make.cfg
+base_tests
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-base-with-eq-diff"
+{
+ cat fcm-make.cfg.0
+ echo "extract.location[t]=$T_REPOS/trunk"
+ echo "extract.location{diff}[t]=$T_REPOS/trunk"
+} >fcm-make.cfg
+base_tests
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-base-with-diff"
+{
+ cat fcm-make.cfg.0
+ echo "extract.location[t]=$T_REPOS/trunk"
+ echo "extract.location{diff}[t]=$T_REPOS/branch"
+ echo "extract.location[t]="
+} >fcm-make.cfg
+diff_tests
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-base-with-1-eq-diff"
+{
+ cat fcm-make.cfg.0
+ echo "extract.location[t]=$T_REPOS/trunk"
+ echo "extract.location{diff}[t]=$T_REPOS/trunk $T_REPOS/branch"
+ echo "extract.location[t]="
+} >fcm-make.cfg
+diff_tests
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-diff-0"
+{
+ cat fcm-make.cfg.0
+ echo "extract.location{diff}[t]="
+} >fcm-make.cfg
+base_tests
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-diff"
+{
+ cat fcm-make.cfg.0
+ echo "extract.location{diff}[t]=$T_REPOS/branch"
+ echo "extract.location{diff}[t]="
+} >fcm-make.cfg
+base_tests
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-diff-with-base"
+{
+ cat fcm-make.cfg.0
+ echo "extract.location{diff}[t]=$T_REPOS/branch"
+ echo "extract.location[t]=$T_REPOS/trunk"
+ echo "extract.location{diff}[t]="
+} >fcm-make.cfg
+base_tests
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-inherit-base-eq-my-diff"
+mkdir -p i0 i1
+cat fcm-make.cfg.0 >i0/fcm-make.cfg
+fcm make --new -q -C i0
+{
+ echo 'use=$HERE/../i0'
+ echo "extract.location{diff}[t]=$T_REPOS/trunk"
+} >i1/fcm-make.cfg
+run_pass "$TEST_KEY" fcm make --new -C i1
+sed '/^\[info\] location t: /!d; s/^\[info\] location t: //' \
+ i1/.fcm-make/log > "$TEST_KEY.log"
+file_cmp "$TEST_KEY.log" "$TEST_KEY.log" <<__LOG__
+0: $T_REPOS/trunk at 2 (1)
+__LOG__
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-inherit-base-eq-1-of-my-diff"
+# N.B. The 3 lines below have been done in the test above.
+# Uncomment them if the above test is removed.
+#mkdir -p i0 i1
+#cat fcm-make.cfg.0 >i0/fcm-make.cfg
+#fcm make --new -q -C i0
+{
+ echo 'use=$HERE/../i0'
+ echo "extract.location{diff}[t]=$T_REPOS/branch $T_REPOS/trunk"
+} >i1/fcm-make.cfg
+run_pass "$TEST_KEY" fcm make --new -C i1
+sed '/^\[info\] location t: /!d; s/^\[info\] location t: //' \
+ i1/.fcm-make/log > "$TEST_KEY.log"
+file_cmp "$TEST_KEY.log" "$TEST_KEY.log" <<__LOG__
+0: $T_REPOS/trunk at 2 (1)
+1: $T_REPOS/branch at 2 (2)
+__LOG__
+#-------------------------------------------------------------------------------
+exit 0
diff --git a/t/fcm-make/16-build-dep-o-2.t b/t/fcm-make/16-build-dep-o-2.t
new file mode 100755
index 0000000..3622669
--- /dev/null
+++ b/t/fcm-make/16-build-dep-o-2.t
@@ -0,0 +1,50 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+# Tests "fcm make", "build.prop{dep.o}" top namespace, complicated by a module.
+# See also "09-build.dep-o.t".
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+tests 2
+cp -r $TEST_SOURCE_DIR/$TEST_KEY_BASE/* .
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE"
+run_pass "$TEST_KEY" fcm make
+sed '/^\[info\] \(source->target\|target\) /!d' .fcm-make/log >"$TEST_KEY.log"
+file_cmp "$TEST_KEY.log" "$TEST_KEY.log" <<'__LOG__'
+[info] source->target / -> (archive) lib/ libo.a
+[info] source->target hello.f90 -> (link) bin/ hello.exe
+[info] source->target hello.f90 -> (install) include/ hello.f90
+[info] source->target hello.f90 -> (compile) o/ hello.o
+[info] source->target hello_mod.f90 -> (install) include/ hello_mod.f90
+[info] source->target hello_mod.f90 -> (compile+) include/ hello_mod.mod
+[info] source->target hello_mod.f90 -> (compile) o/ hello_mod.o
+[info] source->target hello_sub.f90 -> (install) include/ hello_sub.f90
+[info] source->target hello_sub.f90 -> (ext-iface) include/ hello_sub.interface
+[info] source->target hello_sub.f90 -> (compile) o/ hello_sub.o
+[info] target hello.exe
+[info] target - hello.o
+[info] target - hello_sub.o
+[info] target - - hello_mod.mod
+[info] target - - - hello_mod.o
+[info] target - hello_mod.o
+__LOG__
+#-------------------------------------------------------------------------------
+exit 0
diff --git a/t/fcm-make/16-build-dep-o-2/fcm-make.cfg b/t/fcm-make/16-build-dep-o-2/fcm-make.cfg
new file mode 100644
index 0000000..eda1238
--- /dev/null
+++ b/t/fcm-make/16-build-dep-o-2/fcm-make.cfg
@@ -0,0 +1,4 @@
+steps=build
+build.source=$HERE/src
+build.target{task}=link
+build.prop{dep.o}=hello_sub.o
diff --git a/t/fcm-make/16-build-dep-o-2/src/hello.f90 b/t/fcm-make/16-build-dep-o-2/src/hello.f90
new file mode 100644
index 0000000..6f92631
--- /dev/null
+++ b/t/fcm-make/16-build-dep-o-2/src/hello.f90
@@ -0,0 +1,3 @@
+program hello
+call hello_sub()
+end program hello
diff --git a/t/fcm-make/16-build-dep-o-2/src/hello_mod.f90 b/t/fcm-make/16-build-dep-o-2/src/hello_mod.f90
new file mode 100644
index 0000000..191124a
--- /dev/null
+++ b/t/fcm-make/16-build-dep-o-2/src/hello_mod.f90
@@ -0,0 +1,3 @@
+module hello_mod
+character(*), parameter :: greet='Hello'
+end module hello_mod
diff --git a/t/fcm-make/16-build-dep-o-2/src/hello_sub.f90 b/t/fcm-make/16-build-dep-o-2/src/hello_sub.f90
new file mode 100644
index 0000000..3e7f146
--- /dev/null
+++ b/t/fcm-make/16-build-dep-o-2/src/hello_sub.f90
@@ -0,0 +1,4 @@
+subroutine hello_sub
+use hello_mod, only: greet
+write(*, '(a,1x,a)') greet, 'world'
+end subroutine hello_sub
diff --git a/t/fcm-make/17-build-cyclic.t b/t/fcm-make/17-build-cyclic.t
new file mode 100755
index 0000000..713c14d
--- /dev/null
+++ b/t/fcm-make/17-build-cyclic.t
@@ -0,0 +1,42 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+# Tests "fcm make", self cyclic dependency.
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+tests 2
+cp -r $TEST_SOURCE_DIR/$TEST_KEY_BASE/* .
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE"
+run_fail "$TEST_KEY" fcm make
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" <<'__ERR__'
+[FAIL] baz.mod: target depends on itself
+[FAIL] required by: bar.o
+[FAIL] required by: bar.mod
+[FAIL] required by: baz.o
+[FAIL] required by: baz.mod
+[FAIL] required by: foo.o
+[FAIL] required by: foo.mod
+[FAIL] required by: hello.o
+[FAIL] required by: hello.exe
+
+__ERR__
+#-------------------------------------------------------------------------------
+exit 0
diff --git a/t/fcm-make/17-build-cyclic/fcm-make.cfg b/t/fcm-make/17-build-cyclic/fcm-make.cfg
new file mode 100644
index 0000000..eda29bc
--- /dev/null
+++ b/t/fcm-make/17-build-cyclic/fcm-make.cfg
@@ -0,0 +1,3 @@
+steps=build
+build.source=$HERE/src
+build.target{task}=link
diff --git a/t/fcm-make/17-build-cyclic/src/bar.f90 b/t/fcm-make/17-build-cyclic/src/bar.f90
new file mode 100644
index 0000000..e62620e
--- /dev/null
+++ b/t/fcm-make/17-build-cyclic/src/bar.f90
@@ -0,0 +1,9 @@
+module bar
+use quack, only: quack_type
+use baz, only: baz_type
+implicit none
+private
+
+type, public, abstract, extends(quack_type) :: bar_type
+end type
+end module bar
diff --git a/t/fcm-make/17-build-cyclic/src/baz.f90 b/t/fcm-make/17-build-cyclic/src/baz.f90
new file mode 100644
index 0000000..096a17a
--- /dev/null
+++ b/t/fcm-make/17-build-cyclic/src/baz.f90
@@ -0,0 +1,10 @@
+module baz
+use quack, only: quack_type
+use bar, only: bar_type
+implicit none
+private
+
+type, public, abstract :: baz_type
+end type
+
+end module baz
diff --git a/t/fcm-make/17-build-cyclic/src/foo.f90 b/t/fcm-make/17-build-cyclic/src/foo.f90
new file mode 100644
index 0000000..22ab0b4
--- /dev/null
+++ b/t/fcm-make/17-build-cyclic/src/foo.f90
@@ -0,0 +1,10 @@
+module foo
+use bar, only: bar_type
+use baz, only: baz_type
+implicit none
+private
+
+type, public, extends(bar_type) :: foo_type
+end type
+
+end module foo
diff --git a/t/fcm-make/17-build-cyclic/src/hello.f90 b/t/fcm-make/17-build-cyclic/src/hello.f90
new file mode 100644
index 0000000..9ed47b3
--- /dev/null
+++ b/t/fcm-make/17-build-cyclic/src/hello.f90
@@ -0,0 +1,7 @@
+program hello
+ use foo, only: foo_type
+ use meow, only: meow_type
+ use baz, only: baz_type
+ implicit none
+ write(*, '(A)') 'Hello'
+end program hello
diff --git a/t/fcm-make/17-build-cyclic/src/meow.f90 b/t/fcm-make/17-build-cyclic/src/meow.f90
new file mode 100644
index 0000000..bde5706
--- /dev/null
+++ b/t/fcm-make/17-build-cyclic/src/meow.f90
@@ -0,0 +1,11 @@
+module meow
+use quack, only: quack_type
+use baz, only: baz_type
+use foo, only: foo_type
+implicit none
+private
+
+type, public, extends(baz_type) :: meow_type
+end type
+
+end module meow
diff --git a/t/fcm-make/17-build-cyclic/src/quack.f90 b/t/fcm-make/17-build-cyclic/src/quack.f90
new file mode 100644
index 0000000..a898744
--- /dev/null
+++ b/t/fcm-make/17-build-cyclic/src/quack.f90
@@ -0,0 +1,9 @@
+module quack
+implicit none
+private
+
+type, public, abstract :: quack_type
+ private
+end type
+
+end module quack
diff --git a/t/fcm-make/18-build-use-intrinsic.t b/t/fcm-make/18-build-use-intrinsic.t
new file mode 100755
index 0000000..ec6ceaa
--- /dev/null
+++ b/t/fcm-make/18-build-use-intrinsic.t
@@ -0,0 +1,60 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+# Tests "fcm make", build, Fortran source file, "use, intrinsic" statement.
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+tests 7
+cp -r $TEST_SOURCE_DIR/$TEST_KEY_BASE/* .
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE"
+run_pass "$TEST_KEY" fcm make
+sed '/^\[info\] target /!d' .fcm-make/log >"$TEST_KEY.log"
+file_cmp "$TEST_KEY.log" "$TEST_KEY.log" <<'__LOG__'
+[info] target greet.exe
+[info] target - greet.o
+[info] target - - hi.interface
+[info] target - - hello.interface
+[info] target - hello.o
+[info] target - hi.o
+__LOG__
+file_cmp "$TEST_KEY.hello.interface" "build/include/hello.interface" \
+ <<'__INTERFACE__'
+interface
+subroutine hello()
+end subroutine hello
+end interface
+__INTERFACE__
+file_cmp "$TEST_KEY.hi.interface" "build/include/hi.interface" \
+ <<'__INTERFACE__'
+interface
+subroutine hi()
+use, intrinsic :: iso_fortran_env
+end subroutine hi
+end interface
+__INTERFACE__
+run_pass "$TEST_KEY.exe" ./build/bin/greet.exe
+file_cmp "$TEST_KEY.exe.out" "$TEST_KEY.exe.out" <<'__OUT__'
+Hello
+Hi
+__OUT__
+file_cmp "$TEST_KEY.exe.err" "$TEST_KEY.exe.err" </dev/null
+#-------------------------------------------------------------------------------
+exit 0
diff --git a/t/fcm-make/18-build-use-intrinsic/fcm-make.cfg b/t/fcm-make/18-build-use-intrinsic/fcm-make.cfg
new file mode 100644
index 0000000..eda29bc
--- /dev/null
+++ b/t/fcm-make/18-build-use-intrinsic/fcm-make.cfg
@@ -0,0 +1,3 @@
+steps=build
+build.source=$HERE/src
+build.target{task}=link
diff --git a/t/fcm-make/18-build-use-intrinsic/src/greet.f90 b/t/fcm-make/18-build-use-intrinsic/src/greet.f90
new file mode 100644
index 0000000..d9c2833
--- /dev/null
+++ b/t/fcm-make/18-build-use-intrinsic/src/greet.f90
@@ -0,0 +1,14 @@
+program greet
+implicit none
+include 'hello.interface'
+include 'hi.interface'
+abstract interface
+subroutine abstract_greet()
+end subroutine abstract_greet
+end interface
+procedure(abstract_greet), pointer :: say
+say => hello
+call say()
+say => hi
+call say()
+end program greet
diff --git a/t/fcm-make/18-build-use-intrinsic/src/hello.f90 b/t/fcm-make/18-build-use-intrinsic/src/hello.f90
new file mode 100644
index 0000000..f59c7d7
--- /dev/null
+++ b/t/fcm-make/18-build-use-intrinsic/src/hello.f90
@@ -0,0 +1,4 @@
+subroutine hello()
+use, intrinsic :: iso_fortran_env, only: output_unit
+write(output_unit, '(a)') 'Hello'
+end subroutine hello
diff --git a/t/fcm-make/18-build-use-intrinsic/src/hi.f90 b/t/fcm-make/18-build-use-intrinsic/src/hi.f90
new file mode 100644
index 0000000..9385278
--- /dev/null
+++ b/t/fcm-make/18-build-use-intrinsic/src/hi.f90
@@ -0,0 +1,4 @@
+subroutine hi()
+use, intrinsic :: iso_fortran_env !, only: output_unit
+write(output_unit, '(a)') 'Hi'
+end subroutine hi
diff --git a/t/fcm-make/19-build-inherit-prop.t b/t/fcm-make/19-build-inherit-prop.t
new file mode 100755
index 0000000..6b1db5d
--- /dev/null
+++ b/t/fcm-make/19-build-inherit-prop.t
@@ -0,0 +1,50 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+# Tests "fcm make", ensure that properties can be declared before or after use=
+# declaration.
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+tests 2
+set -e
+mkdir -p i0 i1 i2
+cp -r $TEST_SOURCE_DIR/$TEST_KEY_BASE/* i0
+fcm make -q -C i0
+set +e
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-i1"
+cat >i1/fcm-make.cfg <<'__FCM_MAKE_CFG__'
+use=$HERE/../i0
+build.prop{fc.defs}=WORLD='"Mars"'
+__FCM_MAKE_CFG__
+fcm make -q -C i1
+$PWD/i1/build/bin/hello.exe >"$TEST_KEY.exe.out"
+file_cmp "$TEST_KEY.exe.out" "$TEST_KEY.exe.out" <<<'Hello Mars'
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-i2"
+cat >i2/fcm-make.cfg <<'__FCM_MAKE_CFG__'
+build.prop{fc.defs}=WORLD='"Venus"'
+use=$HERE/../i0
+__FCM_MAKE_CFG__
+fcm make -q -C i2
+$PWD/i2/build/bin/hello.exe >"$TEST_KEY.exe.out"
+file_cmp "$TEST_KEY.exe.out" "$TEST_KEY.exe.out" <<<'Hello Venus'
+#-------------------------------------------------------------------------------
+exit 0
diff --git a/t/fcm-make/19-build-inherit-prop/fcm-make.cfg b/t/fcm-make/19-build-inherit-prop/fcm-make.cfg
new file mode 100644
index 0000000..9aad525
--- /dev/null
+++ b/t/fcm-make/19-build-inherit-prop/fcm-make.cfg
@@ -0,0 +1,4 @@
+steps=build
+build.source=$HERE/src
+build.target{task}=link
+build.prop{fc.defs}=WORLD='"Earth"'
diff --git a/t/fcm-make/19-build-inherit-prop/src/hello.F90 b/t/fcm-make/19-build-inherit-prop/src/hello.F90
new file mode 100644
index 0000000..e178c67
--- /dev/null
+++ b/t/fcm-make/19-build-inherit-prop/src/hello.F90
@@ -0,0 +1,6 @@
+program hello
+#ifndef WORLD
+#define WORLD "World"
+#endif
+write(*, '(a,1x,a)') 'Hello', WORLD
+end program hello
diff --git a/t/fcm-make/20-args.t b/t/fcm-make/20-args.t
new file mode 100755
index 0000000..b60f227
--- /dev/null
+++ b/t/fcm-make/20-args.t
@@ -0,0 +1,50 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+# Tests "fcm make", CLI arguments.
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+tests 6
+set -e
+cp -r $TEST_SOURCE_DIR/$TEST_KEY_BASE/* .
+set +e
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-hello"
+run_pass "$TEST_KEY" fcm make 'build.prop{file-ext.bin}=' 'build.target=hello'
+grep '^\[info\] required-target:' .fcm-make/log >$TEST_KEY.log
+file_cmp "$TEST_KEY.log" "$TEST_KEY.log" <<'__LOG__'
+[info] required-target: link bin hello
+__LOG__
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-greet"
+run_pass "$TEST_KEY" fcm make 'build.target=greet.exe'
+grep '^\[info\] required-target:' .fcm-make/log >$TEST_KEY.log
+file_cmp "$TEST_KEY.log" "$TEST_KEY.log" <<'__LOG__'
+[info] required-target: link bin greet.exe
+__LOG__
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-new-greet"
+run_pass "$TEST_KEY" fcm make --new 'build.target=greet.exe'
+grep '^\[info\] required-target:' .fcm-make/log >$TEST_KEY.log
+file_cmp "$TEST_KEY.log" "$TEST_KEY.log" <<'__LOG__'
+[info] required-target: link bin greet.exe
+__LOG__
+#-------------------------------------------------------------------------------
+exit 0
diff --git a/t/fcm-make/20-args/fcm-make.cfg b/t/fcm-make/20-args/fcm-make.cfg
new file mode 100644
index 0000000..d614aa9
--- /dev/null
+++ b/t/fcm-make/20-args/fcm-make.cfg
@@ -0,0 +1,3 @@
+steps=build
+build.source=$HERE/src
+build.target=hello.exe
diff --git a/t/fcm-make/20-args/src/greet.f90 b/t/fcm-make/20-args/src/greet.f90
new file mode 100644
index 0000000..60255bf
--- /dev/null
+++ b/t/fcm-make/20-args/src/greet.f90
@@ -0,0 +1,3 @@
+program greet
+write(*, '(a)') 'Greetings'
+end program greet
diff --git a/t/fcm-make/20-args/src/hello.f90 b/t/fcm-make/20-args/src/hello.f90
new file mode 100644
index 0000000..2a3a4b0
--- /dev/null
+++ b/t/fcm-make/20-args/src/hello.f90
@@ -0,0 +1,3 @@
+program hello
+write(*, '(a)') 'Hello'
+end program hello
diff --git a/t/fcm-make/21-inherit-steps.t b/t/fcm-make/21-inherit-steps.t
new file mode 100755
index 0000000..06607df
--- /dev/null
+++ b/t/fcm-make/21-inherit-steps.t
@@ -0,0 +1,68 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+# Tests "fcm make", ensure that steps can be declared before or after use=
+# declaration.
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+tests 3
+set -e
+mkdir -p i0 i1 i2 i3
+cp -r $TEST_SOURCE_DIR/$TEST_KEY_BASE/* i0
+fcm make -q -C i0
+set +e
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-i1"
+cat >i1/fcm-make.cfg <<'__FCM_MAKE_CFG__'
+use=$HERE/../i0
+__FCM_MAKE_CFG__
+fcm make -q -C i1
+find i1/*/bin -type f | sort >"$TEST_KEY.ls"
+file_cmp "$TEST_KEY.ls" "$TEST_KEY.ls" <<'__LIST__'
+i1/build1/bin/hello.exe
+i1/build2/bin/salute.exe
+i1/build3/bin/greet.exe
+__LIST__
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-i2"
+cat >i2/fcm-make.cfg <<'__FCM_MAKE_CFG__'
+step.class[build1 build2]=build
+steps=build1 build2
+use=$HERE/../i0
+__FCM_MAKE_CFG__
+fcm make -q -C i2
+find i2/*/bin -type f | sort >"$TEST_KEY.ls"
+file_cmp "$TEST_KEY.ls" "$TEST_KEY.ls" <<'__LIST__'
+i2/build1/bin/hello.exe
+i2/build2/bin/salute.exe
+__LIST__
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-i3"
+cat >i3/fcm-make.cfg <<'__FCM_MAKE_CFG__'
+use=$HERE/../i0
+steps=build3
+__FCM_MAKE_CFG__
+fcm make -q -C i3
+find i3/*/bin -type f | sort >"$TEST_KEY.ls"
+file_cmp "$TEST_KEY.ls" "$TEST_KEY.ls" <<'__LIST__'
+i3/build3/bin/greet.exe
+__LIST__
+#-------------------------------------------------------------------------------
+exit 0
diff --git a/t/fcm-make/21-inherit-steps/fcm-make.cfg b/t/fcm-make/21-inherit-steps/fcm-make.cfg
new file mode 100644
index 0000000..5bc7b49
--- /dev/null
+++ b/t/fcm-make/21-inherit-steps/fcm-make.cfg
@@ -0,0 +1,8 @@
+step.class[build1 build2 build3]=build
+steps=build1 build2 build3
+build1.source=$HERE/src1
+build1.target{task}=link
+build2.source=$HERE/src2
+build2.target{task}=link
+build3.source=$HERE/src3
+build3.target{task}=link
diff --git a/t/fcm-make/21-inherit-steps/src1/hello.f90 b/t/fcm-make/21-inherit-steps/src1/hello.f90
new file mode 100644
index 0000000..0fecec0
--- /dev/null
+++ b/t/fcm-make/21-inherit-steps/src1/hello.f90
@@ -0,0 +1,3 @@
+program hello
+write(*, '(a)') 'Hello!'
+end program hello
diff --git a/t/fcm-make/21-inherit-steps/src2/salute.f90 b/t/fcm-make/21-inherit-steps/src2/salute.f90
new file mode 100644
index 0000000..a0f056a
--- /dev/null
+++ b/t/fcm-make/21-inherit-steps/src2/salute.f90
@@ -0,0 +1,3 @@
+program salute
+write(*, '(a)') 'Salute!'
+end program salute
diff --git a/t/fcm-make/21-inherit-steps/src3/greet.f90 b/t/fcm-make/21-inherit-steps/src3/greet.f90
new file mode 100644
index 0000000..ab26cc7
--- /dev/null
+++ b/t/fcm-make/21-inherit-steps/src3/greet.f90
@@ -0,0 +1,3 @@
+program greet
+write(*, '(a)') 'Greeting!'
+end program greet
diff --git a/t/fcm-make/22-build-2-bad-mod-over-inherit.t b/t/fcm-make/22-build-2-bad-mod-over-inherit.t
new file mode 100755
index 0000000..4cacfeb
--- /dev/null
+++ b/t/fcm-make/22-build-2-bad-mod-over-inherit.t
@@ -0,0 +1,59 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+# Tests "fcm make", inherit build correctness. metomi/fcm#110
+# * 2 source files with bad syntax override.
+# * Build fails on first source file.
+# * Fix first source file.
+# * Build should fail on second source file.
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+tests 7
+set -e
+mkdir -p i0 i1
+cp -r $TEST_SOURCE_DIR/$TEST_KEY_BASE/{fcm-make.cfg,src} i0
+fcm make -q -C i0
+set +e
+#-------------------------------------------------------------------------------
+cp -r $TEST_SOURCE_DIR/$TEST_KEY_BASE/src-i i1/src
+cat >i1/fcm-make.cfg <<'__CFG__'
+use=$HERE/../i0
+build.source=$HERE/src
+__CFG__
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-i1-1"
+run_fail "$TEST_KEY" fcm make -q -C i1
+file_grep "$TEST_KEY.err" '\[FAIL\].*i1/src/m1.f90' "$TEST_KEY.err"
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-i1-2"
+sed 's/^writ/write/' $TEST_SOURCE_DIR/$TEST_KEY_BASE/src-i/m1.f90 >i1/src/m1.f90
+run_fail "$TEST_KEY" fcm make -q -C i1
+file_grep "$TEST_KEY.err" '\[FAIL\].*i1/src/m2.f90' "$TEST_KEY.err"
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-i1-3"
+sed 's/^writ/write/' $TEST_SOURCE_DIR/$TEST_KEY_BASE/src-i/m2.f90 >i1/src/m2.f90
+run_pass "$TEST_KEY" fcm make -q -C i1 --new
+run_pass "$TEST_KEY.exe" $PWD/i1/build/bin/p1.exe
+file_cmp "$TEST_KEY.exe.out" "$TEST_KEY.exe.out" <<'__OUT__'
+Greet from m1-s1!
+Greet from m2-s2!
+__OUT__
+#-------------------------------------------------------------------------------
+exit
diff --git a/t/fcm-make/22-build-2-bad-mod-over-inherit/fcm-make.cfg b/t/fcm-make/22-build-2-bad-mod-over-inherit/fcm-make.cfg
new file mode 100644
index 0000000..d063951
--- /dev/null
+++ b/t/fcm-make/22-build-2-bad-mod-over-inherit/fcm-make.cfg
@@ -0,0 +1,3 @@
+steps=build
+build.source=$HERE/src
+build.target=p1.exe
diff --git a/t/fcm-make/22-build-2-bad-mod-over-inherit/src-i/m1.f90 b/t/fcm-make/22-build-2-bad-mod-over-inherit/src-i/m1.f90
new file mode 100644
index 0000000..91cdf39
--- /dev/null
+++ b/t/fcm-make/22-build-2-bad-mod-over-inherit/src-i/m1.f90
@@ -0,0 +1,6 @@
+module m1
+contains
+subroutine s1()
+writ(*, '(a)') 'Greet from m1-s1!'
+end subroutine s1
+end module m1
diff --git a/t/fcm-make/22-build-2-bad-mod-over-inherit/src-i/m2.f90 b/t/fcm-make/22-build-2-bad-mod-over-inherit/src-i/m2.f90
new file mode 100644
index 0000000..57b1cd2
--- /dev/null
+++ b/t/fcm-make/22-build-2-bad-mod-over-inherit/src-i/m2.f90
@@ -0,0 +1,6 @@
+module m2
+contains
+subroutine s2()
+writ(*, '(a)') 'Greet from m2-s2!'
+end subroutine s2
+end module m2
diff --git a/t/fcm-make/22-build-2-bad-mod-over-inherit/src/m1.f90 b/t/fcm-make/22-build-2-bad-mod-over-inherit/src/m1.f90
new file mode 100644
index 0000000..87ece14
--- /dev/null
+++ b/t/fcm-make/22-build-2-bad-mod-over-inherit/src/m1.f90
@@ -0,0 +1,6 @@
+module m1
+contains
+subroutine s1()
+write(*, '(a)') 'Hello from m1:s1!'
+end subroutine s1
+end module m1
diff --git a/t/fcm-make/22-build-2-bad-mod-over-inherit/src/m2.f90 b/t/fcm-make/22-build-2-bad-mod-over-inherit/src/m2.f90
new file mode 100644
index 0000000..3640c47
--- /dev/null
+++ b/t/fcm-make/22-build-2-bad-mod-over-inherit/src/m2.f90
@@ -0,0 +1,6 @@
+module m2
+contains
+subroutine s2()
+write(*, '(a)') 'Hello from m2:s2!'
+end subroutine s2
+end module m2
diff --git a/t/fcm-make/22-build-2-bad-mod-over-inherit/src/p1.f90 b/t/fcm-make/22-build-2-bad-mod-over-inherit/src/p1.f90
new file mode 100644
index 0000000..9321db8
--- /dev/null
+++ b/t/fcm-make/22-build-2-bad-mod-over-inherit/src/p1.f90
@@ -0,0 +1,6 @@
+program p1
+use m1, only: s1
+use m2, only: s2
+call s1()
+call s2()
+end program p1
diff --git a/t/fcm-make/23-build-omp.t b/t/fcm-make/23-build-omp.t
new file mode 100755
index 0000000..d370242
--- /dev/null
+++ b/t/fcm-make/23-build-omp.t
@@ -0,0 +1,66 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+# Tests "fcm make", build detects dependencies in OMP sentinels.
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+tests 16
+cp -r $TEST_SOURCE_DIR/$TEST_KEY_BASE/* .
+yes 6.0 | head -n 100 >"$TEST_KEY_BASE.exe.on.out"
+yes 1.0 | head -n 100 >"$TEST_KEY_BASE.exe.off.out"
+#-------------------------------------------------------------------------------
+TEST_KEY=$TEST_KEY_BASE-on # fc.flag-omp on in new mode
+run_pass "$TEST_KEY" fcm make
+grep ' !\$' fcm-make.log | sort >"$TEST_KEY.log.deps.expected"
+file_cmp "$TEST_KEY.log.deps" "$TEST_KEY.log.deps.expected" <<'__LOG__'
+[info] -> ( include) !$i1.f90
+[info] -> ( include) !$i2.f90
+[info] -> ( f.module) !$m1
+[info] -> ( f.module) !$m2
+__LOG__
+run_pass "$TEST_KEY.exe" $PWD/build/bin/p1.exe
+file_cmp "$TEST_KEY.exe.out" "$TEST_KEY_BASE.exe.on.out" "$TEST_KEY.exe.out"
+#-------------------------------------------------------------------------------
+TEST_KEY=$TEST_KEY_BASE-on-off # fc.flag-omp on->off in incremental mode
+echo 'build.prop{fc.flag-omp}=' >>fcm-make.cfg
+run_pass "$TEST_KEY" fcm make
+run_pass "$TEST_KEY.exe" $PWD/build/bin/p1.exe
+file_cmp "$TEST_KEY.exe.out" "$TEST_KEY_BASE.exe.off.out" "$TEST_KEY.exe.out"
+#-------------------------------------------------------------------------------
+TEST_KEY=$TEST_KEY_BASE-on-off-on # fc.flag-omp on->off->on in incremental mode
+echo 'build.prop{fc.flag-omp}=-fopenmp' >>fcm-make.cfg
+run_pass "$TEST_KEY" fcm make
+run_pass "$TEST_KEY.exe" $PWD/build/bin/p1.exe
+file_cmp "$TEST_KEY.exe.out" "$TEST_KEY_BASE.exe.on.out" "$TEST_KEY.exe.out"
+#-------------------------------------------------------------------------------
+TEST_KEY=$TEST_KEY_BASE-off # fc.flag-omp off in new mode
+cp -r $TEST_SOURCE_DIR/$TEST_KEY_BASE/fcm-make.cfg .
+echo 'build.prop{fc.flag-omp}=' >>fcm-make.cfg
+run_pass "$TEST_KEY" fcm make --new
+run_pass "$TEST_KEY.exe" $PWD/build/bin/p1.exe
+file_cmp "$TEST_KEY.exe.out" "$TEST_KEY_BASE.exe.off.out" "$TEST_KEY.exe.out"
+#-------------------------------------------------------------------------------
+TEST_KEY=$TEST_KEY_BASE-off-on # fc.flag-omp off->on in incremental mode
+cp -r $TEST_SOURCE_DIR/$TEST_KEY_BASE/fcm-make.cfg .
+run_pass "$TEST_KEY" fcm make
+run_pass "$TEST_KEY.exe" $PWD/build/bin/p1.exe
+file_cmp "$TEST_KEY.exe.out" "$TEST_KEY_BASE.exe.on.out" "$TEST_KEY.exe.out"
+#-------------------------------------------------------------------------------
+exit
diff --git a/t/fcm-make/23-build-omp/fcm-make.cfg b/t/fcm-make/23-build-omp/fcm-make.cfg
new file mode 100644
index 0000000..4c10ee8
--- /dev/null
+++ b/t/fcm-make/23-build-omp/fcm-make.cfg
@@ -0,0 +1,4 @@
+steps=build
+build.source=$HERE/src
+build.target=p1.exe
+build.prop{fc.flag-omp}=-fopenmp
diff --git a/t/fcm-make/23-build-omp/src/i1.f90 b/t/fcm-make/23-build-omp/src/i1.f90
new file mode 100644
index 0000000..966d4d3
--- /dev/null
+++ b/t/fcm-make/23-build-omp/src/i1.f90
@@ -0,0 +1 @@
+call s1(n, y, x)
diff --git a/t/fcm-make/23-build-omp/src/i2.f90 b/t/fcm-make/23-build-omp/src/i2.f90
new file mode 100644
index 0000000..36dda9b
--- /dev/null
+++ b/t/fcm-make/23-build-omp/src/i2.f90
@@ -0,0 +1 @@
+call s2(n, z, y)
diff --git a/t/fcm-make/23-build-omp/src/m1.f90 b/t/fcm-make/23-build-omp/src/m1.f90
new file mode 100644
index 0000000..0828202
--- /dev/null
+++ b/t/fcm-make/23-build-omp/src/m1.f90
@@ -0,0 +1,14 @@
+module m1
+contains
+subroutine s1(n, y, x)
+integer, intent(in) :: n
+real, intent(out) :: y(:)
+real, intent(in) :: x(:)
+integer :: i
+!$omp parallel do shared(y)
+do i = 1, n
+ y(i) = x(i) * 2.0
+end do
+!$omp end parallel do
+end subroutine s1
+end module m1
diff --git a/t/fcm-make/23-build-omp/src/m2.f90 b/t/fcm-make/23-build-omp/src/m2.f90
new file mode 100644
index 0000000..1d1f6ac
--- /dev/null
+++ b/t/fcm-make/23-build-omp/src/m2.f90
@@ -0,0 +1,14 @@
+module m2
+contains
+subroutine s2(n, z, y)
+integer, intent(in) :: n
+real, intent(out) :: z(:)
+real, intent(in) :: y(:)
+integer :: i
+!$omp parallel do shared(z)
+do i = 1, n
+ z(i) = y(i) * 3.0
+end do
+!$omp end parallel do
+end subroutine s2
+end module m2
diff --git a/t/fcm-make/23-build-omp/src/p1.f90 b/t/fcm-make/23-build-omp/src/p1.f90
new file mode 100644
index 0000000..48b3c64
--- /dev/null
+++ b/t/fcm-make/23-build-omp/src/p1.f90
@@ -0,0 +1,21 @@
+program p1
+
+!$ use omp_lib
+!$ use m1, only: s1
+
+integer, parameter :: n=100
+integer :: i
+real :: x(n), y(n), z(n)
+
+include 's3.interface'
+
+x(:) = 1.0
+y(:) = 1.0
+z(:) = 1.0
+!$ include "i1.f90"
+call s3(n, z, y)
+do i = 1, n
+ write(*, '(f3.1)') z(i)
+end do
+
+end program p1
diff --git a/t/fcm-make/23-build-omp/src/s3.f90 b/t/fcm-make/23-build-omp/src/s3.f90
new file mode 100644
index 0000000..568c1b8
--- /dev/null
+++ b/t/fcm-make/23-build-omp/src/s3.f90
@@ -0,0 +1,7 @@
+subroutine s3(n, z, y)
+!$ use m2, only: s2
+integer, intent(in) :: n
+real, intent(out) :: z(:)
+real, intent(in) :: y(:)
+!$ include "i2.f90"
+end subroutine s3
diff --git a/t/fcm-make/24-build-c-main-camel.t b/t/fcm-make/24-build-c-main-camel.t
new file mode 100755
index 0000000..87604c5
--- /dev/null
+++ b/t/fcm-make/24-build-c-main-camel.t
@@ -0,0 +1,36 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+# Test build C source file with mixed case name and has main function.
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+tests 3
+cp -r $TEST_SOURCE_DIR/$TEST_KEY_BASE/* .
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE"
+run_pass "$TEST_KEY" fcm make
+grep '^\[info\] target ' fcm-make.log >"$TEST_KEY.target.log"
+file_cmp "$TEST_KEY.target.log" "$TEST_KEY.target.log" <<'__LOG__'
+[info] target Hello
+[info] target - hello.o
+__LOG__
+run_pass "$TEST_KEY.chello" $PWD/build/bin/Hello
+#-------------------------------------------------------------------------------
+exit 0
diff --git a/t/fcm-make/24-build-c-main-camel/fcm-make.cfg b/t/fcm-make/24-build-c-main-camel/fcm-make.cfg
new file mode 100644
index 0000000..9216a4d
--- /dev/null
+++ b/t/fcm-make/24-build-c-main-camel/fcm-make.cfg
@@ -0,0 +1,4 @@
+steps=build
+build.source=$HERE/src
+build.target{task}=link
+build.prop{file-ext.bin}=
diff --git a/t/fcm-make/24-build-c-main-camel/src/Hello.c b/t/fcm-make/24-build-c-main-camel/src/Hello.c
new file mode 100644
index 0000000..ccd6fe0
--- /dev/null
+++ b/t/fcm-make/24-build-c-main-camel/src/Hello.c
@@ -0,0 +1,5 @@
+#include <stdio.h>
+int main(void) {
+ printf("Hello World\n");
+ return 0;
+}
diff --git a/t/fcm-make/25-build-cyclic-2.t b/t/fcm-make/25-build-cyclic-2.t
new file mode 100755
index 0000000..bfa79e5
--- /dev/null
+++ b/t/fcm-make/25-build-cyclic-2.t
@@ -0,0 +1,40 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+# Tests "fcm make", adjacent cyclic dependency.
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+tests 2
+cp -r $TEST_SOURCE_DIR/$TEST_KEY_BASE/* .
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE"
+run_fail "$TEST_KEY" fcm make
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" <<'__ERR__'
+[FAIL] m1.mod: target depends on itself
+[FAIL] required by: m2.o
+[FAIL] required by: m2.mod
+[FAIL] required by: m1.o
+[FAIL] required by: m1.mod
+[FAIL] required by: foo.o
+[FAIL] required by: foo.exe
+
+__ERR__
+#-------------------------------------------------------------------------------
+exit 0
diff --git a/t/fcm-make/25-build-cyclic-2/fcm-make.cfg b/t/fcm-make/25-build-cyclic-2/fcm-make.cfg
new file mode 100644
index 0000000..eda29bc
--- /dev/null
+++ b/t/fcm-make/25-build-cyclic-2/fcm-make.cfg
@@ -0,0 +1,3 @@
+steps=build
+build.source=$HERE/src
+build.target{task}=link
diff --git a/t/fcm-make/25-build-cyclic-2/src/foo.f90 b/t/fcm-make/25-build-cyclic-2/src/foo.f90
new file mode 100644
index 0000000..0f5f635
--- /dev/null
+++ b/t/fcm-make/25-build-cyclic-2/src/foo.f90
@@ -0,0 +1,4 @@
+program foo
+use m1, only s1
+call s1()
+end program foo
diff --git a/t/fcm-make/25-build-cyclic-2/src/m1.f90 b/t/fcm-make/25-build-cyclic-2/src/m1.f90
new file mode 100644
index 0000000..af0c4df
--- /dev/null
+++ b/t/fcm-make/25-build-cyclic-2/src/m1.f90
@@ -0,0 +1,12 @@
+module m1
+character(*), parameter :: WHATEVER
+contains
+subroutine s1()
+write(*, '(a)') f1() // ' from s1'
+end subroutine s1
+function f1()
+use m2, only: HELLO
+character(len=len(HELLO)) :: f1
+f1 = hello
+end function f1
+end module m1
diff --git a/t/fcm-make/25-build-cyclic-2/src/m2.f90 b/t/fcm-make/25-build-cyclic-2/src/m2.f90
new file mode 100644
index 0000000..e727c76
--- /dev/null
+++ b/t/fcm-make/25-build-cyclic-2/src/m2.f90
@@ -0,0 +1,8 @@
+module m2
+use m1, only: WHATEVER
+character(*), parameter :: HELLO='Hello'
+contains
+subroutine s2()
+write(*, '(a)') HELLO // ' from s2'
+end subroutine s2
+end module m2
diff --git a/t/fcm-make/26-no-config.t b/t/fcm-make/26-no-config.t
new file mode 100755
index 0000000..ce7ebe8
--- /dev/null
+++ b/t/fcm-make/26-no-config.t
@@ -0,0 +1,33 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+# Tests "fcm make", no configuration and no arguments.
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+tests 2
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE"
+run_fail "$TEST_KEY" fcm make
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" <<'__ERR__'
+[FAIL] no configuration specified or found
+
+__ERR__
+#-------------------------------------------------------------------------------
+exit 0
diff --git a/t/fcm-make/27-args-only.t b/t/fcm-make/27-args-only.t
new file mode 100755
index 0000000..59feaa4
--- /dev/null
+++ b/t/fcm-make/27-args-only.t
@@ -0,0 +1,41 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+# Tests "fcm make", no configuration files, only CLI arguments.
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+tests 3
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE"
+mkdir src
+cat >src/hello.f90 <<'__FORTRAN__'
+program hello
+write(*, '(a)') 'Hello World!'
+end program hello
+__FORTRAN__
+run_pass "$TEST_KEY" fcm make \
+ 'steps=build' "build.source=$PWD/src" 'build.target=hello.exe'
+file_test "$TEST_KEY.hello.exe" $PWD/build/bin/hello.exe
+$PWD/build/bin/hello.exe >"$TEST_KEY.hello.exe.out"
+file_cmp "$TEST_KEY.hello.exe.out" "$TEST_KEY.hello.exe.out" <<'__OUT__'
+Hello World!
+__OUT__
+#-------------------------------------------------------------------------------
+exit 0
diff --git a/t/fcm-make/28-bad-arg.t b/t/fcm-make/28-bad-arg.t
new file mode 100755
index 0000000..fddb3db
--- /dev/null
+++ b/t/fcm-make/28-bad-arg.t
@@ -0,0 +1,41 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+# Tests "fcm make", bad arguments.
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+tests 4
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE"
+run_fail "$TEST_KEY" fcm make 'foo'
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" <<'__ERROR__'
+[FAIL] arg 0 (foo): invalid config declaration
+
+__ERROR__
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-cfg"
+run_fail "$TEST_KEY" fcm make 'foo.cfg'
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" <<'__ERROR__'
+[FAIL] arg 0 (foo.cfg): invalid config declaration
+[FAIL] did you mean "-f foo.cfg"?
+
+__ERROR__
+#-------------------------------------------------------------------------------
+exit 0
diff --git a/t/fcm-make/29-relative-cfg.t b/t/fcm-make/29-relative-cfg.t
new file mode 100755
index 0000000..3978621
--- /dev/null
+++ b/t/fcm-make/29-relative-cfg.t
@@ -0,0 +1,85 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+# Tests "fcm make", config as relative paths
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+
+clean() {
+ rm -fr \
+ .fcm-make \
+ build \
+ fcm-make-as-parsed.cfg \
+ fcm-make-on-success.cfg \
+ fcm-make.log
+}
+#-------------------------------------------------------------------------------
+tests 11
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE"
+
+mkdir etc
+cat >etc/fcm-make.cfg <<'__CFG__'
+steps=build
+build.source=$HERE/../src
+build.target=hello.exe
+__CFG__
+
+mkdir src
+cat >src/hello.f90 <<'__FORTRAN__'
+program hello
+write(*, '(a)') 'Hello World!'
+end program hello
+__FORTRAN__
+
+#-------------------------------------------------------------------------------
+clean
+run_fail "$TEST_KEY-control" fcm make
+file_cmp "$TEST_KEY-control.err" "$TEST_KEY-control.err" <<'__ERROR__'
+[FAIL] no configuration specified or found
+
+__ERROR__
+#-------------------------------------------------------------------------------
+clean
+run_pass "$TEST_KEY-pwd" fcm make -f etc/fcm-make.cfg
+file_test "$TEST_KEY.hello.exe" $PWD/build/bin/hello.exe
+$PWD/build/bin/hello.exe >"$TEST_KEY.hello.exe.out"
+file_cmp "$TEST_KEY.hello.exe.out" "$TEST_KEY.hello.exe.out" <<'__OUT__'
+Hello World!
+__OUT__
+#-------------------------------------------------------------------------------
+clean
+run_pass "$TEST_KEY-path" fcm make -F $PWD/etc
+file_test "$TEST_KEY.hello.exe" $PWD/build/bin/hello.exe
+$PWD/build/bin/hello.exe >"$TEST_KEY.hello.exe.out"
+file_cmp "$TEST_KEY.hello.exe.out" "$TEST_KEY.hello.exe.out" <<'__OUT__'
+Hello World!
+__OUT__
+#-------------------------------------------------------------------------------
+clean
+cd src
+run_pass "$TEST_KEY-path" fcm make -C .. -f etc/fcm-make.cfg
+file_test "$TEST_KEY.hello.exe" ../build/bin/hello.exe
+../build/bin/hello.exe >"$TEST_KEY.hello.exe.out"
+file_cmp "$TEST_KEY.hello.exe.out" "$TEST_KEY.hello.exe.out" <<'__OUT__'
+Hello World!
+__OUT__
+cd ..
+#-------------------------------------------------------------------------------
+exit 0
diff --git a/t/fcm-make/30-relative-cfg-in-svn.t b/t/fcm-make/30-relative-cfg-in-svn.t
new file mode 100755
index 0000000..ca4fe2a
--- /dev/null
+++ b/t/fcm-make/30-relative-cfg-in-svn.t
@@ -0,0 +1,52 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+# Tests "fcm make", relative config in a Subversion repository
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+tests 9
+#-------------------------------------------------------------------------------
+mkdir etc
+cat >etc/fcm-make.cfg <<'__CFG__'
+steps=build
+build.source=src
+build.target=hello.exe
+__CFG__
+
+mkdir src
+cat >src/hello.f90 <<'__FORTRAN__'
+program hello
+write(*, '(a)') 'Hello World!'
+end program hello
+__FORTRAN__
+
+svnadmin create svn-repos
+svn import -m 'test stuff' etc file://$PWD/svn-repos/etc
+rm -fr etc
+
+#-------------------------------------------------------------------------------
+fcm_make_build_hello_tests "$TEST_KEY_BASE" \
+ '.exe' -F "file://$PWD/svn-repos" -f 'etc/fcm-make.cfg'
+fcm_make_build_hello_tests "$TEST_KEY_BASE-1" \
+ '.exe' -F "file://$PWD/svn-repos@1" -f 'etc/fcm-make.cfg'
+fcm_make_build_hello_tests "$TEST_KEY_BASE-HEAD" \
+ '.exe' -F "file://$PWD/svn-repos@HEAD" -f 'etc/fcm-make.cfg'
+#-------------------------------------------------------------------------------
+exit 0
diff --git a/t/fcm-make/31-relative-cfg-in-ssh.t b/t/fcm-make/31-relative-cfg-in-ssh.t
new file mode 100755
index 0000000..21cd98d
--- /dev/null
+++ b/t/fcm-make/31-relative-cfg-in-ssh.t
@@ -0,0 +1,69 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+# Tests "fcm make", relative config in a remote host accessible via SSH
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+# Get a remote host for testing
+T_HOST=
+for FILE in $HOME/.metomi/fcm/t.cfg $FCM_HOME/etc/fcm/t.cfg; do
+ if [[ ! -f $FILE || ! -r $FILE ]]; then
+ continue
+ fi
+ T_HOST=$(fcm cfg $FILE | sed '/^ *host *=/!d; s/^ *host *= *//' | tail -1)
+ if [[ -n $T_HOST ]]; then
+ break
+ fi
+done
+if [[ -z $T_HOST ]]; then
+ skip_all 'fcm/t.cfg: "host" not defined'
+fi
+#-------------------------------------------------------------------------------
+tests 3
+#-------------------------------------------------------------------------------
+mkdir etc
+cat >etc/fcm-make.cfg <<'__CFG__'
+steps=build
+build.source=src
+build.target=hello.exe
+__CFG__
+
+T_HOST_WORK_DIR=$(ssh -oBatchMode=yes $T_HOST mktemp -d)
+rsync -a etc $T_HOST:$T_HOST_WORK_DIR
+rm -r etc
+
+mkdir src
+cat >src/hello.f90 <<'__FORTRAN__'
+program hello
+write(*, '(a)') 'Hello World!'
+end program hello
+__FORTRAN__
+
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE"
+run_pass "$TEST_KEY-pwd" fcm make -F $T_HOST:$T_HOST_WORK_DIR/etc
+file_test "$TEST_KEY.hello.exe" $PWD/build/bin/hello.exe
+$PWD/build/bin/hello.exe >"$TEST_KEY.hello.exe.out"
+file_cmp "$TEST_KEY.hello.exe.out" "$TEST_KEY.hello.exe.out" <<'__OUT__'
+Hello World!
+__OUT__
+#-------------------------------------------------------------------------------
+ssh -oBatchMode=yes $T_HOST rm -r $T_HOST_WORK_DIR
+exit 0
diff --git a/t/fcm-make/32-include-relative-cfg.t b/t/fcm-make/32-include-relative-cfg.t
new file mode 100755
index 0000000..132def8
--- /dev/null
+++ b/t/fcm-make/32-include-relative-cfg.t
@@ -0,0 +1,54 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+# Tests "fcm make", include relative config
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+tests 6
+#-------------------------------------------------------------------------------
+mkdir etc
+cat >etc/fcm-make-build.cfg <<'__CFG__'
+steps=build
+build.source=src
+build.target=hello.exe
+__CFG__
+
+mkdir src
+cat >src/hello.f90 <<'__FORTRAN__'
+program hello
+write(*, '(a)') 'Hello World!'
+end program hello
+__FORTRAN__
+
+#-------------------------------------------------------------------------------
+cat >fcm-make.cfg <<'__CFG__'
+include = fcm-make-build.cfg
+__CFG__
+
+fcm_make_build_hello_tests "$TEST_KEY_BASE-config-file-path" '.exe' -F $PWD/etc
+#-------------------------------------------------------------------------------
+cat >fcm-make.cfg <<'__CFG__'
+include-path=$HERE/etc
+include=fcm-make-build.cfg
+__CFG__
+
+fcm_make_build_hello_tests "$TEST_KEY_BASE-include-path" '.exe'
+#-------------------------------------------------------------------------------
+exit 0
diff --git a/t/fcm-make/33-include-relative-cfg-in-svn.t b/t/fcm-make/33-include-relative-cfg-in-svn.t
new file mode 100755
index 0000000..1eab85f
--- /dev/null
+++ b/t/fcm-make/33-include-relative-cfg-in-svn.t
@@ -0,0 +1,73 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+# Tests "fcm make", include relative config in a Subversion repository
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+tests 12
+#-------------------------------------------------------------------------------
+mkdir etc
+cat >etc/fcm-make-build.cfg <<'__CFG__'
+steps=build
+build.source=src
+build.target=hello.exe
+__CFG__
+
+mkdir src
+cat >src/hello.f90 <<'__FORTRAN__'
+program hello
+write(*, '(a)') 'Hello World!'
+end program hello
+__FORTRAN__
+
+svnadmin create svn-repos
+svn import -m 'test stuff' etc file://$PWD/svn-repos/etc
+rm -fr etc
+
+#-------------------------------------------------------------------------------
+cat >fcm-make.cfg <<'__CFG__'
+include = fcm-make-build.cfg
+__CFG__
+
+fcm_make_build_hello_tests \
+ "$TEST_KEY_BASE-config-file-path" '.exe' -F "file://$PWD/svn-repos/etc"
+#-------------------------------------------------------------------------------
+cat >fcm-make.cfg <<'__CFG__'
+include = fcm-make-build.cfg
+__CFG__
+
+fcm_make_build_hello_tests\
+ "$TEST_KEY_BASE-config-file-path-1" '.exe' -F "file://$PWD/svn-repos/etc@1"
+#-------------------------------------------------------------------------------
+cat >fcm-make.cfg <<'__CFG__'
+include-path=file://$PWD/svn-repos/etc
+include=fcm-make-build.cfg
+__CFG__
+
+fcm_make_build_hello_tests "$TEST_KEY_BASE-include-paths" '.exe'
+#-------------------------------------------------------------------------------
+cat >fcm-make.cfg <<'__CFG__'
+include-path=file://$PWD/svn-repos/etc@1
+include=fcm-make-build.cfg
+__CFG__
+
+fcm_make_build_hello_tests "$TEST_KEY_BASE-include-path-1" '.exe'
+#-------------------------------------------------------------------------------
+exit 0
diff --git a/t/fcm-make/34-include-relative-cfg-in-ssh.t b/t/fcm-make/34-include-relative-cfg-in-ssh.t
new file mode 100755
index 0000000..645e56d
--- /dev/null
+++ b/t/fcm-make/34-include-relative-cfg-in-ssh.t
@@ -0,0 +1,75 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+# Tests "fcm make", include relative config in a remote host accessible via SSH
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+# Get a remote host for testing
+T_HOST=
+for FILE in $HOME/.metomi/fcm/t.cfg $FCM_HOME/etc/fcm/t.cfg; do
+ if [[ ! -f $FILE || ! -r $FILE ]]; then
+ continue
+ fi
+ T_HOST=$(fcm cfg $FILE | sed '/^ *host *=/!d; s/^ *host *= *//' | tail -1)
+ if [[ -n $T_HOST ]]; then
+ break
+ fi
+done
+if [[ -z $T_HOST ]]; then
+ skip_all 'fcm/t.cfg: "host" not defined'
+fi
+#-------------------------------------------------------------------------------
+tests 6
+#-------------------------------------------------------------------------------
+mkdir etc
+cat >etc/fcm-make-build.cfg <<'__CFG__'
+steps=build
+build.source=src
+build.target=hello.exe
+__CFG__
+
+T_HOST_WORK_DIR=$(ssh -oBatchMode=yes $T_HOST mktemp -d)
+rsync -a etc $T_HOST:$T_HOST_WORK_DIR
+rm -r etc
+
+mkdir src
+cat >src/hello.f90 <<'__FORTRAN__'
+program hello
+write(*, '(a)') 'Hello World!'
+end program hello
+__FORTRAN__
+
+#-------------------------------------------------------------------------------
+cat >fcm-make.cfg <<'__CFG__'
+include = fcm-make-build.cfg
+__CFG__
+
+fcm_make_build_hello_tests "$TEST_KEY_BASE-config-file-path" '.exe' \
+ -F "$T_HOST:$T_HOST_WORK_DIR/etc"
+#-------------------------------------------------------------------------------
+cat >fcm-make.cfg <<__CFG__
+include-path=$T_HOST:$T_HOST_WORK_DIR/etc
+include=fcm-make-build.cfg
+__CFG__
+
+fcm_make_build_hello_tests "$TEST_KEY_BASE-include-paths" '.exe'
+#-------------------------------------------------------------------------------
+ssh -oBatchMode=yes $T_HOST rm -r $T_HOST_WORK_DIR
+exit 0
diff --git a/t/fcm-make/35-include-relative-cfg-in-2-dirs.t b/t/fcm-make/35-include-relative-cfg-in-2-dirs.t
new file mode 100755
index 0000000..1e0bb22
--- /dev/null
+++ b/t/fcm-make/35-include-relative-cfg-in-2-dirs.t
@@ -0,0 +1,62 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+# Tests "fcm make", include relative config
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+tests 6
+#-------------------------------------------------------------------------------
+mkdir cfg1 cfg2
+cat >cfg1/fcm-make-head.cfg <<'__CFG__'
+steps=build
+build.source=src
+__CFG__
+cat >cfg1/fcm-make-tail.cfg <<'__CFG__'
+build.target=hello.exe
+__CFG__
+cat >cfg2/fcm-make-tail.cfg <<'__CFG__'
+build.target=hello
+build.prop{file-ext.bin}=
+__CFG__
+
+mkdir src
+cat >src/hello.f90 <<'__FORTRAN__'
+program hello
+write(*, '(a)') 'Hello World!'
+end program hello
+__FORTRAN__
+
+#-------------------------------------------------------------------------------
+cat >fcm-make.cfg <<'__CFG__'
+include-path = $HERE/cfg1 $HERE/cfg2
+include = fcm-make-head.cfg fcm-make-tail.cfg
+__CFG__
+
+fcm_make_build_hello_tests "$TEST_KEY_BASE-1" '.exe'
+#-------------------------------------------------------------------------------
+cat >fcm-make.cfg <<'__CFG__'
+include-path = $HERE/cfg2
+include-path{+} = $HERE/cfg1
+include = fcm-make-head.cfg fcm-make-tail.cfg
+__CFG__
+
+fcm_make_build_hello_tests "$TEST_KEY_BASE-2" ''
+#-------------------------------------------------------------------------------
+exit 0
diff --git a/t/fcm-make/36-build-fail-cont-basic.t b/t/fcm-make/36-build-fail-cont-basic.t
new file mode 100644
index 0000000..b62af82
--- /dev/null
+++ b/t/fcm-make/36-build-fail-cont-basic.t
@@ -0,0 +1,135 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+# Tests "fcm make", build, continue on failure.
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+
+task_lines_from_log() {
+ sed '
+ /\[\(info\|FAIL\)\] \(compile\+\?\|ext-iface\|install\|link\) *\(----\|[0-9][0-9]*\.[0-9][0-9]*\) /!d
+ s/ [0-9][0-9]*\.[0-9][0-9]* / ???? /
+ ' fcm-make.log
+}
+
+fail_lines_from_log() {
+ sed '/\[FAIL\] !/!d' fcm-make.log
+}
+
+#-------------------------------------------------------------------------------
+tests 12
+#-------------------------------------------------------------------------------
+cp -r $TEST_SOURCE_DIR/$TEST_KEY_BASE/* .
+#-------------------------------------------------------------------------------
+# Break hello1 and hello2
+TEST_KEY="$TEST_KEY_BASE-1-2-bad"
+sed -i 's/implicit none/implicit non/' src/greet_mod.f90 # introduce typo
+run_fail "$TEST_KEY" fcm make --new
+task_lines_from_log >"$TEST_KEY-log-tasks"
+file_cmp "$TEST_KEY-log-tasks" "$TEST_KEY-log-tasks" <<'__LOG__'
+[FAIL] compile ???? ! greet_mod.o <- greet_mod.f90
+[info] compile ???? M world_mod.o <- world_mod.f90
+[FAIL] compile ---- ! hello.o <- hello.f90
+[FAIL] compile ---- ! hello2.o <- hello2.f90
+[info] compile ???? M hello_sub.o <- hello_sub.f90
+[info] ext-iface ???? M hello_sub.interface <- hello_sub.f90
+[info] compile ???? M hello3.o <- hello3.f90
+[info] link ???? M hello3 <- hello3.f90
+[info] compile ???? M hello4.o <- hello4.f90
+[info] link ???? M hello4 <- hello4.f90
+__LOG__
+fail_lines_from_log >"$TEST_KEY-log-fails"
+file_cmp "$TEST_KEY-log-fails" "$TEST_KEY-log-fails" <<'__LOG__'
+[FAIL] ! greet_mod.mod : depends on failed target: greet_mod.o
+[FAIL] ! greet_mod.o : update task failed
+[FAIL] ! hello : depends on failed target: hello.o
+[FAIL] ! hello.o : depends on failed target: greet_mod.mod
+[FAIL] ! hello2 : depends on failed target: hello2.o
+[FAIL] ! hello2.o : depends on failed target: greet_mod.mod
+__LOG__
+
+TEST_KEY="$TEST_KEY_BASE-1-2-fix"
+sed -i 's/implicit non/implicit none/' src/greet_mod.f90 # fix typo
+run_pass "$TEST_KEY" fcm make
+task_lines_from_log >"$TEST_KEY-log-tasks"
+file_cmp "$TEST_KEY-log-tasks" "$TEST_KEY-log-tasks" <<'__LOG__'
+[info] compile ???? M greet_mod.o <- greet_mod.f90
+[info] compile ---- U world_mod.o <- world_mod.f90
+[info] compile ???? M hello.o <- hello.f90
+[info] link ???? M hello <- hello.f90
+[info] compile ???? M hello2.o <- hello2.f90
+[info] link ???? M hello2 <- hello2.f90
+[info] compile ---- U hello_sub.o <- hello_sub.f90
+[info] ext-iface ---- U hello_sub.interface <- hello_sub.f90
+[info] compile ---- U hello3.o <- hello3.f90
+[info] link ---- U hello3 <- hello3.f90
+[info] compile ---- U hello4.o <- hello4.f90
+[info] link ---- U hello4 <- hello4.f90
+__LOG__
+fail_lines_from_log >"$TEST_KEY-log-fails"
+file_cmp "$TEST_KEY-log-fails" "$TEST_KEY-log-fails" </dev/null
+#-------------------------------------------------------------------------------
+# Break hello3 and hello4
+TEST_KEY="$TEST_KEY_BASE-3-4-bad"
+sed -i 's/implicit none/implicit non/' src/hello_sub.f90 # introduce typo
+run_fail "$TEST_KEY" fcm make --new
+task_lines_from_log >"$TEST_KEY-log-tasks"
+file_cmp "$TEST_KEY-log-tasks" "$TEST_KEY-log-tasks" <<'__LOG__'
+[info] compile ???? M greet_mod.o <- greet_mod.f90
+[info] compile ???? M world_mod.o <- world_mod.f90
+[info] compile ???? M hello.o <- hello.f90
+[info] link ???? M hello <- hello.f90
+[info] compile ???? M hello2.o <- hello2.f90
+[info] link ???? M hello2 <- hello2.f90
+[FAIL] compile ???? ! hello_sub.o <- hello_sub.f90
+[info] ext-iface ???? M hello_sub.interface <- hello_sub.f90
+[info] compile ???? M hello3.o <- hello3.f90
+[FAIL] link ---- ! hello3 <- hello3.f90
+[info] compile ???? M hello4.o <- hello4.f90
+[FAIL] link ---- ! hello4 <- hello4.f90
+__LOG__
+fail_lines_from_log >"$TEST_KEY-log-fails"
+file_cmp "$TEST_KEY-log-fails" "$TEST_KEY-log-fails" <<'__LOG__'
+[FAIL] ! hello3 : depends on failed target: hello_sub.o
+[FAIL] ! hello4 : depends on failed target: hello_sub.o
+[FAIL] ! hello_sub.o : update task failed
+__LOG__
+
+TEST_KEY="$TEST_KEY_BASE-3-4-fix"
+sed -i 's/implicit non/implicit none/' src/hello_sub.f90 # fix typo
+run_pass "$TEST_KEY" fcm make
+task_lines_from_log >"$TEST_KEY-log-tasks"
+file_cmp "$TEST_KEY-log-tasks" "$TEST_KEY-log-tasks" <<'__LOG__'
+[info] compile ---- U greet_mod.o <- greet_mod.f90
+[info] compile ---- U world_mod.o <- world_mod.f90
+[info] compile ---- U hello.o <- hello.f90
+[info] link ---- U hello <- hello.f90
+[info] compile ---- U hello2.o <- hello2.f90
+[info] link ---- U hello2 <- hello2.f90
+[info] compile ???? M hello_sub.o <- hello_sub.f90
+[info] ext-iface ???? U hello_sub.interface <- hello_sub.f90
+[info] compile ---- U hello3.o <- hello3.f90
+[info] link ???? M hello3 <- hello3.f90
+[info] compile ---- U hello4.o <- hello4.f90
+[info] link ???? M hello4 <- hello4.f90
+__LOG__
+fail_lines_from_log >"$TEST_KEY-log-fails"
+file_cmp "$TEST_KEY-log-fails" "$TEST_KEY-log-fails" </dev/null
+#-------------------------------------------------------------------------------
+exit 0
diff --git a/t/fcm-make/36-build-fail-cont-basic/fcm-make.cfg b/t/fcm-make/36-build-fail-cont-basic/fcm-make.cfg
new file mode 100644
index 0000000..9216a4d
--- /dev/null
+++ b/t/fcm-make/36-build-fail-cont-basic/fcm-make.cfg
@@ -0,0 +1,4 @@
+steps=build
+build.source=$HERE/src
+build.target{task}=link
+build.prop{file-ext.bin}=
diff --git a/t/fcm-make/36-build-fail-cont-basic/src/greet_mod.f90 b/t/fcm-make/36-build-fail-cont-basic/src/greet_mod.f90
new file mode 100644
index 0000000..a701b34
--- /dev/null
+++ b/t/fcm-make/36-build-fail-cont-basic/src/greet_mod.f90
@@ -0,0 +1,9 @@
+module greet_mod
+implicit none
+character(*), parameter :: greet_word = 'Hello'
+contains
+subroutine greet(world)
+character(*), intent(in) :: world
+write(*, '(a,1x,a)') greet_word, world
+end subroutine greet
+end module greet_mod
diff --git a/t/fcm-make/36-build-fail-cont-basic/src/hello.f90 b/t/fcm-make/36-build-fail-cont-basic/src/hello.f90
new file mode 100644
index 0000000..49c2820
--- /dev/null
+++ b/t/fcm-make/36-build-fail-cont-basic/src/hello.f90
@@ -0,0 +1,6 @@
+program hello
+use greet_mod, only: greet
+use world_mod, only: world
+implicit none
+call greet(world)
+end program hello
diff --git a/t/fcm-make/36-build-fail-cont-basic/src/hello2.f90 b/t/fcm-make/36-build-fail-cont-basic/src/hello2.f90
new file mode 100644
index 0000000..0f0a921
--- /dev/null
+++ b/t/fcm-make/36-build-fail-cont-basic/src/hello2.f90
@@ -0,0 +1,6 @@
+program hello2
+use greet_mod, only: greet
+use world_mod, only: world
+implicit none
+call greet(trim(world))
+end program hello2
diff --git a/t/fcm-make/36-build-fail-cont-basic/src/hello3.f90 b/t/fcm-make/36-build-fail-cont-basic/src/hello3.f90
new file mode 100644
index 0000000..2b0cc73
--- /dev/null
+++ b/t/fcm-make/36-build-fail-cont-basic/src/hello3.f90
@@ -0,0 +1,6 @@
+program hello3
+use world_mod, only: world
+implicit none
+include 'hello_sub.interface'
+call hello_sub(world)
+end program hello3
diff --git a/t/fcm-make/36-build-fail-cont-basic/src/hello4.f90 b/t/fcm-make/36-build-fail-cont-basic/src/hello4.f90
new file mode 100644
index 0000000..7a88936
--- /dev/null
+++ b/t/fcm-make/36-build-fail-cont-basic/src/hello4.f90
@@ -0,0 +1,5 @@
+program hello4
+implicit none
+include 'hello_sub.interface'
+call hello_sub('Earth')
+end program hello4
diff --git a/t/fcm-make/36-build-fail-cont-basic/src/hello_sub.f90 b/t/fcm-make/36-build-fail-cont-basic/src/hello_sub.f90
new file mode 100644
index 0000000..c854671
--- /dev/null
+++ b/t/fcm-make/36-build-fail-cont-basic/src/hello_sub.f90
@@ -0,0 +1,5 @@
+subroutine hello_sub(world)
+implicit none
+character(*), intent(in) :: world
+write(*, '(a)'), 'Hello ' // trim(world)
+end subroutine hello_sub
diff --git a/t/fcm-make/36-build-fail-cont-basic/src/world_mod.f90 b/t/fcm-make/36-build-fail-cont-basic/src/world_mod.f90
new file mode 100644
index 0000000..9f9fb72
--- /dev/null
+++ b/t/fcm-make/36-build-fail-cont-basic/src/world_mod.f90
@@ -0,0 +1,4 @@
+module world_mod
+implicit none
+character(*), parameter :: world = 'Earth'
+end module world_mod
diff --git a/t/fcm-make/test_header b/t/fcm-make/test_header
new file mode 120000
index 0000000..90bd5a3
--- /dev/null
+++ b/t/fcm-make/test_header
@@ -0,0 +1 @@
+../lib/bash/test_header
\ No newline at end of file
diff --git a/t/fcm-merge/00-simple.t b/t/fcm-merge/00-simple.t
new file mode 100644
index 0000000..7e3b1d7
--- /dev/null
+++ b/t/fcm-merge/00-simple.t
@@ -0,0 +1,321 @@
+#!/bin/bash
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# Basic tests for "fcm merge".
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+tests 18
+#-------------------------------------------------------------------------------
+setup
+init_repos
+init_merge_branches merge1 merge2 $REPOS_URL
+cd $TEST_DIR/wc
+#-------------------------------------------------------------------------------
+# Tests fcm merge --dry-run
+TEST_KEY=$TEST_KEY_BASE-dry-run
+export SVN_EDITOR="sed -i 1i\foo"
+run_pass "$TEST_KEY" fcm merge --dry-run $ROOT_URL/branches/dev/Share/merge1
+if $SVN_VERSION_IS_16; then
+ START_REV=2
+else
+ START_REV=4
+fi
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+Eligible merge(s) from /${PROJECT}branches/dev/Share/merge1 at 9: 5
+--------------------------------------------------------------------------------
+Merge: /${PROJECT}branches/dev/Share/merge1 at 5
+ c.f.: /${PROJECT}trunk at 1
+-------------------------------------------------------------------------dry-run
+--- Merging r$START_REV through r5 into '.':
+U subroutine/hello_sub_dummy.h
+A added_file
+A added_directory
+A added_directory/hello_constants_dummy.inc
+A added_directory/hello_constants.inc
+A added_directory/hello_constants.f90
+A module/tree_conflict_file
+U module/hello_constants_dummy.inc
+U module/hello_constants.inc
+U module/hello_constants.f90
+U lib/python/info/poems.py
+-------------------------------------------------------------------------dry-run
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests svn status result of fcm merge --dry-run
+TEST_KEY=$TEST_KEY_BASE-dry-run-status
+run_pass "$TEST_KEY" svn status --config-dir=$TEST_DIR/.subversion/
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+? unversioned_file
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests svn diff result of fcm merge --dry-run
+TEST_KEY=$TEST_KEY_BASE-dry-run-diff
+run_pass "$TEST_KEY" svn diff
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm merge --non-interactive
+TEST_KEY=$TEST_KEY_BASE-non-interactive
+export SVN_EDITOR="sed -i 1i\foo"
+run_pass "$TEST_KEY" fcm merge --non-interactive $ROOT_URL/branches/dev/Share/merge1
+if $SVN_VERSION_IS_16; then
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+Eligible merge(s) from /${PROJECT}branches/dev/Share/merge1 at 9: 5
+--------------------------------------------------------------------------------
+Merge: /${PROJECT}branches/dev/Share/merge1 at 5
+ c.f.: /${PROJECT}trunk at 1
+Merge succeeded.
+__OUT__
+else
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+Eligible merge(s) from /${PROJECT}branches/dev/Share/merge1 at 9: 5
+--------------------------------------------------------------------------------
+Merge: /${PROJECT}branches/dev/Share/merge1 at 5
+ c.f.: /${PROJECT}trunk at 1
+Merge succeeded.
+--------------------------------------------------------------------------actual
+--- Merging r$START_REV through r5 into '.':
+U subroutine/hello_sub_dummy.h
+A added_file
+A added_directory
+A added_directory/hello_constants_dummy.inc
+A added_directory/hello_constants.inc
+A added_directory/hello_constants.f90
+A module/tree_conflict_file
+U module/hello_constants_dummy.inc
+U module/hello_constants.inc
+U module/hello_constants.f90
+U lib/python/info/poems.py
+--- Recording mergeinfo for merge of r$START_REV through r5 into '.':
+ U .
+--------------------------------------------------------------------------actual
+__OUT__
+fi
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests svn status result of fcm merge --non-interactive
+TEST_KEY=$TEST_KEY_BASE-non-interactive-status
+run_pass "$TEST_KEY" svn status --config-dir=$TEST_DIR/.subversion/
+sort $TEST_DIR/"$TEST_KEY.out" -o $TEST_DIR/"$TEST_KEY.out"
+if $SVN_VERSION_IS_16; then
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+ M .
+? unversioned_file
+A + added_directory
+A + added_directory/hello_constants.f90
+A + added_directory/hello_constants.inc
+A + added_directory/hello_constants_dummy.inc
+A + added_file
+A + module/tree_conflict_file
+M lib/python/info/poems.py
+M module/hello_constants.f90
+M module/hello_constants.inc
+M module/hello_constants_dummy.inc
+M subroutine/hello_sub_dummy.h
+__OUT__
+else
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+ M .
+? unversioned_file
+A + added_directory
+A + added_file
+A + module/tree_conflict_file
+M lib/python/info/poems.py
+M module/hello_constants.f90
+M module/hello_constants.inc
+M module/hello_constants_dummy.inc
+M subroutine/hello_sub_dummy.h
+__OUT__
+fi
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests svn diff result of fcm merge --non-interactive
+TEST_KEY=$TEST_KEY_BASE-non-interactive-diff
+run_pass "$TEST_KEY" svn diff
+if $SVN_VERSION_IS_16; then
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+
+Property changes on: .
+___________________________________________________________________
+Added: svn:mergeinfo
+ Merged /${PROJECT}branches/dev/Share/merge1:r4-5
+
+Index: subroutine/hello_sub_dummy.h
+===================================================================
+--- subroutine/hello_sub_dummy.h (revision 9)
++++ subroutine/hello_sub_dummy.h (working copy)
+@@ -1 +1,2 @@
+ #include "hello_sub.h"
++Modified a line
+Index: module/hello_constants_dummy.inc
+===================================================================
+--- module/hello_constants_dummy.inc (revision 9)
++++ module/hello_constants_dummy.inc (working copy)
+@@ -1 +1 @@
+-INCLUDE 'hello_constants.inc'
++INCLUDE 'hello_constants.INc'
+Index: module/hello_constants.inc
+===================================================================
+--- module/hello_constants.inc (revision 9)
++++ module/hello_constants.inc (working copy)
+@@ -1 +1,2 @@
+-CHARACTER (LEN=80), PARAMETER :: hello_string = 'Hello Earth!'
++CHARACTER (
++LEN=80), PARAMETER :: hello_strINg = 'Hello Earth!!'
+Index: module/hello_constants.f90
+===================================================================
+--- module/hello_constants.f90 (revision 9)
++++ module/hello_constants.f90 (working copy)
+@@ -1,5 +1,5 @@
+ MODULE Hello_Constants
+
+-INCLUDE 'hello_constants_dummy.inc'
++INCLUDE 'hello_constants_dummy.INc'
+
+ END MODULE Hello_Constants
+Index: lib/python/info/poems.py
+===================================================================
+--- lib/python/info/poems.py (revision 9)
++++ lib/python/info/poems.py (working copy)
+@@ -1,24 +1,23 @@
+-#!/usr/bin/env python
+-# -*- coding: utf-8 -*-
+ """The Python, by Hilaire Belloc
+
+ A Python I should not advise,--
+-It needs a doctor for its eyes,
++It needs a doctor FOR its eyes,
+ And has the measles yearly.
+-However, if you feel inclined
+-To get one (to improve your mind,
++However, if you feel INclINed
++To get one (
++to improve your mINd,
+ And not from fashion merely),
+ Allow no music near its cage;
+-And when it flies into a rage
++And when it flies INto a rage
+ Chastise it, most severely.
+-I had an aunt in Yucatan
++I had an aunt IN Yucatan
+ Who bought a Python from a man
+-And kept it for a pet.
++And kept it FOR a pet.
+ She died, because she never knew
+ These simple little rules and few;--
+-The Snake is living yet.
++The Snake is livINg yet.
+ """
+
+ import this
+
+-print "\n", __doc__
++prINt "\n", __doc__
+__OUT__
+else
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+Index: lib/python/info/poems.py
+===================================================================
+--- lib/python/info/poems.py (revision 9)
++++ lib/python/info/poems.py (working copy)
+@@ -1,24 +1,23 @@
+-#!/usr/bin/env python
+-# -*- coding: utf-8 -*-
+ """The Python, by Hilaire Belloc
+
+ A Python I should not advise,--
+-It needs a doctor for its eyes,
++It needs a doctor FOR its eyes,
+ And has the measles yearly.
+-However, if you feel inclined
+-To get one (to improve your mind,
++However, if you feel INclINed
++To get one (
++to improve your mINd,
+ And not from fashion merely),
+ Allow no music near its cage;
+-And when it flies into a rage
++And when it flies INto a rage
+ Chastise it, most severely.
+-I had an aunt in Yucatan
++I had an aunt IN Yucatan
+ Who bought a Python from a man
+-And kept it for a pet.
++And kept it FOR a pet.
+ She died, because she never knew
+ These simple little rules and few;--
+-The Snake is living yet.
++The Snake is livINg yet.
+ """
+
+ import this
+
+-print "\n", __doc__
++prINt "\n", __doc__
+Index: module/hello_constants.f90
+===================================================================
+--- module/hello_constants.f90 (revision 9)
++++ module/hello_constants.f90 (working copy)
+@@ -1,5 +1,5 @@
+ MODULE Hello_Constants
+
+-INCLUDE 'hello_constants_dummy.inc'
++INCLUDE 'hello_constants_dummy.INc'
+
+ END MODULE Hello_Constants
+Index: module/hello_constants.inc
+===================================================================
+--- module/hello_constants.inc (revision 9)
++++ module/hello_constants.inc (working copy)
+@@ -1 +1,2 @@
+-CHARACTER (LEN=80), PARAMETER :: hello_string = 'Hello Earth!'
++CHARACTER (
++LEN=80), PARAMETER :: hello_strINg = 'Hello Earth!!'
+Index: module/hello_constants_dummy.inc
+===================================================================
+--- module/hello_constants_dummy.inc (revision 9)
++++ module/hello_constants_dummy.inc (working copy)
+@@ -1 +1 @@
+-INCLUDE 'hello_constants.inc'
++INCLUDE 'hello_constants.INc'
+Index: subroutine/hello_sub_dummy.h
+===================================================================
+--- subroutine/hello_sub_dummy.h (revision 9)
++++ subroutine/hello_sub_dummy.h (working copy)
+@@ -1 +1,2 @@
+ #include "hello_sub.h"
++Modified a line
+Index: .
+===================================================================
+--- . (revision 9)
++++ . (working copy)
+
+Property changes on: .
+___________________________________________________________________
+Added: svn:mergeinfo
+ Merged /${PROJECT}branches/dev/Share/merge1:r4-5
+__OUT__
+fi
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+teardown
+#-------------------------------------------------------------------------------
diff --git a/t/fcm-merge/01-complex.t b/t/fcm-merge/01-complex.t
new file mode 100644
index 0000000..66fb4f6
--- /dev/null
+++ b/t/fcm-merge/01-complex.t
@@ -0,0 +1,1646 @@
+#!/bin/bash
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# More complex tests for "fcm merge".
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+tests 90
+#-------------------------------------------------------------------------------
+setup
+init_repos
+init_merge_branches merge1 merge2 $REPOS_URL
+export SVN_EDITOR="sed -i 1i\foo"
+cd $TEST_DIR/wc
+#-------------------------------------------------------------------------------
+# Tests fcm merge of trunk-into-branch (1)
+svn switch -q $ROOT_URL/branches/dev/Share/merge1
+TEST_KEY=$TEST_KEY_BASE-trunk-into-branch-1-non-root
+cd module
+run_pass "$TEST_KEY" fcm merge --non-interactive trunk
+if $SVN_VERSION_IS_16; then
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+$TEST_DIR/wc: working directory changed to top of working copy.
+Eligible merge(s) from /${PROJECT}trunk at 9: 9 8
+--------------------------------------------------------------------------------
+Merge: /${PROJECT}trunk at 9
+ c.f.: /${PROJECT}trunk at 1
+Merge succeeded.
+__OUT__
+else
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+$TEST_DIR/wc: working directory changed to top of working copy.
+Eligible merge(s) from /${PROJECT}trunk at 9: 9 8
+--------------------------------------------------------------------------------
+Merge: /${PROJECT}trunk at 9
+ c.f.: /${PROJECT}trunk at 1
+Merge succeeded.
+--------------------------------------------------------------------------actual
+--- Merging r2 through r9 into '.':
+U lib/python/info/__init__.py
+--- Recording mergeinfo for merge of r2 through r9 into '.':
+ U .
+--------------------------------------------------------------------------actual
+__OUT__
+fi
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+cd ..
+#-------------------------------------------------------------------------------
+# Tests svn status result of fcm merge (1)
+TEST_KEY=$TEST_KEY_BASE-trunk-into-branch-1-status
+run_pass "$TEST_KEY" svn status --config-dir=$TEST_DIR/.subversion/
+sort $TEST_DIR/"$TEST_KEY.out" -o $TEST_DIR/"$TEST_KEY.out"
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+ M .
+? unversioned_file
+M lib/python/info/__init__.py
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests svn diff result of fcm merge (1)
+TEST_KEY=$TEST_KEY_BASE-trunk-into-branch-1-diff
+run_pass "$TEST_KEY" svn diff
+if $SVN_VERSION_IS_16; then
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+
+Property changes on: .
+___________________________________________________________________
+Added: svn:mergeinfo
+ Merged /${PROJECT}trunk:r2-9
+
+Index: lib/python/info/__init__.py
+===================================================================
+--- lib/python/info/__init__.py (revision 9)
++++ lib/python/info/__init__.py (working copy)
+@@ -0,0 +1,2 @@
++trunk change
++another trunk change
+__OUT__
+else
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+Index: lib/python/info/__init__.py
+===================================================================
+--- lib/python/info/__init__.py (revision 9)
++++ lib/python/info/__init__.py (working copy)
+@@ -0,0 +1,2 @@
++trunk change
++another trunk change
+Index: .
+===================================================================
+--- . (revision 9)
++++ . (working copy)
+
+Property changes on: .
+___________________________________________________________________
+Added: svn:mergeinfo
+ Merged /${PROJECT}trunk:r2-9
+__OUT__
+fi
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm commit of fcm merge (1)
+TEST_KEY=$TEST_KEY_BASE-trunk-into-branch-1-commit
+run_pass "$TEST_KEY" fcm commit <<__IN__
+y
+__IN__
+if $SVN_VERSION_IS_16; then
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+[info] sed -i 1i\foo: starting commit message editor...
+Change summary:
+--------------------------------------------------------------------------------
+[Root : $REPOS_URL]
+[Project: ${TEST_PROJECT:-}]
+[Branch : branches/dev/Share/merge1]
+[Sub-dir: ]
+
+ M .
+M lib/python/info/__init__.py
+--------------------------------------------------------------------------------
+Commit message is as follows:
+--------------------------------------------------------------------------------
+foo
+Merged into /${PROJECT}branches/dev/Share/merge1: /${PROJECT}trunk at 9 cf. /${PROJECT}trunk at 1
+--------------------------------------------------------------------------------
+
+*** WARNING: YOU ARE COMMITTING TO A Share BRANCH.
+*** Please ensure that you have the owner's permission.
+
+Would you like to commit this change?
+Enter "y" or "n" (or just press <return> for "n"): Sending .
+Sending lib/python/info/__init__.py
+Transmitting file data .
+Committed revision 10.
+At revision 10.
+__OUT__
+else
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+[info] sed -i 1i\foo: starting commit message editor...
+Change summary:
+--------------------------------------------------------------------------------
+[Root : $REPOS_URL]
+[Project: ${TEST_PROJECT:-}]
+[Branch : branches/dev/Share/merge1]
+[Sub-dir: ]
+
+ M .
+M lib/python/info/__init__.py
+--------------------------------------------------------------------------------
+Commit message is as follows:
+--------------------------------------------------------------------------------
+foo
+Merged into /${PROJECT}branches/dev/Share/merge1: /${PROJECT}trunk at 9 cf. /${PROJECT}trunk at 1
+--------------------------------------------------------------------------------
+
+*** WARNING: YOU ARE COMMITTING TO A Share BRANCH.
+*** Please ensure that you have the owner's permission.
+
+Would you like to commit this change?
+Enter "y" or "n" (or just press <return> for "n"): Sending .
+Sending lib/python/info/__init__.py
+Transmitting file data .
+Committed revision 10.
+Updating '.':
+At revision 10.
+__OUT__
+fi
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm log of fcm merge (1)
+TEST_KEY=$TEST_KEY_BASE-trunk-into-branch-1-log
+run_pass "$TEST_KEY" fcm log
+sed -i "s/\(.*|.*|\).*\(|.*\)$/\1 date \2/g" $TEST_DIR/$TEST_KEY.out
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+------------------------------------------------------------------------
+r10 | $LOGNAME | date | 3 lines
+
+foo
+Merged into /${PROJECT}branches/dev/Share/merge1: /${PROJECT}trunk at 9 cf. /${PROJECT}trunk at 1
+
+------------------------------------------------------------------------
+r5 | $LOGNAME | date | 1 line
+
+Made changes for future merge of this branch
+------------------------------------------------------------------------
+r4 | $LOGNAME | date | 1 line
+
+Made a branch Created /${PROJECT}branches/dev/Share/merge1 from /trunk at 1.
+------------------------------------------------------------------------
+r1 | $LOGNAME | date | 1 line
+
+initial trunk import
+------------------------------------------------------------------------
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm merge of branch-into-trunk (1)
+TEST_KEY=$TEST_KEY_BASE-branch-into-trunk-1
+BRANCH_MOD_FILE=$(find . -type f | sed "/\.svn/d" | sort | head -3| tail -1)
+echo "# added this line for simple repeat testing" >>$BRANCH_MOD_FILE
+svn commit -q -m "edit on branch for merge repeat test"
+svn update -q
+cd $TEST_DIR
+rm -rf $TEST_DIR/wc
+mkdir $TEST_DIR/wc
+svn checkout -q $ROOT_URL/trunk $TEST_DIR/wc
+cd $TEST_DIR/wc
+run_pass "$TEST_KEY" fcm merge --non-interactive branches/dev/Share/merge1
+if $SVN_VERSION_IS_16; then
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+Eligible merge(s) from /${PROJECT}branches/dev/Share/merge1 at 11: 11 10
+--------------------------------------------------------------------------------
+Merge: /${PROJECT}branches/dev/Share/merge1 at 11
+ c.f.: /${PROJECT}trunk at 9
+Merge succeeded.
+__OUT__
+else
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+Eligible merge(s) from /${PROJECT}branches/dev/Share/merge1 at 11: 11 10
+--------------------------------------------------------------------------------
+Merge: /${PROJECT}branches/dev/Share/merge1 at 11
+ c.f.: /${PROJECT}trunk at 9
+Merge succeeded.
+--------------------------------------------------------------------------actual
+--- Merging differences between repository URLs into '.':
+U subroutine/hello_sub_dummy.h
+A added_file
+A added_directory
+A added_directory/hello_constants_dummy.inc
+A added_directory/hello_constants.inc
+A added_directory/hello_constants.f90
+A module/tree_conflict_file
+U module/hello_constants_dummy.inc
+U module/hello_constants.inc
+U module/hello_constants.f90
+U lib/python/info/poems.py
+--- Recording mergeinfo for merge between repository URLs into '.':
+ U .
+--------------------------------------------------------------------------actual
+__OUT__
+fi
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests svn status result of fcm merge (1)
+TEST_KEY=$TEST_KEY_BASE-branch-into-trunk-1-status
+run_pass "$TEST_KEY" svn status --config-dir=$TEST_DIR/.subversion/
+if $SVN_VERSION_IS_16; then
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+ M .
+M subroutine/hello_sub_dummy.h
+A + added_file
+A + module/tree_conflict_file
+M module/hello_constants_dummy.inc
+M module/hello_constants.inc
+M module/hello_constants.f90
+A + added_directory
+A + added_directory/hello_constants_dummy.inc
+A + added_directory/hello_constants.inc
+A + added_directory/hello_constants.f90
+M lib/python/info/poems.py
+__OUT__
+else
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+ M .
+A + added_directory
+A + added_file
+M lib/python/info/poems.py
+M module/hello_constants.f90
+M module/hello_constants.inc
+M module/hello_constants_dummy.inc
+A + module/tree_conflict_file
+M subroutine/hello_sub_dummy.h
+__OUT__
+fi
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests svn diff result of fcm merge (1)
+TEST_KEY=$TEST_KEY_BASE-branch-into-trunk-1-diff
+run_pass "$TEST_KEY" svn diff
+if $SVN_VERSION_IS_16; then
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+
+Property changes on: .
+___________________________________________________________________
+Added: svn:mergeinfo
+ Merged /${PROJECT}branches/dev/Share/merge1:r2-11
+
+Index: subroutine/hello_sub_dummy.h
+===================================================================
+--- subroutine/hello_sub_dummy.h (revision 11)
++++ subroutine/hello_sub_dummy.h (working copy)
+@@ -1 +1,2 @@
+ #include "hello_sub.h"
++Modified a line
+Index: module/hello_constants_dummy.inc
+===================================================================
+--- module/hello_constants_dummy.inc (revision 11)
++++ module/hello_constants_dummy.inc (working copy)
+@@ -1 +1 @@
+-INCLUDE 'hello_constants.inc'
++INCLUDE 'hello_constants.INc'
+Index: module/hello_constants.inc
+===================================================================
+--- module/hello_constants.inc (revision 11)
++++ module/hello_constants.inc (working copy)
+@@ -1 +1,2 @@
+-CHARACTER (LEN=80), PARAMETER :: hello_string = 'Hello Earth!'
++CHARACTER (
++LEN=80), PARAMETER :: hello_strINg = 'Hello Earth!!'
+Index: module/hello_constants.f90
+===================================================================
+--- module/hello_constants.f90 (revision 11)
++++ module/hello_constants.f90 (working copy)
+@@ -1,5 +1,5 @@
+ MODULE Hello_Constants
+
+-INCLUDE 'hello_constants_dummy.inc'
++INCLUDE 'hello_constants_dummy.INc'
+
+ END MODULE Hello_Constants
+Index: lib/python/info/poems.py
+===================================================================
+--- lib/python/info/poems.py (revision 11)
++++ lib/python/info/poems.py (working copy)
+@@ -1,24 +1,23 @@
+-#!/usr/bin/env python
+-# -*- coding: utf-8 -*-
+ """The Python, by Hilaire Belloc
+
+ A Python I should not advise,--
+-It needs a doctor for its eyes,
++It needs a doctor FOR its eyes,
+ And has the measles yearly.
+-However, if you feel inclined
+-To get one (to improve your mind,
++However, if you feel INclINed
++To get one (
++to improve your mINd,
+ And not from fashion merely),
+ Allow no music near its cage;
+-And when it flies into a rage
++And when it flies INto a rage
+ Chastise it, most severely.
+-I had an aunt in Yucatan
++I had an aunt IN Yucatan
+ Who bought a Python from a man
+-And kept it for a pet.
++And kept it FOR a pet.
+ She died, because she never knew
+ These simple little rules and few;--
+-The Snake is living yet.
++The Snake is livINg yet.
+ """
+
+ import this
+
+-print "\n", __doc__
++prINt "\n", __doc__
+__OUT__
+else
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+Index: lib/python/info/poems.py
+===================================================================
+--- lib/python/info/poems.py (revision 11)
++++ lib/python/info/poems.py (working copy)
+@@ -1,24 +1,23 @@
+-#!/usr/bin/env python
+-# -*- coding: utf-8 -*-
+ """The Python, by Hilaire Belloc
+
+ A Python I should not advise,--
+-It needs a doctor for its eyes,
++It needs a doctor FOR its eyes,
+ And has the measles yearly.
+-However, if you feel inclined
+-To get one (to improve your mind,
++However, if you feel INclINed
++To get one (
++to improve your mINd,
+ And not from fashion merely),
+ Allow no music near its cage;
+-And when it flies into a rage
++And when it flies INto a rage
+ Chastise it, most severely.
+-I had an aunt in Yucatan
++I had an aunt IN Yucatan
+ Who bought a Python from a man
+-And kept it for a pet.
++And kept it FOR a pet.
+ She died, because she never knew
+ These simple little rules and few;--
+-The Snake is living yet.
++The Snake is livINg yet.
+ """
+
+ import this
+
+-print "\n", __doc__
++prINt "\n", __doc__
+Index: module/hello_constants.f90
+===================================================================
+--- module/hello_constants.f90 (revision 11)
++++ module/hello_constants.f90 (working copy)
+@@ -1,5 +1,5 @@
+ MODULE Hello_Constants
+
+-INCLUDE 'hello_constants_dummy.inc'
++INCLUDE 'hello_constants_dummy.INc'
+
+ END MODULE Hello_Constants
+Index: module/hello_constants.inc
+===================================================================
+--- module/hello_constants.inc (revision 11)
++++ module/hello_constants.inc (working copy)
+@@ -1 +1,2 @@
+-CHARACTER (LEN=80), PARAMETER :: hello_string = 'Hello Earth!'
++CHARACTER (
++LEN=80), PARAMETER :: hello_strINg = 'Hello Earth!!'
+Index: module/hello_constants_dummy.inc
+===================================================================
+--- module/hello_constants_dummy.inc (revision 11)
++++ module/hello_constants_dummy.inc (working copy)
+@@ -1 +1 @@
+-INCLUDE 'hello_constants.inc'
++INCLUDE 'hello_constants.INc'
+Index: subroutine/hello_sub_dummy.h
+===================================================================
+--- subroutine/hello_sub_dummy.h (revision 11)
++++ subroutine/hello_sub_dummy.h (working copy)
+@@ -1 +1,2 @@
+ #include "hello_sub.h"
++Modified a line
+Index: .
+===================================================================
+--- . (revision 11)
++++ . (working copy)
+
+Property changes on: .
+___________________________________________________________________
+Added: svn:mergeinfo
+ Merged /${PROJECT}branches/dev/Share/merge1:r4-11
+__OUT__
+fi
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm commit of fcm merge (1)
+TEST_KEY=$TEST_KEY_BASE-branch-into-trunk-1-commit
+run_pass "$TEST_KEY" fcm commit <<__IN__
+y
+__IN__
+if $SVN_VERSION_IS_16; then
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+[info] sed -i 1i\foo: starting commit message editor...
+Change summary:
+--------------------------------------------------------------------------------
+[Root : $REPOS_URL]
+[Project: ${TEST_PROJECT:-}]
+[Branch : trunk]
+[Sub-dir: ]
+
+ M .
+M subroutine/hello_sub_dummy.h
+A + added_file
+A + module/tree_conflict_file
+M module/hello_constants_dummy.inc
+M module/hello_constants.inc
+M module/hello_constants.f90
+A + added_directory
+A + added_directory/hello_constants_dummy.inc
+A + added_directory/hello_constants.inc
+A + added_directory/hello_constants.f90
+M lib/python/info/poems.py
+--------------------------------------------------------------------------------
+Commit message is as follows:
+--------------------------------------------------------------------------------
+foo
+Merged into /${PROJECT}trunk: /${PROJECT}branches/dev/Share/merge1 at 11 cf. /${PROJECT}trunk at 9
+--------------------------------------------------------------------------------
+
+*** WARNING: YOU ARE COMMITTING TO THE TRUNK.
+*** Please ensure that your change conforms to your project's working practices.
+
+Would you like to commit this change?
+Enter "y" or "n" (or just press <return> for "n"): Sending .
+Adding added_directory
+Adding added_directory/hello_constants.f90
+Adding added_directory/hello_constants.inc
+Adding added_directory/hello_constants_dummy.inc
+Adding added_file
+Sending lib/python/info/poems.py
+Sending module/hello_constants.f90
+Sending module/hello_constants.inc
+Sending module/hello_constants_dummy.inc
+Adding module/tree_conflict_file
+Sending subroutine/hello_sub_dummy.h
+Transmitting file data .....
+Committed revision 12.
+At revision 12.
+__OUT__
+else
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+[info] sed -i 1i\foo: starting commit message editor...
+Change summary:
+--------------------------------------------------------------------------------
+[Root : $REPOS_URL]
+[Project: ${TEST_PROJECT:-}]
+[Branch : trunk]
+[Sub-dir: ]
+
+ M .
+A + added_directory
+A + added_file
+M lib/python/info/poems.py
+M module/hello_constants.f90
+M module/hello_constants.inc
+M module/hello_constants_dummy.inc
+A + module/tree_conflict_file
+M subroutine/hello_sub_dummy.h
+--------------------------------------------------------------------------------
+Commit message is as follows:
+--------------------------------------------------------------------------------
+foo
+Merged into /${PROJECT}trunk: /${PROJECT}branches/dev/Share/merge1 at 11 cf. /${PROJECT}trunk at 9
+--------------------------------------------------------------------------------
+
+*** WARNING: YOU ARE COMMITTING TO THE TRUNK.
+*** Please ensure that your change conforms to your project's working practices.
+
+Would you like to commit this change?
+Enter "y" or "n" (or just press <return> for "n"): Sending .
+Adding added_directory
+Adding added_file
+Sending lib/python/info/poems.py
+Sending module/hello_constants.f90
+Sending module/hello_constants.inc
+Sending module/hello_constants_dummy.inc
+Adding module/tree_conflict_file
+Sending subroutine/hello_sub_dummy.h
+Transmitting file data .....
+Committed revision 12.
+Updating '.':
+At revision 12.
+__OUT__
+fi
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm log of fcm merge branch-into-trunk (1)
+TEST_KEY=$TEST_KEY_BASE-branch-into-trunk-1-log
+run_pass "$TEST_KEY" fcm log
+sed -i "s/\(.*|.*|\).*\(|.*\)$/\1 date \2/g" $TEST_DIR/$TEST_KEY.out
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+------------------------------------------------------------------------
+r12 | $LOGNAME | date | 3 lines
+
+foo
+Merged into /${PROJECT}trunk: /${PROJECT}branches/dev/Share/merge1 at 11 cf. /${PROJECT}trunk at 9
+
+------------------------------------------------------------------------
+r9 | $LOGNAME | date | 1 line
+
+Made another trunk change
+------------------------------------------------------------------------
+r8 | $LOGNAME | date | 1 line
+
+Made trunk change
+------------------------------------------------------------------------
+r1 | $LOGNAME | date | 1 line
+
+initial trunk import
+------------------------------------------------------------------------
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm merge of branch-into-trunk (2)
+svn switch -q $ROOT_URL/branches/dev/Share/merge1
+MOD_FILE=$(find . -type f | sed "/\.svn/d" | sort | head -4 | tail -1)
+echo "call_extra_feature()" >>$MOD_FILE
+svn commit -q -m "Made branch change to add extra feature"
+svn update -q
+svn switch -q $ROOT_URL/trunk
+TEST_KEY=$TEST_KEY_BASE-branch-into-trunk-2
+run_pass "$TEST_KEY" fcm merge --non-interactive branches/dev/Share/merge1
+if $SVN_VERSION_IS_16; then
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+Eligible merge(s) from /${PROJECT}branches/dev/Share/merge1 at 13: 13
+--------------------------------------------------------------------------------
+Merge: /${PROJECT}branches/dev/Share/merge1 at 13
+ c.f.: /${PROJECT}branches/dev/Share/merge1 at 11
+Merge succeeded.
+__OUT__
+else
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+Eligible merge(s) from /${PROJECT}branches/dev/Share/merge1 at 13: 13
+--------------------------------------------------------------------------------
+Merge: /${PROJECT}branches/dev/Share/merge1 at 13
+ c.f.: /${PROJECT}branches/dev/Share/merge1 at 11
+Merge succeeded.
+--------------------------------------------------------------------------actual
+--- Merging r12 through r13 into '.':
+U added_file
+--- Recording mergeinfo for merge of r12 through r13 into '.':
+ U .
+--------------------------------------------------------------------------actual
+__OUT__
+fi
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests svn status result of fcm merge branch-into-trunk (2)
+TEST_KEY=$TEST_KEY_BASE-branch-into-trunk-2-status
+run_pass "$TEST_KEY" svn status --config-dir=$TEST_DIR/.subversion/
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+ M .
+M added_file
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests svn diff result of fcm merge branch-into-trunk (2)
+TEST_KEY=$TEST_KEY_BASE-branch-into-trunk-2-diff
+run_pass "$TEST_KEY" svn diff
+if $SVN_VERSION_IS_16; then
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+
+Property changes on: .
+___________________________________________________________________
+Modified: svn:mergeinfo
+ Merged /${PROJECT}branches/dev/Share/merge1:r12-13
+
+Index: added_file
+===================================================================
+--- added_file (revision 13)
++++ added_file (working copy)
+@@ -1 +1,2 @@
+ INCLUDE 'hello_constants.INc'
++call_extra_feature()
+__OUT__
+else
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+Index: added_file
+===================================================================
+--- added_file (revision 13)
++++ added_file (working copy)
+@@ -1 +1,2 @@
+ INCLUDE 'hello_constants.INc'
++call_extra_feature()
+Index: .
+===================================================================
+--- . (revision 13)
++++ . (working copy)
+
+Property changes on: .
+___________________________________________________________________
+Modified: svn:mergeinfo
+ Merged /${PROJECT}branches/dev/Share/merge1:r12-13
+__OUT__
+fi
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm commit of fcm merge branch-into-trunk (2)
+TEST_KEY=$TEST_KEY_BASE-branch-into-trunk-2-commit
+run_pass "$TEST_KEY" fcm commit <<__IN__
+y
+__IN__
+sed -i "/^Updating '.':$/d" $TEST_DIR/"$TEST_KEY.out"
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+[info] sed -i 1i\foo: starting commit message editor...
+Change summary:
+--------------------------------------------------------------------------------
+[Root : $REPOS_URL]
+[Project: ${TEST_PROJECT:-}]
+[Branch : trunk]
+[Sub-dir: ]
+
+ M .
+M added_file
+--------------------------------------------------------------------------------
+Commit message is as follows:
+--------------------------------------------------------------------------------
+foo
+Merged into /${PROJECT}trunk: /${PROJECT}branches/dev/Share/merge1 at 13 cf. /${PROJECT}branches/dev/Share/merge1 at 11
+--------------------------------------------------------------------------------
+
+*** WARNING: YOU ARE COMMITTING TO THE TRUNK.
+*** Please ensure that your change conforms to your project's working practices.
+
+Would you like to commit this change?
+Enter "y" or "n" (or just press <return> for "n"): Sending .
+Sending added_file
+Transmitting file data .
+Committed revision 14.
+At revision 14.
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm log of fcm merge branch-into-trunk (2)
+TEST_KEY=$TEST_KEY_BASE-branch-into-trunk-2-log
+run_pass "$TEST_KEY" fcm log
+sed -i "s/\(.*|.*|\).*\(|.*\)$/\1 date \2/g" $TEST_DIR/$TEST_KEY.out
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+------------------------------------------------------------------------
+r14 | $LOGNAME | date | 3 lines
+
+foo
+Merged into /${PROJECT}trunk: /${PROJECT}branches/dev/Share/merge1 at 13 cf. /${PROJECT}branches/dev/Share/merge1 at 11
+
+------------------------------------------------------------------------
+r12 | $LOGNAME | date | 3 lines
+
+foo
+Merged into /${PROJECT}trunk: /${PROJECT}branches/dev/Share/merge1 at 11 cf. /${PROJECT}trunk at 9
+
+------------------------------------------------------------------------
+r9 | $LOGNAME | date | 1 line
+
+Made another trunk change
+------------------------------------------------------------------------
+r8 | $LOGNAME | date | 1 line
+
+Made trunk change
+------------------------------------------------------------------------
+r1 | $LOGNAME | date | 1 line
+
+initial trunk import
+------------------------------------------------------------------------
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm merge of trunk-into-branch (2)
+echo "# trunk modification" >>$MOD_FILE
+svn commit -q -m "Made trunk change - a simple edit of $MOD_FILE"
+svn update -q
+svn switch -q $ROOT_URL/branches/dev/Share/merge1
+TEST_KEY=$TEST_KEY_BASE-trunk-into-branch-2
+echo "# added another line for simple repeat testing" >>$BRANCH_MOD_FILE
+svn commit -q -m "Made branch change for merge repeat test"
+svn update -q
+run_pass "$TEST_KEY" fcm merge --non-interactive trunk
+if $SVN_VERSION_IS_16; then
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+Eligible merge(s) from /${PROJECT}trunk at 16: 15 14
+--------------------------------------------------------------------------------
+Merge: /${PROJECT}trunk at 15
+ c.f.: /${PROJECT}branches/dev/Share/merge1 at 13
+Merge succeeded.
+__OUT__
+else
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+Eligible merge(s) from /${PROJECT}trunk at 16: 15 14
+--------------------------------------------------------------------------------
+Merge: /${PROJECT}trunk at 15
+ c.f.: /${PROJECT}branches/dev/Share/merge1 at 13
+Merge succeeded.
+--------------------------------------------------------------------------actual
+--- Merging differences between repository URLs into '.':
+U added_file
+--- Recording mergeinfo for merge between repository URLs into '.':
+ U .
+--------------------------------------------------------------------------actual
+__OUT__
+fi
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests svn status result of fcm merge trunk-into-branch (2)
+TEST_KEY=$TEST_KEY_BASE-trunk-into-branch-2-status
+run_pass "$TEST_KEY" svn status --config-dir=$TEST_DIR/.subversion/
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+ M .
+M added_file
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests svn diff result of fcm merge trunk-into-branch (2)
+TEST_KEY=$TEST_KEY_BASE-trunk-into-branch-2-diff
+run_pass "$TEST_KEY" svn diff
+if $SVN_VERSION_IS_16; then
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+
+Property changes on: .
+___________________________________________________________________
+Modified: svn:mergeinfo
+ Merged /${PROJECT}trunk:r10-15
+ Merged /${PROJECT}branches/dev/Share/merge1:r2-3
+
+Index: added_file
+===================================================================
+--- added_file (revision 16)
++++ added_file (working copy)
+@@ -1,2 +1,3 @@
+ INCLUDE 'hello_constants.INc'
+ call_extra_feature()
++# trunk modification
+__OUT__
+else
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+Index: added_file
+===================================================================
+--- added_file (revision 16)
++++ added_file (working copy)
+@@ -1,2 +1,3 @@
+ INCLUDE 'hello_constants.INc'
+ call_extra_feature()
++# trunk modification
+Index: .
+===================================================================
+--- . (revision 16)
++++ . (working copy)
+
+Property changes on: .
+___________________________________________________________________
+Modified: svn:mergeinfo
+ Merged /${PROJECT}trunk:r10-15
+__OUT__
+fi
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm commit of fcm merge trunk-into-branch (2)
+TEST_KEY=$TEST_KEY_BASE-trunk-into-branch-2-commit
+run_pass "$TEST_KEY" fcm commit <<__IN__
+y
+__IN__
+sed -i "/^Updating '.':$/d" $TEST_DIR/"$TEST_KEY.out"
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+[info] sed -i 1i\foo: starting commit message editor...
+Change summary:
+--------------------------------------------------------------------------------
+[Root : $REPOS_URL]
+[Project: ${TEST_PROJECT:-}]
+[Branch : branches/dev/Share/merge1]
+[Sub-dir: ]
+
+ M .
+M added_file
+--------------------------------------------------------------------------------
+Commit message is as follows:
+--------------------------------------------------------------------------------
+foo
+Merged into /${PROJECT}branches/dev/Share/merge1: /${PROJECT}trunk at 15 cf. /${PROJECT}branches/dev/Share/merge1 at 13
+--------------------------------------------------------------------------------
+
+*** WARNING: YOU ARE COMMITTING TO A Share BRANCH.
+*** Please ensure that you have the owner's permission.
+
+Would you like to commit this change?
+Enter "y" or "n" (or just press <return> for "n"): Sending .
+Sending added_file
+Transmitting file data .
+Committed revision 17.
+At revision 17.
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm log of fcm merge trunk-into-branch (2)
+TEST_KEY=$TEST_KEY_BASE-trunk-into-branch-2-log
+run_pass "$TEST_KEY" fcm log
+sed -i "s/\(.*|.*|\).*\(|.*\)$/\1 date \2/g" $TEST_DIR/$TEST_KEY.out
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+------------------------------------------------------------------------
+r17 | $LOGNAME | date | 3 lines
+
+foo
+Merged into /${PROJECT}branches/dev/Share/merge1: /${PROJECT}trunk at 15 cf. /${PROJECT}branches/dev/Share/merge1 at 13
+
+------------------------------------------------------------------------
+r16 | $LOGNAME | date | 1 line
+
+Made branch change for merge repeat test
+------------------------------------------------------------------------
+r13 | $LOGNAME | date | 1 line
+
+Made branch change to add extra feature
+------------------------------------------------------------------------
+r11 | $LOGNAME | date | 1 line
+
+edit on branch for merge repeat test
+------------------------------------------------------------------------
+r10 | $LOGNAME | date | 3 lines
+
+foo
+Merged into /${PROJECT}branches/dev/Share/merge1: /${PROJECT}trunk at 9 cf. /${PROJECT}trunk at 1
+
+------------------------------------------------------------------------
+r5 | $LOGNAME | date | 1 line
+
+Made changes for future merge of this branch
+------------------------------------------------------------------------
+r4 | $LOGNAME | date | 1 line
+
+Made a branch Created /${PROJECT}branches/dev/Share/merge1 from /trunk at 1.
+------------------------------------------------------------------------
+r1 | $LOGNAME | date | 1 line
+
+initial trunk import
+------------------------------------------------------------------------
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm merge of branch-into-trunk (3)
+svn delete -q $BRANCH_MOD_FILE
+svn copy -q $MOD_FILE $MOD_FILE.add
+svn commit -q -m "Made branch change - deleted $BRANCH_MOD_FILE, copied $MOD_FILE"
+svn update -q
+svn switch -q $ROOT_URL/trunk
+TEST_KEY=$TEST_KEY_BASE-branch-into-trunk-3
+run_pass "$TEST_KEY" fcm merge --non-interactive branches/dev/Share/merge1
+if $SVN_VERSION_IS_16; then
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+Eligible merge(s) from /${PROJECT}branches/dev/Share/merge1 at 18: 18 17
+--------------------------------------------------------------------------------
+Merge: /${PROJECT}branches/dev/Share/merge1 at 18
+ c.f.: /${PROJECT}trunk at 15
+Merge succeeded.
+__OUT__
+else
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+Eligible merge(s) from /${PROJECT}branches/dev/Share/merge1 at 18: 18 17
+--------------------------------------------------------------------------------
+Merge: /${PROJECT}branches/dev/Share/merge1 at 18
+ c.f.: /${PROJECT}trunk at 15
+Merge succeeded.
+--------------------------------------------------------------------------actual
+--- Merging differences between repository URLs into '.':
+D added_directory/hello_constants_dummy.inc
+A added_file.add
+--- Recording mergeinfo for merge between repository URLs into '.':
+ U .
+--------------------------------------------------------------------------actual
+__OUT__
+fi
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests svn status result of fcm merge branch-into-trunk (3)
+TEST_KEY=$TEST_KEY_BASE-branch-into-trunk-3-status
+run_pass "$TEST_KEY" svn status --config-dir=$TEST_DIR/.subversion/
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+ M .
+D added_directory/hello_constants_dummy.inc
+A + added_file.add
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests svn diff result of fcm merge branch-into-trunk (3)
+TEST_KEY=$TEST_KEY_BASE-branch-into-trunk-3-diff
+run_pass "$TEST_KEY" svn diff
+if $SVN_VERSION_IS_16; then
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+
+Property changes on: .
+___________________________________________________________________
+Modified: svn:mergeinfo
+ Merged /${PROJECT}branches/dev/Share/merge1:r14-18
+
+Index: added_directory/hello_constants_dummy.inc
+===================================================================
+--- added_directory/hello_constants_dummy.inc (revision 18)
++++ added_directory/hello_constants_dummy.inc (working copy)
+@@ -1,2 +0,0 @@
+-INCLUDE 'hello_constants.INc'
+-# added this line for simple repeat testing
+__OUT__
+else
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+Index: added_directory/hello_constants_dummy.inc
+===================================================================
+--- added_directory/hello_constants_dummy.inc (revision 18)
++++ added_directory/hello_constants_dummy.inc (working copy)
+@@ -1,2 +0,0 @@
+-INCLUDE 'hello_constants.INc'
+-# added this line for simple repeat testing
+Index: .
+===================================================================
+--- . (revision 18)
++++ . (working copy)
+
+Property changes on: .
+___________________________________________________________________
+Modified: svn:mergeinfo
+ Merged /${PROJECT}branches/dev/Share/merge1:r14-18
+__OUT__
+fi
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm commit of fcm merge branch-into-trunk (3)
+TEST_KEY=$TEST_KEY_BASE-branch-into-trunk-3-commit
+run_pass "$TEST_KEY" fcm commit <<__IN__
+y
+__IN__
+sed -i "/^Updating '.':$/d" $TEST_DIR/"$TEST_KEY.out"
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+[info] sed -i 1i\foo: starting commit message editor...
+Change summary:
+--------------------------------------------------------------------------------
+[Root : $REPOS_URL]
+[Project: ${TEST_PROJECT:-}]
+[Branch : trunk]
+[Sub-dir: ]
+
+ M .
+D added_directory/hello_constants_dummy.inc
+A + added_file.add
+--------------------------------------------------------------------------------
+Commit message is as follows:
+--------------------------------------------------------------------------------
+foo
+Merged into /${PROJECT}trunk: /${PROJECT}branches/dev/Share/merge1 at 18 cf. /${PROJECT}trunk at 15
+--------------------------------------------------------------------------------
+
+*** WARNING: YOU ARE COMMITTING TO THE TRUNK.
+*** Please ensure that your change conforms to your project's working practices.
+
+Would you like to commit this change?
+Enter "y" or "n" (or just press <return> for "n"): Sending .
+Deleting added_directory/hello_constants_dummy.inc
+Adding added_file.add
+
+Committed revision 19.
+At revision 19.
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm log of fcm merge branch-into-trunk (3)
+TEST_KEY=$TEST_KEY_BASE-branch-into-trunk-3-log
+run_pass "$TEST_KEY" fcm log $ROOT_URL
+sed -i "s/\(.*|.*|\).*\(|.*\)$/\1 date \2/g" $TEST_DIR/$TEST_KEY.out
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+------------------------------------------------------------------------
+r19 | $LOGNAME | date | 3 lines
+
+foo
+Merged into /${PROJECT}trunk: /${PROJECT}branches/dev/Share/merge1 at 18 cf. /${PROJECT}trunk at 15
+
+------------------------------------------------------------------------
+r18 | $LOGNAME | date | 1 line
+
+Made branch change - deleted ./added_directory/hello_constants_dummy.inc, copied ./added_file
+------------------------------------------------------------------------
+r17 | $LOGNAME | date | 3 lines
+
+foo
+Merged into /${PROJECT}branches/dev/Share/merge1: /${PROJECT}trunk at 15 cf. /${PROJECT}branches/dev/Share/merge1 at 13
+
+------------------------------------------------------------------------
+r16 | $LOGNAME | date | 1 line
+
+Made branch change for merge repeat test
+------------------------------------------------------------------------
+r15 | $LOGNAME | date | 1 line
+
+Made trunk change - a simple edit of ./added_file
+------------------------------------------------------------------------
+r14 | $LOGNAME | date | 3 lines
+
+foo
+Merged into /${PROJECT}trunk: /${PROJECT}branches/dev/Share/merge1 at 13 cf. /${PROJECT}branches/dev/Share/merge1 at 11
+
+------------------------------------------------------------------------
+r13 | $LOGNAME | date | 1 line
+
+Made branch change to add extra feature
+------------------------------------------------------------------------
+r12 | $LOGNAME | date | 3 lines
+
+foo
+Merged into /${PROJECT}trunk: /${PROJECT}branches/dev/Share/merge1 at 11 cf. /${PROJECT}trunk at 9
+
+------------------------------------------------------------------------
+r11 | $LOGNAME | date | 1 line
+
+edit on branch for merge repeat test
+------------------------------------------------------------------------
+r10 | $LOGNAME | date | 3 lines
+
+foo
+Merged into /${PROJECT}branches/dev/Share/merge1: /${PROJECT}trunk at 9 cf. /${PROJECT}trunk at 1
+
+------------------------------------------------------------------------
+r9 | $LOGNAME | date | 1 line
+
+Made another trunk change
+------------------------------------------------------------------------
+r8 | $LOGNAME | date | 1 line
+
+Made trunk change
+------------------------------------------------------------------------
+r7 | $LOGNAME | date | 1 line
+
+Made changes for future merge
+------------------------------------------------------------------------
+r6 | $LOGNAME | date | 1 line
+
+Made a branch Created /${PROJECT}branches/dev/Share/merge2 from /trunk at 1.
+------------------------------------------------------------------------
+r5 | $LOGNAME | date | 1 line
+
+Made changes for future merge of this branch
+------------------------------------------------------------------------
+r4 | $LOGNAME | date | 1 line
+
+Made a branch Created /${PROJECT}branches/dev/Share/merge1 from /trunk at 1.
+------------------------------------------------------------------------
+r3 | $LOGNAME | date | 1 line
+
+
+------------------------------------------------------------------------
+r2 | $LOGNAME | date | 1 line
+
+make tags
+------------------------------------------------------------------------
+r1 | $LOGNAME | date | 1 line
+
+initial trunk import
+------------------------------------------------------------------------
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm merge of branch-into-branch (1)
+TEST_KEY=$TEST_KEY_BASE-branch-into-branch-1
+cd $TEST_DIR
+rm -rf $TEST_DIR/wc
+mkdir $TEST_DIR/wc
+svn checkout -q $ROOT_URL/branches/dev/Share/merge2 $TEST_DIR/wc
+cd $TEST_DIR/wc
+BRANCH_2_MOD_FILE=$(find . -type f | sed "/\.svn/d" | sort | head -3| tail -1)
+echo "Second branch change" >>$BRANCH_2_MOD_FILE
+svn commit -q -m "Made branch change - added to $BRANCH_2_MOD_FILE"
+svn update -q
+run_pass "$TEST_KEY" fcm merge $ROOT_URL/branches/dev/Share/merge1 <<__IN__
+13
+y
+__IN__
+if $SVN_VERSION_IS_16; then
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+Eligible merge(s) from /${PROJECT}branches/dev/Share/merge1 at 20: 18 17 16 13 11 10 5
+Enter a revision (or just press <return> for "18"): --------------------------------------------------------------------------------
+Merge: /${PROJECT}branches/dev/Share/merge1 at 13
+ c.f.: /${PROJECT}trunk at 1
+-------------------------------------------------------------------------dry-run
+--- Merging r2 through r13 into '.':
+U subroutine/hello_sub_dummy.h
+A added_file
+A added_directory
+A added_directory/hello_constants_dummy.inc
+A added_directory/hello_constants.inc
+A added_directory/hello_constants.f90
+A module/tree_conflict_file
+U module/hello_constants_dummy.inc
+U module/hello_constants.inc
+U module/hello_constants.f90
+U lib/python/info/__init__.py
+U lib/python/info/poems.py
+ U .
+-------------------------------------------------------------------------dry-run
+Would you like to go ahead with the merge?
+Enter "y" or "n" (or just press <return> for "n"): Merge succeeded.
+__OUT__
+else
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+Eligible merge(s) from /${PROJECT}branches/dev/Share/merge1 at 20: 18 17 16 13 11 10 5
+Enter a revision (or just press <return> for "18"): --------------------------------------------------------------------------------
+Merge: /${PROJECT}branches/dev/Share/merge1 at 13
+ c.f.: /${PROJECT}trunk at 1
+-------------------------------------------------------------------------dry-run
+--- Merging r4 through r13 into '.':
+U subroutine/hello_sub_dummy.h
+A added_file
+A added_directory
+A added_directory/hello_constants_dummy.inc
+A added_directory/hello_constants.inc
+A added_directory/hello_constants.f90
+A module/tree_conflict_file
+U module/hello_constants_dummy.inc
+U module/hello_constants.inc
+U module/hello_constants.f90
+U lib/python/info/__init__.py
+U lib/python/info/poems.py
+ U .
+-------------------------------------------------------------------------dry-run
+Would you like to go ahead with the merge?
+Enter "y" or "n" (or just press <return> for "n"): Merge succeeded.
+--------------------------------------------------------------------------actual
+--- Merging r4 through r13 into '.':
+U subroutine/hello_sub_dummy.h
+A added_file
+A added_directory
+A added_directory/hello_constants_dummy.inc
+A added_directory/hello_constants.inc
+A added_directory/hello_constants.f90
+A module/tree_conflict_file
+U module/hello_constants_dummy.inc
+U module/hello_constants.inc
+U module/hello_constants.f90
+U lib/python/info/__init__.py
+U lib/python/info/poems.py
+ U .
+--- Recording mergeinfo for merge of r4 through r13 into '.':
+ G .
+--------------------------------------------------------------------------actual
+__OUT__
+fi
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests svn status result of fcm merge branch-into-branch (1)
+TEST_KEY=$TEST_KEY_BASE-branch-into-branch-1-status
+run_pass "$TEST_KEY" svn status --config-dir=$TEST_DIR/.subversion/
+if $SVN_VERSION_IS_16; then
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+ M .
+M subroutine/hello_sub_dummy.h
+A + added_file
+A + module/tree_conflict_file
+M module/hello_constants_dummy.inc
+M module/hello_constants.inc
+M module/hello_constants.f90
+A + added_directory
+A + added_directory/hello_constants_dummy.inc
+A + added_directory/hello_constants.inc
+A + added_directory/hello_constants.f90
+M lib/python/info/__init__.py
+M lib/python/info/poems.py
+__OUT__
+else
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+ M .
+A + added_directory
+A + added_file
+M lib/python/info/__init__.py
+M lib/python/info/poems.py
+M module/hello_constants.f90
+M module/hello_constants.inc
+M module/hello_constants_dummy.inc
+A + module/tree_conflict_file
+M subroutine/hello_sub_dummy.h
+__OUT__
+fi
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests svn diff result of fcm merge branch-into-branch (1)
+TEST_KEY=$TEST_KEY_BASE-branch-into-branch-1-diff
+run_pass "$TEST_KEY" svn diff
+if $SVN_VERSION_IS_16; then
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+
+Property changes on: .
+___________________________________________________________________
+Added: svn:mergeinfo
+ Merged /${PROJECT}trunk:r2-9
+ Merged /${PROJECT}branches/dev/Share/merge1:r4-13
+
+Index: subroutine/hello_sub_dummy.h
+===================================================================
+--- subroutine/hello_sub_dummy.h (revision 20)
++++ subroutine/hello_sub_dummy.h (working copy)
+@@ -1 +1,2 @@
+ #include "hello_sub.h"
++Modified a line
+Index: module/hello_constants_dummy.inc
+===================================================================
+--- module/hello_constants_dummy.inc (revision 20)
++++ module/hello_constants_dummy.inc (working copy)
+@@ -1 +1 @@
+-INCLUDE 'hello_constants.inc'
++INCLUDE 'hello_constants.INc'
+Index: module/hello_constants.inc
+===================================================================
+--- module/hello_constants.inc (revision 20)
++++ module/hello_constants.inc (working copy)
+@@ -1 +1,2 @@
+-CHARACTER (LEN=80), PARAMETER :: hello_string = 'Hello Earth!'
++CHARACTER (
++LEN=80), PARAMETER :: hello_strINg = 'Hello Earth!!'
+Index: module/hello_constants.f90
+===================================================================
+--- module/hello_constants.f90 (revision 20)
++++ module/hello_constants.f90 (working copy)
+@@ -1,6 +1,6 @@
+ MODULE Hello_Constants
+
+-INCLUDE 'hello_constants_dummy.inc'
++INCLUDE 'hello_constants_dummy.INc'
+
+ END MODULE Hello_Constants
+ Second branch change
+Index: lib/python/info/__init__.py
+===================================================================
+--- lib/python/info/__init__.py (revision 20)
++++ lib/python/info/__init__.py (working copy)
+@@ -0,0 +1,2 @@
++trunk change
++another trunk change
+Index: lib/python/info/poems.py
+===================================================================
+--- lib/python/info/poems.py (revision 20)
++++ lib/python/info/poems.py (working copy)
+@@ -1,24 +1,23 @@
+-#!/usr/bin/env python
+-# -*- coding: utf-8 -*-
+ """The Python, by Hilaire Belloc
+
+ A Python I should not advise,--
+-It needs a doctor for its eyes,
++It needs a doctor FOR its eyes,
+ And has the measles yearly.
+-However, if you feel inclined
+-To get one (to improve your mind,
++However, if you feel INclINed
++To get one (
++to improve your mINd,
+ And not from fashion merely),
+ Allow no music near its cage;
+-And when it flies into a rage
++And when it flies INto a rage
+ Chastise it, most severely.
+-I had an aunt in Yucatan
++I had an aunt IN Yucatan
+ Who bought a Python from a man
+-And kept it for a pet.
++And kept it FOR a pet.
+ She died, because she never knew
+ These simple little rules and few;--
+-The Snake is living yet.
++The Snake is livINg yet.
+ """
+
+ import this
+
+-print "\n", __doc__
++prINt "\n", __doc__
+__OUT__
+else
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+Index: lib/python/info/__init__.py
+===================================================================
+--- lib/python/info/__init__.py (revision 20)
++++ lib/python/info/__init__.py (working copy)
+@@ -0,0 +1,2 @@
++trunk change
++another trunk change
+Index: lib/python/info/poems.py
+===================================================================
+--- lib/python/info/poems.py (revision 20)
++++ lib/python/info/poems.py (working copy)
+@@ -1,24 +1,23 @@
+-#!/usr/bin/env python
+-# -*- coding: utf-8 -*-
+ """The Python, by Hilaire Belloc
+
+ A Python I should not advise,--
+-It needs a doctor for its eyes,
++It needs a doctor FOR its eyes,
+ And has the measles yearly.
+-However, if you feel inclined
+-To get one (to improve your mind,
++However, if you feel INclINed
++To get one (
++to improve your mINd,
+ And not from fashion merely),
+ Allow no music near its cage;
+-And when it flies into a rage
++And when it flies INto a rage
+ Chastise it, most severely.
+-I had an aunt in Yucatan
++I had an aunt IN Yucatan
+ Who bought a Python from a man
+-And kept it for a pet.
++And kept it FOR a pet.
+ She died, because she never knew
+ These simple little rules and few;--
+-The Snake is living yet.
++The Snake is livINg yet.
+ """
+
+ import this
+
+-print "\n", __doc__
++prINt "\n", __doc__
+Index: module/hello_constants.f90
+===================================================================
+--- module/hello_constants.f90 (revision 20)
++++ module/hello_constants.f90 (working copy)
+@@ -1,6 +1,6 @@
+ MODULE Hello_Constants
+
+-INCLUDE 'hello_constants_dummy.inc'
++INCLUDE 'hello_constants_dummy.INc'
+
+ END MODULE Hello_Constants
+ Second branch change
+Index: module/hello_constants.inc
+===================================================================
+--- module/hello_constants.inc (revision 20)
++++ module/hello_constants.inc (working copy)
+@@ -1 +1,2 @@
+-CHARACTER (LEN=80), PARAMETER :: hello_string = 'Hello Earth!'
++CHARACTER (
++LEN=80), PARAMETER :: hello_strINg = 'Hello Earth!!'
+Index: module/hello_constants_dummy.inc
+===================================================================
+--- module/hello_constants_dummy.inc (revision 20)
++++ module/hello_constants_dummy.inc (working copy)
+@@ -1 +1 @@
+-INCLUDE 'hello_constants.inc'
++INCLUDE 'hello_constants.INc'
+Index: subroutine/hello_sub_dummy.h
+===================================================================
+--- subroutine/hello_sub_dummy.h (revision 20)
++++ subroutine/hello_sub_dummy.h (working copy)
+@@ -1 +1,2 @@
+ #include "hello_sub.h"
++Modified a line
+Index: .
+===================================================================
+--- . (revision 20)
++++ . (working copy)
+
+Property changes on: .
+___________________________________________________________________
+Added: svn:mergeinfo
+ Merged /${PROJECT}trunk:r2-9
+ Merged /${PROJECT}branches/dev/Share/merge1:r4-13
+__OUT__
+fi
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm commit of fcm merge branch-into-branch (1)
+TEST_KEY=$TEST_KEY_BASE-branch-into-branch-1-commit
+run_pass "$TEST_KEY" fcm commit <<__IN__
+y
+__IN__
+if $SVN_VERSION_IS_16; then
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+[info] sed -i 1i\foo: starting commit message editor...
+Change summary:
+--------------------------------------------------------------------------------
+[Root : $REPOS_URL]
+[Project: ${TEST_PROJECT:-}]
+[Branch : branches/dev/Share/merge2]
+[Sub-dir: ]
+
+ M .
+M subroutine/hello_sub_dummy.h
+A + added_file
+A + module/tree_conflict_file
+M module/hello_constants_dummy.inc
+M module/hello_constants.inc
+M module/hello_constants.f90
+A + added_directory
+A + added_directory/hello_constants_dummy.inc
+A + added_directory/hello_constants.inc
+A + added_directory/hello_constants.f90
+M lib/python/info/__init__.py
+M lib/python/info/poems.py
+--------------------------------------------------------------------------------
+Commit message is as follows:
+--------------------------------------------------------------------------------
+foo
+Merged into /${PROJECT}branches/dev/Share/merge2: /${PROJECT}branches/dev/Share/merge1 at 13 cf. /${PROJECT}trunk at 1
+--------------------------------------------------------------------------------
+
+*** WARNING: YOU ARE COMMITTING TO A Share BRANCH.
+*** Please ensure that you have the owner's permission.
+
+Would you like to commit this change?
+Enter "y" or "n" (or just press <return> for "n"): Sending .
+Adding added_directory
+Adding added_directory/hello_constants.f90
+Adding added_directory/hello_constants.inc
+Adding added_directory/hello_constants_dummy.inc
+Adding added_file
+Sending lib/python/info/__init__.py
+Sending lib/python/info/poems.py
+Sending module/hello_constants.f90
+Sending module/hello_constants.inc
+Sending module/hello_constants_dummy.inc
+Adding module/tree_conflict_file
+Sending subroutine/hello_sub_dummy.h
+Transmitting file data ......
+Committed revision 21.
+At revision 21.
+__OUT__
+else
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+[info] sed -i 1i\foo: starting commit message editor...
+Change summary:
+--------------------------------------------------------------------------------
+[Root : $REPOS_URL]
+[Project: ${TEST_PROJECT:-}]
+[Branch : branches/dev/Share/merge2]
+[Sub-dir: ]
+
+ M .
+A + added_directory
+A + added_file
+M lib/python/info/__init__.py
+M lib/python/info/poems.py
+M module/hello_constants.f90
+M module/hello_constants.inc
+M module/hello_constants_dummy.inc
+A + module/tree_conflict_file
+M subroutine/hello_sub_dummy.h
+--------------------------------------------------------------------------------
+Commit message is as follows:
+--------------------------------------------------------------------------------
+foo
+Merged into /${PROJECT}branches/dev/Share/merge2: /${PROJECT}branches/dev/Share/merge1 at 13 cf. /${PROJECT}trunk at 1
+--------------------------------------------------------------------------------
+
+*** WARNING: YOU ARE COMMITTING TO A Share BRANCH.
+*** Please ensure that you have the owner's permission.
+
+Would you like to commit this change?
+Enter "y" or "n" (or just press <return> for "n"): Sending .
+Adding added_directory
+Adding added_file
+Sending lib/python/info/__init__.py
+Sending lib/python/info/poems.py
+Sending module/hello_constants.f90
+Sending module/hello_constants.inc
+Sending module/hello_constants_dummy.inc
+Adding module/tree_conflict_file
+Sending subroutine/hello_sub_dummy.h
+Transmitting file data ......
+Committed revision 21.
+Updating '.':
+At revision 21.
+__OUT__
+fi
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm log of fcm merge branch-into-branch (1)
+TEST_KEY=$TEST_KEY_BASE-branch-into-branch-1-log
+run_pass "$TEST_KEY" fcm log $REPOS_URL
+sed -i "s/\(.*|.*|\).*\(|.*\)$/\1 date \2/g" $TEST_DIR/$TEST_KEY.out
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+------------------------------------------------------------------------
+r21 | $LOGNAME | date | 3 lines
+
+foo
+Merged into /${PROJECT}branches/dev/Share/merge2: /${PROJECT}branches/dev/Share/merge1 at 13 cf. /${PROJECT}trunk at 1
+
+------------------------------------------------------------------------
+r20 | $LOGNAME | date | 1 line
+
+Made branch change - added to ./module/hello_constants.f90
+------------------------------------------------------------------------
+r19 | $LOGNAME | date | 3 lines
+
+foo
+Merged into /${PROJECT}trunk: /${PROJECT}branches/dev/Share/merge1 at 18 cf. /${PROJECT}trunk at 15
+
+------------------------------------------------------------------------
+r18 | $LOGNAME | date | 1 line
+
+Made branch change - deleted ./added_directory/hello_constants_dummy.inc, copied ./added_file
+------------------------------------------------------------------------
+r17 | $LOGNAME | date | 3 lines
+
+foo
+Merged into /${PROJECT}branches/dev/Share/merge1: /${PROJECT}trunk at 15 cf. /${PROJECT}branches/dev/Share/merge1 at 13
+
+------------------------------------------------------------------------
+r16 | $LOGNAME | date | 1 line
+
+Made branch change for merge repeat test
+------------------------------------------------------------------------
+r15 | $LOGNAME | date | 1 line
+
+Made trunk change - a simple edit of ./added_file
+------------------------------------------------------------------------
+r14 | $LOGNAME | date | 3 lines
+
+foo
+Merged into /${PROJECT}trunk: /${PROJECT}branches/dev/Share/merge1 at 13 cf. /${PROJECT}branches/dev/Share/merge1 at 11
+
+------------------------------------------------------------------------
+r13 | $LOGNAME | date | 1 line
+
+Made branch change to add extra feature
+------------------------------------------------------------------------
+r12 | $LOGNAME | date | 3 lines
+
+foo
+Merged into /${PROJECT}trunk: /${PROJECT}branches/dev/Share/merge1 at 11 cf. /${PROJECT}trunk at 9
+
+------------------------------------------------------------------------
+r11 | $LOGNAME | date | 1 line
+
+edit on branch for merge repeat test
+------------------------------------------------------------------------
+r10 | $LOGNAME | date | 3 lines
+
+foo
+Merged into /${PROJECT}branches/dev/Share/merge1: /${PROJECT}trunk at 9 cf. /${PROJECT}trunk at 1
+
+------------------------------------------------------------------------
+r9 | $LOGNAME | date | 1 line
+
+Made another trunk change
+------------------------------------------------------------------------
+r8 | $LOGNAME | date | 1 line
+
+Made trunk change
+------------------------------------------------------------------------
+r7 | $LOGNAME | date | 1 line
+
+Made changes for future merge
+------------------------------------------------------------------------
+r6 | $LOGNAME | date | 1 line
+
+Made a branch Created /${PROJECT}branches/dev/Share/merge2 from /trunk at 1.
+------------------------------------------------------------------------
+r5 | $LOGNAME | date | 1 line
+
+Made changes for future merge of this branch
+------------------------------------------------------------------------
+r4 | $LOGNAME | date | 1 line
+
+Made a branch Created /${PROJECT}branches/dev/Share/merge1 from /trunk at 1.
+------------------------------------------------------------------------
+r3 | $LOGNAME | date | 1 line
+
+
+------------------------------------------------------------------------
+r2 | $LOGNAME | date | 1 line
+
+make tags
+------------------------------------------------------------------------
+r1 | $LOGNAME | date | 1 line
+
+initial trunk import
+------------------------------------------------------------------------
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+teardown
+#-------------------------------------------------------------------------------
diff --git a/t/fcm-merge/test_header b/t/fcm-merge/test_header
new file mode 100644
index 0000000..672ce12
--- /dev/null
+++ b/t/fcm-merge/test_header
@@ -0,0 +1,232 @@
+#!/bin/bash
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# Optional enviroment variables:
+# TEST_PROJECT (tests using given project name)
+# TEST_REMOTE_HOST (tests using svn+ssh repositories located on given host)
+# ------------------------------------------------------------------------------
+
+. $(dirname $0)/../lib/bash/test_header
+
+function file_cmp() {
+ local TEST_KEY=$1
+ local FILE_ACTUAL=$2
+ local FILE_EXPECT=${3:--}
+ if cmp $TEST_DIR/$FILE_ACTUAL $FILE_EXPECT; then
+ pass $TEST_KEY
+ return
+ fi
+ fail $TEST_KEY
+}
+
+function file_grep() {
+ local TEST_KEY=$1
+ local PATTERN=$2
+ local FILE=$3
+ if grep -q -e "$PATTERN" $TEST_DIR/$FILE; then
+ pass $TEST_KEY
+ return
+ fi
+ fail $TEST_KEY
+}
+
+function file_test() {
+ local TEST_KEY=$1
+ local FILE=$2
+ local OPTION=${3:--e}
+ if test $OPTION $TEST_DIR/$FILE; then
+ pass $TEST_KEY
+ else
+ fail $TEST_KEY
+ fi
+}
+
+function file_xxdiff() {
+ local TEST_KEY=$1
+ local FILE_ACTUAL=$2
+ local FILE_EXPECT=${3:--}
+ if xxdiff -D $TEST_DIR/$FILE_ACTUAL $FILE_EXPECT; then
+ pass $TEST_KEY
+ return
+ fi
+ fail $TEST_KEY
+}
+
+function init_repos() {
+ if [[ -n ${TEST_REMOTE_HOST:-} ]]; then
+ TEST_REMOTE_DIR=$(ssh $TEST_REMOTE_HOST "mktemp -d")
+ ssh $TEST_REMOTE_HOST "svnadmin create --fs-type fsfs $TEST_REMOTE_DIR"
+ REPOS_URL="svn+ssh://${TEST_REMOTE_HOST}$TEST_REMOTE_DIR"
+ else
+ svnadmin create --fs-type fsfs $TEST_DIR/test_repos
+ REPOS_URL="file://$TEST_DIR/test_repos"
+ fi
+ ROOT_URL=$REPOS_URL
+ PROJECT=
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ PROJECT=$TEST_PROJECT"/"
+ fi
+ svn import -q $TEST_SOURCE_DIR/../etc/repo_files \
+ $REPOS_URL/$PROJECT/trunk -m "initial trunk import"
+ svn mkdir -q $REPOS_URL/$PROJECT/tags -m "make tags"
+ svn mkdir -q --parents $REPOS_URL/$PROJECT/branches/dev/Share -m " "
+}
+
+function init_repos_layout_roses() {
+ if [[ -n ${TEST_REMOTE_HOST:-} ]]; then
+ TEST_REMOTE_DIR=$(ssh $TEST_REMOTE_HOST "mktemp -d")
+ ssh $TEST_REMOTE_HOST "svnadmin create --fs-type fsfs $TEST_REMOTE_DIR"
+ REPOS_URL="svn+ssh://${TEST_REMOTE_HOST}$TEST_REMOTE_DIR"
+ else
+ svnadmin create --fs-type fsfs $TEST_DIR/test_repos
+ REPOS_URL="file://$TEST_DIR/test_repos"
+ fi
+ svn mkdir -q --parents $REPOS_URL/a/a/0/0/0/trunk
+ svn import -q $TEST_SOURCE_DIR/../etc/repo_files \
+ $REPOS_URL/a/a/0/0/0/trunk -m "initial trunk import"
+ TMPFILE=$(mktemp)
+ cat >$TMPFILE <<__LAYOUT__
+depth-project = 5
+depth-branch = 1
+depth-tag = 1
+dir-trunk = trunk
+dir-branch =
+dir-tag =
+level-owner-branch =
+level-owner-tag =
+template-branch =
+template-tag =
+__LAYOUT__
+ TMPDIR=$(mktemp -d)
+ svn checkout -q $REPOS_URL $TMPDIR
+ svn propset -q --file=$TMPFILE fcm:layout $TMPDIR
+ svn commit -q -m " " $TMPDIR
+ rm -f $TMPFILE
+ rm -rf $TMPDIR
+ ROOT_URL=$REPOS_URL
+}
+
+function init_branch() {
+ local BRANCH_NAME=$1
+ local REPOS_URL=$2
+ local ROOT_URL=$REPOS_URL
+ local ROOT_PATH=
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ ROOT_PATH=$ROOT_PATH/$TEST_PROJECT
+ fi
+ MESSAGE=$(echo -e "Created $ROOT_PATH/branches/dev/Share/$BRANCH_NAME from /trunk at 1.")
+ svn copy -q -r1 $ROOT_URL/trunk $ROOT_URL/branches/dev/Share/$BRANCH_NAME \
+ -m "Made a branch $MESSAGE"
+}
+
+function init_branch_wc() {
+ local BRANCH_NAME=$1
+ local REPOS_URL=$2
+ local ROOT_URL=$REPOS_URL
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ fi
+ init_branch $BRANCH_NAME $REPOS_URL
+ svn checkout -q $ROOT_URL/branches/dev/Share/$BRANCH_NAME $TEST_DIR/wc
+}
+
+function init_merge_branches() {
+ local BRANCH_NAME=$1
+ local OTHER_BRANCH_NAME=$2
+ local REPOS_URL=$3
+ local ROOT_URL=$REPOS_URL
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ fi
+ init_branch_wc $BRANCH_NAME $REPOS_URL
+ cd $TEST_DIR/wc
+ file_list=$(find . -type f | sed "/\.svn/d" | sort | head -5)
+ other_file=$(find . -type f | sed "/\.svn/d" | sort | tail -1)
+ for file in $file_list; do
+ sed -i "s/for/FOR/g; s/fi/end if/g; s/in/IN/g;" $file
+ sed -i "/#/d; /^ *!/d" $file
+ sed -i "s/!/!!/g; s/q/\nq/g; s/[(]/(\n/g" $file
+ done
+ file_dir=$(dirname $file)
+ svn copy -q $file ./added_file
+ svn copy -q module added_directory
+ touch module/tree_conflict_file
+ svn add -q $file_dir/tree_conflict_file
+ echo "Modified a line" >>$other_file
+ svn commit -q -m "Made changes for future merge of this branch"
+ svn update -q
+ init_branch $OTHER_BRANCH_NAME $REPOS_URL
+ svn switch -q $ROOT_URL/branches/dev/Share/$OTHER_BRANCH_NAME
+ echo " " > unversioned_file
+ properties_file=$(find . -type f | sed " /\.svn/d" | sort | tail -3 | head -1)
+ svn propset -q svn:executable "executable" $properties_file
+ svn copy -q $file renamed_added_file
+ svn commit -q -m "Made changes for future merge"
+ svn update -q
+ svn switch -q $ROOT_URL/trunk
+ echo "trunk change" >>$(find . -type f | sed "/\.svn/d" | sort | head -1)
+ svn commit -q -m "Made trunk change"
+ svn update -q
+ echo "another trunk change" >>$(find . -type f | sed "/\.svn/d" | sort | head -1)
+ svn commit -q -m "Made another trunk change"
+ svn update -q
+}
+
+function run_pass() {
+ local TEST_KEY=$1
+ shift 1
+ if ! "$@" 1>$TEST_DIR/$TEST_KEY.out 2>$TEST_DIR/$TEST_KEY.err; then
+ fail $TEST_KEY
+ return
+ fi
+ pass $TEST_KEY
+}
+
+function run_fail() {
+ local TEST_KEY=$1
+ shift 1
+ if "$@" 1>$TEST_DIR/$TEST_KEY.out 2>$TEST_DIR/$TEST_KEY.err; then
+ fail $TEST_KEY
+ return
+ fi
+ pass $TEST_KEY
+}
+
+function setup() {
+ mkdir -p $TEST_DIR/.subversion
+ mkdir -p $TEST_DIR/run
+ cd $TEST_DIR/run
+}
+
+function teardown() {
+ cd $TEST_DIR
+ rm -rf $TEST_DIR/test_repos
+ rm -rf $TEST_DIR/wc
+ rm -rf $TEST_DIR/run
+ rm -rf $TEST_DIR/.subversion
+ if [[ -n ${TEST_REMOTE_HOST:-} ]]; then
+ ssh $TEST_REMOTE_HOST "rm -rf $TEST_REMOTE_DIR"
+ fi
+}
+
+REPOS_URL=
+ROOT_URL=
+PROJECT=
diff --git a/t/fcm-recover-svn-repos/00-basic.t b/t/fcm-recover-svn-repos/00-basic.t
new file mode 100755
index 0000000..d334ec2
--- /dev/null
+++ b/t/fcm-recover-svn-repos/00-basic.t
@@ -0,0 +1,108 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+# Basic tests for "fcm-recover-svn-repos".
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+if ! which svnadmin 1>/dev/null 2>/dev/null; then
+ skip_all 'svnadmin not available'
+fi
+tests 26
+#-------------------------------------------------------------------------------
+set -e
+mkdir -p etc srv/svn var/svn/{backups,cache,dumps}
+# Configuration
+export FCM_CONF_PATH="$PWD/etc"
+cat >etc/admin.cfg <<__CONF__
+svn_backup_dir=$PWD/var/svn/backups
+svn_dump_dir=$PWD/var/svn/dumps
+svn_group=
+svn_live_dir=$PWD/srv/svn
+__CONF__
+# Create some repositories and populate them
+# Repository 1
+svnadmin create srv/svn/bar
+svn co -q file://$PWD/srv/svn/bar
+echo 'Barley drink.' >bar/barley
+svn add -q bar/*
+svn ci -q -m'test 1' bar
+svnadmin hotcopy srv/svn/bar var/svn/backups/bar
+tar -C var/svn/backups -czf $PWD/var/svn/backups/bar.tgz bar
+svnadmin dump srv/svn/bar -r 1 --incremental --deltas -q \
+ | gzip >var/svn/dumps/bar-1.gz
+# Repository 2
+svnadmin create srv/svn/foo
+svn co -q file://$PWD/srv/svn/foo
+echo 'Number of football players = 0' >foo/football
+echo 'Food is yummy.' >foo/food
+svn add -q foo/*
+svn ci -q -m'test 1' foo
+svnadmin hotcopy srv/svn/foo var/svn/backups/foo
+tar -C var/svn/backups -czf $PWD/var/svn/backups/foo.tgz foo
+rm -fr var/svn/backups/foo
+echo 'Fool is a clown.' >foo/fool
+svn add -q foo/fool
+svn ci -q -m'test 2' foo
+for I in {1..11}; do
+ echo "Number of football players = $I" >foo/football
+ svn ci -q -m"incr football player" foo
+done
+for I in {1..13}; do
+ svnadmin dump srv/svn/foo -r $I --incremental --deltas -q \
+ | gzip >var/svn/dumps/foo-$I.gz
+done
+rm -fr bar foo
+set +e
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-live-exists"
+run_fail "$TEST_KEY" "$FCM_HOME/sbin/fcm-recover-svn-repos"
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" </dev/null
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" <<__ERR__
+$PWD/srv/svn/bar: live repository exists.
+__ERR__
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE"
+mv srv/svn/{bar,foo} var/svn/cache
+run_pass "$TEST_KEY" "$FCM_HOME/sbin/fcm-recover-svn-repos"
+# Check revisions
+for NAME in bar foo; do
+ svnlook youngest var/svn/cache/$NAME >"$TEST_KEY-youngest-$NAME.exp"
+ svnlook youngest srv/svn/$NAME >"$TEST_KEY-youngest-$NAME"
+ file_cmp "$TEST_KEY-youngest-$NAME" \
+ "$TEST_KEY-youngest-$NAME" "$TEST_KEY-youngest-$NAME.exp"
+ for I in $(seq 1 $(<"$TEST_KEY-youngest-$NAME.exp")); do
+ svnlook changed -r $I var/svn/cache/$NAME \
+ >"$TEST_KEY-changed-$NAME-$I.exp"
+ svnlook changed -r $I srv/svn/$NAME >"$TEST_KEY-changed-$NAME-$I"
+ file_cmp "$TEST_KEY-changed-$NAME-$I" \
+ "$TEST_KEY-changed-$NAME-$I" "$TEST_KEY-changed-$NAME-$I.exp"
+ done
+ svn export -q file://$PWD/var/svn/cache/$NAME $NAME.orig
+ svn export -q file://$PWD/srv/svn/$NAME $NAME
+ FILES_ORIG=$(cd $NAME.orig; find -type f)
+ FILES=$(cd $NAME; find -type f)
+ run_pass "$TEST_KEY-$NAME-n-files" \
+ test $(wc -l <<<"$FILES_ORIG") -eq $(wc -l <<<"$FILES")
+ for FILE in $FILES_ORIG; do
+ file_cmp "$TEST_KEY-cmp-$NAME-$FILE" "$NAME.orig/$FILE" "$NAME/$FILE"
+ done
+done
+#-------------------------------------------------------------------------------
+exit
diff --git a/t/fcm-recover-svn-repos/test_header b/t/fcm-recover-svn-repos/test_header
new file mode 120000
index 0000000..90bd5a3
--- /dev/null
+++ b/t/fcm-recover-svn-repos/test_header
@@ -0,0 +1 @@
+../lib/bash/test_header
\ No newline at end of file
diff --git a/t/fcm-status/00-simple.t b/t/fcm-status/00-simple.t
new file mode 100644
index 0000000..9d2bad3
--- /dev/null
+++ b/t/fcm-status/00-simple.t
@@ -0,0 +1,83 @@
+#!/bin/bash
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# Basic tests for "fcm status".
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+tests 4
+#-------------------------------------------------------------------------------
+setup
+init_repos
+init_merge_branches merge1 merge2 $REPOS_URL
+export SVN_EDITOR="sed -i 1i\foo"
+cd $TEST_DIR/wc
+#-------------------------------------------------------------------------------
+# Tests the setup for fcm status testing
+svn switch -q $ROOT_URL/trunk
+touch added_file
+svn add -q added_file
+svn commit -q -m "trunk modifications"
+svn update -q
+TEST_KEY=$TEST_KEY_BASE-setup
+run_pass "$TEST_KEY" fcm merge --non-interactive branches/dev/Share/merge1
+rm subroutine/hello_sub.h
+svn delete -q --force lib/python/info/poems.py
+#-------------------------------------------------------------------------------
+# Tests fcm status result of fcm merge (1)
+TEST_KEY=$TEST_KEY_BASE-status
+run_pass "$TEST_KEY" fcm status --config-dir=$TEST_DIR/.subversion
+if $SVN_VERSION_IS_16; then
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+ M .
+? unversioned_file
+! subroutine/hello_sub.h
+M subroutine/hello_sub_dummy.h
+ C added_file
+ > local add, incoming add upon merge
+A + module/tree_conflict_file
+M module/hello_constants_dummy.inc
+M module/hello_constants.inc
+M module/hello_constants.f90
+A + added_directory
+A + added_directory/hello_constants_dummy.inc
+A + added_directory/hello_constants.inc
+A + added_directory/hello_constants.f90
+D lib/python/info/poems.py
+__OUT__
+else
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+ M .
+A + added_directory
+ C added_file
+ > local file obstruction, incoming file add upon merge
+D lib/python/info/poems.py
+M module/hello_constants.f90
+M module/hello_constants.inc
+M module/hello_constants_dummy.inc
+A + module/tree_conflict_file
+! subroutine/hello_sub.h
+M subroutine/hello_sub_dummy.h
+? unversioned_file
+Summary of conflicts:
+ Tree conflicts: 1
+__OUT__
+fi
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
diff --git a/t/fcm-status/test_header b/t/fcm-status/test_header
new file mode 100644
index 0000000..672ce12
--- /dev/null
+++ b/t/fcm-status/test_header
@@ -0,0 +1,232 @@
+#!/bin/bash
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# Optional enviroment variables:
+# TEST_PROJECT (tests using given project name)
+# TEST_REMOTE_HOST (tests using svn+ssh repositories located on given host)
+# ------------------------------------------------------------------------------
+
+. $(dirname $0)/../lib/bash/test_header
+
+function file_cmp() {
+ local TEST_KEY=$1
+ local FILE_ACTUAL=$2
+ local FILE_EXPECT=${3:--}
+ if cmp $TEST_DIR/$FILE_ACTUAL $FILE_EXPECT; then
+ pass $TEST_KEY
+ return
+ fi
+ fail $TEST_KEY
+}
+
+function file_grep() {
+ local TEST_KEY=$1
+ local PATTERN=$2
+ local FILE=$3
+ if grep -q -e "$PATTERN" $TEST_DIR/$FILE; then
+ pass $TEST_KEY
+ return
+ fi
+ fail $TEST_KEY
+}
+
+function file_test() {
+ local TEST_KEY=$1
+ local FILE=$2
+ local OPTION=${3:--e}
+ if test $OPTION $TEST_DIR/$FILE; then
+ pass $TEST_KEY
+ else
+ fail $TEST_KEY
+ fi
+}
+
+function file_xxdiff() {
+ local TEST_KEY=$1
+ local FILE_ACTUAL=$2
+ local FILE_EXPECT=${3:--}
+ if xxdiff -D $TEST_DIR/$FILE_ACTUAL $FILE_EXPECT; then
+ pass $TEST_KEY
+ return
+ fi
+ fail $TEST_KEY
+}
+
+function init_repos() {
+ if [[ -n ${TEST_REMOTE_HOST:-} ]]; then
+ TEST_REMOTE_DIR=$(ssh $TEST_REMOTE_HOST "mktemp -d")
+ ssh $TEST_REMOTE_HOST "svnadmin create --fs-type fsfs $TEST_REMOTE_DIR"
+ REPOS_URL="svn+ssh://${TEST_REMOTE_HOST}$TEST_REMOTE_DIR"
+ else
+ svnadmin create --fs-type fsfs $TEST_DIR/test_repos
+ REPOS_URL="file://$TEST_DIR/test_repos"
+ fi
+ ROOT_URL=$REPOS_URL
+ PROJECT=
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ PROJECT=$TEST_PROJECT"/"
+ fi
+ svn import -q $TEST_SOURCE_DIR/../etc/repo_files \
+ $REPOS_URL/$PROJECT/trunk -m "initial trunk import"
+ svn mkdir -q $REPOS_URL/$PROJECT/tags -m "make tags"
+ svn mkdir -q --parents $REPOS_URL/$PROJECT/branches/dev/Share -m " "
+}
+
+function init_repos_layout_roses() {
+ if [[ -n ${TEST_REMOTE_HOST:-} ]]; then
+ TEST_REMOTE_DIR=$(ssh $TEST_REMOTE_HOST "mktemp -d")
+ ssh $TEST_REMOTE_HOST "svnadmin create --fs-type fsfs $TEST_REMOTE_DIR"
+ REPOS_URL="svn+ssh://${TEST_REMOTE_HOST}$TEST_REMOTE_DIR"
+ else
+ svnadmin create --fs-type fsfs $TEST_DIR/test_repos
+ REPOS_URL="file://$TEST_DIR/test_repos"
+ fi
+ svn mkdir -q --parents $REPOS_URL/a/a/0/0/0/trunk
+ svn import -q $TEST_SOURCE_DIR/../etc/repo_files \
+ $REPOS_URL/a/a/0/0/0/trunk -m "initial trunk import"
+ TMPFILE=$(mktemp)
+ cat >$TMPFILE <<__LAYOUT__
+depth-project = 5
+depth-branch = 1
+depth-tag = 1
+dir-trunk = trunk
+dir-branch =
+dir-tag =
+level-owner-branch =
+level-owner-tag =
+template-branch =
+template-tag =
+__LAYOUT__
+ TMPDIR=$(mktemp -d)
+ svn checkout -q $REPOS_URL $TMPDIR
+ svn propset -q --file=$TMPFILE fcm:layout $TMPDIR
+ svn commit -q -m " " $TMPDIR
+ rm -f $TMPFILE
+ rm -rf $TMPDIR
+ ROOT_URL=$REPOS_URL
+}
+
+function init_branch() {
+ local BRANCH_NAME=$1
+ local REPOS_URL=$2
+ local ROOT_URL=$REPOS_URL
+ local ROOT_PATH=
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ ROOT_PATH=$ROOT_PATH/$TEST_PROJECT
+ fi
+ MESSAGE=$(echo -e "Created $ROOT_PATH/branches/dev/Share/$BRANCH_NAME from /trunk at 1.")
+ svn copy -q -r1 $ROOT_URL/trunk $ROOT_URL/branches/dev/Share/$BRANCH_NAME \
+ -m "Made a branch $MESSAGE"
+}
+
+function init_branch_wc() {
+ local BRANCH_NAME=$1
+ local REPOS_URL=$2
+ local ROOT_URL=$REPOS_URL
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ fi
+ init_branch $BRANCH_NAME $REPOS_URL
+ svn checkout -q $ROOT_URL/branches/dev/Share/$BRANCH_NAME $TEST_DIR/wc
+}
+
+function init_merge_branches() {
+ local BRANCH_NAME=$1
+ local OTHER_BRANCH_NAME=$2
+ local REPOS_URL=$3
+ local ROOT_URL=$REPOS_URL
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ fi
+ init_branch_wc $BRANCH_NAME $REPOS_URL
+ cd $TEST_DIR/wc
+ file_list=$(find . -type f | sed "/\.svn/d" | sort | head -5)
+ other_file=$(find . -type f | sed "/\.svn/d" | sort | tail -1)
+ for file in $file_list; do
+ sed -i "s/for/FOR/g; s/fi/end if/g; s/in/IN/g;" $file
+ sed -i "/#/d; /^ *!/d" $file
+ sed -i "s/!/!!/g; s/q/\nq/g; s/[(]/(\n/g" $file
+ done
+ file_dir=$(dirname $file)
+ svn copy -q $file ./added_file
+ svn copy -q module added_directory
+ touch module/tree_conflict_file
+ svn add -q $file_dir/tree_conflict_file
+ echo "Modified a line" >>$other_file
+ svn commit -q -m "Made changes for future merge of this branch"
+ svn update -q
+ init_branch $OTHER_BRANCH_NAME $REPOS_URL
+ svn switch -q $ROOT_URL/branches/dev/Share/$OTHER_BRANCH_NAME
+ echo " " > unversioned_file
+ properties_file=$(find . -type f | sed " /\.svn/d" | sort | tail -3 | head -1)
+ svn propset -q svn:executable "executable" $properties_file
+ svn copy -q $file renamed_added_file
+ svn commit -q -m "Made changes for future merge"
+ svn update -q
+ svn switch -q $ROOT_URL/trunk
+ echo "trunk change" >>$(find . -type f | sed "/\.svn/d" | sort | head -1)
+ svn commit -q -m "Made trunk change"
+ svn update -q
+ echo "another trunk change" >>$(find . -type f | sed "/\.svn/d" | sort | head -1)
+ svn commit -q -m "Made another trunk change"
+ svn update -q
+}
+
+function run_pass() {
+ local TEST_KEY=$1
+ shift 1
+ if ! "$@" 1>$TEST_DIR/$TEST_KEY.out 2>$TEST_DIR/$TEST_KEY.err; then
+ fail $TEST_KEY
+ return
+ fi
+ pass $TEST_KEY
+}
+
+function run_fail() {
+ local TEST_KEY=$1
+ shift 1
+ if "$@" 1>$TEST_DIR/$TEST_KEY.out 2>$TEST_DIR/$TEST_KEY.err; then
+ fail $TEST_KEY
+ return
+ fi
+ pass $TEST_KEY
+}
+
+function setup() {
+ mkdir -p $TEST_DIR/.subversion
+ mkdir -p $TEST_DIR/run
+ cd $TEST_DIR/run
+}
+
+function teardown() {
+ cd $TEST_DIR
+ rm -rf $TEST_DIR/test_repos
+ rm -rf $TEST_DIR/wc
+ rm -rf $TEST_DIR/run
+ rm -rf $TEST_DIR/.subversion
+ if [[ -n ${TEST_REMOTE_HOST:-} ]]; then
+ ssh $TEST_REMOTE_HOST "rm -rf $TEST_REMOTE_DIR"
+ fi
+}
+
+REPOS_URL=
+ROOT_URL=
+PROJECT=
diff --git a/t/fcm-switch/00-simple.t b/t/fcm-switch/00-simple.t
new file mode 100644
index 0000000..62583aa
--- /dev/null
+++ b/t/fcm-switch/00-simple.t
@@ -0,0 +1,109 @@
+#!/bin/bash
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# Basic tests for "fcm switch".
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+tests 12
+#-------------------------------------------------------------------------------
+setup
+init_repos
+init_merge_branches merge1 merge2 $REPOS_URL
+export SVN_EDITOR="sed -i 1i\foo"
+cd $TEST_DIR/wc
+#-------------------------------------------------------------------------------
+# Tests fcm switch trunk
+svn switch -q $ROOT_URL/branches/dev/Share/merge1
+TEST_KEY=$TEST_KEY_BASE-trunk
+run_pass "$TEST_KEY" fcm switch trunk <<<'y'
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+switch: status of "$TEST_DIR/wc":
+? unversioned_file
+switch: continue?
+Enter "y" or "n" (or just press <return> for "n"): D added_file
+D added_directory
+U subroutine/hello_sub_dummy.h
+D module/tree_conflict_file
+U module/hello_constants_dummy.inc
+U module/hello_constants.inc
+U module/hello_constants.f90
+U lib/python/info/__init__.py
+U lib/python/info/poems.py
+Updated to revision 9.
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm switch merge1 branch
+rm unversioned_file
+TEST_KEY=$TEST_KEY_BASE-branch-1
+run_pass "$TEST_KEY" fcm switch branches/dev/Share/merge1 <<<'y'
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+U subroutine/hello_sub_dummy.h
+A added_file
+A added_directory
+A added_directory/hello_constants_dummy.inc
+A added_directory/hello_constants.inc
+A added_directory/hello_constants.f90
+A module/tree_conflict_file
+U module/hello_constants_dummy.inc
+U module/hello_constants.inc
+U module/hello_constants.f90
+U lib/python/info/__init__.py
+U lib/python/info/poems.py
+Updated to revision 9.
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm switch merge2 branch
+TEST_KEY=$TEST_KEY_BASE-branch-2
+run_pass "$TEST_KEY" fcm switch --non-interactive dev/Share/merge2
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+D added_file
+D added_directory
+ U subroutine/hello_sub.h
+U subroutine/hello_sub_dummy.h
+D module/tree_conflict_file
+U module/hello_constants_dummy.inc
+U module/hello_constants.inc
+U module/hello_constants.f90
+U lib/python/info/poems.py
+A renamed_added_file
+Updated to revision 9.
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm switch trunk, without .svn/entries
+TEST_KEY=$TEST_KEY_BASE-trunk-2
+if $SVN_VERSION_IS_16; then
+ skip 3 "$TEST_KEY won't work under Subversion 1.6"
+else
+ rm -f .svn/entries
+ run_pass "$TEST_KEY" fcm switch trunk <<<'y'
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<'__OUT__'
+D renamed_added_file
+ U subroutine/hello_sub.h
+U lib/python/info/__init__.py
+Updated to revision 9.
+__OUT__
+ file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+fi
+#-------------------------------------------------------------------------------
+teardown
+exit
diff --git a/t/fcm-switch/01-subtree.t b/t/fcm-switch/01-subtree.t
new file mode 100644
index 0000000..cccb0b7
--- /dev/null
+++ b/t/fcm-switch/01-subtree.t
@@ -0,0 +1,149 @@
+#!/bin/bash
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# Basic tests for "fcm switch".
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+tests 9
+#-------------------------------------------------------------------------------
+setup
+init_repos
+init_merge_branches merge1 merge2 $REPOS_URL
+export SVN_EDITOR="sed -i 1i\foo"
+cd $TEST_DIR/wc
+#-------------------------------------------------------------------------------
+# Tests fcm switch trunk
+svn switch -q $ROOT_URL/branches/dev/Share/merge1
+TEST_KEY=$TEST_KEY_BASE-trunk
+cd module
+run_pass "$TEST_KEY" fcm switch trunk <<__IN__
+y
+__IN__
+if $SVN_VERSION_IS_16; then
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+switch: status of "$TEST_DIR/wc":
+? $TEST_DIR/wc/unversioned_file
+switch: continue?
+Enter "y" or "n" (or just press <return> for "n"): D $TEST_DIR/wc/added_file
+D $TEST_DIR/wc/added_directory
+U $TEST_DIR/wc/subroutine/hello_sub_dummy.h
+D $TEST_DIR/wc/module/tree_conflict_file
+U $TEST_DIR/wc/module/hello_constants_dummy.inc
+U $TEST_DIR/wc/module/hello_constants.inc
+U $TEST_DIR/wc/module/hello_constants.f90
+U $TEST_DIR/wc/lib/python/info/__init__.py
+U $TEST_DIR/wc/lib/python/info/poems.py
+Updated to revision 9.
+__OUT__
+else
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+switch: status of "$TEST_DIR/wc":
+? $TEST_DIR/wc/unversioned_file
+switch: continue?
+Enter "y" or "n" (or just press <return> for "n"): D $TEST_DIR/wc/added_file
+D $TEST_DIR/wc/added_directory
+U $TEST_DIR/wc/subroutine/hello_sub_dummy.h
+D tree_conflict_file
+U hello_constants_dummy.inc
+U hello_constants.inc
+U hello_constants.f90
+U $TEST_DIR/wc/lib/python/info/__init__.py
+U $TEST_DIR/wc/lib/python/info/poems.py
+Updated to revision 9.
+__OUT__
+fi
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm switch merge1 branch
+rm ../unversioned_file
+TEST_KEY=$TEST_KEY_BASE-branch-1
+run_pass "$TEST_KEY" fcm switch branches/dev/Share/merge1 <<__IN__
+y
+__IN__
+if $SVN_VERSION_IS_16; then
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+U $TEST_DIR/wc/subroutine/hello_sub_dummy.h
+A $TEST_DIR/wc/added_file
+A $TEST_DIR/wc/added_directory
+A $TEST_DIR/wc/added_directory/hello_constants_dummy.inc
+A $TEST_DIR/wc/added_directory/hello_constants.inc
+A $TEST_DIR/wc/added_directory/hello_constants.f90
+A $TEST_DIR/wc/module/tree_conflict_file
+U $TEST_DIR/wc/module/hello_constants_dummy.inc
+U $TEST_DIR/wc/module/hello_constants.inc
+U $TEST_DIR/wc/module/hello_constants.f90
+U $TEST_DIR/wc/lib/python/info/__init__.py
+U $TEST_DIR/wc/lib/python/info/poems.py
+Updated to revision 9.
+__OUT__
+else
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+U $TEST_DIR/wc/subroutine/hello_sub_dummy.h
+A $TEST_DIR/wc/added_file
+A $TEST_DIR/wc/added_directory
+A $TEST_DIR/wc/added_directory/hello_constants_dummy.inc
+A $TEST_DIR/wc/added_directory/hello_constants.inc
+A $TEST_DIR/wc/added_directory/hello_constants.f90
+A tree_conflict_file
+U hello_constants_dummy.inc
+U hello_constants.inc
+U hello_constants.f90
+U $TEST_DIR/wc/lib/python/info/__init__.py
+U $TEST_DIR/wc/lib/python/info/poems.py
+Updated to revision 9.
+__OUT__
+fi
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm switch merge2 branch
+TEST_KEY=$TEST_KEY_BASE-branch-2
+run_pass "$TEST_KEY" fcm switch --non-interactive dev/Share/merge2
+if $SVN_VERSION_IS_16; then
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+D $TEST_DIR/wc/added_file
+D $TEST_DIR/wc/added_directory
+ U $TEST_DIR/wc/subroutine/hello_sub.h
+U $TEST_DIR/wc/subroutine/hello_sub_dummy.h
+D $TEST_DIR/wc/module/tree_conflict_file
+U $TEST_DIR/wc/module/hello_constants_dummy.inc
+U $TEST_DIR/wc/module/hello_constants.inc
+U $TEST_DIR/wc/module/hello_constants.f90
+U $TEST_DIR/wc/lib/python/info/poems.py
+A $TEST_DIR/wc/renamed_added_file
+Updated to revision 9.
+__OUT__
+else
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+D $TEST_DIR/wc/added_file
+D $TEST_DIR/wc/added_directory
+ U $TEST_DIR/wc/subroutine/hello_sub.h
+U $TEST_DIR/wc/subroutine/hello_sub_dummy.h
+D tree_conflict_file
+U hello_constants_dummy.inc
+U hello_constants.inc
+U hello_constants.f90
+U $TEST_DIR/wc/lib/python/info/poems.py
+A $TEST_DIR/wc/renamed_added_file
+Updated to revision 9.
+__OUT__
+fi
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+teardown
+#-------------------------------------------------------------------------------
diff --git a/t/fcm-switch/test_header b/t/fcm-switch/test_header
new file mode 100644
index 0000000..672ce12
--- /dev/null
+++ b/t/fcm-switch/test_header
@@ -0,0 +1,232 @@
+#!/bin/bash
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# Optional enviroment variables:
+# TEST_PROJECT (tests using given project name)
+# TEST_REMOTE_HOST (tests using svn+ssh repositories located on given host)
+# ------------------------------------------------------------------------------
+
+. $(dirname $0)/../lib/bash/test_header
+
+function file_cmp() {
+ local TEST_KEY=$1
+ local FILE_ACTUAL=$2
+ local FILE_EXPECT=${3:--}
+ if cmp $TEST_DIR/$FILE_ACTUAL $FILE_EXPECT; then
+ pass $TEST_KEY
+ return
+ fi
+ fail $TEST_KEY
+}
+
+function file_grep() {
+ local TEST_KEY=$1
+ local PATTERN=$2
+ local FILE=$3
+ if grep -q -e "$PATTERN" $TEST_DIR/$FILE; then
+ pass $TEST_KEY
+ return
+ fi
+ fail $TEST_KEY
+}
+
+function file_test() {
+ local TEST_KEY=$1
+ local FILE=$2
+ local OPTION=${3:--e}
+ if test $OPTION $TEST_DIR/$FILE; then
+ pass $TEST_KEY
+ else
+ fail $TEST_KEY
+ fi
+}
+
+function file_xxdiff() {
+ local TEST_KEY=$1
+ local FILE_ACTUAL=$2
+ local FILE_EXPECT=${3:--}
+ if xxdiff -D $TEST_DIR/$FILE_ACTUAL $FILE_EXPECT; then
+ pass $TEST_KEY
+ return
+ fi
+ fail $TEST_KEY
+}
+
+function init_repos() {
+ if [[ -n ${TEST_REMOTE_HOST:-} ]]; then
+ TEST_REMOTE_DIR=$(ssh $TEST_REMOTE_HOST "mktemp -d")
+ ssh $TEST_REMOTE_HOST "svnadmin create --fs-type fsfs $TEST_REMOTE_DIR"
+ REPOS_URL="svn+ssh://${TEST_REMOTE_HOST}$TEST_REMOTE_DIR"
+ else
+ svnadmin create --fs-type fsfs $TEST_DIR/test_repos
+ REPOS_URL="file://$TEST_DIR/test_repos"
+ fi
+ ROOT_URL=$REPOS_URL
+ PROJECT=
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ PROJECT=$TEST_PROJECT"/"
+ fi
+ svn import -q $TEST_SOURCE_DIR/../etc/repo_files \
+ $REPOS_URL/$PROJECT/trunk -m "initial trunk import"
+ svn mkdir -q $REPOS_URL/$PROJECT/tags -m "make tags"
+ svn mkdir -q --parents $REPOS_URL/$PROJECT/branches/dev/Share -m " "
+}
+
+function init_repos_layout_roses() {
+ if [[ -n ${TEST_REMOTE_HOST:-} ]]; then
+ TEST_REMOTE_DIR=$(ssh $TEST_REMOTE_HOST "mktemp -d")
+ ssh $TEST_REMOTE_HOST "svnadmin create --fs-type fsfs $TEST_REMOTE_DIR"
+ REPOS_URL="svn+ssh://${TEST_REMOTE_HOST}$TEST_REMOTE_DIR"
+ else
+ svnadmin create --fs-type fsfs $TEST_DIR/test_repos
+ REPOS_URL="file://$TEST_DIR/test_repos"
+ fi
+ svn mkdir -q --parents $REPOS_URL/a/a/0/0/0/trunk
+ svn import -q $TEST_SOURCE_DIR/../etc/repo_files \
+ $REPOS_URL/a/a/0/0/0/trunk -m "initial trunk import"
+ TMPFILE=$(mktemp)
+ cat >$TMPFILE <<__LAYOUT__
+depth-project = 5
+depth-branch = 1
+depth-tag = 1
+dir-trunk = trunk
+dir-branch =
+dir-tag =
+level-owner-branch =
+level-owner-tag =
+template-branch =
+template-tag =
+__LAYOUT__
+ TMPDIR=$(mktemp -d)
+ svn checkout -q $REPOS_URL $TMPDIR
+ svn propset -q --file=$TMPFILE fcm:layout $TMPDIR
+ svn commit -q -m " " $TMPDIR
+ rm -f $TMPFILE
+ rm -rf $TMPDIR
+ ROOT_URL=$REPOS_URL
+}
+
+function init_branch() {
+ local BRANCH_NAME=$1
+ local REPOS_URL=$2
+ local ROOT_URL=$REPOS_URL
+ local ROOT_PATH=
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ ROOT_PATH=$ROOT_PATH/$TEST_PROJECT
+ fi
+ MESSAGE=$(echo -e "Created $ROOT_PATH/branches/dev/Share/$BRANCH_NAME from /trunk at 1.")
+ svn copy -q -r1 $ROOT_URL/trunk $ROOT_URL/branches/dev/Share/$BRANCH_NAME \
+ -m "Made a branch $MESSAGE"
+}
+
+function init_branch_wc() {
+ local BRANCH_NAME=$1
+ local REPOS_URL=$2
+ local ROOT_URL=$REPOS_URL
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ fi
+ init_branch $BRANCH_NAME $REPOS_URL
+ svn checkout -q $ROOT_URL/branches/dev/Share/$BRANCH_NAME $TEST_DIR/wc
+}
+
+function init_merge_branches() {
+ local BRANCH_NAME=$1
+ local OTHER_BRANCH_NAME=$2
+ local REPOS_URL=$3
+ local ROOT_URL=$REPOS_URL
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ fi
+ init_branch_wc $BRANCH_NAME $REPOS_URL
+ cd $TEST_DIR/wc
+ file_list=$(find . -type f | sed "/\.svn/d" | sort | head -5)
+ other_file=$(find . -type f | sed "/\.svn/d" | sort | tail -1)
+ for file in $file_list; do
+ sed -i "s/for/FOR/g; s/fi/end if/g; s/in/IN/g;" $file
+ sed -i "/#/d; /^ *!/d" $file
+ sed -i "s/!/!!/g; s/q/\nq/g; s/[(]/(\n/g" $file
+ done
+ file_dir=$(dirname $file)
+ svn copy -q $file ./added_file
+ svn copy -q module added_directory
+ touch module/tree_conflict_file
+ svn add -q $file_dir/tree_conflict_file
+ echo "Modified a line" >>$other_file
+ svn commit -q -m "Made changes for future merge of this branch"
+ svn update -q
+ init_branch $OTHER_BRANCH_NAME $REPOS_URL
+ svn switch -q $ROOT_URL/branches/dev/Share/$OTHER_BRANCH_NAME
+ echo " " > unversioned_file
+ properties_file=$(find . -type f | sed " /\.svn/d" | sort | tail -3 | head -1)
+ svn propset -q svn:executable "executable" $properties_file
+ svn copy -q $file renamed_added_file
+ svn commit -q -m "Made changes for future merge"
+ svn update -q
+ svn switch -q $ROOT_URL/trunk
+ echo "trunk change" >>$(find . -type f | sed "/\.svn/d" | sort | head -1)
+ svn commit -q -m "Made trunk change"
+ svn update -q
+ echo "another trunk change" >>$(find . -type f | sed "/\.svn/d" | sort | head -1)
+ svn commit -q -m "Made another trunk change"
+ svn update -q
+}
+
+function run_pass() {
+ local TEST_KEY=$1
+ shift 1
+ if ! "$@" 1>$TEST_DIR/$TEST_KEY.out 2>$TEST_DIR/$TEST_KEY.err; then
+ fail $TEST_KEY
+ return
+ fi
+ pass $TEST_KEY
+}
+
+function run_fail() {
+ local TEST_KEY=$1
+ shift 1
+ if "$@" 1>$TEST_DIR/$TEST_KEY.out 2>$TEST_DIR/$TEST_KEY.err; then
+ fail $TEST_KEY
+ return
+ fi
+ pass $TEST_KEY
+}
+
+function setup() {
+ mkdir -p $TEST_DIR/.subversion
+ mkdir -p $TEST_DIR/run
+ cd $TEST_DIR/run
+}
+
+function teardown() {
+ cd $TEST_DIR
+ rm -rf $TEST_DIR/test_repos
+ rm -rf $TEST_DIR/wc
+ rm -rf $TEST_DIR/run
+ rm -rf $TEST_DIR/.subversion
+ if [[ -n ${TEST_REMOTE_HOST:-} ]]; then
+ ssh $TEST_REMOTE_HOST "rm -rf $TEST_REMOTE_DIR"
+ fi
+}
+
+REPOS_URL=
+ROOT_URL=
+PROJECT=
diff --git a/t/fcm-update/00-simple.t b/t/fcm-update/00-simple.t
new file mode 100644
index 0000000..1948c9d
--- /dev/null
+++ b/t/fcm-update/00-simple.t
@@ -0,0 +1,144 @@
+#!/bin/bash
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# Basic tests for "fcm update".
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+tests 6
+#-------------------------------------------------------------------------------
+setup
+init_repos
+init_merge_branches merge1 merge2 $REPOS_URL
+export SVN_EDITOR="sed -i 1i\foo"
+cd $TEST_DIR/wc
+#-------------------------------------------------------------------------------
+# Tests fcm update -r PREV
+svn switch -q $ROOT_URL/branches/dev/Share/merge1
+TEST_KEY=$TEST_KEY_BASE-r-PREV
+run_pass "$TEST_KEY" fcm update -r PREV <<__IN__
+y
+__IN__
+if $SVN_VERSION_IS_16; then
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+update: status of ".":
+? unversioned_file
+update: continue?
+Enter "y" or "n" (or just press <return> for "n"): D added_file
+D added_directory
+U subroutine/hello_sub_dummy.h
+D module/tree_conflict_file
+U module/hello_constants_dummy.inc
+U module/hello_constants.inc
+U module/hello_constants.f90
+U lib/python/info/poems.py
+Updated to revision 4.
+__OUT__
+else
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+update: status of ".":
+? unversioned_file
+update: continue?
+Enter "y" or "n" (or just press <return> for "n"): Updating '.':
+D added_file
+D added_directory
+U subroutine/hello_sub_dummy.h
+D module/tree_conflict_file
+U module/hello_constants_dummy.inc
+U module/hello_constants.inc
+U module/hello_constants.f90
+U lib/python/info/poems.py
+Updated to revision 4.
+__OUT__
+fi
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm update
+rm unversioned_file
+TEST_KEY=$TEST_KEY_BASE-normal
+run_pass "$TEST_KEY" fcm update <<__IN__
+y
+__IN__
+if $SVN_VERSION_IS_16; then
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+update: status of ".":
+ * 4 subroutine/hello_sub_dummy.h
+ * added_directory/hello_constants.f90
+ * added_directory/hello_constants_dummy.inc
+ * added_directory/hello_constants.inc
+ * added_directory
+ * 4 module/hello_constants.f90
+ * module/tree_conflict_file
+ * 4 module/hello_constants_dummy.inc
+ * 4 module/hello_constants.inc
+ * 4 module
+ * 4 lib/python/info/poems.py
+ * added_file
+ * 4 .
+update: continue?
+Enter "y" or "n" (or just press <return> for "n"): U subroutine/hello_sub_dummy.h
+A added_file
+A added_directory
+A added_directory/hello_constants_dummy.inc
+A added_directory/hello_constants.inc
+A added_directory/hello_constants.f90
+A module/tree_conflict_file
+U module/hello_constants_dummy.inc
+U module/hello_constants.inc
+U module/hello_constants.f90
+U lib/python/info/poems.py
+Updated to revision 9.
+__OUT__
+else
+ # The output is now not deterministic for svn update!!
+ sort $TEST_DIR/"$TEST_KEY.out" -o $TEST_DIR/"$TEST_KEY.out"
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+ * added_directory
+ * added_directory/hello_constants.f90
+ * added_directory/hello_constants.inc
+ * added_directory/hello_constants_dummy.inc
+ * added_file
+ * module/tree_conflict_file
+ * 4 .
+ * 4 lib/python/info/poems.py
+ * 4 module
+ * 4 module/hello_constants.f90
+ * 4 module/hello_constants.inc
+ * 4 module/hello_constants_dummy.inc
+ * 4 subroutine/hello_sub_dummy.h
+A added_directory
+A added_directory/hello_constants.f90
+A added_directory/hello_constants.inc
+A added_directory/hello_constants_dummy.inc
+A added_file
+A module/tree_conflict_file
+Enter "y" or "n" (or just press <return> for "n"): Updating '.':
+U lib/python/info/poems.py
+U module/hello_constants.f90
+U module/hello_constants.inc
+U module/hello_constants_dummy.inc
+U subroutine/hello_sub_dummy.h
+Updated to revision 9.
+update: continue?
+update: status of ".":
+__OUT__
+fi
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+teardown
+#-------------------------------------------------------------------------------
diff --git a/t/fcm-update/01-subtree.t b/t/fcm-update/01-subtree.t
new file mode 100644
index 0000000..061eba4
--- /dev/null
+++ b/t/fcm-update/01-subtree.t
@@ -0,0 +1,146 @@
+#!/bin/bash
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# Basic tests for "fcm update".
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+tests 6
+#-------------------------------------------------------------------------------
+setup
+init_repos
+init_merge_branches merge1 merge2 $REPOS_URL
+export SVN_EDITOR="sed -i 1i\foo"
+cd $TEST_DIR/wc
+#-------------------------------------------------------------------------------
+# Tests fcm update -r PREV
+svn switch -q $ROOT_URL/branches/dev/Share/merge1
+TEST_KEY=$TEST_KEY_BASE-r-PREV
+cd module
+run_pass "$TEST_KEY" fcm update -r PREV <<__IN__
+y
+__IN__
+if $SVN_VERSION_IS_16; then
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+update: status of "$TEST_DIR/wc":
+? $TEST_DIR/wc/unversioned_file
+update: continue?
+Enter "y" or "n" (or just press <return> for "n"): D $TEST_DIR/wc/added_file
+D $TEST_DIR/wc/added_directory
+U $TEST_DIR/wc/subroutine/hello_sub_dummy.h
+D $TEST_DIR/wc/module/tree_conflict_file
+U $TEST_DIR/wc/module/hello_constants_dummy.inc
+U $TEST_DIR/wc/module/hello_constants.inc
+U $TEST_DIR/wc/module/hello_constants.f90
+U $TEST_DIR/wc/lib/python/info/poems.py
+Updated to revision 4.
+__OUT__
+else
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+update: status of "$TEST_DIR/wc":
+? $TEST_DIR/wc/unversioned_file
+update: continue?
+Enter "y" or "n" (or just press <return> for "n"): Updating '$TEST_DIR/wc':
+D $TEST_DIR/wc/added_file
+D $TEST_DIR/wc/added_directory
+U $TEST_DIR/wc/subroutine/hello_sub_dummy.h
+D tree_conflict_file
+U hello_constants_dummy.inc
+U hello_constants.inc
+U hello_constants.f90
+U $TEST_DIR/wc/lib/python/info/poems.py
+Updated to revision 4.
+__OUT__
+fi
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+# Tests fcm update
+rm ../unversioned_file
+TEST_KEY=$TEST_KEY_BASE-normal
+run_pass "$TEST_KEY" fcm update <<__IN__
+y
+__IN__
+if $SVN_VERSION_IS_16; then
+ sort $TEST_DIR/"$TEST_KEY.out" -o $TEST_DIR/"$TEST_KEY.out"
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+ * $TEST_DIR/wc/added_directory
+ * $TEST_DIR/wc/added_directory/hello_constants.f90
+ * $TEST_DIR/wc/added_directory/hello_constants.inc
+ * $TEST_DIR/wc/added_directory/hello_constants_dummy.inc
+ * $TEST_DIR/wc/added_file
+ * $TEST_DIR/wc/module/tree_conflict_file
+ * 4 $TEST_DIR/wc
+ * 4 $TEST_DIR/wc/lib/python/info/poems.py
+ * 4 $TEST_DIR/wc/module
+ * 4 $TEST_DIR/wc/module/hello_constants.f90
+ * 4 $TEST_DIR/wc/module/hello_constants.inc
+ * 4 $TEST_DIR/wc/module/hello_constants_dummy.inc
+ * 4 $TEST_DIR/wc/subroutine/hello_sub_dummy.h
+A $TEST_DIR/wc/added_directory
+A $TEST_DIR/wc/added_directory/hello_constants.f90
+A $TEST_DIR/wc/added_directory/hello_constants.inc
+A $TEST_DIR/wc/added_directory/hello_constants_dummy.inc
+A $TEST_DIR/wc/added_file
+A $TEST_DIR/wc/module/tree_conflict_file
+Enter "y" or "n" (or just press <return> for "n"): U $TEST_DIR/wc/subroutine/hello_sub_dummy.h
+U $TEST_DIR/wc/lib/python/info/poems.py
+U $TEST_DIR/wc/module/hello_constants.f90
+U $TEST_DIR/wc/module/hello_constants.inc
+U $TEST_DIR/wc/module/hello_constants_dummy.inc
+Updated to revision 9.
+update: continue?
+update: status of "$TEST_DIR/wc":
+__OUT__
+else
+ # The output is now not deterministic for svn update!!
+ sort $TEST_DIR/"$TEST_KEY.out" -o $TEST_DIR/"$TEST_KEY.out"
+ file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+ * $TEST_DIR/wc/added_directory
+ * $TEST_DIR/wc/added_directory/hello_constants.f90
+ * $TEST_DIR/wc/added_directory/hello_constants.inc
+ * $TEST_DIR/wc/added_directory/hello_constants_dummy.inc
+ * $TEST_DIR/wc/added_file
+ * $TEST_DIR/wc/module/tree_conflict_file
+ * 4 $TEST_DIR/wc
+ * 4 $TEST_DIR/wc/lib/python/info/poems.py
+ * 4 $TEST_DIR/wc/module
+ * 4 $TEST_DIR/wc/module/hello_constants.f90
+ * 4 $TEST_DIR/wc/module/hello_constants.inc
+ * 4 $TEST_DIR/wc/module/hello_constants_dummy.inc
+ * 4 $TEST_DIR/wc/subroutine/hello_sub_dummy.h
+A $TEST_DIR/wc/added_directory
+A $TEST_DIR/wc/added_directory/hello_constants.f90
+A $TEST_DIR/wc/added_directory/hello_constants.inc
+A $TEST_DIR/wc/added_directory/hello_constants_dummy.inc
+A $TEST_DIR/wc/added_file
+A tree_conflict_file
+Enter "y" or "n" (or just press <return> for "n"): Updating '$TEST_DIR/wc':
+U $TEST_DIR/wc/lib/python/info/poems.py
+U $TEST_DIR/wc/subroutine/hello_sub_dummy.h
+U hello_constants.f90
+U hello_constants.inc
+U hello_constants_dummy.inc
+Updated to revision 9.
+update: continue?
+update: status of "$TEST_DIR/wc":
+__OUT__
+fi
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+teardown
+#-------------------------------------------------------------------------------
diff --git a/t/fcm-update/test_header b/t/fcm-update/test_header
new file mode 100644
index 0000000..672ce12
--- /dev/null
+++ b/t/fcm-update/test_header
@@ -0,0 +1,232 @@
+#!/bin/bash
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# Optional enviroment variables:
+# TEST_PROJECT (tests using given project name)
+# TEST_REMOTE_HOST (tests using svn+ssh repositories located on given host)
+# ------------------------------------------------------------------------------
+
+. $(dirname $0)/../lib/bash/test_header
+
+function file_cmp() {
+ local TEST_KEY=$1
+ local FILE_ACTUAL=$2
+ local FILE_EXPECT=${3:--}
+ if cmp $TEST_DIR/$FILE_ACTUAL $FILE_EXPECT; then
+ pass $TEST_KEY
+ return
+ fi
+ fail $TEST_KEY
+}
+
+function file_grep() {
+ local TEST_KEY=$1
+ local PATTERN=$2
+ local FILE=$3
+ if grep -q -e "$PATTERN" $TEST_DIR/$FILE; then
+ pass $TEST_KEY
+ return
+ fi
+ fail $TEST_KEY
+}
+
+function file_test() {
+ local TEST_KEY=$1
+ local FILE=$2
+ local OPTION=${3:--e}
+ if test $OPTION $TEST_DIR/$FILE; then
+ pass $TEST_KEY
+ else
+ fail $TEST_KEY
+ fi
+}
+
+function file_xxdiff() {
+ local TEST_KEY=$1
+ local FILE_ACTUAL=$2
+ local FILE_EXPECT=${3:--}
+ if xxdiff -D $TEST_DIR/$FILE_ACTUAL $FILE_EXPECT; then
+ pass $TEST_KEY
+ return
+ fi
+ fail $TEST_KEY
+}
+
+function init_repos() {
+ if [[ -n ${TEST_REMOTE_HOST:-} ]]; then
+ TEST_REMOTE_DIR=$(ssh $TEST_REMOTE_HOST "mktemp -d")
+ ssh $TEST_REMOTE_HOST "svnadmin create --fs-type fsfs $TEST_REMOTE_DIR"
+ REPOS_URL="svn+ssh://${TEST_REMOTE_HOST}$TEST_REMOTE_DIR"
+ else
+ svnadmin create --fs-type fsfs $TEST_DIR/test_repos
+ REPOS_URL="file://$TEST_DIR/test_repos"
+ fi
+ ROOT_URL=$REPOS_URL
+ PROJECT=
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ PROJECT=$TEST_PROJECT"/"
+ fi
+ svn import -q $TEST_SOURCE_DIR/../etc/repo_files \
+ $REPOS_URL/$PROJECT/trunk -m "initial trunk import"
+ svn mkdir -q $REPOS_URL/$PROJECT/tags -m "make tags"
+ svn mkdir -q --parents $REPOS_URL/$PROJECT/branches/dev/Share -m " "
+}
+
+function init_repos_layout_roses() {
+ if [[ -n ${TEST_REMOTE_HOST:-} ]]; then
+ TEST_REMOTE_DIR=$(ssh $TEST_REMOTE_HOST "mktemp -d")
+ ssh $TEST_REMOTE_HOST "svnadmin create --fs-type fsfs $TEST_REMOTE_DIR"
+ REPOS_URL="svn+ssh://${TEST_REMOTE_HOST}$TEST_REMOTE_DIR"
+ else
+ svnadmin create --fs-type fsfs $TEST_DIR/test_repos
+ REPOS_URL="file://$TEST_DIR/test_repos"
+ fi
+ svn mkdir -q --parents $REPOS_URL/a/a/0/0/0/trunk
+ svn import -q $TEST_SOURCE_DIR/../etc/repo_files \
+ $REPOS_URL/a/a/0/0/0/trunk -m "initial trunk import"
+ TMPFILE=$(mktemp)
+ cat >$TMPFILE <<__LAYOUT__
+depth-project = 5
+depth-branch = 1
+depth-tag = 1
+dir-trunk = trunk
+dir-branch =
+dir-tag =
+level-owner-branch =
+level-owner-tag =
+template-branch =
+template-tag =
+__LAYOUT__
+ TMPDIR=$(mktemp -d)
+ svn checkout -q $REPOS_URL $TMPDIR
+ svn propset -q --file=$TMPFILE fcm:layout $TMPDIR
+ svn commit -q -m " " $TMPDIR
+ rm -f $TMPFILE
+ rm -rf $TMPDIR
+ ROOT_URL=$REPOS_URL
+}
+
+function init_branch() {
+ local BRANCH_NAME=$1
+ local REPOS_URL=$2
+ local ROOT_URL=$REPOS_URL
+ local ROOT_PATH=
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ ROOT_PATH=$ROOT_PATH/$TEST_PROJECT
+ fi
+ MESSAGE=$(echo -e "Created $ROOT_PATH/branches/dev/Share/$BRANCH_NAME from /trunk at 1.")
+ svn copy -q -r1 $ROOT_URL/trunk $ROOT_URL/branches/dev/Share/$BRANCH_NAME \
+ -m "Made a branch $MESSAGE"
+}
+
+function init_branch_wc() {
+ local BRANCH_NAME=$1
+ local REPOS_URL=$2
+ local ROOT_URL=$REPOS_URL
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ fi
+ init_branch $BRANCH_NAME $REPOS_URL
+ svn checkout -q $ROOT_URL/branches/dev/Share/$BRANCH_NAME $TEST_DIR/wc
+}
+
+function init_merge_branches() {
+ local BRANCH_NAME=$1
+ local OTHER_BRANCH_NAME=$2
+ local REPOS_URL=$3
+ local ROOT_URL=$REPOS_URL
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ fi
+ init_branch_wc $BRANCH_NAME $REPOS_URL
+ cd $TEST_DIR/wc
+ file_list=$(find . -type f | sed "/\.svn/d" | sort | head -5)
+ other_file=$(find . -type f | sed "/\.svn/d" | sort | tail -1)
+ for file in $file_list; do
+ sed -i "s/for/FOR/g; s/fi/end if/g; s/in/IN/g;" $file
+ sed -i "/#/d; /^ *!/d" $file
+ sed -i "s/!/!!/g; s/q/\nq/g; s/[(]/(\n/g" $file
+ done
+ file_dir=$(dirname $file)
+ svn copy -q $file ./added_file
+ svn copy -q module added_directory
+ touch module/tree_conflict_file
+ svn add -q $file_dir/tree_conflict_file
+ echo "Modified a line" >>$other_file
+ svn commit -q -m "Made changes for future merge of this branch"
+ svn update -q
+ init_branch $OTHER_BRANCH_NAME $REPOS_URL
+ svn switch -q $ROOT_URL/branches/dev/Share/$OTHER_BRANCH_NAME
+ echo " " > unversioned_file
+ properties_file=$(find . -type f | sed " /\.svn/d" | sort | tail -3 | head -1)
+ svn propset -q svn:executable "executable" $properties_file
+ svn copy -q $file renamed_added_file
+ svn commit -q -m "Made changes for future merge"
+ svn update -q
+ svn switch -q $ROOT_URL/trunk
+ echo "trunk change" >>$(find . -type f | sed "/\.svn/d" | sort | head -1)
+ svn commit -q -m "Made trunk change"
+ svn update -q
+ echo "another trunk change" >>$(find . -type f | sed "/\.svn/d" | sort | head -1)
+ svn commit -q -m "Made another trunk change"
+ svn update -q
+}
+
+function run_pass() {
+ local TEST_KEY=$1
+ shift 1
+ if ! "$@" 1>$TEST_DIR/$TEST_KEY.out 2>$TEST_DIR/$TEST_KEY.err; then
+ fail $TEST_KEY
+ return
+ fi
+ pass $TEST_KEY
+}
+
+function run_fail() {
+ local TEST_KEY=$1
+ shift 1
+ if "$@" 1>$TEST_DIR/$TEST_KEY.out 2>$TEST_DIR/$TEST_KEY.err; then
+ fail $TEST_KEY
+ return
+ fi
+ pass $TEST_KEY
+}
+
+function setup() {
+ mkdir -p $TEST_DIR/.subversion
+ mkdir -p $TEST_DIR/run
+ cd $TEST_DIR/run
+}
+
+function teardown() {
+ cd $TEST_DIR
+ rm -rf $TEST_DIR/test_repos
+ rm -rf $TEST_DIR/wc
+ rm -rf $TEST_DIR/run
+ rm -rf $TEST_DIR/.subversion
+ if [[ -n ${TEST_REMOTE_HOST:-} ]]; then
+ ssh $TEST_REMOTE_HOST "rm -rf $TEST_REMOTE_DIR"
+ fi
+}
+
+REPOS_URL=
+ROOT_URL=
+PROJECT=
diff --git a/t/lib/bash/test_header b/t/lib/bash/test_header
new file mode 100644
index 0000000..1d5e010
--- /dev/null
+++ b/t/lib/bash/test_header
@@ -0,0 +1,216 @@
+#!/bin/bash
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# NAME
+# test_header
+#
+# SYNOPSIS
+# . $FCM_HOME/t/lib/bash/test_header
+#
+# DESCRIPTION
+# Provide bash shell functions for writing tests for "fcm" commands to
+# output in Perl's TAP format. Add "set -eu". Create a temporary working
+# directory $TEST_DIR and change to it. Automatically increment test number.
+# If $FCM_HOME is not specified, set it to point to the "fcm" source tree
+# containing this script. Add $FCM_HOME/bin to the front of $PATH.
+#
+# FUNCTIONS
+# tests N
+# echo "1..$N".
+# skip N REASON
+# echo "ok $((++T)) # skip REASON" N times, where T is the test number.
+# skip_all REASON
+# echo "1..0 # SKIP $REASON" and exit.
+# pass TEST_KEY
+# echo "ok $T - $TEST_KEY" where T is the current test number.
+# fail TEST_KEY
+# echo "not ok $T - $TEST_KEY" where T is the current test number.
+# run_pass TEST_KEY COMMAND ...
+# Run $COMMAND. pass/fail $TEST_KEY if $COMMAND returns true/false.
+# Write STDOUT and STDERR in $TEST_KEY.out and $TEST_KEY.err.
+# run_fail TEST_KEY COMMAND ...
+# Run $COMMAND. pass/fail $TEST_KEY if $COMMAND returns false/true.
+# Write STDOUT and STDERR in $TEST_KEY.out and $TEST_KEY.err.
+# file_cmp TEST_KEY FILE_ACTUAL [$FILE_EXPECT]
+# Compare contents in $FILE_ACTUAL and $FILE_EXPECT. pass/fail
+# $TEST_KEY if contents are identical/different. If $FILE_EXPECT is "-"
+# or not defined, compare $FILE_ACTUAL with STDIN to this function.
+# file_test TEST_KEY FILE [OPTION]
+# pass/fail $TEST_KEY if "test $OPTION $FILE" returns 0/1. $OPTION is
+# -e if not specified.
+# file_grep TEST_KEY PATTERN FILE
+# Run "grep -q PATTERN FILE". pass/fail $TEST_KEY accordingly.
+# FINALLY
+# This is run on EXIT or INT to remove the temporary working directory
+# for the test. Call FINALLY_MORE if it is declared.
+#
+# VARIABLES
+# FCM_HOME
+# Root of FCM's installation. (Exported.)
+# SIGNALS
+# List of signals trapped by FINALLY, currently EXIT and INT.
+# SVN_VERSION_IS_16
+# True if "svn --version" returns 1.6*.
+# TEST_DIR
+# Temporary directory that is also the working directory for this test.
+# TEST_KEY_BASE
+# Base root name of current test file.
+# TEST_NUMBER
+# Test number of latest test.
+# TEST_SOURCE_DIR
+# Directory containing the current test file.
+#-------------------------------------------------------------------------------
+set -eu
+
+SIGNALS="EXIT INT"
+TEST_DIR=
+function FINALLY() {
+ for S in $SIGNALS; do
+ trap '' $S
+ done
+ if [[ -n $TEST_DIR ]]; then
+ cd ~
+ rm -rf $TEST_DIR
+ fi
+ if declare -F FINALLY_MORE >/dev/null; then
+ FINALLY_MORE
+ fi
+
+}
+for S in $SIGNALS; do
+ trap "FINALLY $S" $S
+done
+
+TEST_NUMBER=0
+
+function tests() {
+ echo "1..$1"
+}
+
+function skip() {
+ local N_SKIPS=$1
+ shift 1
+ local I=0
+ while ((I++ < N_SKIPS)); do
+ echo "ok $((++TEST_NUMBER)) # skip $@"
+ done
+}
+
+function skip_all() {
+ echo "1..0 # SKIP $@"
+ exit
+}
+
+function pass() {
+ echo "ok $((++TEST_NUMBER)) - $@"
+}
+
+function fail() {
+ echo "not ok $((++TEST_NUMBER)) - $@"
+}
+
+function run_pass() {
+ local TEST_KEY=$1
+ shift 1
+ if ! "$@" 1>$TEST_KEY.out 2>$TEST_KEY.err; then
+ fail $TEST_KEY
+ return
+ fi
+ pass $TEST_KEY
+}
+
+function run_fail() {
+ local TEST_KEY=$1
+ shift 1
+ if "$@" 1>$TEST_KEY.out 2>$TEST_KEY.err; then
+ fail $TEST_KEY
+ return
+ fi
+ pass $TEST_KEY
+}
+
+function file_cmp() {
+ local TEST_KEY=$1
+ local FILE_ACTUAL=$2
+ local FILE_EXPECT=${3:--}
+ if diff -u $FILE_EXPECT $FILE_ACTUAL >&2; then
+ pass $TEST_KEY
+ return
+ fi
+ fail $TEST_KEY
+}
+
+function file_test() {
+ local TEST_KEY=$1
+ local FILE=$2
+ local OPTION=${3:--e}
+ if test $OPTION $FILE; then
+ pass $TEST_KEY
+ else
+ fail $TEST_KEY
+ fi
+}
+
+function file_grep() {
+ local TEST_KEY=$1
+ local PATTERN=$2
+ local FILE=$3
+ if grep -q -e "$PATTERN" $FILE; then
+ pass $TEST_KEY
+ return
+ fi
+ fail $TEST_KEY
+}
+
+function fcm_make_build_hello_tests() {
+ local TEST_KEY=$1
+ local HELLO_EXT=${2:-}
+ shift 2
+ rm -fr \
+ .fcm-make \
+ build \
+ fcm-make-as-parsed.cfg \
+ fcm-make-on-success.cfg \
+ fcm-make.log
+ run_pass "$TEST_KEY" fcm make "$@"
+ file_test "$TEST_KEY.hello$HELLO_EXT" "$PWD/build/bin/hello$HELLO_EXT"
+ "$PWD/build/bin/hello$HELLO_EXT" >"$TEST_KEY.hello$HELLO_EXT.out"
+ file_cmp "$TEST_KEY.hello$HELLO_EXT.out" \
+ "$TEST_KEY.hello$HELLO_EXT.out" <<'__OUT__'
+Hello World!
+__OUT__
+}
+
+FCM_HOME=${FCM_HOME:-$(cd $(dirname $(readlink -f $BASH_SOURCE))/../../.. && pwd)}
+export FCM_HOME
+PATH=$FCM_HOME/bin:$PATH
+SVN_VERSION_IS_16=false
+SVN_VERSION=$(svn --version)
+if [[ $(echo $SVN_VERSION | head -1 | grep "^svn, version 1.6") ]]; then
+ SVN_VERSION_IS_16=true
+fi
+unset SVN_VERSION
+
+TEST_KEY_BASE=$(basename $0 .t)
+TEST_SOURCE_DIR=$(cd $(dirname $0) && pwd)
+TEST_DIR=$(mktemp -d)
+export LANG=C
+cd $TEST_DIR
+
+set +e
diff --git a/t/svn-hooks/00-pre-revprop-change.t b/t/svn-hooks/00-pre-revprop-change.t
new file mode 100755
index 0000000..a97e9eb
--- /dev/null
+++ b/t/svn-hooks/00-pre-revprop-change.t
@@ -0,0 +1,83 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+# Basic tests for "pre-revprop-change".
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+FCM_SVN_HOOK_ADMIN_EMAIL=your.admin.team
+. $TEST_SOURCE_DIR/test_header_more
+#-------------------------------------------------------------------------------
+tests 16
+#-------------------------------------------------------------------------------
+cp -p "$FCM_HOME/etc/svn-hooks/pre-revprop-change" "$REPOS_PATH/hooks/"
+echo Hello >file
+svn import -q -m'test' file "$REPOS_URL/file"
+#-------------------------------------------------------------------------------
+TEST_KEY=$TEST_KEY_BASE
+rm -f mail.out
+run_pass "$TEST_KEY" \
+ svn propset -q --revprop -r 1 'svn:log' 'Add hello file' "$REPOS_URL"
+run_fail "$TEST_KEY.mail.out" test -f mail.out
+#-------------------------------------------------------------------------------
+TEST_KEY=$TEST_KEY_BASE-bad-prop
+run_fail "$TEST_KEY" \
+ svn propset -q --revprop -r 1 'svn:author' 'boogeyman' "$REPOS_URL"
+file_grep "$TEST_KEY.err" \
+ "\[M svn:author\] permission denied." \
+ "$TEST_KEY.err"
+EXPR="\[! .....*-..-..T..:..:..Z\] $REPOS_PATH 1 $USER svn:author M"
+file_grep "$TEST_KEY.log" "$EXPR" "$REPOS_PATH/log/pre-revprop-change.log"
+file_grep "$TEST_KEY.mail.out" "$EXPR" mail.out
+#-------------------------------------------------------------------------------
+TEST_KEY=$TEST_KEY_BASE-bad-action
+run_fail "$TEST_KEY" \
+ svn propdel -q --revprop -r 1 'svn:log' "$REPOS_URL"
+file_grep "$TEST_KEY.err" \
+ "\[D svn:log\] permission denied. Can only do: \[M svn:log\]" \
+ "$TEST_KEY.err"
+EXPR="\[! .....*-..-..T..:..:..Z\] $REPOS_PATH 1 $USER svn:log D"
+file_grep "$TEST_KEY.log" "$EXPR" "$REPOS_PATH/log/pre-revprop-change.log"
+file_grep "$TEST_KEY.mail.out" "$EXPR" mail.out
+#-------------------------------------------------------------------------------
+TEST_KEY=$TEST_KEY_BASE-conf-bad
+cat >"$REPOS_PATH/hooks/pre-revprop-change-ok.conf" <<'__CONF__'
+M svn:author
+M svn:log
+__CONF__
+run_fail "$TEST_KEY" svn propdel -q --revprop -r 1 'svn:author' "$REPOS_URL"
+file_grep "$TEST_KEY.err" \
+ "\[D svn:author\] permission denied. Can only do: \[M svn:author\] \[M svn:log\]" \
+ "$TEST_KEY.err"
+EXPR="\[! .....*-..-..T..:..:..Z\] $REPOS_PATH 1 $USER svn:author D"
+file_grep "$TEST_KEY.log" "$EXPR" "$REPOS_PATH/log/pre-revprop-change.log"
+file_grep "$TEST_KEY.mail.out" "$EXPR" mail.out
+rm -f "$REPOS_PATH/hooks/pre-revprop-change-ok.conf"
+#-------------------------------------------------------------------------------
+TEST_KEY=$TEST_KEY_BASE-conf-good
+rm -f mail.out
+cat >"$REPOS_PATH/hooks/pre-revprop-change-ok.conf" <<'__CONF__'
+M svn:author
+M svn:log
+__CONF__
+run_pass "$TEST_KEY" \
+ svn propset -q --revprop -r 1 'svn:author' 'arthur' "$REPOS_URL"
+run_fail "$TEST_KEY.mail.out" test -f mail.out
+rm -f "$REPOS_PATH/hooks/pre-revprop-change-ok.conf"
+#-------------------------------------------------------------------------------
+exit
diff --git a/t/svn-hooks/01-post-revprop-change-bg.t b/t/svn-hooks/01-post-revprop-change-bg.t
new file mode 100755
index 0000000..1d28961
--- /dev/null
+++ b/t/svn-hooks/01-post-revprop-change-bg.t
@@ -0,0 +1,119 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+# Basic tests for "post-revprop-change-bg".
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+. $TEST_SOURCE_DIR/test_header_more
+#-------------------------------------------------------------------------------
+tests 9
+#-------------------------------------------------------------------------------
+# Add pre-revprop-change to allow revprop change.
+cat >"$REPOS_PATH/hooks/pre-revprop-change" <<__BASH__
+#!/bin/bash
+exit
+__BASH__
+chmod +x "$REPOS_PATH/hooks/pre-revprop-change"
+# Add post-revprop-change
+cp -p "$FCM_HOME/etc/svn-hooks/post-revprop-change" \
+ "$REPOS_PATH/hooks/post-revprop-change"
+echo Hello >file
+svn import --no-auth-cache -q -m'test' file "$REPOS_URL/file"
+if [[ -n ${TRAC_ENV_PATH:-} ]]; then
+ if $TRAC_RESYNC; then
+ trac-admin "$TRAC_ENV_PATH" resync 1>/dev/null
+ else
+ trac-admin "$TRAC_ENV_PATH" changeset added "$REPOS_PATH" 1 1>/dev/null
+ fi
+fi
+#-------------------------------------------------------------------------------
+TEST_KEY=$TEST_KEY_BASE
+run_pass "$TEST_KEY" \
+ svn propset --no-auth-cache -q --revprop -r 1 'svn:log' 'Add hello file' \
+ "$REPOS_URL"
+poll 10 grep -q '^RET_CODE=' "$REPOS_PATH/log/post-revprop-change.log"
+date2datefmt "$REPOS_PATH/log/post-revprop-change.log" \
+ | sed '/^trac-admin/,$d; /^RET_CODE=/d' >"$TEST_KEY.log.expected"
+file_cmp "$TEST_KEY.log" "$TEST_KEY.log.expected" <<__LOG__
+YYYY-mm-ddTHH:MM:SSZ+ M svn:log @1 by $USER
+--- old-value
++++ new-value
+@@ -1 +1 @@
+-test
+\ No newline at end of file
++Add hello file
+\ No newline at end of file
+__LOG__
+run_fail "$TEST_KEY.mail.out" test -f mail.out
+if [[ -z ${TRAC_ENV_PATH:-} ]]; then
+ skip 1 "$TEST_KEY.trac.db: Trac unavailable"
+else
+ sqlite3 "$TRAC_ENV_PATH/db/trac.db" \
+ 'SELECT cast(rev as integer),message FROM revision;' \
+ >"$TEST_KEY.trac.db.expected"
+ file_cmp "$TEST_KEY.trac.db" \
+ "$TEST_KEY.trac.db.expected" <<<'1|Add hello file'
+fi
+#-------------------------------------------------------------------------------
+TEST_KEY=$TEST_KEY_BASE-author
+cat /dev/null >"$REPOS_PATH/log/post-revprop-change.log"
+run_pass "$TEST_KEY" \
+ svn propset --no-auth-cache --username=not-a-user --revprop -r 1 'svn:log' \
+ 'Add welcome file' "$REPOS_URL"
+poll 10 grep -q '^RET_CODE=' "$REPOS_PATH/log/post-revprop-change.log"
+date2datefmt "$REPOS_PATH/log/post-revprop-change.log" \
+ | sed '/^trac-admin/,$d; /^RET_CODE=/d' >"$TEST_KEY.log.expected"
+file_cmp "$TEST_KEY.log" "$TEST_KEY.log.expected" <<'__LOG__'
+YYYY-mm-ddTHH:MM:SSZ+ M svn:log @1 by not-a-user
+--- old-value
++++ new-value
+@@ -1 +1 @@
+-Add hello file
+\ No newline at end of file
++Add welcome file
+\ No newline at end of file
+__LOG__
+if [[ -z ${TRAC_ENV_PATH:-} ]]; then
+ skip 1 "$TEST_KEY.trac.db: Trac unavailable"
+else
+ sqlite3 "$TRAC_ENV_PATH/db/trac.db" \
+ 'SELECT cast(rev as integer),message FROM revision;' \
+ >"$TEST_KEY.trac.db.expected"
+ file_cmp "$TEST_KEY.trac.db" \
+ "$TEST_KEY.trac.db.expected" <<<'1|Add welcome file'
+fi
+date2datefmt mail.out | sed '/^trac-admin/,$d; /^RET_CODE=/d' \
+ >"$TEST_KEY.mail.out.expected"
+file_grep "$TEST_KEY.mail.out.01" \
+ '-rnotifications at localhost -sfoo at 1 \[M svn:log\] by not-a-user' \
+ "$TEST_KEY.mail.out.expected"
+sed '1d' "$TEST_KEY.mail.out.expected" >"$TEST_KEY.mail.out.expected.02"
+file_cmp "$TEST_KEY.mail.out.02" "$TEST_KEY.mail.out.expected.02" <<__LOG__
+YYYY-mm-ddTHH:MM:SSZ+ M svn:log @1 by not-a-user
+========================================================================
+--- old-value
++++ new-value
+@@ -1 +1 @@
+-Add hello file
+\ No newline at end of file
++Add welcome file
+\ No newline at end of file
+__LOG__
+#-------------------------------------------------------------------------------
+exit
diff --git a/t/svn-hooks/02-pre-commit.t b/t/svn-hooks/02-pre-commit.t
new file mode 100755
index 0000000..2194919
--- /dev/null
+++ b/t/svn-hooks/02-pre-commit.t
@@ -0,0 +1,287 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+# Basic tests for "pre-commit".
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+FCM_SVN_HOOK_ADMIN_EMAIL='your.admin.team'
+. $TEST_SOURCE_DIR/test_header_more
+
+test_tidy() {
+ rm -f \
+ "$REPOS_PATH/hooks/pre-commit-custom" \
+ "$REPOS_PATH/hooks/pre-commit-size-threshold.conf" \
+ "$REPOS_PATH/hooks/commit.conf" \
+ "$REPOS_PATH/hooks/svnperms.conf" \
+ "$REPOS_PATH/log/pre-commit.log" \
+ README \
+ bin/svnperms.py \
+ file1 \
+ file2 \
+ file3 \
+ file4 \
+ mail.out \
+ pre-commit-custom.out \
+ svnperms.py.out
+}
+#-------------------------------------------------------------------------------
+tests 50
+#-------------------------------------------------------------------------------
+cp -p "$FCM_HOME/etc/svn-hooks/pre-commit" "$REPOS_PATH/hooks/"
+sed -i "/set -eu/a\
+echo \$2 >$PWD/txn" "$REPOS_PATH/hooks/pre-commit"
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-svnperm-1" # Blocked by svnperms.py
+# Install fake svnperms.py
+test_tidy
+cat >bin/svnperms.py <<__BASH__
+#!/bin/bash
+echo "\$@" >$PWD/svnperms.py.out
+echo "Access denied!" >&2
+false
+__BASH__
+chmod +x "bin/svnperms.py"
+echo '[foo]' >"$REPOS_PATH/hooks/svnperms.conf"
+# Try commit
+touch file1
+run_fail "$TEST_KEY" \
+ svn import --no-auth-cache -q -m'test' file1 "$REPOS_URL/file1"
+TXN=$(<txn)
+# Tests
+file_grep "$TEST_KEY.err" 'Access denied!' "$TEST_KEY.err"
+date2datefmt "$REPOS_PATH/log/pre-commit.log" \
+ >"$TEST_KEY.pre-commit.log.expected"
+file_cmp "$TEST_KEY.pre-commit.log" "$TEST_KEY.pre-commit.log.expected" <<__LOG__
+YYYY-mm-ddTHH:MM:SSZ+ $TXN by $USER
+A file1
+Access denied!
+__LOG__
+file_cmp "$TEST_KEY.svnperms.py.out" svnperms.py.out <<__OUT__
+-r $REPOS_PATH -t $TXN -f $REPOS_PATH/hooks/svnperms.conf
+__OUT__
+date2datefmt mail.out >"$TEST_KEY.mail.out.expected"
+file_cmp "$TEST_KEY.mail.out" "$TEST_KEY.mail.out.expected" <<__LOG__
+-s [pre-commit] $REPOS_PATH@$TXN your.admin.team
+YYYY-mm-ddTHH:MM:SSZ+ $TXN by $USER
+A file1
+Access denied!
+__LOG__
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-svnperm-2" # svnperms.conf is bad symlink
+# Install fake svnperms.py
+test_tidy
+ln -f -s "no-such-file" "$REPOS_PATH/hooks/svnperms.conf"
+# Try commit
+touch file1
+run_fail "$TEST_KEY" \
+ svn import --no-auth-cache -q -m'test' file1 "$REPOS_URL/file1"
+TXN=$(<txn)
+# Tests
+file_grep "$TEST_KEY.err" 'foo: permission configuration file not found.' \
+ "$TEST_KEY.err"
+file_grep "$TEST_KEY.err-2" 'your.admin.team has been notified.' "$TEST_KEY.err"
+date2datefmt "$REPOS_PATH/log/pre-commit.log" \
+ >"$TEST_KEY.pre-commit.log.expected"
+file_cmp "$TEST_KEY.pre-commit.log" "$TEST_KEY.pre-commit.log.expected" <<__LOG__
+YYYY-mm-ddTHH:MM:SSZ+ $TXN by $USER
+A file1
+foo: permission configuration file not found.
+your.admin.team has been notified.
+__LOG__
+run_fail "$TEST_KEY.svnperms.py.out" test -e svnperms.py.out
+date2datefmt mail.out >"$TEST_KEY.mail.out.expected"
+file_cmp "$TEST_KEY.mail.out" "$TEST_KEY.mail.out.expected" <<__LOG__
+-s [pre-commit] $REPOS_PATH@$TXN your.admin.team
+YYYY-mm-ddTHH:MM:SSZ+ $TXN by $USER
+A file1
+foo: permission configuration file not found.
+your.admin.team has been notified.
+__LOG__
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-svnperm-3" # Good svnperms.conf
+test_tidy
+cat >bin/svnperms.py <<__BASH__
+#!/bin/bash
+echo "\$@" >$PWD/svnperms.py.out
+__BASH__
+chmod +x bin/svnperms.py
+echo '[foo]' >"$REPOS_PATH/hooks/svnperms.conf"
+# Try commit
+touch file1
+run_pass "$TEST_KEY" \
+ svn import --no-auth-cache -q -m'test' file1 "$REPOS_URL/file1"
+TXN=$(<txn)
+# Tests
+run_fail "$TEST_KEY.pre-commit.log" test -s "$REPOS_PATH/log/pre-commit.log"
+file_cmp "$TEST_KEY.svnperms.py.out" "svnperms.py.out" <<__OUT__
+-r $REPOS_PATH -t $TXN -f $REPOS_PATH/hooks/svnperms.conf
+__OUT__
+run_fail "$TEST_KEY.mail.out" test -e mail.out
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-svnperm-4" # No svnperms.conf
+test_tidy
+# Try commit
+touch file2
+run_pass "$TEST_KEY" \
+ svn import --no-auth-cache -q -m'test' file2 "$REPOS_URL/file2"
+# Tests
+run_fail "$TEST_KEY.pre-commit.log" test -s "$REPOS_PATH/log/pre-commit.log"
+run_fail "$TEST_KEY.svnperms.py.out" test -e svnperms.py.out
+run_fail "$TEST_KEY.mail.out" test -e mail.out
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-size-1" # bigger than default
+test_tidy
+perl -e 'map {print(rand())} 1..2097152' >file3 # a large file
+run_fail "$TEST_KEY" \
+ svn import --no-auth-cache -q -m'test' file3 "$REPOS_URL/file3"
+TXN=$(<txn)
+file_grep "$TEST_KEY.err" "foo@$TXN: changeset size ..MB exceeds 10MB." \
+ "$TEST_KEY.err"
+file_grep "$TEST_KEY.err-2" \
+ 'Email your.admin.team if you need to bypass this restriction.' "$TEST_KEY.err"
+date2datefmt "$REPOS_PATH/log/pre-commit.log" \
+ | sed 's/\(size \).*\(MB exceeds\)/\1??\2/' \
+ >"$TEST_KEY.pre-commit.log.expected"
+file_cmp "$TEST_KEY.pre-commit.log" "$TEST_KEY.pre-commit.log.expected" <<__LOG__
+YYYY-mm-ddTHH:MM:SSZ+ $TXN by $USER
+A file3
+foo@$TXN: changeset size ??MB exceeds 10MB.
+Email your.admin.team if you need to bypass this restriction.
+__LOG__
+date2datefmt mail.out | sed 's/\(size \).*\(MB exceeds\)/\1??\2/' \
+ >"$TEST_KEY.mail.out.expected"
+file_cmp "$TEST_KEY.mail.out.expected" "$TEST_KEY.mail.out.expected" <<__OUT__
+-s [pre-commit] $REPOS_PATH@$TXN your.admin.team
+YYYY-mm-ddTHH:MM:SSZ+ $TXN by $USER
+A file3
+foo@$TXN: changeset size ??MB exceeds 10MB.
+Email your.admin.team if you need to bypass this restriction.
+__OUT__
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-size-2" # bigger than default, threshold increased
+test_tidy
+echo '20' >"$REPOS_PATH/hooks/pre-commit-size-threshold.conf"
+perl -e 'map {print(rand())} 1..2097152' >file3 # a large file
+run_pass "$TEST_KEY" \
+ svn import --no-auth-cache -q -m'test' file3 "$REPOS_URL/file3"
+# Tests
+run_fail "$TEST_KEY.pre-commit.log" test -s "$REPOS_PATH/log/pre-commit.log"
+run_fail "$TEST_KEY.mail.out" test -e mail.out
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-custom-1" # block by custom script
+test_tidy
+cat >"$REPOS_PATH/hooks/pre-commit-custom" <<__BASH__
+#!/bin/bash
+echo "\$@" >$PWD/pre-commit-custom.out
+echo 'I am a blocker.' >&2
+false
+__BASH__
+chmod +x "$REPOS_PATH/hooks/pre-commit-custom"
+touch file4
+run_fail "$TEST_KEY" \
+ svn import --no-auth-cache -q -m'test' file4 "$REPOS_URL/file4"
+TXN=$(<txn)
+# Tests
+file_grep "$TEST_KEY.err" 'I am a blocker.' "$TEST_KEY.err"
+file_cmp "$TEST_KEY-custom.out" pre-commit-custom.out <<__OUT__
+$REPOS_PATH $TXN
+__OUT__
+date2datefmt "$REPOS_PATH/log/pre-commit.log" \
+ | sed 's/\(size \).*\(MB exceeds\)/\1??\2/' \
+ >"$TEST_KEY.pre-commit.log"
+file_cmp "$TEST_KEY.pre-commit.log" "$TEST_KEY.pre-commit.log" <<__OUT__
+YYYY-mm-ddTHH:MM:SSZ+ $TXN by $USER
+A file4
+I am a blocker.
+__OUT__
+date2datefmt mail.out | sed 's/\(size \).*\(MB exceeds\)/\1??\2/' \
+ >"$TEST_KEY.mail.out.expected"
+file_cmp "$TEST_KEY.mail.out.expected" "$TEST_KEY.mail.out.expected" <<__OUT__
+-s [pre-commit] $REPOS_PATH@$TXN your.admin.team
+YYYY-mm-ddTHH:MM:SSZ+ $TXN by $USER
+A file4
+I am a blocker.
+__OUT__
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-custom-2" # custom script OK
+test_tidy
+cat >"$REPOS_PATH/hooks/pre-commit-custom" <<__BASH__
+#!/bin/bash
+echo "\$@" >$PWD/pre-commit-custom.out
+__BASH__
+chmod +x "$REPOS_PATH/hooks/pre-commit-custom"
+touch file4
+run_pass "$TEST_KEY" \
+ svn import --no-auth-cache -q -m'test' file4 "$REPOS_URL/file4"
+TXN=$(<txn)
+# Tests
+file_cmp "$TEST_KEY-custom.out" pre-commit-custom.out <<__OUT__
+$REPOS_PATH $TXN
+__OUT__
+run_fail "$TEST_KEY.pre-commit.log" test -s "$REPOS_PATH/log/pre-commit.log"
+run_fail "$TEST_KEY.mail.out" test -e mail.out
+#-------------------------------------------------------------------------------
+# Branch create owner verify, goods
+echo 'Hello World' >README
+svn import -m "hello: new project" README "$REPOS_URL/hello/trunk/README"
+rm README
+for KEY in $USER Share Config Rel; do
+ test_tidy
+ TEST_KEY="$TEST_KEY_BASE-branch-owner-$KEY"
+ run_pass "$TEST_KEY" svn cp --parents -m "$TEST_KEY" \
+ "$REPOS_URL/hello/trunk" "$REPOS_URL/hello/branches/dev/$KEY/whatever"
+ run_fail "$TEST_KEY.pre-commit.log" test -s "$REPOS_PATH/log/pre-commit.log"
+done
+#-------------------------------------------------------------------------------
+# Branch create owner verify, bad
+test_tidy
+TEST_KEY="$TEST_KEY_BASE-branch-owner-bad"
+run_fail "$TEST_KEY" svn cp --parents -m "$TEST_KEY" \
+ "$REPOS_URL/hello/trunk" "$REPOS_URL/hello/branches/dev/nosuchuser/whatever"
+TXN=$(<txn)
+file_grep "$TEST_KEY.err" \
+ '\[INVALID BRANCH OWNER\] A hello/branches/dev/nosuchuser/whatever/' \
+ "$TEST_KEY.err"
+date2datefmt "$REPOS_PATH/log/pre-commit.log" \
+ >"$TEST_KEY.pre-commit.log.expected"
+file_cmp "$TEST_KEY.pre-commit.log" \
+ "$TEST_KEY.pre-commit.log.expected" <<__LOG__
+YYYY-mm-ddTHH:MM:SSZ+ $TXN by $USER
+A hello/branches/dev/nosuchuser/
+A hello/branches/dev/nosuchuser/whatever/
+[INVALID BRANCH OWNER] A hello/branches/dev/nosuchuser/whatever/
+__LOG__
+date2datefmt mail.out >"$TEST_KEY.mail.out.expected"
+file_cmp "$TEST_KEY.mail.out" "$TEST_KEY.mail.out.expected" <<__LOG__
+-s [pre-commit] $REPOS_PATH@$TXN your.admin.team
+YYYY-mm-ddTHH:MM:SSZ+ $TXN by $USER
+A hello/branches/dev/nosuchuser/
+A hello/branches/dev/nosuchuser/whatever/
+[INVALID BRANCH OWNER] A hello/branches/dev/nosuchuser/whatever/
+__LOG__
+#-------------------------------------------------------------------------------
+# Branch create owner no verify, bad
+test_tidy
+TEST_KEY="$TEST_KEY_BASE-branch-owner-no-verify-bad"
+echo 'no-verify-branch-owner' >"$REPOS_PATH/hooks/commit.conf"
+run_pass "$TEST_KEY" svn cp --parents -m "$TEST_KEY" \
+ "$REPOS_URL/hello/trunk" "$REPOS_URL/hello/branches/dev/nosuchuser/whatever"
+run_fail "$TEST_KEY.pre-commit.log" test -s "$REPOS_PATH/log/pre-commit.log"
+#-------------------------------------------------------------------------------
+exit
diff --git a/t/svn-hooks/03-post-commit-bg.t b/t/svn-hooks/03-post-commit-bg.t
new file mode 100755
index 0000000..d596f83
--- /dev/null
+++ b/t/svn-hooks/03-post-commit-bg.t
@@ -0,0 +1,301 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+# Basic tests for "post-commit-bg".
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+FCM_SVN_HOOK_ADMIN_EMAIL=fcm.admin.team
+. $TEST_SOURCE_DIR/test_header_more
+
+test_tidy() {
+ rm -f \
+ "$REPOS_PATH/hooks/post-commit-bg-custom" \
+ "$REPOS_PATH/hooks/post-commit-background-custom" \
+ "$REPOS_PATH/hooks/commit.conf" \
+ "$REPOS_PATH/log/post-commit.log" \
+ file1 \
+ file2 \
+ file3 \
+ file4 \
+ svnperms.conf \
+ mail.out
+}
+#-------------------------------------------------------------------------------
+tests 32
+#-------------------------------------------------------------------------------
+cp -p "$FCM_HOME/etc/svn-hooks/post-commit" "$REPOS_PATH/hooks/"
+sed -i "/set -eu/a\
+echo \$2 >$PWD/rev; echo \$3 >$PWD/txn" "$REPOS_PATH/hooks/post-commit"
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-basic"
+test_tidy
+touch file1
+svn import --no-auth-cache -q -m"$TEST_KEY" file1 "$REPOS_URL/file1"
+REV=$(<rev)
+poll 10 grep -q '^RET_CODE=' "$REPOS_PATH/log/post-commit.log"
+date2datefmt "$REPOS_PATH/log/post-commit.log" \
+ | sed '/^trac-admin/d; s/^\(REV_FILE_SIZE=\).*\( #\)/\1???\2/' \
+ >"$TEST_KEY.log"
+file_cmp "$TEST_KEY.log" "$TEST_KEY.log" <<__LOG__
+YYYY-mm-ddTHH:MM:SSZ+ $REV by $USER
+svnadmin dump -r$REV --incremental --deltas $REPOS_PATH | gzip 1>$PWD/svn-dumps/foo-$REV.gz
+* Dumped revision $REV.
+REV_FILE_SIZE=??? # within 1048576
+RET_CODE=0
+__LOG__
+if [[ -n ${TRAC_ENV_PATH:-} ]] && ! $TRAC_RESYNC; then
+ sqlite3 "$TRAC_ENV_PATH/db/trac.db" \
+ 'SELECT cast(rev as integer),message FROM revision;' \
+ >"$TEST_KEY.trac.db.expected"
+ file_cmp "$TEST_KEY.trac.db" \
+ "$TEST_KEY.trac.db.expected" <<<"$REV|$TEST_KEY"
+ cat "$TEST_KEY.trac.db.expected"
+else
+ skip 1 '"trac-admin changeset added" not available'
+fi
+run_pass "$TEST_KEY.dump" test -s "$PWD/svn-dumps/foo-$REV.gz"
+run_fail "$TEST_KEY.mail.out" test -e mail.out
+#-------------------------------------------------------------------------------
+# Install and remove commit.conf, svnperms.conf
+for NAME in 'commit.conf' 'svnperms.conf'; do
+ TEST_KEY="$TEST_KEY_BASE-add-${NAME}"
+ test_tidy
+ # (Use "svnperms.conf" syntax. Doesn't matter for the purpose of this test.)
+ cat >${NAME} <<'__CONF__'
+[foo]
+.*=*(add,remove,update)
+__CONF__
+ svn import --no-auth-cache -q -m"$TEST_KEY" ${NAME} \
+ "$REPOS_URL/${NAME}"
+ REV=$(<rev)
+ poll 10 grep -q '^RET_CODE=' "$REPOS_PATH/log/post-commit.log"
+ date2datefmt "$REPOS_PATH/log/post-commit.log" \
+ | sed '/^trac-admin/d; s/^\(REV_FILE_SIZE=\).*\( #\)/\1???\2/' \
+ >"$TEST_KEY.log"
+ file_cmp "$TEST_KEY.log" "$TEST_KEY.log" <<__LOG__
+YYYY-mm-ddTHH:MM:SSZ+ $REV by $USER
+svnadmin dump -r$REV --incremental --deltas $REPOS_PATH | gzip 1>$PWD/svn-dumps/foo-$REV.gz
+* Dumped revision $REV.
+REV_FILE_SIZE=??? # within 1048576
+svnlook cat $REPOS_PATH ${NAME} >$REPOS_PATH/hooks/${NAME}
+RET_CODE=0
+__LOG__
+ file_cmp "$TEST_KEY.conf" ${NAME} "$REPOS_PATH/hooks/${NAME}"
+
+ TEST_KEY="$TEST_KEY_BASE-modify-${NAME}"
+ test_tidy
+ svn co -q "$REPOS_URL" work
+ # (Use "svnperms.conf" syntax. Doesn't matter for the purpose of this test.)
+ cat >work/${NAME} <<'__CONF__'
+[foo]
+.*=*(add,remove,update)
+
+[bar]
+__CONF__
+ svn commit --no-auth-cache -q -m"$TEST_KEY" work
+ REV=$(<rev)
+ poll 10 grep -q '^RET_CODE=' "$REPOS_PATH/log/post-commit.log"
+ date2datefmt "$REPOS_PATH/log/post-commit.log" \
+ | sed '/^trac-admin/d; s/^\(REV_FILE_SIZE=\).*\( #\)/\1???\2/' \
+ >"$TEST_KEY.log"
+ file_cmp "$TEST_KEY.log" "$TEST_KEY.log" <<__LOG__
+YYYY-mm-ddTHH:MM:SSZ+ $REV by $USER
+svnadmin dump -r$REV --incremental --deltas $REPOS_PATH | gzip 1>$PWD/svn-dumps/foo-$REV.gz
+* Dumped revision $REV.
+REV_FILE_SIZE=??? # within 1048576
+svnlook cat $REPOS_PATH ${NAME} >$REPOS_PATH/hooks/${NAME}
+RET_CODE=0
+__LOG__
+ file_cmp "$TEST_KEY.conf" work/${NAME} "$REPOS_PATH/hooks/${NAME}"
+ rm -f -r work
+
+ TEST_KEY="$TEST_KEY_BASE-remove-${NAME}"
+ test_tidy
+ touch "$REPOS_PATH/hooks/${NAME}"
+ svn rm --no-auth-cache -q -m'remove ${NAME}' "$REPOS_URL/${NAME}"
+ REV=$(<rev)
+ poll 10 grep -q '^RET_CODE=' "$REPOS_PATH/log/post-commit.log"
+ date2datefmt "$REPOS_PATH/log/post-commit.log" \
+ | sed '/^trac-admin/d; s/^\(REV_FILE_SIZE=\).*\( #\)/\1???\2/' \
+ >"$TEST_KEY.log"
+ file_cmp "$TEST_KEY.log" "$TEST_KEY.log" <<__LOG__
+YYYY-mm-ddTHH:MM:SSZ+ $REV by $USER
+svnadmin dump -r$REV --incremental --deltas $REPOS_PATH | gzip 1>$PWD/svn-dumps/foo-$REV.gz
+* Dumped revision $REV.
+REV_FILE_SIZE=??? # within 1048576
+rm -f $REPOS_PATH/hooks/${NAME}
+RET_CODE=0
+__LOG__
+ run_fail "$TEST_KEY.conf" test -e "$REPOS_PATH/hooks/${NAME}"
+done
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-size"
+test_tidy
+perl -e 'map {print(rand())} 1..524288' >file2 # compress should be >1MB
+svn import --no-auth-cache -q -m"$TEST_KEY" file2 "$REPOS_URL/file2"
+REV=$(<rev)
+poll 10 grep -q '^RET_CODE=' "$REPOS_PATH/log/post-commit.log"
+date2datefmt "$REPOS_PATH/log/post-commit.log" \
+ | sed '/^trac-admin/d; s/^\(REV_FILE_SIZE=\).*\( #\)/\1???\2/' \
+ >"$TEST_KEY.log"
+file_cmp "$TEST_KEY.log" "$TEST_KEY.log" <<__LOG__
+YYYY-mm-ddTHH:MM:SSZ+ $REV by $USER
+svnadmin dump -r$REV --incremental --deltas $REPOS_PATH | gzip 1>$PWD/svn-dumps/foo-$REV.gz
+* Dumped revision $REV.
+REV_FILE_SIZE=??? # EXCEED 1048576
+RET_CODE=1
+__LOG__
+date2datefmt mail.out \
+ | sed '/^trac-admin/d; s/^\(REV_FILE_SIZE=\).*\( #\)/\1???\2/' \
+ >"$TEST_KEY.mail.out"
+file_cmp "$TEST_KEY.mail.out" "$TEST_KEY.mail.out" <<__LOG__
+-s [post-commit-bg] $REPOS_PATH@$REV fcm.admin.team
+YYYY-mm-ddTHH:MM:SSZ+ $REV by $USER
+svnadmin dump -r$REV --incremental --deltas $REPOS_PATH | gzip 1>$PWD/svn-dumps/foo-$REV.gz
+* Dumped revision $REV.
+REV_FILE_SIZE=??? # EXCEED 1048576
+RET_CODE=1
+__LOG__
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-custom-1" # good custom
+test_tidy
+touch file3
+cat >"$REPOS_PATH/hooks/post-commit-bg-custom" <<'__BASH__'
+#!/bin/bash
+echo "$@"
+__BASH__
+chmod +x "$REPOS_PATH/hooks/post-commit-bg-custom"
+svn import --no-auth-cache -q -m"$TEST_KEY" file3 "$REPOS_URL/file3"
+REV=$(<rev)
+TXN=$(<txn)
+poll 10 grep -q '^RET_CODE=' "$REPOS_PATH/log/post-commit.log"
+date2datefmt "$REPOS_PATH/log/post-commit.log" \
+ | sed '/^trac-admin/d; s/^\(REV_FILE_SIZE=\).*\( #\)/\1???\2/' \
+ >"$TEST_KEY.log"
+file_cmp "$TEST_KEY.log" "$TEST_KEY.log" <<__LOG__
+YYYY-mm-ddTHH:MM:SSZ+ $REV by $USER
+svnadmin dump -r$REV --incremental --deltas $REPOS_PATH | gzip 1>$PWD/svn-dumps/foo-$REV.gz
+* Dumped revision $REV.
+REV_FILE_SIZE=??? # within 1048576
+$REPOS_PATH/hooks/post-commit-bg-custom $REPOS_PATH $REV $TXN
+$REPOS_PATH $REV $TXN
+RET_CODE=0
+__LOG__
+run_fail "$TEST_KEY.mail.out" test -e mail.out
+#-------------------------------------------------------------------------------
+TEST_KEY="$TEST_KEY_BASE-custom-2" # bad custom
+test_tidy
+cat >"$REPOS_PATH/hooks/post-commit-background-custom" <<'__BASH__'
+#!/bin/bash
+echo 'I have gone to the dark side.' >&2
+false
+__BASH__
+chmod +x "$REPOS_PATH/hooks/post-commit-background-custom"
+touch file4
+svn import --no-auth-cache -q -m"$TEST_KEY" file4 "$REPOS_URL/file4"
+REV=$(<rev)
+TXN=$(<txn)
+poll 10 grep -q '^RET_CODE=' "$REPOS_PATH/log/post-commit.log"
+date2datefmt "$REPOS_PATH/log/post-commit.log" \
+ | sed '/^trac-admin/d; s/^\(REV_FILE_SIZE=\).*\( #\)/\1???\2/' \
+ >"$TEST_KEY.log"
+file_cmp "$TEST_KEY.log" "$TEST_KEY.log" <<__LOG__
+YYYY-mm-ddTHH:MM:SSZ+ $REV by $USER
+svnadmin dump -r$REV --incremental --deltas $REPOS_PATH | gzip 1>$PWD/svn-dumps/foo-$REV.gz
+* Dumped revision $REV.
+REV_FILE_SIZE=??? # within 1048576
+$REPOS_PATH/hooks/post-commit-background-custom $REPOS_PATH $REV $TXN
+I have gone to the dark side.
+RET_CODE=1
+__LOG__
+file_test "$TEST_KEY.mail.out" mail.out
+#-------------------------------------------------------------------------------
+# Test branch owner notification
+echo 'Hello World' >file
+svn import -q -m'hello world' file "$REPOS_URL/hello/trunk/file"
+poll 10 grep -q '^RET_CODE=' "$REPOS_PATH/log/post-commit.log"
+
+TEST_KEY="$TEST_KEY_BASE-branch-create-owner-1" # create author is owner
+test_tidy
+svn cp -q -m '' --parents \
+ "$REPOS_URL/hello/trunk" \
+ "$REPOS_URL/hello/branches/dev/$USER/whatever"
+poll 10 grep -q '^RET_CODE=' "$REPOS_PATH/log/post-commit.log"
+run_fail "$TEST_KEY.mail.out" test -s mail.out
+
+TEST_KEY="$TEST_KEY_BASE-branch-create-owner-2" # create author not owner
+test_tidy
+svn cp -q -m '' --parents \
+ --username=root \
+ --no-auth-cache \
+ "$REPOS_URL/hello/trunk" \
+ "$REPOS_URL/hello/branches/test/$USER/whatever"
+REV=$(<rev)
+poll 10 grep -q '^RET_CODE=' "$REPOS_PATH/log/post-commit.log"
+file_grep "$TEST_KEY.mail.out.1" \
+ "^-rnotifications at localhost -sfoo@${REV} by root" mail.out
+file_grep "$TEST_KEY.mail.out.2" "^r${REV} | root" mail.out
+
+TEST_KEY="$TEST_KEY_BASE-branch-create-owner-3" # same as 2, but no notify
+test_tidy
+echo 'no-notify-branch-owner' >"${REPOS_PATH}/hooks/commit.conf"
+svn cp -q -m '' --parents \
+ --username=root \
+ --no-auth-cache \
+ "$REPOS_URL/hello/trunk" \
+ "$REPOS_URL/hello/branches/test/$USER/whatever2"
+poll 10 grep -q '^RET_CODE=' "$REPOS_PATH/log/post-commit.log"
+run_fail "$TEST_KEY.mail.out" test -s mail.out
+
+TEST_KEY="$TEST_KEY_BASE-branch-modify-owner-1" # modify author is owner
+test_tidy
+svn co -q "$REPOS_URL/hello/branches/dev/$USER/whatever" hello
+echo 'Hello Earth' >hello/file
+svn ci -q -m'Hello Earth' hello/file
+poll 10 grep -q '^RET_CODE=' "$REPOS_PATH/log/post-commit.log"
+run_fail "$TEST_KEY.mail.out" test -s mail.out
+
+TEST_KEY="$TEST_KEY_BASE-branch-modify-owner-2" # modify author not owner
+test_tidy
+#svn co -q "$REPOS_URL/hello/branches/dev/$USER/whatever" hello
+echo 'Hello Alien' >hello/file
+svn ci -q -m'Hello Earth' --username=root --no-auth-cache hello/file
+poll 10 grep -q '^RET_CODE=' "$REPOS_PATH/log/post-commit.log"
+REV=$(<rev)
+file_grep "$TEST_KEY.mail.out.1" \
+ "^-rnotifications at localhost -sfoo@${REV} by root" mail.out
+file_grep "$TEST_KEY.mail.out.2" "^r${REV} | root" mail.out
+
+TEST_KEY="$TEST_KEY_BASE-branch-delete-owner-1" # delete author is owner
+test_tidy
+svn rm -q -m'No Hello' "$REPOS_URL/hello/branches/dev/$USER/whatever"
+poll 10 grep -q '^RET_CODE=' "$REPOS_PATH/log/post-commit.log"
+run_fail "$TEST_KEY.mail.out" test -s mail.out
+
+TEST_KEY="$TEST_KEY_BASE-branch-delete-owner-2" # delete author not owner
+test_tidy
+svn rm -q -m'No Hello' --username=root --no-auth-cache \
+ "$REPOS_URL/hello/branches/test/$USER/whatever"
+poll 10 grep -q '^RET_CODE=' "$REPOS_PATH/log/post-commit.log"
+REV=$(<rev)
+file_grep "$TEST_KEY.mail.out.1" \
+ "^-rnotifications at localhost -sfoo@${REV} by root" mail.out
+file_grep "$TEST_KEY.mail.out.2" "^r${REV} | root" mail.out
+#-------------------------------------------------------------------------------
+exit
diff --git a/t/svn-hooks/test_header b/t/svn-hooks/test_header
new file mode 120000
index 0000000..90bd5a3
--- /dev/null
+++ b/t/svn-hooks/test_header
@@ -0,0 +1 @@
+../lib/bash/test_header
\ No newline at end of file
diff --git a/t/svn-hooks/test_header_more b/t/svn-hooks/test_header_more
new file mode 100644
index 0000000..78ec710
--- /dev/null
+++ b/t/svn-hooks/test_header_more
@@ -0,0 +1,114 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+# NAME
+# test_header_more
+#
+# SYNOPSIS
+# . $(dirname $0)/test_header
+# . $(dirname $0)/test_header_more
+#
+# DESCRIPTION
+# Provide more bash shell functions for testing svn-hooks. Create a
+# subversion repository. Create a Trac environment if possible. Set up
+# repository hooks environment file.
+#
+# FUNCTIONS
+# date2datefmt FILE
+# Convert date time in YYYY-mm-ddTHH:MM:SSZ format into the format
+# string itself.
+# poll HOW_LONG COND ...
+# Poll for COND to become true for HOW_LONG seconds.
+#
+# VARIABLES
+# FCM_SVN_HOOK_REPOS_SUFFIX
+# FCM_SVN_HOOK_ADMIN_EMAIL
+# Optional. See definition in hook scripts.
+# HOOK_PATH
+# PATH for the hook scripts.
+# REPOS_PATH
+# Path to the Subversion repository ($PWD/svn-repos/foo).
+# REPOS_URL
+# URL of the Subversion repository root (file://$REPOS_PATH).
+# TRAC_ENV_PATH
+# Path to the Trac environment ($PWD/trac-env/foo).
+# TRAC_RESYNC
+# If true, use "trac-admin resync". If false, use "trac-admin
+# changeset added|modified".
+#-------------------------------------------------------------------------------
+
+date2datefmt() {
+ perl -p -e 's/\d+-\d\d-\d\dT\d\d:\d\d:\d\dZ/YYYY-mm-ddTHH:MM:SSZ/' "$@"
+}
+
+poll() {
+ local HOW_LONG=$1
+ shift 1
+ local WAIT_UNTIL=$(($(date +%s) + $HOW_LONG))
+ while (($(date +%s) < $WAIT_UNTIL)) && ! "$@" 2>/dev/null; do
+ sleep 1
+ done
+ "$@"
+}
+
+HOOK_PATH='/usr/local/bin:/usr/bin:/bin'
+mkdir bin svn-dumps svn-repos trac-env
+svnadmin create svn-repos/foo 2>/dev/null \
+ || skip_all 'cannot create Subversion repository'
+REPOS_PATH="$PWD/svn-repos/foo${FCM_SVN_HOOK_REPOS_SUFFIX:-}"
+REPOS_URL="file://$REPOS_PATH"
+ADMIN_DIR=$(dirname "$(which svnadmin)")
+if ! grep -q '^\(/usr/local/bin\|/usr/bin\|/bin\)$' <<<"$ADMIN_DIR"; then
+ HOOK_PATH="$ADMIN_DIR:$HOOK_PATH"
+fi
+if trac-admin trac-env/foo initenv foo sqlite:db/trac.db svn "$REPOS_PATH" \
+ 1>/dev/null 2>&1
+then
+ FCM_SVN_HOOK_TRAC_ROOT_DIR="$PWD/trac-env"
+ TRAC_ENV_PATH="$FCM_SVN_HOOK_TRAC_ROOT_DIR/foo"
+ ADMIN_DIR=$(dirname "$(which trac-admin)")
+ if ! grep -q '^\(/usr/local/bin\|/usr/bin\|/bin\)$' <<<"$ADMIN_DIR"; then
+ HOOK_PATH="$ADMIN_DIR:$HOOK_PATH"
+ fi
+ if [[ $(trac-admin --version) == trac-admin\ 0.11* ]]; then
+ TRAC_RESYNC=true
+ else
+ TRAC_RESYNC=false
+ fi
+fi
+unset ADMIN_DIR
+cat >bin/mail <<__BASH__
+#!/bin/bash
+{
+ echo "\$@"
+ cat
+} >$PWD/mail.out
+__BASH__
+chmod +x bin/mail
+HOOK_PATH="$PWD/bin:$HOOK_PATH"
+cat >"$REPOS_PATH/conf/hooks-env" <<__CONF__
+[default]
+FCM_HOME=$FCM_HOME
+FCM_SVN_HOOK_ADMIN_EMAIL=${FCM_SVN_HOOK_ADMIN_EMAIL:-}
+FCM_SVN_HOOK_COMMIT_DUMP_DIR=$PWD/svn-dumps
+FCM_SVN_HOOK_NOTIFICATION_FROM=notifications at localhost
+FCM_SVN_HOOK_REPOS_SUFFIX=${FCM_SVN_HOOK_REPOS_SUFFIX:-}
+FCM_SVN_HOOK_TRAC_ROOT_DIR=${FCM_SVN_HOOK_TRAC_ROOT_DIR:-}
+PATH=$HOOK_PATH
+__CONF__
diff --git a/t/svn-username/00-branch.t b/t/svn-username/00-branch.t
new file mode 100755
index 0000000..02b2edd
--- /dev/null
+++ b/t/svn-username/00-branch.t
@@ -0,0 +1,103 @@
+#!/bin/bash
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# Test "fcm branch-create" and "fcm branch-list", alternate username.
+#-------------------------------------------------------------------------------
+. $(dirname $0)/test_header
+#-------------------------------------------------------------------------------
+setup
+init_repos
+cd $TEST_DIR
+svn cp -m 't1' --parents -q \
+ $ROOT_URL/trunk at 1 $ROOT_URL/branches/dev/barn.owl/r1_wing
+if ! svnserve -r $TEST_DIR -d --pid-file pid-file; then
+ if [[ -s pid-file ]]; then
+ kill $(cat pid-file)
+ fi
+ skip_all 'svnserve failed'
+ teardown
+ exit
+fi
+tests 9
+#-------------------------------------------------------------------------------
+# Tests fcm branch-create, alternate username
+TEST_KEY="$TEST_KEY_BASE"
+FCM_SUBVERSION_SERVERS_CONF="$PWD/$TEST_KEY-svn-servers-conf"
+cat >$FCM_SUBVERSION_SERVERS_CONF <<'__CONF__'
+[groups]
+bar=localhost
+
+[bar]
+username=barn.owl
+__CONF__
+SVN_EDITOR=true \
+FCM_SUBVERSION_SERVERS_CONF=$FCM_SUBVERSION_SERVERS_CONF run_pass "$TEST_KEY" \
+ fcm branch-create hello svn://localhost/test_repos <<<'n'
+echo >>"$TEST_KEY.out" # Insert newline
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<__OUT__
+[info] Source: svn://localhost/test_repos/trunk@1 (4)
+[info] true: starting commit message editor...
+Change summary:
+--------------------------------------------------------------------------------
+A svn://localhost/test_repos/branches/dev/barn.owl/r1_hello
+--------------------------------------------------------------------------------
+Commit message is as follows:
+--------------------------------------------------------------------------------
+Created /branches/dev/barn.owl/r1_hello from /trunk at 1.
+--------------------------------------------------------------------------------
+Create the branch?
+Enter "y" or "n" (or just press <return> for "n")
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+TEST_KEY=$TEST_KEY_BASE-1
+FCM_SUBVERSION_SERVERS_CONF="$PWD/$TEST_KEY-svn-servers-conf"
+cat >$FCM_SUBVERSION_SERVERS_CONF <<'__CONF__'
+[groups]
+bar=localhost
+
+[bar]
+username=barn.owl
+__CONF__
+FCM_SUBVERSION_SERVERS_CONF=$FCM_SUBVERSION_SERVERS_CONF run_pass "$TEST_KEY" \
+ fcm branch-list svn://localhost/test_repos
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<'__OUT__'
+[info] svn://localhost/test_repos@4: 1 match(es)
+svn://localhost/test_repos/branches/dev/barn.owl/r1_wing@4
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+TEST_KEY=$TEST_KEY_BASE-0
+cat >$FCM_SUBVERSION_SERVERS_CONF <<'__CONF__'
+[groups]
+bar=localhost
+
+[bar]
+username=honey.bee
+__CONF__
+FCM_SUBVERSION_SERVERS_CONF=$FCM_SUBVERSION_SERVERS_CONF run_pass "$TEST_KEY" \
+ fcm branch-list svn://localhost/test_repos
+file_cmp "$TEST_KEY.out" "$TEST_KEY.out" <<'__OUT__'
+[info] svn://localhost/test_repos@4: 0 match(es)
+__OUT__
+file_cmp "$TEST_KEY.err" "$TEST_KEY.err" </dev/null
+#-------------------------------------------------------------------------------
+kill $(cat pid-file)
+teardown
+exit
diff --git a/t/svn-username/test_header b/t/svn-username/test_header
new file mode 100644
index 0000000..672ce12
--- /dev/null
+++ b/t/svn-username/test_header
@@ -0,0 +1,232 @@
+#!/bin/bash
+# ------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+# ------------------------------------------------------------------------------
+# Optional enviroment variables:
+# TEST_PROJECT (tests using given project name)
+# TEST_REMOTE_HOST (tests using svn+ssh repositories located on given host)
+# ------------------------------------------------------------------------------
+
+. $(dirname $0)/../lib/bash/test_header
+
+function file_cmp() {
+ local TEST_KEY=$1
+ local FILE_ACTUAL=$2
+ local FILE_EXPECT=${3:--}
+ if cmp $TEST_DIR/$FILE_ACTUAL $FILE_EXPECT; then
+ pass $TEST_KEY
+ return
+ fi
+ fail $TEST_KEY
+}
+
+function file_grep() {
+ local TEST_KEY=$1
+ local PATTERN=$2
+ local FILE=$3
+ if grep -q -e "$PATTERN" $TEST_DIR/$FILE; then
+ pass $TEST_KEY
+ return
+ fi
+ fail $TEST_KEY
+}
+
+function file_test() {
+ local TEST_KEY=$1
+ local FILE=$2
+ local OPTION=${3:--e}
+ if test $OPTION $TEST_DIR/$FILE; then
+ pass $TEST_KEY
+ else
+ fail $TEST_KEY
+ fi
+}
+
+function file_xxdiff() {
+ local TEST_KEY=$1
+ local FILE_ACTUAL=$2
+ local FILE_EXPECT=${3:--}
+ if xxdiff -D $TEST_DIR/$FILE_ACTUAL $FILE_EXPECT; then
+ pass $TEST_KEY
+ return
+ fi
+ fail $TEST_KEY
+}
+
+function init_repos() {
+ if [[ -n ${TEST_REMOTE_HOST:-} ]]; then
+ TEST_REMOTE_DIR=$(ssh $TEST_REMOTE_HOST "mktemp -d")
+ ssh $TEST_REMOTE_HOST "svnadmin create --fs-type fsfs $TEST_REMOTE_DIR"
+ REPOS_URL="svn+ssh://${TEST_REMOTE_HOST}$TEST_REMOTE_DIR"
+ else
+ svnadmin create --fs-type fsfs $TEST_DIR/test_repos
+ REPOS_URL="file://$TEST_DIR/test_repos"
+ fi
+ ROOT_URL=$REPOS_URL
+ PROJECT=
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ PROJECT=$TEST_PROJECT"/"
+ fi
+ svn import -q $TEST_SOURCE_DIR/../etc/repo_files \
+ $REPOS_URL/$PROJECT/trunk -m "initial trunk import"
+ svn mkdir -q $REPOS_URL/$PROJECT/tags -m "make tags"
+ svn mkdir -q --parents $REPOS_URL/$PROJECT/branches/dev/Share -m " "
+}
+
+function init_repos_layout_roses() {
+ if [[ -n ${TEST_REMOTE_HOST:-} ]]; then
+ TEST_REMOTE_DIR=$(ssh $TEST_REMOTE_HOST "mktemp -d")
+ ssh $TEST_REMOTE_HOST "svnadmin create --fs-type fsfs $TEST_REMOTE_DIR"
+ REPOS_URL="svn+ssh://${TEST_REMOTE_HOST}$TEST_REMOTE_DIR"
+ else
+ svnadmin create --fs-type fsfs $TEST_DIR/test_repos
+ REPOS_URL="file://$TEST_DIR/test_repos"
+ fi
+ svn mkdir -q --parents $REPOS_URL/a/a/0/0/0/trunk
+ svn import -q $TEST_SOURCE_DIR/../etc/repo_files \
+ $REPOS_URL/a/a/0/0/0/trunk -m "initial trunk import"
+ TMPFILE=$(mktemp)
+ cat >$TMPFILE <<__LAYOUT__
+depth-project = 5
+depth-branch = 1
+depth-tag = 1
+dir-trunk = trunk
+dir-branch =
+dir-tag =
+level-owner-branch =
+level-owner-tag =
+template-branch =
+template-tag =
+__LAYOUT__
+ TMPDIR=$(mktemp -d)
+ svn checkout -q $REPOS_URL $TMPDIR
+ svn propset -q --file=$TMPFILE fcm:layout $TMPDIR
+ svn commit -q -m " " $TMPDIR
+ rm -f $TMPFILE
+ rm -rf $TMPDIR
+ ROOT_URL=$REPOS_URL
+}
+
+function init_branch() {
+ local BRANCH_NAME=$1
+ local REPOS_URL=$2
+ local ROOT_URL=$REPOS_URL
+ local ROOT_PATH=
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ ROOT_PATH=$ROOT_PATH/$TEST_PROJECT
+ fi
+ MESSAGE=$(echo -e "Created $ROOT_PATH/branches/dev/Share/$BRANCH_NAME from /trunk at 1.")
+ svn copy -q -r1 $ROOT_URL/trunk $ROOT_URL/branches/dev/Share/$BRANCH_NAME \
+ -m "Made a branch $MESSAGE"
+}
+
+function init_branch_wc() {
+ local BRANCH_NAME=$1
+ local REPOS_URL=$2
+ local ROOT_URL=$REPOS_URL
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ fi
+ init_branch $BRANCH_NAME $REPOS_URL
+ svn checkout -q $ROOT_URL/branches/dev/Share/$BRANCH_NAME $TEST_DIR/wc
+}
+
+function init_merge_branches() {
+ local BRANCH_NAME=$1
+ local OTHER_BRANCH_NAME=$2
+ local REPOS_URL=$3
+ local ROOT_URL=$REPOS_URL
+ if [[ -n ${TEST_PROJECT:-} ]]; then
+ ROOT_URL=$REPOS_URL/$TEST_PROJECT
+ fi
+ init_branch_wc $BRANCH_NAME $REPOS_URL
+ cd $TEST_DIR/wc
+ file_list=$(find . -type f | sed "/\.svn/d" | sort | head -5)
+ other_file=$(find . -type f | sed "/\.svn/d" | sort | tail -1)
+ for file in $file_list; do
+ sed -i "s/for/FOR/g; s/fi/end if/g; s/in/IN/g;" $file
+ sed -i "/#/d; /^ *!/d" $file
+ sed -i "s/!/!!/g; s/q/\nq/g; s/[(]/(\n/g" $file
+ done
+ file_dir=$(dirname $file)
+ svn copy -q $file ./added_file
+ svn copy -q module added_directory
+ touch module/tree_conflict_file
+ svn add -q $file_dir/tree_conflict_file
+ echo "Modified a line" >>$other_file
+ svn commit -q -m "Made changes for future merge of this branch"
+ svn update -q
+ init_branch $OTHER_BRANCH_NAME $REPOS_URL
+ svn switch -q $ROOT_URL/branches/dev/Share/$OTHER_BRANCH_NAME
+ echo " " > unversioned_file
+ properties_file=$(find . -type f | sed " /\.svn/d" | sort | tail -3 | head -1)
+ svn propset -q svn:executable "executable" $properties_file
+ svn copy -q $file renamed_added_file
+ svn commit -q -m "Made changes for future merge"
+ svn update -q
+ svn switch -q $ROOT_URL/trunk
+ echo "trunk change" >>$(find . -type f | sed "/\.svn/d" | sort | head -1)
+ svn commit -q -m "Made trunk change"
+ svn update -q
+ echo "another trunk change" >>$(find . -type f | sed "/\.svn/d" | sort | head -1)
+ svn commit -q -m "Made another trunk change"
+ svn update -q
+}
+
+function run_pass() {
+ local TEST_KEY=$1
+ shift 1
+ if ! "$@" 1>$TEST_DIR/$TEST_KEY.out 2>$TEST_DIR/$TEST_KEY.err; then
+ fail $TEST_KEY
+ return
+ fi
+ pass $TEST_KEY
+}
+
+function run_fail() {
+ local TEST_KEY=$1
+ shift 1
+ if "$@" 1>$TEST_DIR/$TEST_KEY.out 2>$TEST_DIR/$TEST_KEY.err; then
+ fail $TEST_KEY
+ return
+ fi
+ pass $TEST_KEY
+}
+
+function setup() {
+ mkdir -p $TEST_DIR/.subversion
+ mkdir -p $TEST_DIR/run
+ cd $TEST_DIR/run
+}
+
+function teardown() {
+ cd $TEST_DIR
+ rm -rf $TEST_DIR/test_repos
+ rm -rf $TEST_DIR/wc
+ rm -rf $TEST_DIR/run
+ rm -rf $TEST_DIR/.subversion
+ if [[ -n ${TEST_REMOTE_HOST:-} ]]; then
+ ssh $TEST_REMOTE_HOST "rm -rf $TEST_REMOTE_DIR"
+ fi
+}
+
+REPOS_URL=
+ROOT_URL=
+PROJECT=
diff --git a/test/compare_results_fcm1 b/test/compare_results_fcm1
new file mode 100755
index 0000000..7a76edc
--- /dev/null
+++ b/test/compare_results_fcm1
@@ -0,0 +1,217 @@
+#!/bin/ksh
+
+TEST_NAME=$1
+BASE_DIR=$PWD
+DIFF=${DIFF:-diff}
+
+if [[ ! -f control/$TEST_NAME/.tests.complete ]]; then
+ echo "WARNING: Control tests did not complete, skipping comparisons"
+ exit
+fi
+
+DIFF_OUTPUT=$BASE_DIR/$TEST_NAME.diff
+
+# Compare directory contents
+for DIR in done .cache/.bld etc flags src
+do
+ if [[ -d test/$TEST_NAME/$DIR ]]; then
+ if [[ $DEBUG == true ]]; then
+ echo "Comparing $DIR directory contents ..."
+ fi
+ $DIFF -r control/$TEST_NAME/$DIR test/$TEST_NAME/$DIR >$DIFF_OUTPUT
+ if [[ $? != 0 ]]; then
+ echo "FAILED: $TEST_NAME - $DIR directory contents differ from control"
+ exit 1
+ fi
+ fi
+done
+
+# Compare file listings from directories
+for DIR in .cache/.ext bin obj lib inc ppsrc
+do
+ if [[ -d test/$TEST_NAME/$DIR ]]; then
+ if [[ $DEBUG == true ]]; then
+ echo "Comparing $DIR directory listings ..."
+ fi
+ cd $BASE_DIR/control/$TEST_NAME
+ ls -R -1 $DIR >$BASE_DIR/$TEST_NAME.control_files
+ cd $BASE_DIR/test/$TEST_NAME
+ ls -R -1 $DIR >$BASE_DIR/$TEST_NAME.test_files
+ cd $BASE_DIR
+ $DIFF $TEST_NAME.control_files $TEST_NAME.test_files >$DIFF_OUTPUT
+ if [[ $? != 0 ]]; then
+ echo "FAILED: $TEST_NAME - $DIR file listing differs from control"
+ exit 1
+ fi
+ rm $TEST_NAME.control_files $TEST_NAME.test_files
+ fi
+done
+
+# Compare file listings from directories (non-recursive)
+for DIR in .
+do
+ if [[ -d test/$TEST_NAME/$DIR ]]; then
+ if [[ $DEBUG == true ]]; then
+ echo "Comparing $DIR directory listings ..."
+ fi
+ cd $BASE_DIR/control/$TEST_NAME
+ ls -1 $DIR >$BASE_DIR/$TEST_NAME.control_files
+ cd $BASE_DIR/test/$TEST_NAME
+ ls -1 $DIR >$BASE_DIR/$TEST_NAME.test_files
+ cd $BASE_DIR/
+ $DIFF $TEST_NAME.control_files $TEST_NAME.test_files >$DIFF_OUTPUT
+ if [[ $? != 0 ]]; then
+ echo "FAILED: $TEST_NAME - $DIR file listing differs from control"
+ exit 1
+ fi
+ rm $TEST_NAME.control_files $TEST_NAME.test_files
+ fi
+done
+
+# Compare files in inc directory (except *.mod)
+if [[ -d test/$TEST_NAME/inc ]]; then
+ if [[ $DEBUG == true ]]; then
+ echo "Comparing inc directory contents ..."
+ fi
+ cd test/$TEST_NAME/inc
+ for FILE in $(ls -1 | grep -v "\.mod$")
+ do
+ $DIFF $BASE_DIR/control/$TEST_NAME/inc/$FILE $FILE >$DIFF_OUTPUT
+ if [[ $? != 0 ]]; then
+ echo "FAILED: $TEST_NAME - $FILE contents differ from control"
+ exit 1
+ fi
+ done
+fi
+cd $BASE_DIR
+
+# Compare files in ppsrc directory (ignoring RUN_DIR in *.c files)
+if [[ -d test/$TEST_NAME/ppsrc ]]; then
+ if [[ $DEBUG == true ]]; then
+ echo "Comparing ppsrc directory contents ..."
+ fi
+ cd test/$TEST_NAME
+ FILES=$(find ppsrc -type f)
+ cd $BASE_DIR
+ for FILE in $FILES
+ do
+ if [[ $FILE == ${FILE%.c} ]]; then
+ $DIFF control/$TEST_NAME/$FILE test/$TEST_NAME/$FILE >$DIFF_OUTPUT
+ else
+ cat control/$TEST_NAME/$FILE | sed "s#/.*/fcm_test_suite/control/##g" >$TEST_NAME.control_file
+ cat test/$TEST_NAME/$FILE | sed "s#/.*/fcm_test_suite/test/##g" >$TEST_NAME.test_file
+ $DIFF $TEST_NAME.control_file $TEST_NAME.test_file >$DIFF_OUTPUT
+ fi
+ if [[ $? != 0 ]]; then
+ echo "FAILED: $TEST_NAME - $FILE contents differ from control"
+ exit 1
+ fi
+ rm -f $TEST_NAME.control_file $TEST_NAME.test_file
+ done
+fi
+
+# Compare build command files
+cd test
+unset TEST_FILES CONTROL_FILES
+if [[ -f $TEST_NAME.build.commands.1 ]]; then
+ TEST_FILES=$(ls -1 $TEST_NAME.build.commands.*)
+fi
+cd $BASE_DIR/control
+if [[ -f $TEST_NAME.build.commands.1 ]]; then
+ CONTROL_FILES=$(ls -1 $TEST_NAME.build.commands.*)
+fi
+if [[ $TEST_FILES != $CONTROL_FILES ]]; then
+ echo "FAILED: $TEST_NAME - number of build command files differs from control"
+ exit 1
+fi
+cd $BASE_DIR
+for FILE in $TEST_FILES
+do
+ if [[ $DEBUG == true ]]; then
+ echo "Comparing $FILE contents ..."
+ fi
+ if [[ $NPROC = 1 ]]; then
+ $DIFF control/$FILE test/$FILE >$DIFF_OUTPUT
+ else
+ cat control/$FILE | grep -v wrap_ar | sort >$TEST_NAME.control_file
+ cat test/$FILE | grep -v wrap_ar | sort >$TEST_NAME.test_file
+ $DIFF $TEST_NAME.control_file $TEST_NAME.test_file >$DIFF_OUTPUT
+ fi
+ if [[ $? != 0 ]]; then
+ echo "FAILED: $TEST_NAME - $FILE contents differ from control"
+ exit 1
+ fi
+ rm -f $TEST_NAME.control_file $TEST_NAME.test_file
+done
+
+# Compare run output files
+cd test
+unset TEST_FILES CONTROL_FILES
+if [[ -f $TEST_NAME.exe.stdout.1 ]]; then
+ TEST_FILES="$TEST_FILES $(ls -1 $TEST_NAME.exe.stdout.*)"
+fi
+cd $BASE_DIR/control
+if [[ -f $TEST_NAME.exe.stdout.1 ]]; then
+ CONTROL_FILES="$CONTROL_FILES $(ls -1 $TEST_NAME.exe.stdout.*)"
+fi
+if [[ $TEST_FILES != $CONTROL_FILES ]]; then
+ echo "FAILED: $TEST_NAME - number of run output files differs from control"
+ exit 1
+fi
+cd $BASE_DIR
+for FILE in $TEST_FILES
+do
+ if [[ $DEBUG == true ]]; then
+ echo "Comparing $FILE contents ..."
+ fi
+ $DIFF control/$FILE test/$FILE >$DIFF_OUTPUT
+ if [[ $? != 0 ]]; then
+ echo "FAILED: $TEST_NAME - $FILE contents differ from control"
+ exit 1
+ fi
+done
+
+# Compare file contents ignoring RUN_DIR
+for FILE in Makefile fcm_env.sh cfg/bld.cfg cfg/parsed_bld.cfg cfg/ext.cfg cfg/parsed_ext.cfg
+do
+ if [[ -f test/$TEST_NAME/$FILE ]]; then
+ if [[ $DEBUG == true ]]; then
+ echo "Comparing $FILE contents ..."
+ fi
+ cat control/$TEST_NAME/$FILE | sed "s#/.*/fcm_test_suite/control/##g" >$TEST_NAME.control_file
+ cat test/$TEST_NAME/$FILE | sed "s#/.*/fcm_test_suite/test/##g" >$TEST_NAME.test_file
+ $DIFF $TEST_NAME.control_file $TEST_NAME.test_file >$DIFF_OUTPUT
+ if [[ $? != 0 ]]; then
+ echo "FAILED: $TEST_NAME - $FILE file contents differ from control"
+ exit 1
+ fi
+ rm $TEST_NAME.control_file $TEST_NAME.test_file
+ fi
+done
+
+if [[ $COMPARE_TIMES == true ]]; then
+ cd control
+ if [[ -f $TEST_NAME.extract.stdout.1 ]]; then
+ for FILE in $(ls -1 $TEST_NAME.extract.stdout.*)
+ do
+ echo "Extract times:"
+ cat $FILE | grep "\->.*second" > $TEST_NAME.control_extract_times
+ cat $BASE_DIR/test/$FILE | grep "\->.*second" > $TEST_NAME.test_extract_times
+ $DIFF --side-by-side $TEST_NAME.control_extract_times $TEST_NAME.test_extract_times
+ rm $TEST_NAME.control_extract_times $TEST_NAME.test_extract_times
+ done
+ fi
+ if [[ -f $TEST_NAME.build.stdout.1 ]]; then
+ for FILE in $(ls -1 $TEST_NAME.build.stdout.*)
+ do
+ echo "Build times:"
+ cat $FILE | grep "\->.*second" > $TEST_NAME.control_build_times
+ cat $BASE_DIR/test/$FILE | grep "\->.*second" > $TEST_NAME.test_build_times
+ $DIFF --side-by-side $TEST_NAME.control_build_times $TEST_NAME.test_build_times
+ rm $TEST_NAME.control_build_times $TEST_NAME.test_build_times
+ done
+ fi
+fi
+
+rm -f $DIFF_OUTPUT
+exit 0
diff --git a/test/compare_results_fcm2 b/test/compare_results_fcm2
new file mode 100755
index 0000000..ceb0128
--- /dev/null
+++ b/test/compare_results_fcm2
@@ -0,0 +1,205 @@
+#!/bin/ksh
+
+TEST_NAME=$1
+BASE_DIR=$PWD
+DIFF=${DIFF:-diff}
+
+if [[ ! -f control/$TEST_NAME/.tests.complete ]]; then
+ echo "WARNING: Control tests did not complete, skipping comparisons"
+ exit
+fi
+
+DIFF_OUTPUT=$BASE_DIR/$TEST_NAME.diff
+
+# Compare directory contents
+for DIR in extract build/etc .fcm-make/cache
+do
+ if [[ -d test/$TEST_NAME/$DIR ]]; then
+ if [[ $DEBUG == true ]]; then
+ echo "Comparing $DIR directory contents ..."
+ fi
+ $DIFF -r --exclude=hello_sub2.F90 control/$TEST_NAME/$DIR test/$TEST_NAME/$DIR >$DIFF_OUTPUT
+ if [[ $? != 0 ]]; then
+ echo "FAILED: $TEST_NAME - $DIR directory contents differ from control"
+ exit 1
+ fi
+ fi
+done
+
+# Compare file listings from directories
+for DIR in build/bin build/o build/include
+do
+ if [[ -d test/$TEST_NAME/$DIR ]]; then
+ if [[ $DEBUG == true ]]; then
+ echo "Comparing $DIR directory listings ..."
+ fi
+ cd $BASE_DIR/control/$TEST_NAME
+ ls -R -1 $DIR >$BASE_DIR/$TEST_NAME.control_files
+ cd $BASE_DIR/test/$TEST_NAME
+ ls -R -1 $DIR >$BASE_DIR/$TEST_NAME.test_files
+ cd $BASE_DIR
+ $DIFF $TEST_NAME.control_files $TEST_NAME.test_files >$DIFF_OUTPUT
+ if [[ $? != 0 ]]; then
+ echo "FAILED: $TEST_NAME - $DIR file listing differs from control"
+ exit 1
+ fi
+ rm $TEST_NAME.control_files $TEST_NAME.test_files
+ fi
+done
+
+# Compare file listings from directories (non-recursive)
+for DIR in . extract build preprocess
+do
+ if [[ -d test/$TEST_NAME/$DIR ]]; then
+ if [[ $DEBUG == true ]]; then
+ echo "Comparing $DIR directory listings ..."
+ fi
+ cd $BASE_DIR/control/$TEST_NAME
+ ls -1 $DIR >$BASE_DIR/$TEST_NAME.control_files
+ cd $BASE_DIR/test/$TEST_NAME
+ ls -1 $DIR >$BASE_DIR/$TEST_NAME.test_files
+ cd $BASE_DIR/
+ $DIFF $TEST_NAME.control_files $TEST_NAME.test_files >$DIFF_OUTPUT
+ if [[ $? != 0 ]]; then
+ echo "FAILED: $TEST_NAME - $DIR file listing differs from control"
+ exit 1
+ fi
+ rm $TEST_NAME.control_files $TEST_NAME.test_files
+ fi
+done
+
+# Compare files in build/include directory (except *.mod)
+if [[ -d test/$TEST_NAME/build/include ]]; then
+ if [[ $DEBUG == true ]]; then
+ echo "Comparing build/include directory contents ..."
+ fi
+ cd test/$TEST_NAME/build/include
+ for FILE in $(ls -1 | grep -v "\.mod$")
+ do
+ $DIFF $BASE_DIR/control/$TEST_NAME/build/include/$FILE $FILE >$DIFF_OUTPUT
+ if [[ $? != 0 ]]; then
+ echo "FAILED: $TEST_NAME - $FILE contents differ from control"
+ exit 1
+ fi
+ done
+fi
+cd $BASE_DIR
+
+# Compare files in preprocess directory (ignoring RUN_DIR in *.c & *.cpp files)
+if [[ -d test/$TEST_NAME/preprocess ]]; then
+ if [[ $DEBUG == true ]]; then
+ echo "Comparing preprocess directory contents ..."
+ fi
+ cd test/$TEST_NAME
+ FILES=$(find preprocess -type f)
+ cd $BASE_DIR
+ for FILE in $FILES
+ do
+ if [[ $FILE == ${FILE%.c} && $FILE == ${FILE%.cpp} ]]; then
+ $DIFF control/$TEST_NAME/$FILE test/$TEST_NAME/$FILE >$DIFF_OUTPUT
+ else
+ sed "s#/.*/fcm_test_suite/control/##g" control/$TEST_NAME/$FILE >$TEST_NAME.control_file
+ sed "s#/.*/fcm_test_suite/test/##g" test/$TEST_NAME/$FILE >$TEST_NAME.test_file
+ $DIFF $TEST_NAME.control_file $TEST_NAME.test_file >$DIFF_OUTPUT
+ fi
+ if [[ $? != 0 ]]; then
+ echo "FAILED: $TEST_NAME - $FILE contents differ from control"
+ exit 1
+ fi
+ rm -f $TEST_NAME.control_file $TEST_NAME.test_file
+ done
+fi
+
+# Compare build command files
+cd test
+unset TEST_FILES CONTROL_FILES
+if [[ -f $TEST_NAME.build.commands.1 ]]; then
+ TEST_FILES=$(ls -1 $TEST_NAME.build.commands.*)
+fi
+cd $BASE_DIR/control
+if [[ -f $TEST_NAME.build.commands.1 ]]; then
+ CONTROL_FILES=$(ls -1 $TEST_NAME.build.commands.*)
+fi
+if [[ $TEST_FILES != $CONTROL_FILES ]]; then
+ echo "FAILED: $TEST_NAME - number of build command files differs from control"
+ exit 1
+fi
+cd $BASE_DIR
+for FILE in $TEST_FILES
+do
+ if [[ $DEBUG == true ]]; then
+ echo "Comparing $FILE contents ..."
+ fi
+ if [[ $NPROC = 1 ]]; then
+ $DIFF control/$FILE test/$FILE >$DIFF_OUTPUT
+ else
+ sort control/$FILE >$TEST_NAME.control_file
+ sort test/$FILE >$TEST_NAME.test_file
+ $DIFF $TEST_NAME.control_file $TEST_NAME.test_file >$DIFF_OUTPUT
+ fi
+ if [[ $? != 0 ]]; then
+ echo "FAILED: $TEST_NAME - $FILE contents differ from control"
+ exit 1
+ fi
+ rm -f $TEST_NAME.control_file $TEST_NAME.test_file
+done
+
+# Compare run output files
+cd test
+unset TEST_FILES CONTROL_FILES
+if [[ -f $TEST_NAME.exe.stdout.1 ]]; then
+ TEST_FILES="$TEST_FILES $(ls -1 $TEST_NAME.exe.stdout.*)"
+fi
+cd $BASE_DIR/control
+if [[ -f $TEST_NAME.exe.stdout.1 ]]; then
+ CONTROL_FILES="$CONTROL_FILES $(ls -1 $TEST_NAME.exe.stdout.*)"
+fi
+if [[ $TEST_FILES != $CONTROL_FILES ]]; then
+ echo "FAILED: $TEST_NAME - number of run output files differs from control"
+ exit 1
+fi
+cd $BASE_DIR
+for FILE in $TEST_FILES
+do
+ if [[ $DEBUG == true ]]; then
+ echo "Comparing $FILE contents ..."
+ fi
+ $DIFF control/$FILE test/$FILE >$DIFF_OUTPUT
+ if [[ $? != 0 ]]; then
+ echo "FAILED: $TEST_NAME - $FILE contents differ from control"
+ exit 1
+ fi
+done
+
+# Compare file contents ignoring RUN_DIR
+for FILE in .fcm-make/config-as-parsed.cfg .fcm-make/config-on-success.cfg
+do
+ if [[ -f test/$TEST_NAME/$FILE ]]; then
+ if [[ $DEBUG == true ]]; then
+ echo "Comparing $FILE contents ..."
+ fi
+ cat control/$TEST_NAME/$FILE | sed "s#/.*/fcm_test_suite/control/##g" >$TEST_NAME.control_file
+ cat test/$TEST_NAME/$FILE | sed "s#/.*/fcm_test_suite/test/##g" >$TEST_NAME.test_file
+ $DIFF $TEST_NAME.control_file $TEST_NAME.test_file >$DIFF_OUTPUT
+ if [[ $? != 0 ]]; then
+ echo "FAILED: $TEST_NAME - $FILE file contents differ from control"
+ exit 1
+ fi
+ rm $TEST_NAME.control_file $TEST_NAME.test_file
+ fi
+done
+
+if [[ $COMPARE_TIMES == true ]]; then
+ cd control
+ for FILE in $(ls -1 $TEST_NAME.make.stdout.*)
+ do
+ echo "Make times:"
+ grep -e '\[done\]' -e '\[FAIL\]' $FILE >$TEST_NAME.control_make_times
+ grep -e '\[done\]' -e '\[FAIL\]' $BASE_DIR/test/$FILE >$TEST_NAME.test_make_times
+ $DIFF --side-by-side $TEST_NAME.control_make_times $TEST_NAME.test_make_times
+ rm $TEST_NAME.control_make_times $TEST_NAME.test_make_times
+ done
+fi
+
+rm -f $DIFF_OUTPUT
+exit 0
diff --git a/test/compare_times_fcm1-2 b/test/compare_times_fcm1-2
new file mode 100755
index 0000000..60ae23d
--- /dev/null
+++ b/test/compare_times_fcm1-2
@@ -0,0 +1,27 @@
+#!/bin/ksh
+
+TEST=$1
+DIFF=${DIFF:-diff}
+
+for FILE in $(ls -1 $TEST.make.stdout.*)
+do
+ FCM1_EXTRACT=$(echo "fcm1_${FILE#fcm2_}" | sed 's/make/extract/')
+ FCM1_BUILD=$(echo "fcm1_${FILE#fcm2_}" | sed 's/make/build/')
+ if [[ -f $FCM1_EXTRACT || -f $FCM1_BUILD ]]; then
+ if [[ $DEBUG == true ]]; then
+ echo "Comparing $FILE with $FCM1_EXTRACT & $FCM1_BUILD ..."
+ fi
+ echo "Times:"
+ if [[ -f $FCM1_EXTRACT ]]; then
+ grep "\->.*second" $FCM1_EXTRACT | grep -v Setup | grep -v Parse >> $TEST.fcm1_times
+ fi
+ if [[ -f $FCM1_BUILD ]]; then
+ grep "\->.*second" $FCM1_BUILD | grep -v Setup | grep -v Parse >> $TEST.fcm1_times
+ fi
+ grep -e '\[done\]' -e '\[FAIL\]' $FILE > $TEST.fcm2_times
+ $DIFF --side-by-side $TEST.fcm1_times $TEST.fcm2_times
+ rm $TEST.fcm1_times $TEST.fcm2_times
+ fi
+done
+
+exit 0
diff --git a/test/create_hpc_batch_script b/test/create_hpc_batch_script
new file mode 100755
index 0000000..ded90ef
--- /dev/null
+++ b/test/create_hpc_batch_script
@@ -0,0 +1,90 @@
+#!/bin/ksh
+
+cat <<EOF >$BATCH_SCRIPT
+#!/bin/ksh
+#
+# @ job_type = serial
+# @ class = serial
+# @ resources = ConsumableCpus(6) ConsumableMemory(1gb)
+# @ initialdir = $RUN_DIR_HPC
+# @ output = hpc_batch.stdout
+# @ error = hpc_batch.stderr
+# @ queue
+
+. prg_12_1_0_9
+export PATH=$RUN_DIR_HPC/fcm/bin:\$PATH
+export DIFF="/opt/freeware/bin/diff"
+export DEBUG=$DEBUG
+
+. \$MY_BIN/$BATCH_DIRS_NAME
+let failed=0
+for TEST in \$TESTS_FCM1
+do
+ cd $RUN_DIR_HPC/\$TEST
+ echo "\$(date): Running \$TEST ..."
+ find src -exec touch {} \\;
+ fcm build -v 2 -j 6 >../\$TEST.build.stdout.1 2>../\$TEST.build.stderr.1
+ RC=\$?
+ if [[ \$RC != 0 ]]; then
+ echo "FAILED: \$TEST failed"
+ let failed=failed+1
+ else
+EOF
+
+if [[ $TYPE == control ]]; then
+ echo " touch .tests.complete" >>$BATCH_SCRIPT
+else
+ cat <<EOF >>$BATCH_SCRIPT
+ export COMPARE_TIMES=true
+ cd $BASE_DIR_HPC
+ $MY_BIN/compare_results_fcm1 \$TEST
+ if [[ \$? != 0 ]]; then
+ let failed=failed+1
+ fi
+EOF
+fi
+
+cat <<EOF >>$BATCH_SCRIPT
+ fi
+done
+for TEST in \$TESTS_FCM2
+do
+ cd $RUN_DIR_HPC/\$TEST
+ echo "\$(date): Running \$TEST ..."
+ find extract -exec touch {} \\;
+ fcm make -j 6 >../\$TEST.make.stdout.1 2>../\$TEST.make.stderr.1
+ RC=\$?
+ if [[ \$RC != 0 ]]; then
+ echo "FAILED: \$TEST failed"
+ let failed=failed+1
+ else
+EOF
+
+if [[ $TYPE == control ]]; then
+ cat <<EOF >>$BATCH_SCRIPT
+ touch .tests.complete
+ cd ..
+ $MY_BIN/compare_times_fcm1-2 \$TEST
+EOF
+else
+ cat <<EOF >>$BATCH_SCRIPT
+ export COMPARE_TIMES=true
+ cd $BASE_DIR_HPC
+ $MY_BIN/compare_results_fcm2 \$TEST
+ if [[ \$? != 0 ]]; then
+ let failed=failed+1
+ fi
+EOF
+fi
+
+cat <<EOF >>$BATCH_SCRIPT
+ fi
+done
+
+echo "\$(date): HPC performance tests finished"
+if [[ \$failed == 0 ]]; then
+ echo "SUMMARY: All HPC performance tests succeeded"
+else
+ echo "SUMMARY: \$failed HPC performance tests failed"
+fi
+EOF
diff --git a/test/create_repos b/test/create_repos
new file mode 100755
index 0000000..21b6482
--- /dev/null
+++ b/test/create_repos
@@ -0,0 +1,197 @@
+#!/bin/ksh
+set -eu
+
+REPOS_FILES=$MY_BIN/repos
+
+echo "$(date): Creating repository ..."
+rm -rf $REPOS_DIR
+svnadmin create --fs-type fsfs $REPOS_DIR
+
+echo "$(date): Initial import ..."
+
+svn import -q $REPOS_FILES/trunk $REPOS_URL/trunk -m" "
+svn mkdir -q $REPOS_URL/branches -m" "
+svn mkdir -q $REPOS_URL/tags -m" "
+
+# Modify some files
+branch=modify_files_base
+fcm bc -t SHARE --rev-flag NONE --non-interactive $branch $REPOS_URL at 1
+rm -rf $BASE_DIR/work
+svn co -q $REPOS_URL/branches/dev/Share/$branch $BASE_DIR/work
+cd $BASE_DIR/work
+perl -pi -e 's/IMPLICIT NONE/implicit none/' program/hello.F90
+perl -pi -e 's/Hello Earth/Hello Earthlings/' module/hello_constants.inc
+svn ci -m" "
+
+# Modify some files, one of which can be merged with modify_files_base branch
+branch=modify_files_merge1
+fcm bc -t SHARE --rev-flag NONE --non-interactive $branch $REPOS_URL at 1
+svn sw $REPOS_URL/branches/dev/Share/$branch
+perl -pi -e "s/this = 'Hello'/this = 'HELLO'/" program/hello.F90
+perl -pi -e 's/Hello Earth/Hello Earthlings/' subroutine/hello_c.c
+svn ci -m" "
+
+# Modify a file which can be merged with the modify_files_base & modify_files_merge1 branches
+branch=modify_files_merge2
+fcm bc -t SHARE --rev-flag NONE --non-interactive $branch $REPOS_URL at 1
+svn sw $REPOS_URL/branches/dev/Share/$branch
+perl -pi -e 's/PROGRAM/program/' program/hello.F90
+svn ci -m" "
+
+# Modify a file which clashes with modify_files_base branch
+branch=modify_files_clash
+fcm bc -t SHARE --rev-flag NONE --non-interactive $branch $REPOS_URL at 1
+svn sw $REPOS_URL/branches/dev/Share/$branch
+perl -pi -e 's/IMPLICIT NONE/implicit NONE/' program/hello.F90
+svn ci -m" "
+
+# Modify a subroutine without altering its interface
+branch=modify_subroutine
+fcm bc -t SHARE --rev-flag NONE --non-interactive $branch $REPOS_URL at 1
+svn sw $REPOS_URL/branches/dev/Share/$branch
+perl -pi -e 's/integer (common)/integer (com)/' subroutine/hello_sub.F90
+svn ci -m" "
+
+# Modify a subroutine and alter its interface
+branch=modify_subroutine_interface
+fcm bc -t SHARE --rev-flag NONE --non-interactive $branch $REPOS_URL at 1
+svn sw $REPOS_URL/branches/dev/Share/$branch
+perl -pi -e 's/integer_arg/int_arg/' subroutine/hello_sub.F90
+svn ci -m" "
+
+# Modify a pre-processing include file
+branch=modify_pp_include
+fcm bc -t SHARE --rev-flag NONE --non-interactive $branch $REPOS_URL at 1
+svn sw $REPOS_URL/branches/dev/Share/$branch
+perl -pi -e 's/:/: Message - /' subroutine/hello_sub.h
+svn ci -m" "
+
+# Add lines to a file to coincide with following branch
+branch=add_lines
+fcm bc -t SHARE --rev-flag NONE --non-interactive $branch $REPOS_URL at 1
+svn sw $REPOS_URL/branches/dev/Share/$branch
+cp $REPOS_FILES/add_subroutine/hello.F90.add_lines program/hello.F90
+svn ci -m" "
+
+# Add a new file
+branch=add_file
+fcm bc -t SHARE --rev-flag NONE --non-interactive $branch $REPOS_URL at 1
+svn sw $REPOS_URL/branches/dev/Share/$branch
+cp $REPOS_FILES/add_subroutine/hello.F90 program/hello.F90
+cp $REPOS_FILES/add_subroutine/hello_sub2.f90 subroutine
+svn add subroutine/hello_sub2.f90
+svn ci -m" "
+
+# Add a new directory
+branch=add_directory
+fcm bc -t SHARE --rev-flag NONE --non-interactive $branch $REPOS_URL at 1
+svn sw $REPOS_URL/branches/dev/Share/$branch
+cp $REPOS_FILES/add_subroutine/hello.F90 program/hello.F90
+mkdir subroutine2
+cp $REPOS_FILES/add_subroutine/hello_sub2.f90 subroutine2
+svn add subroutine2
+svn ci -m" "
+
+# Add a duplicate subroutine
+branch=add_duplicate
+fcm bc -t SHARE --rev-flag NONE --non-interactive $branch $REPOS_URL at 1
+svn sw $REPOS_URL/branches/dev/Share/$branch
+cp subroutine/hello_sub.F90 subroutine/hello_sub2.F90
+svn add subroutine/hello_sub2.F90
+svn ci -m" "
+
+# Delete a file
+branch=delete_file
+fcm bc -t SHARE --rev-flag NONE --non-interactive $branch $REPOS_URL at 1
+svn sw $REPOS_URL/branches/dev/Share/$branch
+svn rm subroutine/hello_c.c
+svn ci -m" "
+
+# Delete a CPP file
+branch=delete_pp_file
+fcm bc -t SHARE --rev-flag NONE --non-interactive $branch $REPOS_URL at 1
+svn sw $REPOS_URL/branches/dev/Share/$branch
+svn rm subroutine/hello_sub.F90
+svn ci -m" "
+
+# Delete a Fortran include file
+branch=delete_inc_file
+fcm bc -t SHARE --rev-flag NONE --non-interactive $branch $REPOS_URL at 1
+svn sw $REPOS_URL/branches/dev/Share/$branch
+svn rm module/hello_constants.inc
+svn ci -m" "
+
+# Delete a CPP include file
+branch=delete_ppinc_file
+fcm bc -t SHARE --rev-flag NONE --non-interactive $branch $REPOS_URL at 1
+svn sw $REPOS_URL/branches/dev/Share/$branch
+svn rm subroutine/hello_sub.h
+svn ci -m" "
+
+# Delete a directory
+branch=delete_directory
+fcm bc -t SHARE --rev-flag NONE --non-interactive $branch $REPOS_URL at 1
+svn sw $REPOS_URL/branches/dev/Share/$branch
+svn rm subroutine
+svn ci -m" "
+
+# Rename the executable
+branch=exe_rename
+fcm bc -t SHARE --rev-flag NONE --non-interactive $branch $REPOS_URL at 1
+svn sw $REPOS_URL/branches/dev/Share/$branch
+perl -pi -e 's/hello.exe/hello_world.exe/' script/hello.sh
+svn ci -m" "
+
+# Use a .f90 file as an include file
+branch=change_src_type
+fcm bc -t SHARE --rev-flag NONE --non-interactive $branch $REPOS_URL at 1
+svn sw $REPOS_URL/branches/dev/Share/$branch
+perl -pi -e 's/hello_constants.inc/hello_constants_inc.f90/g' module/hello_constants_dummy.inc
+svn mv module/hello_constants.inc module/hello_constants_inc.f90
+svn ci -m" "
+
+# Add a symbolic link
+branch=symbolic_link
+fcm bc -t SHARE --rev-flag NONE --non-interactive $branch $REPOS_URL at 1
+svn sw $REPOS_URL/branches/dev/Share/$branch
+cd subroutine
+ln -s hello_sub.F90 hello_sub2.F90
+svn add hello_sub2.F90
+cd $OLDPWD
+svn ci -m" "
+
+# Use space in a path name
+branch=space_in_name
+fcm bc -t SHARE --rev-flag NONE --non-interactive $branch $REPOS_URL at 1
+svn sw $REPOS_URL/branches/dev/Share/$branch
+svn mv blockdata/hello_blockdata.F90 "blockdata/hello blockdata.F90"
+svn mv --force blockdata "block data"
+svn ci -m" "
+
+# Create a cylc dependency which will fail
+branch=cyclic_dep_fail
+fcm bc -t SHARE --rev-flag NONE --non-interactive $branch $REPOS_URL at 1
+svn sw $REPOS_URL/branches/dev/Share/$branch
+cp $REPOS_FILES/cyclic_dependency/hello.F90 program/hello.F90
+cp $REPOS_FILES/cyclic_dependency/hello_constants.f90.fail module/hello_constants.f90
+svn ci -m" "
+
+# Create a cylc dependency which should be OK
+branch=cyclic_dep_ok
+fcm bc -t SHARE --rev-flag NONE --non-interactive $branch $REPOS_URL at 1
+svn sw $REPOS_URL/branches/dev/Share/$branch
+cp $REPOS_FILES/cyclic_dependency/hello.F90 program/hello.F90
+cp $REPOS_FILES/cyclic_dependency/hello_constants.f90.ok module/hello_constants.f90
+cp $REPOS_FILES/cyclic_dependency/hello_sub2.f90 subroutine
+svn add subroutine/hello_sub2.f90
+svn ci -m" "
+
+# Create a dependency which needs to be defined manually
+branch=f77_dep
+fcm bc -t SHARE --rev-flag NONE --non-interactive $branch $REPOS_URL at 1
+svn sw $REPOS_URL/branches/dev/Share/$branch
+perl -pi -e 's/INCLUDE /!INCLUDE /' program/hello.F90
+svn ci -m" "
+
+rm -rf $BASE_DIR/work
+echo "$(date): Finished"
diff --git a/test/get_hpc_results b/test/get_hpc_results
new file mode 100755
index 0000000..3fba9cd
--- /dev/null
+++ b/test/get_hpc_results
@@ -0,0 +1,34 @@
+#!/bin/ksh
+
+while getopts ":ch" opt
+do
+ case $opt in
+ c ) COPY=true ;;
+ h ) HELP=true ;;
+ \? ) echo "Invalid option"
+ HELP=true
+ break ;;
+ esac
+done
+if [[ $# != $(($OPTIND - 1)) ]]; then
+ echo "Invalid argument"
+ HELP=true
+fi
+
+if [[ $HELP == true ]]; then
+ echo 'Usage: get_hpc_results [options]'
+ echo 'Valid options:'
+ echo '-c'
+ echo ' Mirror the full test directory from the HPC'
+ echo '-h'
+ echo ' Print this help message'
+ exit 1
+fi
+
+export HPC=$(rose host-select -q hpc)
+ssh $HPC "cd working/fcm_test_suite; $MY_BIN/report_hpc_results" | tee $LOCALTEMP/fcm_test_suite/hpc.summary
+
+if [[ $COPY == true ]]; then
+ echo "Mirroring results from HPC ..."
+ rsync -a --delete --rsh="ssh" $HPC:fcm_test_suite/ $LOCALTEMP/fcm_test_suite/hpc_mirror
+fi
diff --git a/test/perform_test_fcm1 b/test/perform_test_fcm1
new file mode 100755
index 0000000..c2681ce
--- /dev/null
+++ b/test/perform_test_fcm1
@@ -0,0 +1,165 @@
+#!/bin/ksh
+set -u
+
+echo "$(date): Running $TEST ..."
+
+cfg_name=$TEST
+if [[ -a $MY_BIN/test_config/$TEST ]]; then
+ . $MY_BIN/test_config/$TEST
+fi
+
+export THIS_RUN_DIR=$RUN_DIR/$TEST
+rm -rf $THIS_RUN_DIR
+mkdir $THIS_RUN_DIR
+cd $THIS_RUN_DIR
+mirror=${mirror:-false}
+if [[ $mirror == remote ]]; then
+ export THIS_RUN_DIR_HPC=$RUN_DIR_HPC/$TEST
+fi
+
+NPROC=${NPROC:-1}
+let count=1
+for this_cfg in $cfg_name
+do
+ extract=$(eval "echo \${extract_$count:-}")
+ build=$(eval "echo \${build_$count:-}")
+ run=$(eval "echo \${run_$count:-}")
+ if [[ $DEBUG == true ]]; then
+ echo "Running extract ($count) ..."
+ fi
+ fcm extract -v 3 $REPOS_URL/trunk/cfg/$this_cfg.cfg >../$TEST.extract.stdout.$count 2>../$TEST.extract.stderr.$count
+ RC=$?
+ if [[ $extract == fail ]]; then
+ if [[ $RC == 0 ]]; then
+ echo "FAILED: $TEST ($count) extract did not fail"
+ exit 1
+ fi
+ elif [[ $extract == fail_known ]]; then
+ if [[ $RC == 0 ]]; then
+ echo "FAILED: $TEST ($count) extract did not fail as expected (known problem fixed?)"
+ exit 1
+ else
+ if [[ $DEBUG == true ]]; then
+ echo "Known problem: $TEST ($count) extract failed"
+ fi
+ break
+ fi
+ elif [[ $extract == succeed_known ]]; then
+ if [[ $RC == 0 ]]; then
+ if [[ $DEBUG == true ]]; then
+ echo "Known problem: $TEST ($count) extract did not fail"
+ fi
+ break
+ else
+ echo "FAILED: $TEST ($count) extract did not succeed as expected (known problem fixed?)"
+ exit 1
+ fi
+ else
+ if [[ $RC != 0 ]]; then
+ echo "FAILED: $TEST ($count) extract failed"
+ exit 1
+ else
+ if [[ $mirror == local ]]; then
+ cd ${THIS_RUN_DIR}_mirror
+ elif [[ $mirror == remote ]]; then
+ echo "$TEST" >> $BATCH_DIRS
+ break
+ fi
+ if [[ $DEBUG == true ]]; then
+ echo "Running build ($count) ..."
+ fi
+ export command_file=$RUN_DIR/$TEST.build.commands.$count
+ touch $command_file
+ fcm build -v 2 -j $NPROC >../$TEST.build.stdout.$count 2>../$TEST.build.stderr.$count
+ RC=$?
+ if [[ $build == fail ]]; then
+ if [[ $RC == 0 ]]; then
+ echo "FAILED: $TEST ($count) build did not fail"
+ exit 1
+ fi
+ elif [[ $build == fail_known ]]; then
+ if [[ $RC == 0 ]]; then
+ echo "FAILED: $TEST ($count) build did not fail as expected (known problem fixed?)"
+ exit 1
+ else
+ if [[ $DEBUG == true ]]; then
+ echo "Known problem: $TEST ($count) build failed"
+ fi
+ break
+ fi
+ elif [[ $build == succeed_known ]]; then
+ if [[ $RC == 0 ]]; then
+ if [[ $DEBUG == true ]]; then
+ echo "Known problem: $TEST ($count) build did not fail"
+ fi
+ break
+ else
+ echo "FAILED: $TEST ($count) build did not succeed as expected (known problem fixed?)"
+ exit 1
+ fi
+ else
+ if [[ $RC != 0 ]]; then
+ echo "FAILED: $TEST ($count) build failed"
+ exit 1
+ else
+ if [[ $run != no ]]; then
+ exe_name=hello.sh
+ env_file=fcm_env.sh
+ if [[ ! -a $env_file ]]; then
+ echo "FAILED: $TEST ($count) env file does not exist"
+ exit 1
+ else
+ . $env_file
+ if [[ $DEBUG == true ]]; then
+ echo "Running executable ($count) ..."
+ fi
+ $exe_name >../$TEST.exe.stdout.$count 2>../$TEST.exe.stderr.$count
+ RC=$?
+ if [[ $run == fail ]]; then
+ if [[ $RC == 0 ]]; then
+ echo "FAILED: $TEST ($count) run did not fail"
+ exit 1
+ fi
+ elif [[ $run == fail_known ]]; then
+ if [[ $RC == 0 ]]; then
+ echo "FAILED: $TEST ($count) run did not fail as expected (known problem fixed?)"
+ exit 1
+ else
+ if [[ $DEBUG == true ]]; then
+ echo "Known problem: $TEST ($count) run failed"
+ fi
+ break
+ fi
+ elif [[ $run == succeed_known ]]; then
+ if [[ $RC == 0 ]]; then
+ if [[ $DEBUG == true ]]; then
+ echo "Known problem: $TEST ($count) run did not fail"
+ fi
+ break
+ else
+ echo "FAILED: $TEST ($count) run did not succeed as expected (known problem fixed?)"
+ exit 1
+ fi
+ else
+ if [[ $RC != 0 ]]; then
+ echo "FAILED: $TEST ($count) run failed"
+ exit 1
+ fi
+ fi
+ fi
+ fi
+ fi
+ fi
+ fi
+ fi
+ let count=count+1
+done
+
+rm -rf $BASE_DIR/work
+
+if [[ $TYPE == control ]]; then
+ touch $THIS_RUN_DIR/.tests.complete
+else
+ cd $BASE_DIR
+ $MY_BIN/compare_results_fcm1 $TEST
+fi
diff --git a/test/perform_test_fcm2 b/test/perform_test_fcm2
new file mode 100755
index 0000000..49562d1
--- /dev/null
+++ b/test/perform_test_fcm2
@@ -0,0 +1,204 @@
+#!/bin/ksh
+set -u
+
+echo "$(date): Running $TEST ..."
+
+cfg_name=$TEST
+if [[ -a $MY_BIN/test_config/$TEST ]]; then
+ . $MY_BIN/test_config/$TEST
+fi
+
+export THIS_RUN_DIR=$RUN_DIR/$TEST
+rm -rf $THIS_RUN_DIR
+mkdir $THIS_RUN_DIR
+cd $THIS_RUN_DIR
+mirror=${mirror:-false}
+if [[ $mirror == remote ]]; then
+ export THIS_RUN_DIR_HPC=$RUN_DIR_HPC/$TEST
+fi
+
+NPROC=${NPROC:-1}
+let count=1
+for this_cfg in $cfg_name
+do
+ make=$(eval "echo \${make_$count:-}")
+ run=$(eval "echo \${run_$count:-}")
+ export command_file=$RUN_DIR/$TEST.build.commands.$count
+ touch $command_file
+ if [[ $DEBUG == true ]]; then
+ echo "Running make ($count) ..."
+ fi
+ fcm make -j $NPROC -f $REPOS_URL/trunk/cfg/$this_cfg.cfg >../$TEST.make.stdout.$count 2>../$TEST.make.stderr.$count
+ RC=$?
+ if [[ $make == fail ]]; then
+ if [[ $RC == 0 ]]; then
+ echo "FAILED: $TEST ($count) make did not fail"
+ exit 1
+ fi
+ elif [[ $make == fail_known ]]; then
+ if [[ $RC == 0 ]]; then
+ echo "FAILED: $TEST ($count) make did not fail as expected (known problem fixed?)"
+ exit 1
+ else
+ if [[ $DEBUG == true ]]; then
+ echo "Known problem: $TEST ($count) make failed"
+ fi
+ break
+ fi
+ elif [[ $make == succeed_known ]]; then
+ if [[ $RC == 0 ]]; then
+ if [[ $DEBUG == true ]]; then
+ echo "Known problem: $TEST ($count) make did not fail"
+ fi
+ break
+ else
+ echo "FAILED: $TEST ($count) make did not succeed as expected (known problem fixed?)"
+ exit 1
+ fi
+ else
+ if [[ $RC != 0 ]]; then
+ echo "FAILED: $TEST ($count) make failed"
+ exit 1
+ else
+ if [[ $mirror == remote ]]; then
+ echo "$TEST" >> $BATCH_DIRS
+ break
+ elif [[ $mirror == local ]]; then
+ cd ${THIS_RUN_DIR}_mirror
+ RC=$?
+ if [[ $RC != 0 ]]; then
+ echo "FAILED: $TEST ($count) cd to mirror failed"
+ exit 1
+ fi
+ if [[ $DEBUG == true ]]; then
+ echo "Running make after mirror ($count) ..."
+ fi
+ fcm make -j $NPROC >../$TEST.make2.stdout.$count 2>../$TEST.make2.stderr.$count
+ RC=$?
+ if [[ $RC != 0 ]]; then
+ echo "FAILED: $TEST ($count) make after mirror failed"
+ exit 1
+ fi
+ fi
+ if [[ $run != no ]]; then
+ exe_name=hello.sh
+ build_dir=$PWD/build/bin
+ if [[ ! -a $build_dir ]]; then
+ echo "FAILED: $TEST ($count) build directory does not exist"
+ exit 1
+ else
+ PATH=$build_dir:$PATH
+ if [[ $DEBUG == true ]]; then
+ echo "Running executable ($count) ..."
+ fi
+ $exe_name >../$TEST.exe.stdout.$count 2>../$TEST.exe.stderr.$count
+ RC=$?
+ if [[ $run == fail ]]; then
+ if [[ $RC == 0 ]]; then
+ echo "FAILED: $TEST ($count) run did not fail"
+ exit 1
+ fi
+ elif [[ $run == fail_known ]]; then
+ if [[ $RC == 0 ]]; then
+ echo "FAILED: $TEST ($count) run did not fail as expected (known problem fixed?)"
+ exit 1
+ else
+ if [[ $DEBUG == true ]]; then
+ echo "Known problem: $TEST ($count) run failed"
+ fi
+ break
+ fi
+ elif [[ $run == succeed_known ]]; then
+ if [[ $RC == 0 ]]; then
+ if [[ $DEBUG == true ]]; then
+ echo "Known problem: $TEST ($count) run did not fail"
+ fi
+ break
+ else
+ echo "FAILED: $TEST ($count) run did not succeed as expected (known problem fixed?)"
+ exit 1
+ fi
+ else
+ if [[ $RC != 0 ]]; then
+ echo "FAILED: $TEST ($count) run failed"
+ exit 1
+ else
+ build_dir=$PWD/build2/bin
+ if [[ -a $build_dir ]]; then
+ PATH=$build_dir:$PATH
+ if [[ $DEBUG == true ]]; then
+ echo "Running executable from build2 ($count) ..."
+ fi
+ $exe_name >../$TEST.exe2.stdout.$count 2>../$TEST.exe2.stderr.$count
+ RC=$?
+ if [[ $RC != 0 ]]; then
+ echo "FAILED: $TEST ($count) build2 run failed"
+ exit 1
+ fi
+ fi
+ fi
+ fi
+ fi
+ fi
+ fi
+ fi
+ let count=count+1
+done
+
+rm -rf $BASE_DIR/work
+
+if [[ $TYPE == control ]]; then
+ touch $THIS_RUN_DIR/.tests.complete
+ compare_fcm1=${compare_fcm1:-true}
+ if [[ $compare_fcm1 == true ]]; then
+ cd $RUN_DIR
+ # Test that fcm2 triggers the same build commands as the equivalent from fcm1
+ if [[ -f $TEST.build.commands.1 ]]; then
+ for FILE in $(ls -1 $TEST.build.commands.*)
+ do
+ FCM1_FILE=fcm1_${FILE#fcm2_}
+ if [[ -f $FCM1_FILE ]]; then
+ if [[ $DEBUG == true ]]; then
+ echo "Comparing $FILE with $FCM1_FILE ..."
+ fi
+ cat $FCM1_FILE | grep -v wrap_ar | sed 's#wrap_ld#wrap_fc#' | sed 's# -c # #' | sed 's#__fcm__##' | sed 's#\.F#\.f#' | \
+ sed 's#/fcm1_[^ ]*/##g' | sed 's#-Iinc #-Iinclude #g' | sed 's# -Llib -Llib# -Llib#' | \
+ sed 's# -Llib -lmkmdblseq##' | sort > $TEST.fcm1.build.commands
+ cat $FILE | grep -v wrap_ar | sed 's# -c # #' | sed 's#-oo/#-o #' | sed 's#-obin/#-o #' | sed 's# o/# #g' | \
+ sed 's#\.F#\.f#' | sed 's#/fcm2_[^ ]*/##g' | sed 's#\.\./\.\.##g' | sed 's#wrap_fc2 \(.*\.exe\)#wrap_fc \1#' | \
+ sort > $TEST.fcm2.build.commands
+ diff $TEST.fcm1.build.commands $TEST.fcm2.build.commands > $TEST.diff
+ if [[ $? != 0 ]]; then
+ echo "FAILED: $FILE - build commands differ from FCM1 case"
+ exit 1
+ else
+ rm $TEST.fcm1.build.commands $TEST.fcm2.build.commands $TEST.diff
+ fi
+ fi
+ done
+ fi
+ # Test that fcm2 run results match equivalent from fcm1
+ if [[ -f $TEST.exe.stdout.1 ]]; then
+ for FILE in $(ls -1 $TEST.exe.stdout.*)
+ do
+ FCM1_FILE=fcm1_${FILE#fcm2_}
+ if [[ -f $FCM1_FILE ]]; then
+ if [[ $DEBUG == true ]]; then
+ echo "Comparing $FILE with $FCM1_FILE ..."
+ fi
+ diff -q $FCM1_FILE $FILE
+ if [[ $? != 0 ]]; then
+ echo "FAILED: $FILE - contents differ from FCM1 case"
+ exit 1
+ fi
+ fi
+ done
+ fi
+ if [[ $COMPARE_TIMES == true ]]; then
+ $MY_BIN/compare_times_fcm1-2 $TEST
+ fi
+ fi
+else
+ cd $BASE_DIR
+ $MY_BIN/compare_results_fcm2 $TEST
+fi
diff --git a/test/report_hpc_results b/test/report_hpc_results
new file mode 100755
index 0000000..4abeca7
--- /dev/null
+++ b/test/report_hpc_results
@@ -0,0 +1,14 @@
+#!/bin/ksh
+
+if [[ -f control/hpc_batch.stderr ]]; then
+ echo "##### Control run stderr ############################################"
+ cat control/hpc_batch.stderr
+ echo "##### Control run stdout ############################################"
+ cat control/hpc_batch.stdout | sed -ne '1,/^SUMMARY/p'
+fi
+if [[ -f test/hpc_batch.stderr ]]; then
+ echo "##### Test run stderr ###############################################"
+ cat test/hpc_batch.stderr
+ echo "##### Test run stdout ###############################################"
+ cat test/hpc_batch.stdout | sed -ne '1,/^SUMMARY/p'
+fi
diff --git a/test/repos/add_subroutine/hello.F90 b/test/repos/add_subroutine/hello.F90
new file mode 100644
index 0000000..9785a76
--- /dev/null
+++ b/test/repos/add_subroutine/hello.F90
@@ -0,0 +1,26 @@
+PROGRAM Hello
+
+#if !defined(LOCAL_STRING)
+USE Hello_Constants, ONLY: hello_string
+#endif
+
+IMPLICIT NONE
+
+#if defined(LOCAL_STRING)
+CHARACTER (LEN=80), PARAMETER :: hello_string = 'Hello Mother Earth!'
+#endif
+
+#if defined(CALL_HELLO_SUB)
+INCLUDE 'hello_sub.interface'
+#endif
+INCLUDE 'hello_sub2.interface'
+
+CHARACTER (LEN=*), PARAMETER :: this = 'Hello'
+
+WRITE (*, '(A)') this // ': ' // TRIM (hello_string)
+#if defined(CALL_HELLO_SUB)
+CALL Hello_Sub (HUGE(0))
+#endif
+CALL Hello_Sub2 ()
+
+END PROGRAM Hello
diff --git a/test/repos/add_subroutine/hello.F90.add_lines b/test/repos/add_subroutine/hello.F90.add_lines
new file mode 100644
index 0000000..672a7ee
--- /dev/null
+++ b/test/repos/add_subroutine/hello.F90.add_lines
@@ -0,0 +1,28 @@
+PROGRAM Hello
+
+#if !defined(LOCAL_STRING)
+USE Hello_Constants, ONLY: hello_string
+#endif
+
+IMPLICIT NONE
+
+#if defined(LOCAL_STRING)
+CHARACTER (LEN=80), PARAMETER :: hello_string = 'Hello Mother Earth!'
+#endif
+
+INTEGER :: integer_arg = 1234
+
+#if defined(CALL_HELLO_SUB)
+INCLUDE 'hello_sub.interface'
+#endif
+
+! Just a comment line
+
+CHARACTER (LEN=*), PARAMETER :: this = 'Hello'
+
+WRITE (*, '(A)') this // ': ' // TRIM (hello_string)
+#if defined(CALL_HELLO_SUB)
+CALL Hello_Sub (integer_arg)
+#endif
+
+END PROGRAM Hello
diff --git a/test/repos/add_subroutine/hello_sub2.f90 b/test/repos/add_subroutine/hello_sub2.f90
new file mode 100644
index 0000000..8ebe19c
--- /dev/null
+++ b/test/repos/add_subroutine/hello_sub2.f90
@@ -0,0 +1,11 @@
+SUBROUTINE Hello_Sub2
+
+USE Hello_Constants, ONLY: hello_string
+
+IMPLICIT NONE
+
+CHARACTER (LEN=*), PARAMETER :: this = 'Hello_Sub2'
+
+WRITE (*, '(A)') this // ': ' // TRIM (hello_string)
+
+END SUBROUTINE Hello_Sub2
diff --git a/test/repos/cyclic_dependency/hello.F90 b/test/repos/cyclic_dependency/hello.F90
new file mode 100644
index 0000000..bcc2e27
--- /dev/null
+++ b/test/repos/cyclic_dependency/hello.F90
@@ -0,0 +1,14 @@
+PROGRAM Hello
+
+USE Hello_Constants, ONLY: hello_string, Hello_Sub_Wrapper
+
+IMPLICIT NONE
+
+INTEGER :: integer_arg = 1234
+
+CHARACTER (LEN=*), PARAMETER :: this = 'Hello'
+
+WRITE (*, '(A)') this // ': ' // TRIM (hello_string)
+CALL Hello_Sub_Wrapper (integer_arg)
+
+END PROGRAM Hello
diff --git a/test/repos/cyclic_dependency/hello_constants.f90.fail b/test/repos/cyclic_dependency/hello_constants.f90.fail
new file mode 100644
index 0000000..830077f
--- /dev/null
+++ b/test/repos/cyclic_dependency/hello_constants.f90.fail
@@ -0,0 +1,19 @@
+MODULE Hello_Constants
+
+INCLUDE 'hello_constants_dummy.inc'
+
+CONTAINS
+
+SUBROUTINE Hello_Sub_Wrapper (integer_arg)
+
+IMPLICIT NONE
+
+INTEGER :: integer_arg
+
+INCLUDE 'hello_sub.interface'
+
+CALL Hello_Sub (integer_arg)
+
+END SUBROUTINE Hello_Sub_Wrapper
+
+END MODULE Hello_Constants
diff --git a/test/repos/cyclic_dependency/hello_constants.f90.ok b/test/repos/cyclic_dependency/hello_constants.f90.ok
new file mode 100644
index 0000000..97e0ba3
--- /dev/null
+++ b/test/repos/cyclic_dependency/hello_constants.f90.ok
@@ -0,0 +1,19 @@
+MODULE Hello_Constants
+
+INCLUDE 'hello_constants_dummy.inc'
+
+CONTAINS
+
+SUBROUTINE Hello_Sub_Wrapper (integer_arg)
+
+IMPLICIT NONE
+
+INTEGER :: integer_arg
+
+INCLUDE 'hello_sub2.interface'
+
+CALL Hello_Sub2 (integer_arg)
+
+END SUBROUTINE Hello_Sub_Wrapper
+
+END MODULE Hello_Constants
diff --git a/test/repos/cyclic_dependency/hello_sub2.f90 b/test/repos/cyclic_dependency/hello_sub2.f90
new file mode 100644
index 0000000..57488c9
--- /dev/null
+++ b/test/repos/cyclic_dependency/hello_sub2.f90
@@ -0,0 +1,11 @@
+SUBROUTINE Hello_Sub2 (integer_arg)
+
+IMPLICIT NONE
+
+INTEGER :: integer_arg
+
+INCLUDE 'hello_sub.interface'
+
+CALL Hello_Sub (integer_arg)
+
+END SUBROUTINE Hello_Sub2
diff --git a/test/repos/trunk/blockdata/hello_blockdata.F90 b/test/repos/trunk/blockdata/hello_blockdata.F90
new file mode 100644
index 0000000..6ececec
--- /dev/null
+++ b/test/repos/trunk/blockdata/hello_blockdata.F90
@@ -0,0 +1,9 @@
+BLOCK DATA hello_blockdata
+INTEGER :: integer_common
+COMMON /general/integer_common
+#if defined(ODD)
+DATA integer_common/1357/
+#else
+DATA integer_common/2468/
+#endif
+END BLOCK DATA hello_blockdata
diff --git a/test/repos/trunk/cfg/fcm1_add_directory.cfg b/test/repos/trunk/cfg/fcm1_add_directory.cfg
new file mode 100644
index 0000000..341410e
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_add_directory.cfg
@@ -0,0 +1,6 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+INC $HERE/fcm1_base.cfg
+
+REPOS::test::branch1 fcm:test_suite_br/dev/Share/add_directory
diff --git a/test/repos/trunk/cfg/fcm1_add_directory_expsrc.cfg b/test/repos/trunk/cfg/fcm1_add_directory_expsrc.cfg
new file mode 100644
index 0000000..784de6b
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_add_directory_expsrc.cfg
@@ -0,0 +1,7 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+INC $HERE/fcm1_base.cfg
+
+REPOS::test::branch1 fcm:test_suite_br/dev/Share/add_directory
+EXPSRC::test::branch1
diff --git a/test/repos/trunk/cfg/fcm1_add_file.cfg b/test/repos/trunk/cfg/fcm1_add_file.cfg
new file mode 100644
index 0000000..e333bf7
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_add_file.cfg
@@ -0,0 +1,6 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+INC $HERE/fcm1_base.cfg
+
+REPOS::test::branch1 fcm:test_suite_br/dev/Share/add_file
diff --git a/test/repos/trunk/cfg/fcm1_add_file_inherit.cfg b/test/repos/trunk/cfg/fcm1_add_file_inherit.cfg
new file mode 100644
index 0000000..f78386b
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_add_file_inherit.cfg
@@ -0,0 +1,8 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+DEST::ROOTDIR $PWD
+
+USE $RUN_DIR/fcm1_base
+
+REPOS::test::branch1 fcm:test_suite_br/dev/Share/add_file
diff --git a/test/repos/trunk/cfg/fcm1_base.cfg b/test/repos/trunk/cfg/fcm1_base.cfg
new file mode 100644
index 0000000..e0e257c
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_base.cfg
@@ -0,0 +1,6 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+INC $HERE/fcm1_base_inc.cfg
+
+BLD::TARGET hello.sh test__namelist.etc
diff --git a/test/repos/trunk/cfg/fcm1_base_inc.cfg b/test/repos/trunk/cfg/fcm1_base_inc.cfg
new file mode 100644
index 0000000..7c2a5d2
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_base_inc.cfg
@@ -0,0 +1,23 @@
+DEST::ROOTDIR $PWD
+
+REPOS::test::base fcm:test_suite_tr
+EXPSRC::test::base
+
+BLD::TOOL::FC wrap_fc
+BLD::TOOL::FFLAGS -assume nosource_include
+
+BLD::TOOL::CC wrap_cc
+BLD::TOOL::CFLAGS -O3
+
+BLD::TOOL::FFLAGS::test/subroutine %BLD::TOOL::FFLAGS -O3
+
+BLD::TOOL::LD wrap_ld
+BLD::TOOL::AR wrap_ar
+
+BLD::TOOL::FPP wrap_pp
+BLD::TOOL::FPPKEYS::test/subroutine/hello_sub HELLO_SUB
+BLD::TOOL::FPPKEYS::test/program/hello CALL_HELLO_SUB
+BLD::PP::test/subroutine/hello_sub true
+BLD::PP::test/program true
+
+BLD::BLOCKDATA hello_blockdata
diff --git a/test/repos/trunk/cfg/fcm1_branches_clash.cfg b/test/repos/trunk/cfg/fcm1_branches_clash.cfg
new file mode 100644
index 0000000..a25f37b
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_branches_clash.cfg
@@ -0,0 +1,7 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+INC $HERE/fcm1_base.cfg
+
+REPOS::test::branch1 fcm:test_suite_br/dev/Share/modify_files_base
+REPOS::test::branch2 fcm:test_suite_br/dev/Share/modify_files_clash
diff --git a/test/repos/trunk/cfg/fcm1_branches_merge.cfg b/test/repos/trunk/cfg/fcm1_branches_merge.cfg
new file mode 100644
index 0000000..3244dc1
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_branches_merge.cfg
@@ -0,0 +1,8 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+INC $HERE/fcm1_base.cfg
+
+REPOS::test::branch1 fcm:test_suite_br/dev/Share/modify_files_base
+REPOS::test::branch2 fcm:test_suite_br/dev/Share/modify_files_merge1
+REPOS::test::branch3 fcm:test_suite_br/dev/Share/modify_files_merge2
diff --git a/test/repos/trunk/cfg/fcm1_branches_merge_conflict_fail.cfg b/test/repos/trunk/cfg/fcm1_branches_merge_conflict_fail.cfg
new file mode 100644
index 0000000..fe0f200
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_branches_merge_conflict_fail.cfg
@@ -0,0 +1,10 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+INC $HERE/fcm1_base.cfg
+
+REPOS::test::branch1 fcm:test_suite_br/dev/Share/modify_files_base
+REPOS::test::branch2 fcm:test_suite_br/dev/Share/modify_files_merge1
+REPOS::test::branch3 fcm:test_suite_br/dev/Share/modify_files_merge2
+
+CONFLICT fail
diff --git a/test/repos/trunk/cfg/fcm1_branches_merge_conflict_override.cfg b/test/repos/trunk/cfg/fcm1_branches_merge_conflict_override.cfg
new file mode 100644
index 0000000..0f12d73
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_branches_merge_conflict_override.cfg
@@ -0,0 +1,10 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+INC $HERE/fcm1_base.cfg
+
+REPOS::test::branch1 fcm:test_suite_br/dev/Share/modify_files_base
+REPOS::test::branch2 fcm:test_suite_br/dev/Share/modify_files_merge1
+REPOS::test::branch3 fcm:test_suite_br/dev/Share/modify_files_merge2
+
+CONFLICT override
diff --git a/test/repos/trunk/cfg/fcm1_branches_merge_inherit.cfg b/test/repos/trunk/cfg/fcm1_branches_merge_inherit.cfg
new file mode 100644
index 0000000..dd53278
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_branches_merge_inherit.cfg
@@ -0,0 +1,10 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+DEST::ROOTDIR $PWD
+
+USE $RUN_DIR/fcm1_base
+
+REPOS::test::branch1 fcm:test_suite_br/dev/Share/modify_files_base
+REPOS::test::branch2 fcm:test_suite_br/dev/Share/modify_files_merge1
+REPOS::test::branch3 fcm:test_suite_br/dev/Share/modify_files_merge2
diff --git a/test/repos/trunk/cfg/fcm1_branches_merge_inherit_wrong_include.cfg b/test/repos/trunk/cfg/fcm1_branches_merge_inherit_wrong_include.cfg
new file mode 100644
index 0000000..a9754c6
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_branches_merge_inherit_wrong_include.cfg
@@ -0,0 +1,12 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+DEST::ROOTDIR $PWD
+
+USE $RUN_DIR/fcm1_base
+
+REPOS::test::branch1 fcm:test_suite_br/dev/Share/modify_files_base
+REPOS::test::branch2 fcm:test_suite_br/dev/Share/modify_files_merge1
+REPOS::test::branch3 fcm:test_suite_br/dev/Share/modify_files_merge2
+
+BLD::TOOL::FFLAGS::test/module -O3
diff --git a/test/repos/trunk/cfg/fcm1_branches_merge_wcopies.cfg b/test/repos/trunk/cfg/fcm1_branches_merge_wcopies.cfg
new file mode 100644
index 0000000..85c9cdb
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_branches_merge_wcopies.cfg
@@ -0,0 +1,8 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+INC $HERE/fcm1_base.cfg
+
+REPOS::test::branch1 $BASE_DIR/work/b1
+REPOS::test::branch2 $BASE_DIR/work/b2
+REPOS::test::branch3 $BASE_DIR/work/b3
diff --git a/test/repos/trunk/cfg/fcm1_branches_merge_wcopy.cfg b/test/repos/trunk/cfg/fcm1_branches_merge_wcopy.cfg
new file mode 100644
index 0000000..e632761
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_branches_merge_wcopy.cfg
@@ -0,0 +1,8 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+INC $HERE/fcm1_base.cfg
+
+REPOS::test::branch1 fcm:test_suite_br/dev/Share/modify_files_base
+REPOS::test::branch2 fcm:test_suite_br/dev/Share/modify_files_merge1
+REPOS::test::branch3 $BASE_DIR/work
diff --git a/test/repos/trunk/cfg/fcm1_cflags.cfg b/test/repos/trunk/cfg/fcm1_cflags.cfg
new file mode 100644
index 0000000..606f6d2
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_cflags.cfg
@@ -0,0 +1,6 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+INC $HERE/fcm1_base.cfg
+
+BLD::TOOL::CFLAGS::test/subroutine -O2 -g
diff --git a/test/repos/trunk/cfg/fcm1_change_src_type.cfg b/test/repos/trunk/cfg/fcm1_change_src_type.cfg
new file mode 100644
index 0000000..d71ac16
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_change_src_type.cfg
@@ -0,0 +1,7 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+INC $HERE/fcm1_base.cfg
+
+REPOS::test::branch1 fcm:test_suite_br/dev/Share/change_src_type
+BLD::SRC_TYPE::test/module/hello_constants_inc.f90 FORTRAN::FORTRAN9X::INCLUDE
diff --git a/test/repos/trunk/cfg/fcm1_delete_directory.cfg b/test/repos/trunk/cfg/fcm1_delete_directory.cfg
new file mode 100644
index 0000000..0d33add
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_delete_directory.cfg
@@ -0,0 +1,7 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+INC $HERE/fcm1_base.cfg
+
+REPOS::test::branch1 fcm:test_suite_br/dev/Share/modify_files_base
+REPOS::test::branch2 fcm:test_suite_br/dev/Share/delete_directory
diff --git a/test/repos/trunk/cfg/fcm1_delete_directory_inherit.cfg b/test/repos/trunk/cfg/fcm1_delete_directory_inherit.cfg
new file mode 100644
index 0000000..73cba1a
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_delete_directory_inherit.cfg
@@ -0,0 +1,9 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+DEST::ROOTDIR $PWD
+
+USE $RUN_DIR/fcm1_base
+
+REPOS::test::branch1 fcm:test_suite_br/dev/Share/modify_files_base
+REPOS::test::branch2 fcm:test_suite_br/dev/Share/delete_directory
diff --git a/test/repos/trunk/cfg/fcm1_delete_file.cfg b/test/repos/trunk/cfg/fcm1_delete_file.cfg
new file mode 100644
index 0000000..a67d987
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_delete_file.cfg
@@ -0,0 +1,8 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+INC $HERE/fcm1_base.cfg
+
+REPOS::test::branch1 fcm:test_suite_br/dev/Share/modify_files_base
+REPOS::test::branch2 fcm:test_suite_br/dev/Share/modify_files_merge1
+REPOS::test::branch3 fcm:test_suite_br/dev/Share/delete_file
diff --git a/test/repos/trunk/cfg/fcm1_delete_file_inherit.cfg b/test/repos/trunk/cfg/fcm1_delete_file_inherit.cfg
new file mode 100644
index 0000000..c48ceb1
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_delete_file_inherit.cfg
@@ -0,0 +1,10 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+DEST::ROOTDIR $PWD
+
+USE $RUN_DIR/fcm1_base
+
+REPOS::test::branch1 fcm:test_suite_br/dev/Share/modify_files_base
+REPOS::test::branch2 fcm:test_suite_br/dev/Share/modify_files_merge1
+REPOS::test::branch3 fcm:test_suite_br/dev/Share/delete_file
diff --git a/test/repos/trunk/cfg/fcm1_delete_inc_file.cfg b/test/repos/trunk/cfg/fcm1_delete_inc_file.cfg
new file mode 100644
index 0000000..8639845
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_delete_inc_file.cfg
@@ -0,0 +1,6 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+INC $HERE/fcm1_base.cfg
+
+REPOS::test::branch1 fcm:test_suite_br/dev/Share/delete_inc_file
diff --git a/test/repos/trunk/cfg/fcm1_delete_inc_file_inherit.cfg b/test/repos/trunk/cfg/fcm1_delete_inc_file_inherit.cfg
new file mode 100644
index 0000000..76b52df
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_delete_inc_file_inherit.cfg
@@ -0,0 +1,8 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+DEST::ROOTDIR $PWD
+
+USE $RUN_DIR/fcm1_base
+
+REPOS::test::branch1 fcm:test_suite_br/dev/Share/delete_inc_file
diff --git a/test/repos/trunk/cfg/fcm1_delete_inc_file_inherit_force.cfg b/test/repos/trunk/cfg/fcm1_delete_inc_file_inherit_force.cfg
new file mode 100644
index 0000000..6ff2a3b
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_delete_inc_file_inherit_force.cfg
@@ -0,0 +1,10 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+DEST::ROOTDIR $PWD
+
+USE $RUN_DIR/fcm1_base
+
+REPOS::test::branch1 fcm:test_suite_br/dev/Share/delete_inc_file
+
+BLD::TOOL::FFLAGS::test/module -assume nosource_include -O3
diff --git a/test/repos/trunk/cfg/fcm1_delete_pp_file.cfg b/test/repos/trunk/cfg/fcm1_delete_pp_file.cfg
new file mode 100644
index 0000000..711b899
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_delete_pp_file.cfg
@@ -0,0 +1,8 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+INC $HERE/fcm1_base.cfg
+
+REPOS::test::branch1 fcm:test_suite_br/dev/Share/modify_files_base
+REPOS::test::branch2 fcm:test_suite_br/dev/Share/modify_pp_include
+REPOS::test::branch3 fcm:test_suite_br/dev/Share/delete_pp_file
diff --git a/test/repos/trunk/cfg/fcm1_delete_pp_file_inherit.cfg b/test/repos/trunk/cfg/fcm1_delete_pp_file_inherit.cfg
new file mode 100644
index 0000000..c5ebfdb
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_delete_pp_file_inherit.cfg
@@ -0,0 +1,10 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+DEST::ROOTDIR $PWD
+
+USE $RUN_DIR/fcm1_base
+
+REPOS::test::branch1 fcm:test_suite_br/dev/Share/modify_files_base
+REPOS::test::branch2 fcm:test_suite_br/dev/Share/modify_pp_include
+REPOS::test::branch3 fcm:test_suite_br/dev/Share/delete_pp_file
diff --git a/test/repos/trunk/cfg/fcm1_delete_ppinc_file.cfg b/test/repos/trunk/cfg/fcm1_delete_ppinc_file.cfg
new file mode 100644
index 0000000..ddeb1db
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_delete_ppinc_file.cfg
@@ -0,0 +1,6 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+INC $HERE/fcm1_base.cfg
+
+REPOS::test::branch1 fcm:test_suite_br/dev/Share/delete_ppinc_file
diff --git a/test/repos/trunk/cfg/fcm1_delete_ppinc_file_inherit.cfg b/test/repos/trunk/cfg/fcm1_delete_ppinc_file_inherit.cfg
new file mode 100644
index 0000000..85762e2
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_delete_ppinc_file_inherit.cfg
@@ -0,0 +1,8 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+DEST::ROOTDIR $PWD
+
+USE $RUN_DIR/fcm1_base
+
+REPOS::test::branch1 fcm:test_suite_br/dev/Share/delete_ppinc_file
diff --git a/test/repos/trunk/cfg/fcm1_delete_ppinc_file_inherit_force.cfg b/test/repos/trunk/cfg/fcm1_delete_ppinc_file_inherit_force.cfg
new file mode 100644
index 0000000..7e64e7a
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_delete_ppinc_file_inherit_force.cfg
@@ -0,0 +1,9 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+DEST::ROOTDIR $PWD
+
+USE $RUN_DIR/fcm1_base
+
+REPOS::test::branch1 fcm:test_suite_br/dev/Share/delete_ppinc_file
+BLD::TOOL::FPPKEYS::test/subroutine/hello_sub HELLO_SUB DUMMY
diff --git a/test/repos/trunk/cfg/fcm1_duplicate_target.cfg b/test/repos/trunk/cfg/fcm1_duplicate_target.cfg
new file mode 100644
index 0000000..fc5c04b
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_duplicate_target.cfg
@@ -0,0 +1,7 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+INC $HERE/fcm1_base.cfg
+
+REPOS::test::branch1 fcm:test_suite_br/dev/Share/add_duplicate
+BLD::TOOL::FPPKEYS::test/subroutine/hello_sub2 HELLO_SUB
diff --git a/test/repos/trunk/cfg/fcm1_exclude_dependency.cfg b/test/repos/trunk/cfg/fcm1_exclude_dependency.cfg
new file mode 100644
index 0000000..5467cbd
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_exclude_dependency.cfg
@@ -0,0 +1,6 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+INC $HERE/fcm1_base.cfg
+
+BLD::EXCL_DEP USE::Hello_Constants
diff --git a/test/repos/trunk/cfg/fcm1_exe_permissions.cfg b/test/repos/trunk/cfg/fcm1_exe_permissions.cfg
new file mode 100644
index 0000000..6bc11cd
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_exe_permissions.cfg
@@ -0,0 +1,7 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+INC $HERE/fcm1_base.cfg
+
+REPOS::test::branch1 $BASE_DIR/work
+BLD::EXE_NAME::hello hello_world.exe
diff --git a/test/repos/trunk/cfg/fcm1_exe_rename.cfg b/test/repos/trunk/cfg/fcm1_exe_rename.cfg
new file mode 100644
index 0000000..59e7b55
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_exe_rename.cfg
@@ -0,0 +1,7 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+INC $HERE/fcm1_base.cfg
+
+REPOS::test::branch1 fcm:test_suite_br/dev/Share/exe_rename
+BLD::EXE_NAME::hello hello_world.exe
diff --git a/test/repos/trunk/cfg/fcm1_fc.cfg b/test/repos/trunk/cfg/fcm1_fc.cfg
new file mode 100644
index 0000000..c5fc923
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_fc.cfg
@@ -0,0 +1,6 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+INC $HERE/fcm1_base.cfg
+
+BLD::TOOL::FC wrap_fc2
diff --git a/test/repos/trunk/cfg/fcm1_fflags1.cfg b/test/repos/trunk/cfg/fcm1_fflags1.cfg
new file mode 100644
index 0000000..2964591
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_fflags1.cfg
@@ -0,0 +1,6 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+INC $HERE/fcm1_base.cfg
+
+BLD::TOOL::FFLAGS::test %BLD::TOOL::FFLAGS -O2 -g
diff --git a/test/repos/trunk/cfg/fcm1_fflags2.cfg b/test/repos/trunk/cfg/fcm1_fflags2.cfg
new file mode 100644
index 0000000..c6861b7
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_fflags2.cfg
@@ -0,0 +1,6 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+INC $HERE/fcm1_base.cfg
+
+BLD::TOOL::FFLAGS::test/subroutine/hello_sub -O2 -g
diff --git a/test/repos/trunk/cfg/fcm1_fflags_inherit.cfg b/test/repos/trunk/cfg/fcm1_fflags_inherit.cfg
new file mode 100644
index 0000000..ecc5adf
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_fflags_inherit.cfg
@@ -0,0 +1,8 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+DEST::ROOTDIR $PWD
+
+USE $RUN_DIR/fcm1_base
+
+BLD::TOOL::FFLAGS::test/subroutine/hello_sub -O2 -g
diff --git a/test/repos/trunk/cfg/fcm1_inc_devnull.cfg b/test/repos/trunk/cfg/fcm1_inc_devnull.cfg
new file mode 100644
index 0000000..4744078
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_inc_devnull.cfg
@@ -0,0 +1,9 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+INC $HERE/fcm1_base_inc.cfg
+
+# The UM makes use of the following so we have to support it
+INC /dev/null
+
+BLD::TARGET hello.sh test__namelist.etc
diff --git a/test/repos/trunk/cfg/fcm1_inherit_invalid_path.cfg b/test/repos/trunk/cfg/fcm1_inherit_invalid_path.cfg
new file mode 100644
index 0000000..30ed635
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_inherit_invalid_path.cfg
@@ -0,0 +1,6 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+DEST::ROOTDIR $PWD
+
+USE /invalid/path
diff --git a/test/repos/trunk/cfg/fcm1_inherit_target.cfg b/test/repos/trunk/cfg/fcm1_inherit_target.cfg
new file mode 100644
index 0000000..dd3ffb3
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_inherit_target.cfg
@@ -0,0 +1,8 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+DEST::ROOTDIR $PWD
+
+USE $RUN_DIR/fcm1_base
+
+BLD::INHERIT::TARGET true
diff --git a/test/repos/trunk/cfg/fcm1_invalid_base_url.cfg b/test/repos/trunk/cfg/fcm1_invalid_base_url.cfg
new file mode 100644
index 0000000..087dd0e
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_invalid_base_url.cfg
@@ -0,0 +1,6 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+INC $HERE/fcm1_base.cfg
+
+REPOS::test::base fcm:test_suite/invalid
diff --git a/test/repos/trunk/cfg/fcm1_invalid_branch_url.cfg b/test/repos/trunk/cfg/fcm1_invalid_branch_url.cfg
new file mode 100644
index 0000000..a200f0b
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_invalid_branch_url.cfg
@@ -0,0 +1,6 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+INC $HERE/fcm1_base.cfg
+
+REPOS::test::branch1 fcm:test_suite/invalid
diff --git a/test/repos/trunk/cfg/fcm1_invalid_inc.cfg b/test/repos/trunk/cfg/fcm1_invalid_inc.cfg
new file mode 100644
index 0000000..178bfcd
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_invalid_inc.cfg
@@ -0,0 +1,4 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+INC $HERE/invalid.cfg
diff --git a/test/repos/trunk/cfg/fcm1_invalid_namespace.cfg b/test/repos/trunk/cfg/fcm1_invalid_namespace.cfg
new file mode 100644
index 0000000..8c6461a
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_invalid_namespace.cfg
@@ -0,0 +1,6 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+INC $HERE/fcm1_base.cfg
+
+BLD::TOOL::FFLAGS::test/invalid -O2
diff --git a/test/repos/trunk/cfg/fcm1_invalid_variable.cfg b/test/repos/trunk/cfg/fcm1_invalid_variable.cfg
new file mode 100644
index 0000000..c26ea3c
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_invalid_variable.cfg
@@ -0,0 +1,6 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+INC $HERE/fcm1_base.cfg
+
+BLD::TOOL::FFLAGS::test/subroutine/hello_sub $INVALID
diff --git a/test/repos/trunk/cfg/fcm1_ld.cfg b/test/repos/trunk/cfg/fcm1_ld.cfg
new file mode 100644
index 0000000..20baa28
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_ld.cfg
@@ -0,0 +1,6 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+INC $HERE/fcm1_base.cfg
+
+BLD::TOOL::LD wrap_ld2
diff --git a/test/repos/trunk/cfg/fcm1_library.cfg b/test/repos/trunk/cfg/fcm1_library.cfg
new file mode 100644
index 0000000..4c8d152
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_library.cfg
@@ -0,0 +1,6 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+INC $HERE/fcm1_base_inc.cfg
+
+BLD::TARGET libtest.a
diff --git a/test/repos/trunk/cfg/fcm1_library_rename.cfg b/test/repos/trunk/cfg/fcm1_library_rename.cfg
new file mode 100644
index 0000000..849f62e
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_library_rename.cfg
@@ -0,0 +1,7 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+INC $HERE/fcm1_base_inc.cfg
+
+BLD::LIB::test/module module
+BLD::TARGET libmodule.a
diff --git a/test/repos/trunk/cfg/fcm1_mirror.cfg b/test/repos/trunk/cfg/fcm1_mirror.cfg
new file mode 100644
index 0000000..0274742
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_mirror.cfg
@@ -0,0 +1,8 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+INC $HERE/fcm1_base.cfg
+
+RDEST::MACHINE localhost
+RDEST::REMOTE_SHELL ssh
+RDEST ${THIS_RUN_DIR}_mirror
diff --git a/test/repos/trunk/cfg/fcm1_mirror_inherit.cfg b/test/repos/trunk/cfg/fcm1_mirror_inherit.cfg
new file mode 100644
index 0000000..61d329a
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_mirror_inherit.cfg
@@ -0,0 +1,12 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+DEST::ROOTDIR $PWD
+
+USE $RUN_DIR/fcm1_mirror
+
+REPOS::test::branch1 fcm:test_suite_br/dev/Share/modify_files_base
+REPOS::test::branch2 fcm:test_suite_br/dev/Share/modify_files_merge1
+REPOS::test::branch3 fcm:test_suite_br/dev/Share/modify_files_merge2
+
+RDEST ${THIS_RUN_DIR}_mirror
diff --git a/test/repos/trunk/cfg/fcm1_modify_subroutine_inherit.cfg b/test/repos/trunk/cfg/fcm1_modify_subroutine_inherit.cfg
new file mode 100644
index 0000000..2526b27
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_modify_subroutine_inherit.cfg
@@ -0,0 +1,8 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+DEST::ROOTDIR $PWD
+
+USE $RUN_DIR/fcm1_base
+
+REPOS::test::branch1 fcm:test_suite_br/dev/Share/modify_subroutine
diff --git a/test/repos/trunk/cfg/fcm1_modify_subroutine_interface_inherit.cfg b/test/repos/trunk/cfg/fcm1_modify_subroutine_interface_inherit.cfg
new file mode 100644
index 0000000..517bc9e
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_modify_subroutine_interface_inherit.cfg
@@ -0,0 +1,8 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+DEST::ROOTDIR $PWD
+
+USE $RUN_DIR/fcm1_base
+
+REPOS::test::branch1 fcm:test_suite_br/dev/Share/modify_subroutine_interface
diff --git a/test/repos/trunk/cfg/fcm1_multi_inherit.cfg b/test/repos/trunk/cfg/fcm1_multi_inherit.cfg
new file mode 100644
index 0000000..d0bfb81
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_multi_inherit.cfg
@@ -0,0 +1,9 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+DEST::ROOTDIR $PWD
+
+USE $RUN_DIR/fcm1_fflags_inherit
+USE $RUN_DIR/fcm1_modify_subroutine_inherit
+
+BLD::TOOL::FFLAGS::test/module -O2 -g
diff --git a/test/repos/trunk/cfg/fcm1_no_dep.cfg b/test/repos/trunk/cfg/fcm1_no_dep.cfg
new file mode 100644
index 0000000..2ea4f3e
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_no_dep.cfg
@@ -0,0 +1,6 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+INC $HERE/fcm1_base.cfg
+
+BLD::NO_DEP true
diff --git a/test/repos/trunk/cfg/fcm1_ops.cfg b/test/repos/trunk/cfg/fcm1_ops.cfg
new file mode 100644
index 0000000..74bc1e5
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_ops.cfg
@@ -0,0 +1,211 @@
+# ------------------------------------------------------------------------------
+# File header
+# ------------------------------------------------------------------------------
+
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+# ------------------------------------------------------------------------------
+# Destination
+# ------------------------------------------------------------------------------
+
+DEST $PWD
+
+# ------------------------------------------------------------------------------
+# Build declarations
+# ------------------------------------------------------------------------------
+
+bld::excl_dep INC::mpif.h
+bld::excl_dep USE::F90_UNIX_IO
+bld::excl_dep USE::netcdf
+bld::excl_dep USE::YOMLUN
+bld::excl_dep USE::XLFUTILITY
+bld::exe_dep::OpsProg_BackErrCreate.exe gcom
+bld::exe_dep::OpsProg_ExtractAndProcess.exe ops_admin::src::code::MetDB_BUFR_RETRIEVAL::source ops_admin::src::code::MetDB_Bufr ops_admin::src::code::lapack ops_admin::src::code::blas gcom
+bld::exe_dep::OpsProg_MOPS.exe ops_admin::src::code::MetDB_BUFR_RETRIEVAL::source ops_admin::src::code::MetDB_Bufr gcom
+bld::exe_dep::OpsProg_SatRad_BiasCheck.exe gcom::build::gc
+bld::pp::gcom 1
+bld::pp::ops::src::code::OpsMod_Altimeter::Ops_AltWriteNetCDF 1
+bld::pp::ops::src::code::OpsMod_OceanSound::Ops_OcnWriteNetCDF 1
+bld::pp::ops::src::code::OpsMod_Radar::Ops_WriteBackGrdOutput 1
+bld::pp::ops::src::code::OpsMod_Radar::Ops_WriteQCOutput 1
+bld::pp::ops::src::code::OpsMod_Radar::Ops_WriteSoOutput 1
+bld::pp::ops::src::code::OpsMod_RadarZ::Ops_ReadRadarRefl 1
+bld::pp::ops::src::code::OpsMod_RadarZ::Ops_WriteRadarRTM 1
+bld::pp::ops::src::code::OpsMod_RadarZ::Ops_WriteRadarRefl 1
+bld::pp::ops::src::code::OpsMod_SeaIce::Ops_SeaIceWriteNetCDF 1
+bld::pp::ops::src::code::OpsMod_SurfaceSST::Ops_SSTWriteNetCDF 1
+bld::pp::ops::src::code::OpsProg_ExtractAndProcess::Ops_ObStructureCreate 1
+bld::pp::ops::src::code::OpsProg_ExtractAndProcess::Ops_SetupControlInfo 1
+bld::pp::ops::src::code::Ops_RTTOV9::rttov9_parallel_ad 1
+bld::pp::ops::src::code::Ops_RTTOV9::rttov9_parallel_direct 1
+bld::pp::ops::src::code::Ops_RTTOV9::rttov9_parallel_k 1
+bld::pp::ops::src::code::Ops_RTTOV9::rttov9_parallel_tl 1
+bld::target OpsScr_Build
+bld::tool::cc wrap_cc
+bld::tool::cflags
+bld::tool::cppkeys::gen::src::code::GenMod_Platform UNDERSCORE LOWERCASE C_LONG_LONG_INT FRL8
+bld::tool::cppkeys::gen::src::code::UM_Platform VAROPSVER C_LOW_U LINUX LITTLE_END C_LONG_LONG_INT FRL8
+bld::tool::cppkeys::ops::src::code::MetDB_ClientServer hpux DEBUG LL64 UNDERSCORE
+bld::tool::fc wrap_fc
+bld::tool::fflags -implicitnone -stand f95 -warn all -warn nointerfaces -i8 -r8 -i-static
+bld::tool::fflags::gcom -implicitnone -stand f95 -warn all -i8 -r8 -i-static -warn none -I$(OPSDIR)/mpi/mpich2-1.4-ukmo-v1/ifort-12/include
+bld::tool::fflags::ops::src::code::Ops_RTTOV9 -implicitnone -stand f95 -warn all -warn nointerfaces -i-static -O3
+bld::tool::fppkeys::gcom GC_VERSION="'3.4'" GC_DESCRIP="'MPP'" GC_BUILD_DATE="'17285'" MPI_SRC MPILIB_32B PREC_64B GC__FORTERRUNIT=0 GC__FLUSHUNIT6 MPI_BSEND_BUFFER_SIZE=2560000
+bld::tool::fppkeys::gen::src::code::GenMod_Utilities::Gen_FlushUnit USE_FLUSH
+bld::tool::fppkeys::gen::src::code::UM_COEX VAROPSVER
+bld::tool::fppkeys::gen::src::code::UM_Platform VAROPSVER
+bld::tool::fppkeys::ops::src::code::MetDB_GRIB SX6
+bld::tool::fppkeys::ops::src::code::OpsMod_Extract LITTLE_END
+bld::tool::fppkeys::ops::src::code::Ops_RTTOV9 _RTTOV_TSTRAD_TEMP RTTOV_ARCH_VECTOR
+bld::tool::fppkeys::ops::src::code::Ops_RTTOV9::rttov9_parallel_ad _RTTOV_TSTRAD_TEMP RTTOV_ARCH_VECTOR _RTTOV_PARALLEL_AD
+bld::tool::fppkeys::ops::src::code::Ops_RTTOV9::rttov9_parallel_direct _RTTOV_TSTRAD_TEMP RTTOV_ARCH_VECTOR _RTTOV_PARALLEL_DIRECT
+bld::tool::fppkeys::ops::src::code::Ops_RTTOV9::rttov9_parallel_k _RTTOV_TSTRAD_TEMP RTTOV_ARCH_VECTOR _RTTOV_PARALLEL_K
+bld::tool::fppkeys::ops::src::code::Ops_RTTOV9::rttov9_parallel_tl _RTTOV_TSTRAD_TEMP RTTOV_ARCH_VECTOR _RTTOV_PARALLEL_TL
+bld::tool::fppkeys::ops_admin::src::code::MetDB_Bufr BPATH
+bld::tool::ld wrap_ld
+bld::tool::ldflags -i-static -L$(OPSDIR)/mpi/mpich2-1.4-ukmo-v1/ifort-12/lib -lmpich -lmpl -lpthread
+bld::tool::make gmake
+bld::tool::ar wrap_ar
+bld::tool::fpp wrap_pp
+
+# ------------------------------------------------------------------------------
+# Project and branches
+# ------------------------------------------------------------------------------
+
+REPOS::ops::base svn://fcm4/OPS_svn/OPS/trunk
+REVISION::ops::base 19069
+SRC::ops::base src/code/MetDB_ClientServer
+SRC::ops::base src/code/MetDB_GRIB
+SRC::ops::base src/code/MetDB_GRIB2
+SRC::ops::base src/code/OpsMod_Aircraft
+SRC::ops::base src/code/OpsMod_Altimeter
+SRC::ops::base src/code/OpsMod_AssocData
+SRC::ops::base src/code/OpsMod_BackErrCreate
+SRC::ops::base src/code/OpsMod_BiasCorrect
+SRC::ops::base src/code/OpsMod_Bogus
+SRC::ops::base src/code/OpsMod_CXGenerate
+SRC::ops::base src/code/OpsMod_CXInfo
+SRC::ops::base src/code/OpsMod_Constants
+SRC::ops::base src/code/OpsMod_Control
+SRC::ops::base src/code/OpsMod_Extract
+SRC::ops::base src/code/OpsMod_GPSRO
+SRC::ops::base src/code/OpsMod_GeoCloud
+SRC::ops::base src/code/OpsMod_GeoIR
+SRC::ops::base src/code/OpsMod_GroundGPS
+SRC::ops::base src/code/OpsMod_HorizontalInterp
+SRC::ops::base src/code/OpsMod_Index
+SRC::ops::base src/code/OpsMod_Listing
+SRC::ops::base src/code/OpsMod_MFSST
+SRC::ops::base src/code/OpsMod_MOPS
+SRC::ops::base src/code/OpsMod_ModelColumnIO
+SRC::ops::base src/code/OpsMod_ModelIO
+SRC::ops::base src/code/OpsMod_ModelObInfo
+SRC::ops::base src/code/OpsMod_Monitor
+SRC::ops::base src/code/OpsMod_NSST100
+SRC::ops::base src/code/OpsMod_NetCDF
+SRC::ops::base src/code/OpsMod_ObsIO
+SRC::ops::base src/code/OpsMod_ObsInfo
+SRC::ops::base src/code/OpsMod_OceanSound
+SRC::ops::base src/code/OpsMod_Process
+SRC::ops::base src/code/OpsMod_QC
+SRC::ops::base src/code/OpsMod_RTTOV
+SRC::ops::base src/code/OpsMod_Radar
+SRC::ops::base src/code/OpsMod_RadarZ
+SRC::ops::base src/code/OpsMod_SBUV
+SRC::ops::base src/code/OpsMod_SSMI
+SRC::ops::base src/code/OpsMod_SatSST
+SRC::ops::base src/code/OpsMod_SatSound
+SRC::ops::base src/code/OpsMod_Satwind
+SRC::ops::base src/code/OpsMod_Scatwind
+SRC::ops::base src/code/OpsMod_SeaIce
+SRC::ops::base src/code/OpsMod_Sonde
+SRC::ops::base src/code/OpsMod_Sort
+SRC::ops::base src/code/OpsMod_StationList
+SRC::ops::base src/code/OpsMod_Stats
+SRC::ops::base src/code/OpsMod_Surface
+SRC::ops::base src/code/OpsMod_SurfaceSST
+SRC::ops::base src/code/OpsMod_TCBogus
+SRC::ops::base src/code/OpsMod_TURBO
+SRC::ops::base src/code/OpsMod_TropicalRain
+SRC::ops::base src/code/OpsMod_Utilities
+SRC::ops::base src/code/OpsMod_Varobs
+SRC::ops::base src/code/OpsMod_VerticalInterp
+SRC::ops::base src/code/OpsMod_VisControl
+SRC::ops::base src/code/OpsProg_BackErrCreate
+SRC::ops::base src/code/OpsProg_ExtractAndProcess
+SRC::ops::base src/code/OpsProg_KillRPC
+SRC::ops::base src/code/OpsProg_MOPS
+SRC::ops::base src/code/OpsProg_RTTOV9
+SRC::ops::base src/code/Ops_AIRS_1DVar
+SRC::ops::base src/code/Ops_AIRS_Utilities
+SRC::ops::base src/code/Ops_GPSRO_Info
+SRC::ops::base src/code/Ops_GPSRO_Process
+SRC::ops::base src/code/Ops_RTTOV7
+SRC::ops::base src/code/Ops_RTTOV7_RTTOVCLD
+SRC::ops::base src/code/Ops_RTTOV9
+SRC::ops::base src/code/Ops_SSMI_1DVar
+SRC::ops::base src/code/Ops_SatRad_Info
+SRC::ops::base src/code/Ops_SatRad_Process
+SRC::ops::base src/code/Ops_SatRad_SetUp
+SRC::ops::base src/code/Ops_SatRad_Stats
+SRC::ops::base src/code/Ops_SatRad_Utilities
+SRC::ops::base src/scripts/Ops_SatRad_Scripts
+SRC::ops::base src/scripts/Ops_Scripts
+
+REPOS::gen::base svn://fcm1/GEN_svn/GEN/trunk
+REVISION::gen::base 3194
+SRC::gen::base src/code/GenMod_Constants
+SRC::gen::base src/code/GenMod_Control
+SRC::gen::base src/code/GenMod_FortranIO
+SRC::gen::base src/code/GenMod_GetEnv
+SRC::gen::base src/code/GenMod_ModelIO
+SRC::gen::base src/code/GenMod_Platform
+SRC::gen::base src/code/GenMod_Reporting
+SRC::gen::base src/code/GenMod_Trace
+SRC::gen::base src/code/GenMod_UMConstants
+SRC::gen::base src/code/GenMod_Utilities
+SRC::gen::base src/code/Reconfiguration
+SRC::gen::base src/code/UM_COEX
+SRC::gen::base src/code/UM_General
+SRC::gen::base src/code/UM_Platform
+
+REPOS::ops_admin::base svn://fcm4/OPS_svn/Admin/trunk
+REVISION::ops_admin::base 19069
+SRC::ops_admin::base src/code/MetDB_BUFR_RETRIEVAL/apps/create_mdblseq
+SRC::ops_admin::base src/code/MetDB_BUFR_RETRIEVAL/source
+SRC::ops_admin::base src/code/MetDB_Bufr
+SRC::ops_admin::base src/code/blas
+SRC::ops_admin::base src/code/lapack
+
+REPOS::gcom::base svn://fcm2/UM_svn/GCOM/trunk
+REVISION::gcom::base 17285
+SRC::gcom::base build/gc
+SRC::gcom::base build/gcg
+SRC::gcom::base build/include
+SRC::gcom::base build/mpl
+
+REPOS::um_admin::base svn://fcm2/UM_svn/Admin/trunk
+REVISION::um_admin::base 11210
+SRC::um_admin::base utilities/IBM_signal_hander
+
+# ------------------------------------------------------------------------------
+# Extra stuff to build ENS code
+# ------------------------------------------------------------------------------
+
+repos::ens::base fcm:ens_tr/forecast/code
+REVISION::ens::base 1460
+src::ens::base EnsProg_ETKF
+src::ens::base EnsMod_Header
+src::ens::base EnsMod_Obstore
+src::ens::base EnsMod_Utilities
+src::ens::base EnsMod_Varobs
+src::ens::base EnsProg_TrimObstore
+
+bld::tool::fppkeys::ens IBM
+bld::tool::cppkeys::ens LOWERCASE
+bld::tool::ldflags::ens -i-static -L$(OPSDIR)/mpi/mpich2-1.4-ukmo-v1/ifort-12/lib -lmpich -lmpl -lpthread -llapack
+bld::pp::ens 1
+bld::exe_dep::EnsProg_ETKF.exe gcom
+bld::exe_dep::EnsProg_TrimObstore.exe gcom
+bld::target EnsProg_ETKF.exe EnsProg_TrimObstore.exe
diff --git a/test/repos/trunk/cfg/fcm1_postproc_hpc.cfg b/test/repos/trunk/cfg/fcm1_postproc_hpc.cfg
new file mode 100644
index 0000000..8596543
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_postproc_hpc.cfg
@@ -0,0 +1,209 @@
+# ------------------------------------------------------------------------------
+# File header
+# ------------------------------------------------------------------------------
+
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+# ------------------------------------------------------------------------------
+# Destination
+# ------------------------------------------------------------------------------
+
+DEST $PWD
+RDEST $THIS_RUN_DIR_HPC
+RDEST::MACHINE $HPC
+
+# ------------------------------------------------------------------------------
+# Build declarations
+# ------------------------------------------------------------------------------
+
+bld::target PPQYINTERP.ksh run_nae_pp.ksh run_qv_downscaling.ksh run_glo_pp.ksh run_EuroPP.ksh first_start.ksh PP4KOPER.ksh
+bld::infile_ext::cpp C::SOURCE
+bld::tool::fc xlf90_r
+bld::tool::ld xlf90_r
+bld::tool::cc xlC_r
+bld::tool::make gmake
+bld::tool::cpp xlC_r
+bld::tool::cppflags -E -C
+bld::tool::fpp cpp -E -P -traditional
+bld::excl_dep USE::F90_UNIX_IO
+bld::excl_dep H::gc_constants.h
+bld::excl_dep H::gc_kinds.h
+bld::excl_dep H::gc_options.h
+bld::tool::fflags -c -qarch=pwr6 -qtune=pwr6 -O0 -qextname
+bld::tool::fflags::rst -c -qarch=pwr6 -qtune=pwr6 -O0 -qfullpath -qextname -qrealsize=8 -qintsize=8
+bld::tool::fflags::ver -c -qarch=pwr6 -qtune=pwr6 -O0 -qfullpath -qextname -qrealsize=8 -qintsize=8
+bld::tool::fflags::ops -c -qarch=pwr6 -qtune=pwr6 -O0 -qfullpath -qextname -qrealsize=8 -qintsize=8
+bld::tool::cflags -c -qarch=pwr6 -qtune=pwr6 -O0 -qhot
+bld::tool::ldflags
+bld::tool::ldflags::pp::model_processing::format %bld::tool::ldflags -L/projects/um1/gcom/gcom3.2/meto_ibm_pwr6_serial/lib -lgcom
+bld::tool::ldflags::rst %bld::tool::ldflags -L/projects/um1/gcom/gcom3.2/meto_ibm_pwr6_serial/lib -lgcom -bnoquiet -L/projects/um1/lib -lsig -L/usr/lib -lmass -lessl -qsmp
+bld::pp::pp::utilities 1
+bld::pp::pp::steps 1
+bld::pp::pp::get_metdb_obs::MetDB_source 1
+bld::pp::gen 1
+bld::pp::rst::src 1
+bld::pp::ver 1
+bld::tool::cppkeys::pp::get_metdb_obs::MetDB_source LOWERCASE L64 UNDERSCORE
+bld::tool::cppkeys::gen C_LOW_U FRL8 C_LONG_INT NEC VAROPSVER UTILIO UNDERSCORE LOWERCASE
+bld::tool::fppkeys::gen C_LOW_U FRL8 C_LONG_INT NEC VAROPSVER UTILIO UNDERSCORE UPPERCASE
+bld::tool::fppkeys::rst::src NEC
+bld::tool::fflags::gen %bld::tool::fflags -qrealsize=8 -qintsize=8
+bld::tool::cflags::gen %bld::tool::cflags -qrealsize=8 -qintsize=8
+bld::tool::fflags::gen::UM_Platform %bld::tool::fflags::gen -qfixed=132
+bld::tool::fflags::gen::UM_Platform::IOERROR %bld::tool::fflags::gen
+bld::tool::fflags::gen::UM_General %bld::tool::fflags::gen -qfixed=132
+bld::tool::fflags::gen::UM_COEX %bld::tool::fflags::gen -qfixed=132
+bld::tool::cflags::pp::get_metdb_obs::MetDB_source
+bld::tool::fflags::pp::get_metdb_obs::get_metdb_obs -c -qarch=pwr6 -qtune=pwr6 -O0 -qextname
+bld::tool::fflags::pp::moses-pdm-rfm %bld::tool::fflags -qstrict
+bld::tool::fflags::pp::moses-pdm-rfm::dlongrad %bld::tool::fflags::pp::moses-pdm-rfm -qfixed=132
+bld::tool::fflags::pp::moses-pdm-rfm::down_rad_calc %bld::tool::fflags::pp::moses-pdm-rfm -qfixed=132
+bld::tool::fflags::pp::moses-pdm-rfm::nimrod_extr_comp %bld::tool::fflags::pp::moses-pdm-rfm -qfixed=132
+bld::tool::fflags::pp::moses-pdm-rfm::read_ancil %bld::tool::fflags::pp::moses-pdm-rfm -qfixed=132
+bld::tool::fflags::pp::moses-pdm-rfm::dsolrad %bld::tool::fflags::pp::moses-pdm-rfm -qfixed=132
+bld::tool::fflags::pp::moses-pdm-rfm::nimrod_extr_wind %bld::tool::fflags::pp::moses-pdm-rfm -qfixed=132
+bld::tool::fflags::pp::moses-pdm-rfm::nimrod_hdr_read %bld::tool::fflags::pp::moses-pdm-rfm -qfixed=132
+bld::tool::fflags::pp::moses-pdm-rfm::ssdm %bld::tool::fflags::pp::moses-pdm-rfm -qfixed=132
+bld::tool::fflags::pp::moses-pdm-rfm::morloc %bld::tool::fflags::pp::moses-pdm-rfm -qfixed=132
+bld::tool::fflags::pp::moses-pdm-rfm::get_row_and_column %bld::tool::fflags::pp::moses-pdm-rfm -qfixed=132
+bld::tool::fflags::pp::moses-pdm-rfm::nimrod_idata_read %bld::tool::fflags::pp::moses-pdm-rfm -qfixed=132
+bld::tool::fflags::pp::moses-pdm-rfm::ssdm_var_generator %bld::tool::fflags::pp::moses-pdm-rfm -qfixed=132
+bld::tool::fflags::pp::moses-pdm-rfm::um_solpos %bld::tool::fflags::pp::moses-pdm-rfm -qfixed=132
+bld::tool::fflags::pp::moses-pdm-rfm::morangstrom %bld::tool::fflags::pp::moses-pdm-rfm -qfixed=132
+bld::tool::fflags::pp::moses-pdm-rfm::sun_angles %bld::tool::fflags::pp::moses-pdm-rfm -qfixed=132
+bld::tool::fflags::pp::moses-pdm-rfm::moses_cloud_cover %bld::tool::fflags::pp::moses-pdm-rfm -qfixed=132
+bld::tool::fflags::pp::moses-pdm-rfm::nimrod_3d_idata_read %bld::tool::fflags::pp::moses-pdm-rfm -qfixed=132
+bld::tool::fflags::pp::moses-pdm-rfm::q_vp_from_t_td %bld::tool::fflags::pp::moses-pdm-rfm -qfixed=132
+bld::tool::fflags::pp::moses-pdm-rfm::daynumber %bld::tool::fflags::pp::moses-pdm-rfm -qfixed=132
+bld::tool::fflags::pp::moses-pdm-rfm::nimrod_3dextr_comp %bld::tool::fflags::pp::moses-pdm-rfm -qfixed=132
+bld::tool::fflags::pp::moses-pdm-rfm::moses_qs_from_t %bld::tool::fflags::pp::moses-pdm-rfm -qfixed=132
+bld::tool::fflags::pp::moses-pdm-rfm::route_runoff %bld::tool::fflags::pp::moses-pdm-rfm -qfixed=132
+bld::tool::fflags::pp::moses-pdm-rfm::regrid_real %bld::tool::fflags::pp::moses-pdm-rfm -qfixed=132
+bld::tool::fflags::pp::moses-pdm-rfm::initialise_routing %bld::tool::fflags::pp::moses-pdm-rfm -qfixed=132
+bld::tool::fflags::pp::moses-pdm-rfm::routing %bld::tool::fflags::pp::moses-pdm-rfm -qfixed=132
+bld::tool::fflags::pp::moses-pdm-rfm::nextpoint %bld::tool::fflags::pp::moses-pdm-rfm -qfixed=132
+bld::tool::fflags::pp::moses-pdm-rfm::wavespeed %bld::tool::fflags::pp::moses-pdm-rfm -qfixed=132
+bld::tool::fflags::pp::moses-pdm-rfm::nearest_real %bld::tool::fflags::pp::moses-pdm-rfm -qfixed=132
+bld::tool::fflags::pp::moses-pdm-rfm::sam %bld::tool::fflags::pp::moses-pdm-rfm -qfixed=132
+bld::tool::fflags::pp::pressure_wind %bld::tool::fflags -qfixed=132
+bld::tool::fflags::pp::pressure_wind::an_smear %bld::tool::fflags
+bld::tool::fflags::pp::pressure_wind::gust_adjust %bld::tool::fflags
+bld::tool::fflags::pp::pressure_wind::gust_analysis %bld::tool::fflags
+bld::tool::fflags::pp::pressure_wind::pwindanal %bld::tool::fflags
+bld::tool::fflags::pp::pressure_wind::bilin_mdi %bld::tool::fflags
+bld::tool::fflags::pp::pressure_wind::convert_winduv %bld::tool::fflags
+bld::tool::fflags::pp/precip_fcst %bld::tool::fflags -qfixed=132
+bld::tool::fflags::pp/precip_fcst/accmerge %bld::tool::fflags
+bld::tool::fflags::pp/precip_fcst/object_motion %bld::tool::fflags
+bld::tool::fflags::pp/precip_fcst/scale %bld::tool::fflags
+bld::tool::fflags::pp/precip_fcst/wind_forecast_precip %bld::tool::fflags
+bld::tool::fflags::pp::precip::get_surface_obs %bld::tool::fflags -qfixed=132
+bld::tool::fflags::pp::precip::lightning_forecast %bld::tool::fflags -qfixed=132
+bld::tool::fflags::pp::precip::lightning_merge %bld::tool::fflags -qfixed=132
+bld::tool::fflags::pp::precip::metar_to_synop_weather %bld::tool::fflags -qfixed=132
+bld::tool::fflags::pp::precip::read_adv_fc %bld::tool::fflags -qfixed=132
+bld::tool::fflags::pp::precip::read_rad %bld::tool::fflags -qfixed=132
+bld::tool::fflags::pp::model_processing::get_um_info %bld::tool::fflags -qfixed=132
+bld::tool::fflags::pp::utilities::beammap_ascii_to_nimrod %bld::tool::fflags -qfixed=132
+bld::tool::fflags::pp::utilities::ccitt %bld::tool::fflags -qfixed=132
+bld::tool::fflags::pp::utilities::datetime_c_to_i_secs %bld::tool::fflags -qfixed=132
+bld::tool::fflags::pp::utilities::def_head %bld::tool::fflags -qfixed=132
+bld::tool::fflags::pp::utilities::domain_to_ng %bld::tool::fflags -qfixed=132
+bld::tool::fflags::pp::utilities::european_observations_area %bld::tool::fflags -qfixed=132
+bld::tool::fflags::pp::utilities::get_free_lun %bld::tool::fflags -qfixed=132
+bld::tool::fflags::pp::utilities::icutout %bld::tool::fflags -qfixed=132
+bld::tool::fflags::pp::utilities::ll_to_ng %bld::tool::fflags -qfixed=132
+bld::tool::fflags::pp::utilities::ll_to_ps %bld::tool::fflags -qfixed=132
+bld::tool::fflags::pp::utilities::locate_FCST_string %bld::tool::fflags -qfixed=132
+bld::tool::fflags::pp::utilities::nearest %bld::tool::fflags -qfixed=132
+bld::tool::fflags::pp::utilities::nearest_file %bld::tool::fflags -qfixed=132
+bld::tool::fflags::pp::utilities::ng_to_ll %bld::tool::fflags -qfixed=132
+bld::tool::fflags::pp::utilities::ng_to_ll_array %bld::tool::fflags -qfixed=132
+bld::tool::fflags::pp::utilities::nimrod_i4read %bld::tool::fflags -qfixed=132
+bld::tool::fflags::pp::utilities::nimrod_open2 %bld::tool::fflags -qfixed=132
+bld::tool::fflags::pp::utilities::nimrod_open_i4read %bld::tool::fflags -qfixed=132
+bld::tool::fflags::pp::utilities::nimrod_open_i4write %bld::tool::fflags -qfixed=132
+bld::tool::fflags::pp::utilities::nimrod_regrid %bld::tool::fflags -qfixed=132
+bld::tool::fflags::pp::utilities::observations_area_metdb %bld::tool::fflags -qfixed=132
+bld::tool::fflags::pp::utilities::ps_to_ll %bld::tool::fflags -qfixed=132
+bld::tool::fflags::pp::utilities::regrid %bld::tool::fflags -qfixed=132
+bld::tool::fflags::pp::utilities::round_cycle_string %bld::tool::fflags -qfixed=132
+bld::tool::fflags::pp::utilities::subtract_time %bld::tool::fflags -qfixed=132
+bld::tool::fflags::pp::utilities::time_diff %bld::tool::fflags -qfixed=132
+bld::tool::fflags::pp::utilities::time_difference_prog %bld::tool::fflags -qfixed=132
+bld::tool::fflags::pp::utilities::total_accum %bld::tool::fflags -qfixed=132
+bld::tool::fflags::pp::utilities::trim %bld::tool::fflags -qfixed=132
+bld::tool::fflags::pp::utilities::zpdate %bld::tool::fflags -qfixed=132
+bld::tool::fflags::pp::verification %bld::tool::fflags -qfixed=132
+bld::tool::cflags::pp::steps -c -O0 -qarch=pwr6 -qtune=pwr6 -q64 -I/home/nwp/fr/ihab/fftw_opt/include/
+bld::tool::ld::pp::steps xlC_r
+bld::tool::ldflags::pp::steps -L/home/nwp/fr/ihab/fftw_opt/lib/ -lfftw3 -I/home/nwp/fr/ihab/fftw_opt/include/
+bld::tool::fflags::rst::src -qarch=pwr6 -qtune=pwr6 -O0 -c -qfullpath -qextname -qrealsize=8 -qintsize=8 -qfixed=132
+bld::tool::fflags::rst::src::ReadFrcData -qarch=pwr6 -qtune=pwr6 -O0 -qstrict -c -qfullpath -qextname -qrealsize=8 -qintsize=8
+bld::tool::fflags::rst::src::profile -qarch=pwr6 -qtune=pwr6 -O0 -c -qfullpath -qextname -qrealsize=8 -qintsize=8 -qfixed=132 -qsmp=auto
+bld::tool::fflags::rst::src::MORST_main -qarch=pwr6 -qtune=pwr6 -O0 -c -qfullpath -qextname -qrealsize=8 -qintsize=8 -qfixed=132 -qsmp=omp
+bld::tool::fflags::rst::src::output_status -qarch=pwr6 -qtune=pwr6 -O0 -c -qfullpath -qextname
+
+# ------------------------------------------------------------------------------
+# Project and branches
+# ------------------------------------------------------------------------------
+
+repos::pp::trunk svn://fcm9/PostProc_svn/PostProc/trunk
+repos::ancil::trunk svn://fcm9/PostProc_svn/PostProcAncil/trunk
+repos::site_ext::trunk svn://fcm9/PostProc_svn/SiteExtract/trunk
+repos::gen::code svn://fcm1/GEN_svn/GEN/trunk/src/code
+repos::rst::trunk svn://fcm9/PostProc_svn/RoadTemp/trunk
+repos::ver::code svn://fcm6/VER_svn/VER/trunk/src/code
+repos::ops::code svn://fcm4/OPS_svn/OPS/trunk/src/code
+revision::pp::trunk 2728
+revision::ancil::trunk 2524
+revision::site_ext::trunk 2481
+revision::gen::code 3015
+revision::rst::trunk 2416
+revision::ver::code 4739
+revision::ops::code 18088
+
+src::pp::trunk cloud
+src::pp::trunk CDP
+src::pp::trunk frasia
+src::pp::trunk get_metdb_obs
+src::pp::trunk get_metdb_obs/MetDB_source
+src::pp::trunk model_processing
+src::pp::trunk precip
+src::pp::trunk precip_fcst
+src::pp::trunk pressure_wind
+src::pp::trunk product_gen
+src::pp::trunk scripts
+src::pp::trunk utilities
+src::pp::trunk verification
+src::pp::trunk visibility
+src::pp::trunk moses-pdm-rfm
+src::pp::trunk steps
+src::pp::trunk SCW
+src::site_ext::trunk FssMod_DMO
+src::gen::code GenMod_UMConstants
+src::gen::code UM_Platform
+src::gen::code UM_General
+src::gen::code UM_COEX
+src::gen::code Reconfiguration
+src::gen::code GenMod_Constants
+src::gen::code GenMod_Control
+src::gen::code GenMod_FortranIO
+src::gen::code GenMod_GetEnv
+src::gen::code GenMod_Platform
+src::gen::code GenMod_Reporting
+src::gen::code GenMod_Trace
+src::gen::code GenMod_Utilities
+src::ancil::trunk .
+src::ancil::trunk ./archive
+src::ancil::trunk ./steps_det
+src::ancil::trunk ./steps_ens
+src::rst::trunk src
+src::rst::trunk scripts
+src::ver::code VerMod_FieldsIO
+src::ver::code VerMod_General
+src::ver::code VerMod_Grid
+src::ops::code OpsMod_MOPS
+src::ops::code OpsMod_ObsInfo
+src::ops::code OpsMod_Constants
diff --git a/test/repos/trunk/cfg/fcm1_pp_change_blockdata.cfg b/test/repos/trunk/cfg/fcm1_pp_change_blockdata.cfg
new file mode 100644
index 0000000..2c8a995
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_pp_change_blockdata.cfg
@@ -0,0 +1,6 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+INC $HERE/fcm1_base.cfg
+
+BLD::TOOL::FPPKEYS::test/blockdata ODD
diff --git a/test/repos/trunk/cfg/fcm1_pp_change_dependency.cfg b/test/repos/trunk/cfg/fcm1_pp_change_dependency.cfg
new file mode 100644
index 0000000..0c6e592
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_pp_change_dependency.cfg
@@ -0,0 +1,6 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+INC $HERE/fcm1_base.cfg
+
+BLD::TOOL::FPPKEYS::test/program/hello
diff --git a/test/repos/trunk/cfg/fcm1_pp_change_include.cfg b/test/repos/trunk/cfg/fcm1_pp_change_include.cfg
new file mode 100644
index 0000000..00329fa
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_pp_change_include.cfg
@@ -0,0 +1,6 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+INC $HERE/fcm1_base.cfg
+
+REPOS::test::branch1 fcm:test_suite_br/dev/Share/modify_pp_include
diff --git a/test/repos/trunk/cfg/fcm1_pp_change_include_inherit.cfg b/test/repos/trunk/cfg/fcm1_pp_change_include_inherit.cfg
new file mode 100644
index 0000000..058fc16
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_pp_change_include_inherit.cfg
@@ -0,0 +1,8 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+DEST::ROOTDIR $PWD
+
+USE $RUN_DIR/fcm1_base
+
+REPOS::test::branch1 fcm:test_suite_br/dev/Share/modify_pp_include
diff --git a/test/repos/trunk/cfg/fcm1_pp_empty_subroutine.cfg b/test/repos/trunk/cfg/fcm1_pp_empty_subroutine.cfg
new file mode 100644
index 0000000..6741f8a
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_pp_empty_subroutine.cfg
@@ -0,0 +1,6 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+INC $HERE/fcm1_base.cfg
+
+BLD::TOOL::FPPKEYS::test/subroutine/hello_sub
diff --git a/test/repos/trunk/cfg/fcm1_pp_empty_subroutine_inherit.cfg b/test/repos/trunk/cfg/fcm1_pp_empty_subroutine_inherit.cfg
new file mode 100644
index 0000000..1ae9748
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_pp_empty_subroutine_inherit.cfg
@@ -0,0 +1,8 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+DEST::ROOTDIR $PWD
+
+USE $RUN_DIR/fcm1_base
+
+BLD::TOOL::FPPKEYS::test/subroutine/hello_sub
diff --git a/test/repos/trunk/cfg/fcm1_pp_empty_subroutine_inherit_force.cfg b/test/repos/trunk/cfg/fcm1_pp_empty_subroutine_inherit_force.cfg
new file mode 100644
index 0000000..ce55461
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_pp_empty_subroutine_inherit_force.cfg
@@ -0,0 +1,9 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+DEST::ROOTDIR $PWD
+
+USE $RUN_DIR/fcm1_base
+
+BLD::TOOL::FPPKEYS::test/subroutine/hello_sub
+BLD::TOOL::LD wrap_ld2
diff --git a/test/repos/trunk/cfg/fcm1_revmatch_false.cfg b/test/repos/trunk/cfg/fcm1_revmatch_false.cfg
new file mode 100644
index 0000000..1757ad1
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_revmatch_false.cfg
@@ -0,0 +1,13 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+INC $HERE/fcm1_base.cfg
+
+REPOS::test::branch1 fcm:test_suite_br/dev/Share/modify_files_base
+REPOS::test::branch2 fcm:test_suite_br/dev/Share/modify_files_merge1
+REPOS::test::branch3 fcm:test_suite_br/dev/Share/modify_files_merge2
+
+REVISION::test::base 21
+REVISION::test::branch1 21
+REVISION::test::branch2 21
+REVISION::test::branch3 21
diff --git a/test/repos/trunk/cfg/fcm1_revmatch_true.cfg b/test/repos/trunk/cfg/fcm1_revmatch_true.cfg
new file mode 100644
index 0000000..ac18d80
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_revmatch_true.cfg
@@ -0,0 +1,6 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+INC $HERE/fcm1_revmatch_false.cfg
+
+REVMATCH true
diff --git a/test/repos/trunk/cfg/fcm1_sps.cfg b/test/repos/trunk/cfg/fcm1_sps.cfg
new file mode 100644
index 0000000..efb82fd
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_sps.cfg
@@ -0,0 +1,129 @@
+# ------------------------------------------------------------------------------
+# File header
+# ------------------------------------------------------------------------------
+
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+# ------------------------------------------------------------------------------
+# Destination
+# ------------------------------------------------------------------------------
+
+DEST $PWD
+
+# ------------------------------------------------------------------------------
+# Build declarations
+# ------------------------------------------------------------------------------
+
+%FOPT -CB -traceback -u -convert big_endian
+%SPS_LIBDIR /home/h04/cfsa/SPS/libraries/RHEL6
+bld::tool::fc wrap_fc
+bld::tool::fflags %FOPT -I%{SPS_LIBDIR}/grib_api/include
+bld::tool::cc wrap_cc
+bld::tool::cflags -Wall -O2 -DLOWERCASE -I%{SPS_LIBDIR}/hdf5/include
+bld::tool::ar wrap_ar
+bld::tool::ld wrap_ld
+bld::tool::ldflags -openmp -L%{SPS_LIBDIR}/hdf5/lib -lhdf5 -lhdf5_hl \
+ -L%{SPS_LIBDIR}/bufr_ifort -lbufr \
+ -L%{SPS_LIBDIR}/grib_ifort -lgrib_ifort \
+ -L%{SPS_LIBDIR}/g2lib -lg2 \
+ -L%{SPS_LIBDIR}/grib_api/lib -lgrib_api_f90 -lgrib_api \
+ -L%{SPS_LIBDIR}/jasper/lib -ljasper \
+ -L/usr/lib -llapack \
+ -ljpeg -lpng
+bld::target SpsScr_Install
+bld::target sps__data__Sps_Fire.etc
+bld::target sps__data__coeffs.etc
+bld::target sps__data__palettes.etc
+bld::target sps__data__products.etc
+bld::target sps__data__sad.etc
+bld::target sps__data__slotstore.etc
+bld::exe_name::h5admin h5admin
+bld::exe_name::h5getatt h5getatt
+bld::exe_name::SpsProg_GetCoords sps_get_coords
+bld::excl_dep use::grib_api
+bld::tool::fflags::gen::src::code::GenMod_UMConstants %FOPT -w -132
+bld::tool::cflags::sps::src::code::SpsMod_Image -Wall -DLOWERCASE -I%{SPS_LIBDIR}/hdf5/include
+bld::tool::fflags::sps::src::code::SpsMod_Utilities %FOPT -Duse_f90_unix=''
+bld::tool::fflags::sps::src::code::SpsTask_HDFReader %FOPT -auto -assume byterecl
+bld::tool::fflags::sps::src::code::SpsProg_ImageGrib %FOPT -auto -assume byterecl
+bld::tool::fflags::sps::src::code::SpsProg_GlobalComposite %FOPT -auto -assume byterecl
+bld::tool::fflags::sps::src::code::rttov10 %FOPT -openmp
+bld::tool::fflags::sps::src::code::rttov10::main::rttov_locpat_k.F90 %FOPT
+
+# ------------------------------------------------------------------------------
+# Project and branches
+# ------------------------------------------------------------------------------
+
+repos::sps::base fcm:sps_tr
+revision::sps::base 4964
+src::sps::base src/code/SpsMod_AutosatFormat
+src::sps::base src/code/SpsMod_Calibration
+src::sps::base src/code/SpsMod_CloudContamination
+src::sps::base src/code/SpsMod_CloudStoreTypes
+src::sps::base src/code/SpsMod_Constants
+src::sps::base src/code/SpsMod_Coordinates
+src::sps::base src/code/SpsMod_FogParameters
+src::sps::base src/code/SpsMod_GRIB
+src::sps::base src/code/SpsMod_HDF
+src::sps::base src/code/SpsMod_Image
+src::sps::base src/code/SpsMod_InterpolateModelToPixel
+src::sps::base src/code/SpsMod_LoadCloudMaskInfo
+src::sps::base src/code/SpsMod_MPEFBufrDecode
+src::sps::base src/code/SpsMod_NAME
+src::sps::base src/code/SpsMod_NimrodFile
+src::sps::base src/code/SpsMod_RTTOV
+src::sps::base src/code/SpsMod_ScienceInterface
+src::sps::base src/code/SpsMod_SdiSeg
+src::sps::base src/code/SpsMod_Setup
+src::sps::base src/code/SpsMod_SlotStoreGroups
+src::sps::base src/code/SpsMod_Slotstore
+src::sps::base src/code/SpsMod_Store
+src::sps::base src/code/SpsMod_Utilities
+src::sps::base src/code/SpsProg_ATDnetStrikes
+src::sps::base src/code/SpsProg_BufrEncode_SEVIRI
+src::sps::base src/code/SpsProg_Calibration
+src::sps::base src/code/SpsProg_CloudProducts
+src::sps::base src/code/SpsProg_Geometry
+src::sps::base src/code/SpsProg_ImageGen
+src::sps::base src/code/SpsProg_ImageGrib
+src::sps::base src/code/SpsProg_MakeGRIB
+src::sps::base src/code/SpsProg_ModelFieldsToHDF
+src::sps::base src/code/SpsProg_MPEFBufrDecode
+src::sps::base src/code/SpsProg_PreProcessCMAData
+src::sps::base src/code/SpsProg_ProcessGOES_SA
+src::sps::base src/code/SpsProg_RunRTTOV
+src::sps::base src/code/SpsProg_GlobalComposite
+src::sps::base src/code/SpsTask_Alpha
+src::sps::base src/code/SpsTask_CalcParallaxErrors
+src::sps::base src/code/SpsTask_CloudMask
+src::sps::base src/code/SpsTask_CTHProcessing
+src::sps::base src/code/SpsTask_Dust
+src::sps::base src/code/SpsTask_Fog
+src::sps::base src/code/SpsTask_HDFReader
+src::sps::base src/code/SpsTask_MetOGII
+src::sps::base src/code/SpsTask_PrecipIndex
+src::sps::base src/code/SpsTask_SatPrecip
+src::sps::base src/code/SpsTask_TauRe
+src::sps::base src/code/SpsTask_VolcanicAsh
+src::sps::base src/code/SpsTask_VolcanicSO2
+src::sps::base src/code/Sps_MSGData
+src::sps::base src/code/Sps_SlotstoreUtilities
+src::sps::base src/code/Sps_Utils
+expsrc::sps::base src/code/rttov10
+src::sps::base src/idl/Sps_Generic
+src::sps::base src/idl/Sps_FireDetection
+src::sps::base src/idl/Sps_MSGAOD
+src::sps::base src/idl/Sps_MODISAOD
+src::sps::base src/scripts/Sps_Perl
+src::sps::base src/scripts/Sps_Scripts
+src::sps::base src/scripts/Sps_Slotstore
+src::sps::base src/scripts/Sps_Utils
+expsrc::sps::base src/scripts/sched
+expsrc::sps::base src/scripts/lib
+expsrc::sps::base data
+expsrc::sps::base control
+
+repos::gen::base fcm:gen_tr
+revision::gen::base 3953
+src::gen::base src/code/GenMod_UMConstants
diff --git a/test/repos/trunk/cfg/fcm1_suite.cfg b/test/repos/trunk/cfg/fcm1_suite.cfg
new file mode 100644
index 0000000..2337685
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_suite.cfg
@@ -0,0 +1,8 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+DEST::ROOTDIR $PWD
+
+BLD::SRC::test $RUN_DIR/fcm1_base/bin
+
+BLD::TARGET hello.sh
diff --git a/test/repos/trunk/cfg/fcm1_symbolic_link.cfg b/test/repos/trunk/cfg/fcm1_symbolic_link.cfg
new file mode 100644
index 0000000..c5c4e9a
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_symbolic_link.cfg
@@ -0,0 +1,6 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+INC $HERE/fcm1_base.cfg
+
+REPOS::test::branch1 fcm:test_suite_br/dev/Share/symbolic_link
diff --git a/test/repos/trunk/cfg/fcm1_um.cfg b/test/repos/trunk/cfg/fcm1_um.cfg
new file mode 100644
index 0000000..9b10a10
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_um.cfg
@@ -0,0 +1,54 @@
+# ------------------------------------------------------------------------------
+# File header
+# ------------------------------------------------------------------------------
+
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+# ------------------------------------------------------------------------------
+# Destination
+# ------------------------------------------------------------------------------
+
+DEST $PWD
+
+# ------------------------------------------------------------------------------
+# Build declarations
+# ------------------------------------------------------------------------------
+
+bld::blockdata blkdata.o
+bld::excl_dep USE::NetCDF
+bld::excl_dep INC::netcdf.inc
+bld::excl_dep INC::mpif.h
+bld::excl_dep USE::mpl
+bld::excl_dep USE::mod_prism_proto
+bld::excl_dep USE::mod_prism_grids_writing
+bld::excl_dep USE::mod_prism_def_partition_proto
+bld::excl_dep USE::mod_prism_put_proto
+bld::excl_dep USE::mod_prism_get_proto
+bld::excl_dep::UM::script EXE
+bld::exe_dep portio2a.o pio_data_conv.o pio_io_timer.o
+bld::exe_name::flumeMain um.exe
+bld::pp::UM 1
+bld::target um.exe
+bld::tool::ar ar
+bld::tool::cc wrap_cc
+bld::tool::cpp wrap_mpicc
+bld::tool::cppflags -E
+bld::tool::cppkeys C_LONG_LONG_INT=c_long_long_int MPP=mpp C_LOW_U=c_low_u FRL8=frl8 LINUX=linux BUFRD_IO=bufrd_io LITTLE_END=little_end LINUX_INTEL_COMPILER=linux_intel_compiler CONTROL=control REPROD=reprod ATMOS=atmos GLOBAL=global A04_ALL=a04_all A01_3C=a01_3c A02_3C=a02_3c A03_8C=a03_8c A04_3D=a04_3d A05_4A=a05_4a A06_4A=a06_4a A08_7A=a08_7a A09_2A=a09_2a A10_2A=a10_2a A11_2A=a11_2a A12_2A=a12_2a A13_2A=a13_2a A14_1B=a14_1b A15_ [...]
+bld::tool::fc wrap_mpif90
+bld::tool::fflags -i8 -r8 -w -I /home/h01/frum/gcom/gcom4.1/linux_ifort_mpich2/inc -O0
+bld::tool::fppflags -E -P -traditional -I /home/h04/opsrc/ops0/mpi/mpich2-1.4-ukmo-v1/ifort-12/include
+bld::tool::fppkeys C_LONG_LONG_INT=c_long_long_int MPP=mpp C_LOW_U=c_low_u FRL8=frl8 LINUX=linux BUFRD_IO=bufrd_io LITTLE_END=little_end LINUX_INTEL_COMPILER=linux_intel_compiler CONTROL=control REPROD=reprod ATMOS=atmos GLOBAL=global A04_ALL=a04_all A01_3C=a01_3c A02_3C=a02_3c A03_8C=a03_8c A04_3D=a04_3d A05_4A=a05_4a A06_4A=a06_4a A08_7A=a08_7a A09_2A=a09_2a A10_2A=a10_2a A11_2A=a11_2a A12_2A=a12_2a A13_2A=a13_2a A14_1B=a14_1b A15_ [...]
+bld::tool::geninterface none
+bld::tool::ar wrap_ar
+bld::tool::ld wrap_mpif90
+bld::tool::ldflags -L/home/h01/frum/gcom/gcom4.1/linux_ifort_mpich2/lib -lgcom -Wl,--noinhibit-exec -Vaxlib
+bld::tool::fpp wrap_pp
+
+# ------------------------------------------------------------------------------
+# Project and branches
+# ------------------------------------------------------------------------------
+
+repos::UM::base fcm:um-tr/src/
+version::UM::base vn7.3
+expsrc::UM::base
diff --git a/test/repos/trunk/cfg/fcm1_um_hpc.cfg b/test/repos/trunk/cfg/fcm1_um_hpc.cfg
new file mode 100644
index 0000000..b718387
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_um_hpc.cfg
@@ -0,0 +1,60 @@
+# ------------------------------------------------------------------------------
+# File header
+# ------------------------------------------------------------------------------
+
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+# ------------------------------------------------------------------------------
+# Destination
+# ------------------------------------------------------------------------------
+
+DEST $PWD
+RDEST $THIS_RUN_DIR_HPC
+RDEST::MACHINE $HPC
+
+# ------------------------------------------------------------------------------
+# Build declarations
+# ------------------------------------------------------------------------------
+
+bld::blockdata blkdata.o
+bld::excl_dep USE::NetCDF
+bld::excl_dep INC::netcdf.inc
+bld::excl_dep INC::mpif.h
+bld::excl_dep USE::mpl
+bld::excl_dep USE::mod_prism_proto
+bld::excl_dep USE::mod_prism_grids_writing
+bld::excl_dep USE::mod_prism_def_partition_proto
+bld::excl_dep USE::mod_prism_put_proto
+bld::excl_dep USE::mod_prism_get_proto
+bld::excl_dep::UM::script EXE
+bld::exe_dep portio2a.o pio_data_conv.o pio_io_timer.o print_from_c.o
+bld::exe_name::flumeMain um.exe
+bld::pp::UM 1
+bld::target um.exe
+bld::tool::ar ar
+bld::tool::cc xlc_r
+bld::tool::cpp xlc
+bld::tool::cppflags -E -C
+bld::tool::cppkeys C_LONG_INT=c_long_int MPP=mpp C_LOW_U=c_low_u FRL8=frl8 BUFRD_IO=bufrd_io VECTLIB=vectlib IBM=ibm CONTROL=control REPROD=reprod MPP=mpp ATMOS=atmos GLOBAL=global A04_ALL=a04_all A01_3C=a01_3c A02_3C=a02_3c A03_8C=a03_8c A04_3D=a04_3d A05_4A=a05_4a A06_4A=a06_4a A08_7A=a08_7a A09_2A=a09_2a A10_2A=a10_2a A11_2A=a11_2a A12_2A=a12_2a A13_2A=a13_2a A14_1B=a14_1b A15_1A=a15_1a A16_1A=a16_1a A17_2B=a17_2b A18_0A=a18_0a A1 [...]
+bld::tool::fc mpxlf90_r
+bld::tool::fflags -I/projects/um1/gcom/gcom3.3/meto_ibm_pwr6_mpp/inc -I/projects/um1/lib/netcdf3.20090102/include -qextname -qsuffix=f=f90 -qarch=pwr6 -qtune=pwr6 -qrealsize=8 -qintsize=8 -NS32768 -O0
+bld::tool::fflags::UM::atmosphere::dynamics_advection::eta_vert_weights_e -qextname -qsuffix=f=f90 -qarch=pwr6 -qtune=pwr6 -qrealsize=8 -qintsize=8 -O0 -NS32768
+bld::tool::fflags::UM::control::top_level::atm_step -qextname -qsuffix=f=f90 -qarch=pwr6 -qtune=pwr6 -qrealsize=8 -qintsize=8 -O0 -NS32768
+bld::tool::fflags::UM::control::top_level::u_model -qextname -qsuffix=f=f90 -qarch=pwr6 -qtune=pwr6 -qrealsize=8 -qintsize=8 -O0 -NS32768
+bld::tool::fpp cpp
+bld::tool::fppflags -E -P -traditional
+bld::tool::fppkeys C_LONG_INT=c_long_int MPP=mpp C_LOW_U=c_low_u FRL8=frl8 BUFRD_IO=bufrd_io VECTLIB=vectlib IBM=ibm CONTROL=control REPROD=reprod MPP=mpp ATMOS=atmos GLOBAL=global A04_ALL=a04_all A01_3C=a01_3c A02_3C=a02_3c A03_8C=a03_8c A04_3D=a04_3d A05_4A=a05_4a A06_4A=a06_4a A08_7A=a08_7a A09_2A=a09_2a A10_2A=a10_2a A11_2A=a11_2a A12_2A=a12_2a A13_2A=a13_2a A14_1B=a14_1b A15_1A=a15_1a A16_1A=a16_1a A17_2B=a17_2b A18_0A=a18_0a A1 [...]
+bld::tool::geninterface none
+bld::tool::ld mpxlf90_r
+bld::tool::ldflags -lmass -lmassvp6 -L/projects/um1/gcom/gcom3.3/meto_ibm_pwr6_mpp/lib -lgcom -L/projects/um1/lib -lgrib -lsig -L/projects/um1/lib/netcdf3.20090102/lib64 -lnetcdf
+bld::tool::make gmake
+
+
+# ------------------------------------------------------------------------------
+# Project and branches
+# ------------------------------------------------------------------------------
+
+repos::UM::base fcm:um-tr/src/
+version::UM::base vn7.3
+expsrc::UM::base
diff --git a/test/repos/trunk/cfg/fcm1_um_inherit.cfg b/test/repos/trunk/cfg/fcm1_um_inherit.cfg
new file mode 100644
index 0000000..d926efc
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_um_inherit.cfg
@@ -0,0 +1,49 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+DEST::ROOTDIR $PWD
+
+USE $RUN_DIR/fcm1_um
+
+repos::UM::branch1 fcm:um_br/dev/Share/VN7.3_hg3_dust_443/src
+version::UM::branch1 11858
+expsrc::UM::branch1 /
+repos::UM::branch2 fcm:um_br/dev/Share/VN7.3_hg3_ccw_precip/src
+version::UM::branch2 11857
+expsrc::UM::branch2 /
+repos::UM::branch3 fcm:um_br/dev/hadco/VN7.3_HG3_porting_lsp_fixes/src
+version::UM::branch3 12029
+expsrc::UM::branch3 /
+repos::UM::branch4 fcm:um_br/dev/hadco/VN7.3_pc2_qcl_gt_tiny/src
+version::UM::branch4 12142
+expsrc::UM::branch4 /
+repos::UM::branch5 fcm:um_br/dev/hadas/VN7.3_w_CAPE_diag/src
+version::UM::branch5 12012
+expsrc::UM::branch5 /
+repos::UM::branch7 fcm:um_br/dev/frlk/VN7.3_BLLEVS_fixes/src
+version::UM::branch7 13938
+expsrc::UM::branch7 /
+repos::UM::branch8 fcm:um_br/dev/frwm/VN7.3_Cu_Diag_Low_LCL/src
+version::UM::branch8 12011
+expsrc::UM::branch8 /
+repos::UM::branch9 fcm:um_br/dev/frwm/VN7.3_LimitCnvParcPert/src
+version::UM::branch9 12041
+expsrc::UM::branch9 /
+repos::UM::branch10 fcm:um_br/dev/hadip/VN7.3_ilp_moose/src
+version::UM::branch10 13314
+expsrc::UM::branch10 /
+repos::UM::branch11 fcm:um_br/dev/hadco/VN7.3_temp_fix_solver/src
+version::UM::branch11 12740
+expsrc::UM::branch11 /
+repos::UM::branch12 fcm:um_br/dev/hadng/VN7.3_wetlands_rothc/src
+version::UM::branch12 12603
+expsrc::UM::branch12 /
+repos::UM::branch13 fcm:um_br/dev/frtg/VN7.3_reconf_extern_ancil/src
+version::UM::branch13 13063
+expsrc::UM::branch13 /
+repos::UM::branch15 fcm:um_br/dev/frma/VN7.3_rad_dev/src
+version::UM::branch15 12732
+expsrc::UM::branch15 /
+repos::UM::branch16 fcm:um_br/dev/frlk/VN7.3_melt_fix/src
+version::UM::branch16 13822
+expsrc::UM::branch16 /
diff --git a/test/repos/trunk/cfg/fcm1_um_inherit_hpc.cfg b/test/repos/trunk/cfg/fcm1_um_inherit_hpc.cfg
new file mode 100644
index 0000000..95c9364
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_um_inherit_hpc.cfg
@@ -0,0 +1,51 @@
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+DEST::ROOTDIR $PWD
+RDEST $THIS_RUN_DIR_HPC
+RDEST::MACHINE $HPC
+
+USE $RUN_DIR/fcm1_um_hpc
+
+repos::UM::branch1 fcm:um_br/dev/Share/VN7.3_hg3_dust_443/src
+version::UM::branch1 11858
+expsrc::UM::branch1 /
+repos::UM::branch2 fcm:um_br/dev/Share/VN7.3_hg3_ccw_precip/src
+version::UM::branch2 11857
+expsrc::UM::branch2 /
+repos::UM::branch3 fcm:um_br/dev/hadco/VN7.3_HG3_porting_lsp_fixes/src
+version::UM::branch3 12029
+expsrc::UM::branch3 /
+repos::UM::branch4 fcm:um_br/dev/hadco/VN7.3_pc2_qcl_gt_tiny/src
+version::UM::branch4 12142
+expsrc::UM::branch4 /
+repos::UM::branch5 fcm:um_br/dev/hadas/VN7.3_w_CAPE_diag/src
+version::UM::branch5 12012
+expsrc::UM::branch5 /
+repos::UM::branch7 fcm:um_br/dev/frlk/VN7.3_BLLEVS_fixes/src
+version::UM::branch7 13938
+expsrc::UM::branch7 /
+repos::UM::branch8 fcm:um_br/dev/frwm/VN7.3_Cu_Diag_Low_LCL/src
+version::UM::branch8 12011
+expsrc::UM::branch8 /
+repos::UM::branch9 fcm:um_br/dev/frwm/VN7.3_LimitCnvParcPert/src
+version::UM::branch9 12041
+expsrc::UM::branch9 /
+repos::UM::branch10 fcm:um_br/dev/hadip/VN7.3_ilp_moose/src
+version::UM::branch10 13314
+expsrc::UM::branch10 /
+repos::UM::branch11 fcm:um_br/dev/hadco/VN7.3_temp_fix_solver/src
+version::UM::branch11 12740
+expsrc::UM::branch11 /
+repos::UM::branch12 fcm:um_br/dev/hadng/VN7.3_wetlands_rothc/src
+version::UM::branch12 12603
+expsrc::UM::branch12 /
+repos::UM::branch13 fcm:um_br/dev/frtg/VN7.3_reconf_extern_ancil/src
+version::UM::branch13 13063
+expsrc::UM::branch13 /
+repos::UM::branch15 fcm:um_br/dev/frma/VN7.3_rad_dev/src
+version::UM::branch15 12732
+expsrc::UM::branch15 /
+repos::UM::branch16 fcm:um_br/dev/frlk/VN7.3_melt_fix/src
+version::UM::branch16 13822
+expsrc::UM::branch16 /
diff --git a/test/repos/trunk/cfg/fcm1_var.cfg b/test/repos/trunk/cfg/fcm1_var.cfg
new file mode 100644
index 0000000..3a543c7
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_var.cfg
@@ -0,0 +1,232 @@
+# ------------------------------------------------------------------------------
+# File header
+# ------------------------------------------------------------------------------
+
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+# ------------------------------------------------------------------------------
+# Destination
+# ------------------------------------------------------------------------------
+
+DEST $PWD
+
+# ------------------------------------------------------------------------------
+# Build declarations
+# ------------------------------------------------------------------------------
+
+bld::excl_dep USE::F90_UNIX_IO
+bld::excl_dep USE::XLFUTILITY
+bld::excl_dep INC::mpif.h
+bld::exe_dep gcom varadmin::src::code::VarMod_Lapack varadmin::src::code::VarMod_Blas
+bld::pp::gcom 1
+bld::pp::var::src::code::PF_MPP 1
+bld::target VarScr_HelpCompile
+bld::tool::cc wrap_cc
+bld::tool::cflags
+bld::tool::cppkeys
+bld::tool::cppkeys::gen::src::code::GenMod_Platform LOWERCASE UNDERSCORE FRL8 C_LONG_LONG_INT
+bld::tool::cppkeys::gen::src::code::UM_Platform VAROPSVER C_LOW_U FRL8 C_LONG_LONG_INT LINUX LITTLE_END
+bld::tool::fc wrap_fc
+bld::tool::fflags -implicitnone -integer_size 64 -real_size 64 -ftrapuv
+bld::tool::fflags::gcom -implicitnone -integer_size 64 -real_size 64 -ftrapuv -warn none
+bld::tool::fflags::gcom::build::mpl::mpl -implicitnone -integer_size 64 -real_size 64 -ftrapuv -warn none -I$(OPSDIR)/mpi/mpich2-1.4-ukmo-v1/ifort-12/include
+bld::tool::fflags::gen -implicitnone -integer_size 64 -real_size 64 -ftrapuv -warn noerrors
+bld::tool::fflags::ops::src::code::Ops_RTTOV9 -implicitnone -integer_size 64 -real_size 64 -ftrapuv -warn none
+bld::tool::fflags::var::src::code::PF_Interpolation::Cubic_Lagrange_Adj -implicitnone -integer_size 64 -real_size 64 -ftrapuv -Wp,-P
+bld::tool::fflags::var::src::code::PF_MPP -implicitnone -integer_size 64 -real_size 64 -ftrapuv -warn noerrors
+bld::tool::fflags::var::src::code::VarProg_UMFileUtils -implicitnone -integer_size 64 -real_size 64
+bld::tool::fflags::varadmin::src::code::VarMod_Blas -implicitnone -integer_size 64 -real_size 64 -ftrapuv -warn none
+bld::tool::fflags::varadmin::src::code::VarMod_Lapack -implicitnone -integer_size 64 -real_size 64 -ftrapuv -warn none
+bld::tool::fppkeys IFORT_CDIRS
+bld::tool::fppkeys::gcom::build GC_VERSION="'3.4+'" GC_BUILD_DATE="'15824'" PREC_64B GC__FLUSHUNIT6 GC__FORTERRUNIT=0 GC_DESCRIP="'MPP'" MPI_SRC MPILIB_32B
+bld::tool::fppkeys::gen::src::code::GenMod_Control GCOMHEADERS
+bld::tool::fppkeys::gen::src::code::GenMod_Utilities::Gen_FlushUnit USE_FLUSH
+bld::tool::fppkeys::gen::src::code::UM_COEX VAROPSVER
+bld::tool::fppkeys::gen::src::code::UM_Platform VAROPSVER
+bld::tool::ldflags -L$(OPSDIR)/mpi/mpich2-1.4-ukmo-v1/ifort-12/lib -lmpich -lmpl -lpthread
+bld::tool::make gmake
+
+bld::pp::ops::src::code::Ops_RTTOV9::rttov9_parallel_direct 1
+bld::pp::ops::src::code::Ops_RTTOV9::rttov9_parallel_ad 1
+bld::pp::ops::src::code::Ops_RTTOV9::rttov9_parallel_tl 1
+bld::pp::ops::src::code::Ops_RTTOV9::rttov9_parallel_k 1
+bld::tool::fppkeys::ops::src::code::Ops_RTTOV9::rttov9_parallel_ad _RTTOV_PARALLEL_AD
+bld::tool::fppkeys::ops::src::code::Ops_RTTOV9::rttov9_parallel_direct _RTTOV_PARALLEL_DIRECT
+bld::tool::fppkeys::ops::src::code::Ops_RTTOV9::rttov9_parallel_k _RTTOV_PARALLEL_K
+bld::tool::fppkeys::ops::src::code::Ops_RTTOV9::rttov9_parallel_tl _RTTOV_PARALLEL_TL
+
+BLD::TOOL::LD wrap_ld
+BLD::TOOL::AR wrap_ar
+BLD::TOOL::FPP wrap_pp
+
+# ------------------------------------------------------------------------------
+# Project and branches
+# ------------------------------------------------------------------------------
+
+REPOS::var::base svn://fcm5/VAR_svn/VAR/trunk
+REVISION::var::base 14844
+SRC::var::base src/code/NewDynamics
+SRC::var::base src/code/PFMod_Model
+SRC::var::base src/code/PFMod_SV
+SRC::var::base src/code/PF_Bdy_Layer
+SRC::var::base src/code/PF_Control
+SRC::var::base src/code/PF_EulerianAdv
+SRC::var::base src/code/PF_FirstGuessRhoW
+SRC::var::base src/code/PF_GcrSolver
+SRC::var::base src/code/PF_General
+SRC::var::base src/code/PF_Helmholtz
+SRC::var::base src/code/PF_IO
+SRC::var::base src/code/PF_Interpolation
+SRC::var::base src/code/PF_MPP
+SRC::var::base src/code/PF_MPPUtil
+SRC::var::base src/code/PF_MoistPhys
+SRC::var::base src/code/PF_SemiLagrangianTheta
+SRC::var::base src/code/PF_SemiLagrangianUV
+SRC::var::base src/code/PF_UVTransformation
+SRC::var::base src/code/PF_Update
+SRC::var::base src/code/PF_convec
+SRC::var::base src/code/SG_Interpolation
+SRC::var::base src/code/VarMod_ATOVS
+SRC::var::base src/code/VarMod_ATOVSRad
+SRC::var::base src/code/VarMod_Aircraft
+SRC::var::base src/code/VarMod_BalanceTerms
+SRC::var::base src/code/VarMod_BgPenAndGrad
+SRC::var::base src/code/VarMod_CovCheckTp
+SRC::var::base src/code/VarMod_CovCodes
+SRC::var::base src/code/VarMod_CovCond
+SRC::var::base src/code/VarMod_CovHorizontal
+SRC::var::base src/code/VarMod_CovOptions
+SRC::var::base src/code/VarMod_CovSample
+SRC::var::base src/code/VarMod_CovSpectral
+SRC::var::base src/code/VarMod_CovSpectralData
+SRC::var::base src/code/VarMod_CovStatsIO
+SRC::var::base src/code/VarMod_CovVariances
+SRC::var::base src/code/VarMod_CovVertical
+SRC::var::base src/code/VarMod_CovVerticalData
+SRC::var::base src/code/VarMod_DelSquaredFFT
+SRC::var::base src/code/VarMod_Diagnostics
+SRC::var::base src/code/VarMod_FieldOutput
+SRC::var::base src/code/VarMod_Fourier
+SRC::var::base src/code/VarMod_GPSRO
+SRC::var::base src/code/VarMod_GeneralOptions
+SRC::var::base src/code/VarMod_GroundGPS
+SRC::var::base src/code/VarMod_HorizontalInterp
+SRC::var::base src/code/VarMod_HorizontalInterp_Adj
+SRC::var::base src/code/VarMod_InterpColumns
+SRC::var::base src/code/VarMod_InterpColumns_Adj
+SRC::var::base src/code/VarMod_LS
+SRC::var::base src/code/VarMod_LTinterp
+SRC::var::base src/code/VarMod_MOPS
+SRC::var::base src/code/VarMod_MPP
+SRC::var::base src/code/VarMod_Minimise
+SRC::var::base src/code/VarMod_ModelIO
+SRC::var::base src/code/VarMod_ObsControl
+SRC::var::base src/code/VarMod_ObsIO
+SRC::var::base src/code/VarMod_ObsInfo
+SRC::var::base src/code/VarMod_ObsOptions
+SRC::var::base src/code/VarMod_ObsUtility
+SRC::var::base src/code/VarMod_ObsUtility_Adj
+SRC::var::base src/code/VarMod_PF
+SRC::var::base src/code/VarMod_PFInfo
+SRC::var::base src/code/VarMod_PF_Adj
+SRC::var::base src/code/VarMod_Platform
+SRC::var::base src/code/VarMod_Precip
+SRC::var::base src/code/VarMod_PseudoOb
+SRC::var::base src/code/VarMod_QC
+SRC::var::base src/code/VarMod_Radar
+SRC::var::base src/code/VarMod_SBUV
+SRC::var::base src/code/VarMod_SSMI
+SRC::var::base src/code/VarMod_SatRad
+SRC::var::base src/code/VarMod_Satwind
+SRC::var::base src/code/VarMod_Scatwind
+SRC::var::base src/code/VarMod_Sonde
+SRC::var::base src/code/VarMod_Stats
+SRC::var::base src/code/VarMod_Surface
+SRC::var::base src/code/VarMod_TestCovNL
+SRC::var::base src/code/VarMod_TotalPenAndGrad
+SRC::var::base src/code/VarMod_TpTransform
+SRC::var::base src/code/VarMod_TransSpecH
+SRC::var::base src/code/VarMod_TransformInfo
+SRC::var::base src/code/VarMod_Transform_g
+SRC::var::base src/code/VarMod_Transform_h
+SRC::var::base src/code/VarMod_Transform_p
+SRC::var::base src/code/VarMod_Transform_v
+SRC::var::base src/code/VarMod_Transforms
+SRC::var::base src/code/VarMod_Trig
+SRC::var::base src/code/VarMod_UpPF
+SRC::var::base src/code/VarMod_UpPF_Adj
+SRC::var::base src/code/VarMod_UpTransform
+SRC::var::base src/code/VarMod_UpTransform_Adj
+SRC::var::base src/code/VarMod_VerticalInterp
+SRC::var::base src/code/VarMod_VerticalInterp_Adj
+SRC::var::base src/code/VarMod_Vis
+SRC::var::base src/code/VarMod_Vp
+SRC::var::base src/code/VarProg_AnalysePF
+SRC::var::base src/code/VarProg_CovAccStats
+SRC::var::base src/code/VarProg_CovPFstats
+SRC::var::base src/code/VarProg_SV
+SRC::var::base src/code/VarProg_TestCov
+SRC::var::base src/code/VarProg_TestPFModel
+SRC::var::base src/code/VarProg_UMFileUtils
+SRC::var::base src/code/Var_DiffOperators
+SRC::var::base src/code/Var_General
+SRC::var::base src/code/Var_Initialization
+SRC::var::base src/code/Var_Jc
+SRC::var::base src/code/Var_LAPACK
+SRC::var::base src/scripts/Var_Scripts
+
+REPOS::ops::base svn://fcm4/OPS_svn/OPS/trunk
+REVISION::ops::base 18341
+SRC::ops::base src/code/OpsMod_Constants
+SRC::ops::base src/code/OpsMod_Control
+SRC::ops::base src/code/OpsMod_GeoIR
+SRC::ops::base src/code/OpsMod_ObsInfo
+SRC::ops::base src/code/OpsMod_RTTOV
+SRC::ops::base src/code/OpsMod_Sort
+SRC::ops::base src/code/OpsMod_Utilities
+SRC::ops::base src/code/OpsMod_Varobs
+SRC::ops::base src/code/OpsMod_VerticalInterp
+SRC::ops::base src/code/OpsMod_VisControl
+SRC::ops::base src/code/OpsProg_RTTOV9
+SRC::ops::base src/code/Ops_AIRS_1DVar
+SRC::ops::base src/code/Ops_AIRS_Utilities
+SRC::ops::base src/code/Ops_RTTOV7
+SRC::ops::base src/code/Ops_RTTOV7_RTTOVCLD
+SRC::ops::base src/code/Ops_RTTOV9
+SRC::ops::base src/code/Ops_SatRad_Info
+SRC::ops::base src/code/Ops_SatRad_Process
+SRC::ops::base src/code/Ops_SatRad_SetUp
+SRC::ops::base src/code/Ops_SatRad_Utilities
+
+REPOS::gen::base svn://fcm1/GEN_svn/GEN/trunk
+REVISION::gen::base 3073
+SRC::gen::base src/code/GenMod_Constants
+SRC::gen::base src/code/GenMod_Control
+SRC::gen::base src/code/GenMod_FortranIO
+SRC::gen::base src/code/GenMod_GetEnv
+SRC::gen::base src/code/GenMod_ModelIO
+SRC::gen::base src/code/GenMod_Platform
+SRC::gen::base src/code/GenMod_Reporting
+SRC::gen::base src/code/GenMod_Trace
+SRC::gen::base src/code/GenMod_UMConstants
+SRC::gen::base src/code/GenMod_Utilities
+SRC::gen::base src/code/Reconfiguration
+SRC::gen::base src/code/UM_COEX
+SRC::gen::base src/code/UM_General
+SRC::gen::base src/code/UM_Platform
+
+REPOS::da::base svn://fcm5/DA_svn/DA/trunk
+REVISION::da::base 258
+
+REPOS::varadmin::base svn://fcm5/VAR_svn/Admin/trunk
+REVISION::varadmin::base 14851
+SRC::varadmin::base src/code/VarMod_Blas
+SRC::varadmin::base src/code/VarMod_Lapack
+
+REPOS::gcom::base svn://fcm2/UM_svn/GCOM/branches/dev/ibmjb/r12957_2194_ralltoalle_out_of_order
+REVISION::gcom::base 15824
+SRC::gcom::base build/gc
+SRC::gcom::base build/gcg
+SRC::gcom::base build/include
+SRC::gcom::base build/mpl
diff --git a/test/repos/trunk/cfg/fcm1_var_hpc.cfg b/test/repos/trunk/cfg/fcm1_var_hpc.cfg
new file mode 100644
index 0000000..b7f648e
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm1_var_hpc.cfg
@@ -0,0 +1,238 @@
+# ------------------------------------------------------------------------------
+# File header
+# ------------------------------------------------------------------------------
+
+CFG::TYPE ext
+CFG::VERSION 1.0
+
+# ------------------------------------------------------------------------------
+# Destination
+# ------------------------------------------------------------------------------
+
+DEST $PWD
+RDEST $THIS_RUN_DIR_HPC
+RDEST::MACHINE $HPC
+
+# ------------------------------------------------------------------------------
+# Build declarations
+# ------------------------------------------------------------------------------
+
+bld::excl_dep USE::F90_UNIX_IO
+bld::excl_dep USE::XLFUTILITY
+bld::excl_dep INC::mpif.h
+bld::exe_dep gcom varadmin::src::code::VarMod_Lapack varadmin::src::code::VarMod_Blas
+bld::pp::gcom 1
+bld::target VarScr_HelpCompile
+bld::tool::cc xlc
+bld::tool::cppkeys LOWERCASE
+bld::tool::cppkeys::gen::src::code::GenMod_Platform LOWERCASE FRL8 C_LONG_INT
+bld::tool::cppkeys::gen::src::code::UM_Platform VAROPSVER C_LOW FRL8 C_LONG_INT
+bld::tool::fc mpxlf95_r
+bld::tool::fc_define -WF,-D
+bld::tool::fflags -O0 -qrealsize=8 -qintsize=8 -g -qfullpath -qessl -qarch=pwr6 -qtune=pwr6 -qmaxmem=-1
+bld::tool::fflags::var::src::code::PFMod_Model -O0 -qrealsize=8 -qintsize=8 -g -qfullpath -qessl -qarch=pwr6 -qtune=pwr6 -qmaxmem=-1 -NS1024
+bld::tool::fflags::gcom::build::gc -O0 -qrealsize=8 -qintsize=8 -g -qfullpath -qessl -qarch=pwr6 -qtune=pwr6 -qmaxmem=-1 -qfixed
+bld::tool::fflags::gcom::build::gcg -O0 -qrealsize=8 -qintsize=8 -g -qfullpath -qessl -qarch=pwr6 -qtune=pwr6 -qmaxmem=-1 -qfixed
+bld::tool::fflags::gcom::build::include -O0 -qrealsize=8 -qintsize=8 -g -qfullpath -qessl -qarch=pwr6 -qtune=pwr6 -qmaxmem=-1 -qfixed
+bld::tool::fflags::gen::src::code::GenMod_Reporting::GenMod_Reporting -O0 -qrealsize=8 -qintsize=8 -g -qfullpath -qessl -qarch=pwr6 -qtune=pwr6 -qmaxmem=-1 -WF,-qfpp
+bld::tool::fflags::gen::src::code::GenMod_Utilities::Gen_FlushUnit -O0 -qrealsize=8 -qintsize=8 -g -qfullpath -qessl -qarch=pwr6 -qtune=pwr6 -qmaxmem=-1 -WF,-qfpp
+bld::tool::fflags::gen::src::code::UM_COEX -O0 -qrealsize=8 -qintsize=8 -g -qfullpath -qessl -qarch=pwr6 -qtune=pwr6 -qmaxmem=-1 -qfixed -WF,-qfpp
+bld::tool::fflags::gen::src::code::UM_General -O0 -qrealsize=8 -qintsize=8 -g -qfullpath -qessl -qarch=pwr6 -qtune=pwr6 -qmaxmem=-1 -qfixed
+bld::tool::fflags::gen::src::code::UM_Platform -O0 -qrealsize=8 -qintsize=8 -g -qfullpath -qessl -qarch=pwr6 -qtune=pwr6 -qmaxmem=-1 -qfixed -WF,-qfpp
+bld::tool::fflags::gen::src::code::UM_Platform::IOERROR -O0 -qrealsize=8 -qintsize=8 -g -qfullpath -qessl -qarch=pwr6 -qtune=pwr6 -qmaxmem=-1 -qfixed -WF,-qfpp -qfree=f90
+bld::tool::fflags::var::src::code::VarMod_CovVertical -O0 -qrealsize=8 -qintsize=8 -g -qfullpath -qessl -qarch=pwr6 -qtune=pwr6 -qmaxmem=-1 -qnoessl
+bld::tool::fflags::var::src::code::VarMod_CovVerticalData -O0 -qrealsize=8 -qintsize=8 -g -qfullpath -qessl -qarch=pwr6 -qtune=pwr6 -qmaxmem=-1 -qnoessl
+bld::tool::fflags::varadmin::src::code::VarMod_Blas -O0 -qrealsize=8 -qintsize=8 -g -qfullpath -qessl -qarch=pwr6 -qtune=pwr6 -qmaxmem=-1 -qfixed -qrealsize=4 -qstrict
+bld::tool::fflags::varadmin::src::code::VarMod_Lapack -O0 -qrealsize=8 -qintsize=8 -g -qfullpath -qessl -qarch=pwr6 -qtune=pwr6 -qmaxmem=-1 -qfixed -qrealsize=4 -qstrict
+bld::tool::fppkeys
+bld::tool::fppkeys::gcom::build GC_VERSION="'3.4+'" GC_BUILD_DATE="'15824'" PREC_64B GC__FLUSHUNIT6 GC__FORTERRUNIT=0 MPI_SRC MPILIB_32B GC_DESCRIP="'MPP'" MPI_BSEND_BUFFER_SIZE=10240000 IBM
+bld::tool::fppkeys::gen::src::code::GenMod_Control GCOMHEADERS
+bld::tool::fppkeys::gen::src::code::GenMod_Control::Gen_SetupControl USE_CUSTOM_SIGNAL_HANDLER AIX
+bld::tool::fppkeys::gen::src::code::GenMod_Reporting::GenMod_Reporting GEN_LEN_ERROR_OUT=134
+bld::tool::fppkeys::gen::src::code::GenMod_Utilities::Gen_FlushUnit USE_FLUSH AIX
+bld::tool::fppkeys::gen::src::code::UM_COEX VAROPSVER
+bld::tool::fppkeys::gen::src::code::UM_Platform VAROPSVER
+bld::tool::fppkeys::ops::src::code::Ops_RTTOV9 RTTOV_ARCH_VECTOR
+bld::tool::fppkeys::ops::src::code::Ops_RTTOV9::rttov9_parkind1 DEFAULT_INTEGER_32BIT
+bld::tool::ldflags -L$(UMDIR)/lib -lsig -lessl -lmassvp6 -lmass
+bld::tool::make gmake
+
+bld::pp::ops::src::code::Ops_RTTOV9::rttov9_parallel_direct 1
+bld::pp::ops::src::code::Ops_RTTOV9::rttov9_parallel_ad 1
+bld::pp::ops::src::code::Ops_RTTOV9::rttov9_parallel_tl 1
+bld::pp::ops::src::code::Ops_RTTOV9::rttov9_parallel_k 1
+bld::tool::fppkeys::ops::src::code::Ops_RTTOV9::rttov9_parallel_ad _RTTOV_PARALLEL_AD
+bld::tool::fppkeys::ops::src::code::Ops_RTTOV9::rttov9_parallel_direct _RTTOV_PARALLEL_DIRECT
+bld::tool::fppkeys::ops::src::code::Ops_RTTOV9::rttov9_parallel_k _RTTOV_PARALLEL_K
+bld::tool::fppkeys::ops::src::code::Ops_RTTOV9::rttov9_parallel_tl _RTTOV_PARALLEL_TL
+
+# ------------------------------------------------------------------------------
+# Project and branches
+# ------------------------------------------------------------------------------
+
+REPOS::var::base svn://fcm5/VAR_svn/VAR/trunk
+REVISION::var::base 14844
+SRC::var::base src/code/NewDynamics
+SRC::var::base src/code/PFMod_Model
+SRC::var::base src/code/PFMod_SV
+SRC::var::base src/code/PF_Bdy_Layer
+SRC::var::base src/code/PF_Control
+SRC::var::base src/code/PF_EulerianAdv
+SRC::var::base src/code/PF_FirstGuessRhoW
+SRC::var::base src/code/PF_GcrSolver
+SRC::var::base src/code/PF_General
+SRC::var::base src/code/PF_Helmholtz
+SRC::var::base src/code/PF_IO
+SRC::var::base src/code/PF_Interpolation
+SRC::var::base src/code/PF_MPP
+SRC::var::base src/code/PF_MPPUtil
+SRC::var::base src/code/PF_MoistPhys
+SRC::var::base src/code/PF_SemiLagrangianTheta
+SRC::var::base src/code/PF_SemiLagrangianUV
+SRC::var::base src/code/PF_UVTransformation
+SRC::var::base src/code/PF_Update
+SRC::var::base src/code/PF_convec
+SRC::var::base src/code/SG_Interpolation
+SRC::var::base src/code/VarMod_ATOVS
+SRC::var::base src/code/VarMod_ATOVSRad
+SRC::var::base src/code/VarMod_Aircraft
+SRC::var::base src/code/VarMod_BalanceTerms
+SRC::var::base src/code/VarMod_BgPenAndGrad
+SRC::var::base src/code/VarMod_CovCheckTp
+SRC::var::base src/code/VarMod_CovCodes
+SRC::var::base src/code/VarMod_CovCond
+SRC::var::base src/code/VarMod_CovHorizontal
+SRC::var::base src/code/VarMod_CovOptions
+SRC::var::base src/code/VarMod_CovSample
+SRC::var::base src/code/VarMod_CovSpectral
+SRC::var::base src/code/VarMod_CovSpectralData
+SRC::var::base src/code/VarMod_CovStatsIO
+SRC::var::base src/code/VarMod_CovVariances
+SRC::var::base src/code/VarMod_CovVertical
+SRC::var::base src/code/VarMod_CovVerticalData
+SRC::var::base src/code/VarMod_DelSquaredFFT
+SRC::var::base src/code/VarMod_Diagnostics
+SRC::var::base src/code/VarMod_FieldOutput
+SRC::var::base src/code/VarMod_Fourier
+SRC::var::base src/code/VarMod_GPSRO
+SRC::var::base src/code/VarMod_GeneralOptions
+SRC::var::base src/code/VarMod_GroundGPS
+SRC::var::base src/code/VarMod_HorizontalInterp
+SRC::var::base src/code/VarMod_HorizontalInterp_Adj
+SRC::var::base src/code/VarMod_InterpColumns
+SRC::var::base src/code/VarMod_InterpColumns_Adj
+SRC::var::base src/code/VarMod_LS
+SRC::var::base src/code/VarMod_LTinterp
+SRC::var::base src/code/VarMod_MOPS
+SRC::var::base src/code/VarMod_MPP
+SRC::var::base src/code/VarMod_Minimise
+SRC::var::base src/code/VarMod_ModelIO
+SRC::var::base src/code/VarMod_ObsControl
+SRC::var::base src/code/VarMod_ObsIO
+SRC::var::base src/code/VarMod_ObsInfo
+SRC::var::base src/code/VarMod_ObsOptions
+SRC::var::base src/code/VarMod_ObsUtility
+SRC::var::base src/code/VarMod_ObsUtility_Adj
+SRC::var::base src/code/VarMod_PF
+SRC::var::base src/code/VarMod_PFInfo
+SRC::var::base src/code/VarMod_PF_Adj
+SRC::var::base src/code/VarMod_Platform
+SRC::var::base src/code/VarMod_Precip
+SRC::var::base src/code/VarMod_PseudoOb
+SRC::var::base src/code/VarMod_QC
+SRC::var::base src/code/VarMod_Radar
+SRC::var::base src/code/VarMod_SBUV
+SRC::var::base src/code/VarMod_SSMI
+SRC::var::base src/code/VarMod_SatRad
+SRC::var::base src/code/VarMod_Satwind
+SRC::var::base src/code/VarMod_Scatwind
+SRC::var::base src/code/VarMod_Sonde
+SRC::var::base src/code/VarMod_Stats
+SRC::var::base src/code/VarMod_Surface
+SRC::var::base src/code/VarMod_TestCovNL
+SRC::var::base src/code/VarMod_TotalPenAndGrad
+SRC::var::base src/code/VarMod_TpTransform
+SRC::var::base src/code/VarMod_TransSpecH
+SRC::var::base src/code/VarMod_TransformInfo
+SRC::var::base src/code/VarMod_Transform_g
+SRC::var::base src/code/VarMod_Transform_h
+SRC::var::base src/code/VarMod_Transform_p
+SRC::var::base src/code/VarMod_Transform_v
+SRC::var::base src/code/VarMod_Transforms
+SRC::var::base src/code/VarMod_Trig
+SRC::var::base src/code/VarMod_UpPF
+SRC::var::base src/code/VarMod_UpPF_Adj
+SRC::var::base src/code/VarMod_UpTransform
+SRC::var::base src/code/VarMod_UpTransform_Adj
+SRC::var::base src/code/VarMod_VerticalInterp
+SRC::var::base src/code/VarMod_VerticalInterp_Adj
+SRC::var::base src/code/VarMod_Vis
+SRC::var::base src/code/VarMod_Vp
+SRC::var::base src/code/VarProg_AnalysePF
+SRC::var::base src/code/VarProg_CovAccStats
+SRC::var::base src/code/VarProg_CovPFstats
+SRC::var::base src/code/VarProg_SV
+SRC::var::base src/code/VarProg_TestCov
+SRC::var::base src/code/VarProg_TestPFModel
+SRC::var::base src/code/VarProg_UMFileUtils
+SRC::var::base src/code/Var_DiffOperators
+SRC::var::base src/code/Var_General
+SRC::var::base src/code/Var_Initialization
+SRC::var::base src/code/Var_Jc
+SRC::var::base src/code/Var_LAPACK
+SRC::var::base src/scripts/Var_Scripts
+
+REPOS::ops::base svn://fcm4/OPS_svn/OPS/trunk
+REVISION::ops::base 18341
+SRC::ops::base src/code/OpsMod_Constants
+SRC::ops::base src/code/OpsMod_Control
+SRC::ops::base src/code/OpsMod_GeoIR
+SRC::ops::base src/code/OpsMod_ObsInfo
+SRC::ops::base src/code/OpsMod_RTTOV
+SRC::ops::base src/code/OpsMod_Sort
+SRC::ops::base src/code/OpsMod_Utilities
+SRC::ops::base src/code/OpsMod_Varobs
+SRC::ops::base src/code/OpsMod_VerticalInterp
+SRC::ops::base src/code/OpsMod_VisControl
+SRC::ops::base src/code/OpsProg_RTTOV9
+SRC::ops::base src/code/Ops_AIRS_1DVar
+SRC::ops::base src/code/Ops_AIRS_Utilities
+SRC::ops::base src/code/Ops_RTTOV7
+SRC::ops::base src/code/Ops_RTTOV7_RTTOVCLD
+SRC::ops::base src/code/Ops_RTTOV9
+SRC::ops::base src/code/Ops_SatRad_Info
+SRC::ops::base src/code/Ops_SatRad_Process
+SRC::ops::base src/code/Ops_SatRad_SetUp
+SRC::ops::base src/code/Ops_SatRad_Utilities
+
+REPOS::gen::base svn://fcm1/GEN_svn/GEN/trunk
+REVISION::gen::base 3073
+SRC::gen::base src/code/GenMod_Constants
+SRC::gen::base src/code/GenMod_Control
+SRC::gen::base src/code/GenMod_FortranIO
+SRC::gen::base src/code/GenMod_GetEnv
+SRC::gen::base src/code/GenMod_ModelIO
+SRC::gen::base src/code/GenMod_Platform
+SRC::gen::base src/code/GenMod_Reporting
+SRC::gen::base src/code/GenMod_Trace
+SRC::gen::base src/code/GenMod_UMConstants
+SRC::gen::base src/code/GenMod_Utilities
+SRC::gen::base src/code/Reconfiguration
+SRC::gen::base src/code/UM_COEX
+SRC::gen::base src/code/UM_General
+SRC::gen::base src/code/UM_Platform
+
+REPOS::da::base svn://fcm5/DA_svn/DA/trunk
+REVISION::da::base 258
+
+REPOS::varadmin::base svn://fcm5/VAR_svn/Admin/trunk
+REVISION::varadmin::base 14851
+SRC::varadmin::base src/code/VarMod_Blas
+SRC::varadmin::base src/code/VarMod_Lapack
+
+REPOS::gcom::base svn://fcm2/UM_svn/GCOM/branches/dev/ibmjb/r12957_2194_ralltoalle_out_of_order
+REVISION::gcom::base 15824
+SRC::gcom::base build/gc
+SRC::gcom::base build/gcg
+SRC::gcom::base build/include
+SRC::gcom::base build/mpl
diff --git a/test/repos/trunk/cfg/fcm2_add_directory_expsrc.cfg b/test/repos/trunk/cfg/fcm2_add_directory_expsrc.cfg
new file mode 100644
index 0000000..298f275
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_add_directory_expsrc.cfg
@@ -0,0 +1,3 @@
+include = $HERE/fcm2_base.cfg
+
+extract.location{diff}[test_suite] = branches/dev/Share/add_directory
diff --git a/test/repos/trunk/cfg/fcm2_add_file.cfg b/test/repos/trunk/cfg/fcm2_add_file.cfg
new file mode 100644
index 0000000..3320192
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_add_file.cfg
@@ -0,0 +1,3 @@
+include = $HERE/fcm2_base.cfg
+
+extract.location{diff}[test_suite] = branches/dev/Share/add_file
diff --git a/test/repos/trunk/cfg/fcm2_add_file_inherit.cfg b/test/repos/trunk/cfg/fcm2_add_file_inherit.cfg
new file mode 100644
index 0000000..b13af23
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_add_file_inherit.cfg
@@ -0,0 +1,3 @@
+use = $RUN_DIR/fcm2_base
+
+extract.location{diff}[test_suite] = branches/dev/Share/add_file
diff --git a/test/repos/trunk/cfg/fcm2_base.cfg b/test/repos/trunk/cfg/fcm2_base.cfg
new file mode 100644
index 0000000..6454b65
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_base.cfg
@@ -0,0 +1,3 @@
+include = $HERE/fcm2_base_inc.cfg
+
+build.target = hello.sh test_suite/namelist/.etc
diff --git a/test/repos/trunk/cfg/fcm2_base_inc.cfg b/test/repos/trunk/cfg/fcm2_base_inc.cfg
new file mode 100644
index 0000000..6589f6c
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_base_inc.cfg
@@ -0,0 +1,6 @@
+steps = extract preprocess build
+
+extract.ns = test_suite
+extract.path-excl[test_suite] = cfg
+
+include = $HERE/fcm2_base_inc2.cfg
diff --git a/test/repos/trunk/cfg/fcm2_base_inc2.cfg b/test/repos/trunk/cfg/fcm2_base_inc2.cfg
new file mode 100644
index 0000000..e3a4925
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_base_inc2.cfg
@@ -0,0 +1,15 @@
+preprocess.ns-excl = / test_suite/subroutine/hello_c.c
+preprocess.ns-incl = test_suite/subroutine test_suite/program
+preprocess.prop{fpp} = wrap_pp
+preprocess.prop{fpp.defs}[test_suite/subroutine/hello_sub.F90] = HELLO_SUB
+preprocess.prop{fpp.defs}[test_suite/program/hello.F90] = CALL_HELLO_SUB
+
+build.prop{file-ext.script} = .pro
+build.prop{fc} = wrap_fc
+$fcflags{?} = -assume nosource_include
+build.prop{ fc.flags } = $fcflags
+build.prop{fc.flags}[ test_suite/subroutine ] = $fcflags -O3
+build.prop {cc} = wrap_cc
+build.prop{cc.flags}=-O3
+build.prop{ar} = wrap_ar
+build.prop{dep.o.special} [test_suite/program] = hello_blockdata.o
diff --git a/test/repos/trunk/cfg/fcm2_branches_clash.cfg b/test/repos/trunk/cfg/fcm2_branches_clash.cfg
new file mode 100644
index 0000000..750597b
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_branches_clash.cfg
@@ -0,0 +1,5 @@
+include = $HERE/fcm2_base.cfg
+
+extract.location{diff}[test_suite] = \
+ branches/dev/Share/modify_files_base \
+ branches/dev/Share/modify_files_clash
diff --git a/test/repos/trunk/cfg/fcm2_branches_merge.cfg b/test/repos/trunk/cfg/fcm2_branches_merge.cfg
new file mode 100644
index 0000000..fa18ec5
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_branches_merge.cfg
@@ -0,0 +1,6 @@
+include = $HERE/fcm2_base.cfg
+
+extract.location{diff}[test_suite] = \
+ branches/dev/Share/modify_files_base \
+ branches/dev/Share/modify_files_merge1 \
+ branches/dev/Share/modify_files_merge2
diff --git a/test/repos/trunk/cfg/fcm2_branches_merge_duplicate.cfg b/test/repos/trunk/cfg/fcm2_branches_merge_duplicate.cfg
new file mode 100644
index 0000000..10f05f8
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_branches_merge_duplicate.cfg
@@ -0,0 +1,6 @@
+include = $HERE/fcm2_base.cfg
+
+extract.location{diff}[test_suite] = \
+ branches/dev/Share/add_file \
+ branches/dev/Share/add_lines \
+ branches/dev/Share/add_lines
diff --git a/test/repos/trunk/cfg/fcm2_branches_merge_inherit.cfg b/test/repos/trunk/cfg/fcm2_branches_merge_inherit.cfg
new file mode 100644
index 0000000..4862e5a
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_branches_merge_inherit.cfg
@@ -0,0 +1,6 @@
+use = $RUN_DIR/fcm2_base
+
+extract.location{diff}[test_suite] = \
+ branches/dev/Share/modify_files_base \
+ branches/dev/Share/modify_files_merge1 \
+ branches/dev/Share/modify_files_merge2
diff --git a/test/repos/trunk/cfg/fcm2_branches_merge_inherit_wrong_include.cfg b/test/repos/trunk/cfg/fcm2_branches_merge_inherit_wrong_include.cfg
new file mode 100644
index 0000000..8372e63
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_branches_merge_inherit_wrong_include.cfg
@@ -0,0 +1,8 @@
+use = $RUN_DIR/fcm2_base
+
+extract.location{diff}[test_suite] = \
+ branches/dev/Share/modify_files_base \
+ branches/dev/Share/modify_files_merge1 \
+ branches/dev/Share/modify_files_merge2
+
+build.prop{fc.flags}[test_suite/module] = -O3
diff --git a/test/repos/trunk/cfg/fcm2_branches_merge_wcopies.cfg b/test/repos/trunk/cfg/fcm2_branches_merge_wcopies.cfg
new file mode 100644
index 0000000..19b904d
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_branches_merge_wcopies.cfg
@@ -0,0 +1,6 @@
+include = $HERE/fcm2_base.cfg
+
+extract.location{diff}[test_suite] = \
+ $BASE_DIR/work/b1 \
+ $BASE_DIR/work/b2 \
+ $BASE_DIR/work/b3
diff --git a/test/repos/trunk/cfg/fcm2_branches_merge_wcopy.cfg b/test/repos/trunk/cfg/fcm2_branches_merge_wcopy.cfg
new file mode 100644
index 0000000..73b2231
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_branches_merge_wcopy.cfg
@@ -0,0 +1,6 @@
+include = $HERE/fcm2_base.cfg
+
+extract.location{diff}[test_suite] = \
+ branches/dev/Share/modify_files_base \
+ branches/dev/Share/modify_files_merge1 \
+ $BASE_DIR/work
diff --git a/test/repos/trunk/cfg/fcm2_cflags.cfg b/test/repos/trunk/cfg/fcm2_cflags.cfg
new file mode 100644
index 0000000..e50e0c6
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_cflags.cfg
@@ -0,0 +1,3 @@
+include = $HERE/fcm2_base.cfg
+
+build.prop{cc.flags}[test_suite/subroutine] = -O2 -g
diff --git a/test/repos/trunk/cfg/fcm2_change_variable.cfg b/test/repos/trunk/cfg/fcm2_change_variable.cfg
new file mode 100644
index 0000000..1602432
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_change_variable.cfg
@@ -0,0 +1,4 @@
+include = $HERE/fcm2_base.cfg
+
+$fcflags = -assume nosource_include -g
+build.prop{fc.flags}[test_suite/subroutine/hello_sub.F90] = $fcflags -O2
diff --git a/test/repos/trunk/cfg/fcm2_cyclic_dep_fail.cfg b/test/repos/trunk/cfg/fcm2_cyclic_dep_fail.cfg
new file mode 100644
index 0000000..ab8bed3
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_cyclic_dep_fail.cfg
@@ -0,0 +1,3 @@
+include = $HERE/fcm2_base.cfg
+
+extract.location{diff}[test_suite] = branches/dev/Share/cyclic_dep_fail
diff --git a/test/repos/trunk/cfg/fcm2_cyclic_dep_ok.cfg b/test/repos/trunk/cfg/fcm2_cyclic_dep_ok.cfg
new file mode 100644
index 0000000..a640a6b
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_cyclic_dep_ok.cfg
@@ -0,0 +1,3 @@
+include = $HERE/fcm2_base.cfg
+
+extract.location{diff}[test_suite] = branches/dev/Share/cyclic_dep_ok
diff --git a/test/repos/trunk/cfg/fcm2_delete_directory.cfg b/test/repos/trunk/cfg/fcm2_delete_directory.cfg
new file mode 100644
index 0000000..5573ae4
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_delete_directory.cfg
@@ -0,0 +1,21 @@
+steps = extract preprocess build
+
+extract.ns = test_suite
+extract.path-excl[test_suite] = cfg
+extract.location{diff}[test_suite] = \
+ branches/dev/Share/modify_files_base \
+ branches/dev/Share/delete_directory
+
+preprocess.ns-excl = / test_suite/subroutine/hello_c.c
+preprocess.ns-incl = test_suite/subroutine test_suite/program
+preprocess.prop{fpp} = wrap_pp
+
+build.prop{file-ext.script} = .pro
+build.prop{fc} = wrap_fc
+$fcflags{?} = -assume nosource_include
+build.prop{fc.flags} = $fcflags
+build.prop{cc} = wrap_cc
+build.prop{cc.flags} = -O3
+build.prop{ar} = wrap_ar
+build.prop{dep.o.special} = hello_blockdata.o
+build.target = hello.sh test_suite/namelist/.etc
diff --git a/test/repos/trunk/cfg/fcm2_delete_directory_inherit.cfg b/test/repos/trunk/cfg/fcm2_delete_directory_inherit.cfg
new file mode 100644
index 0000000..c307c77
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_delete_directory_inherit.cfg
@@ -0,0 +1,7 @@
+use = $RUN_DIR/fcm2_base
+
+preprocess.prop{fpp.defs}[test_suite/program/hello.F90] =
+
+extract.location{diff}[test_suite] = \
+ branches/dev/Share/modify_files_base \
+ branches/dev/Share/delete_directory
diff --git a/test/repos/trunk/cfg/fcm2_delete_file.cfg b/test/repos/trunk/cfg/fcm2_delete_file.cfg
new file mode 100644
index 0000000..16d8969
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_delete_file.cfg
@@ -0,0 +1,6 @@
+include = $HERE/fcm2_base.cfg
+
+extract.location{diff}[test_suite] = \
+ branches/dev/Share/modify_files_base \
+ branches/dev/Share/modify_files_merge1 \
+ branches/dev/Share/delete_file
diff --git a/test/repos/trunk/cfg/fcm2_delete_file_inherit.cfg b/test/repos/trunk/cfg/fcm2_delete_file_inherit.cfg
new file mode 100644
index 0000000..065cd02
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_delete_file_inherit.cfg
@@ -0,0 +1,6 @@
+use = $RUN_DIR/fcm2_base
+
+extract.location{diff}[test_suite] = \
+ branches/dev/Share/modify_files_base \
+ branches/dev/Share/modify_files_merge1 \
+ branches/dev/Share/delete_file
diff --git a/test/repos/trunk/cfg/fcm2_delete_inc_file.cfg b/test/repos/trunk/cfg/fcm2_delete_inc_file.cfg
new file mode 100644
index 0000000..cb3288d
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_delete_inc_file.cfg
@@ -0,0 +1,3 @@
+include = $HERE/fcm2_base.cfg
+
+extract.location{diff}[test_suite] = branches/dev/Share/delete_inc_file
diff --git a/test/repos/trunk/cfg/fcm2_delete_inc_file_inherit.cfg b/test/repos/trunk/cfg/fcm2_delete_inc_file_inherit.cfg
new file mode 100644
index 0000000..d0e50d9
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_delete_inc_file_inherit.cfg
@@ -0,0 +1,3 @@
+use = $RUN_DIR/fcm2_base
+
+extract.location{diff}[test_suite] = branches/dev/Share/delete_inc_file
diff --git a/test/repos/trunk/cfg/fcm2_delete_inc_file_inherit_force.cfg b/test/repos/trunk/cfg/fcm2_delete_inc_file_inherit_force.cfg
new file mode 100644
index 0000000..aec0417
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_delete_inc_file_inherit_force.cfg
@@ -0,0 +1,5 @@
+use = $RUN_DIR/fcm2_base
+
+extract.location{diff}[test_suite] = branches/dev/Share/delete_inc_file
+
+build.prop{fc.flags}[test_suite/module] = -assume nosource_include -O3
diff --git a/test/repos/trunk/cfg/fcm2_delete_pp_file.cfg b/test/repos/trunk/cfg/fcm2_delete_pp_file.cfg
new file mode 100644
index 0000000..c50db4c
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_delete_pp_file.cfg
@@ -0,0 +1,24 @@
+steps = extract preprocess build
+
+extract.ns = test_suite
+extract.path-excl[test_suite] = cfg
+
+extract.location{diff}[test_suite] = \
+ branches/dev/Share/modify_files_base \
+ branches/dev/Share/modify_pp_include \
+ branches/dev/Share/delete_pp_file
+
+preprocess.ns-excl = / test_suite/subroutine/hello_c.c
+preprocess.ns-incl = test_suite/subroutine test_suite/program
+preprocess.prop{fpp} = wrap_pp
+preprocess.prop{fpp.defs}[test_suite/program/hello.F90] = CALL_HELLO_SUB
+
+build.target{task} = link
+build.prop{fc} = wrap_fc
+$fcflags{?} = -assume nosource_include
+build.prop{fc.flags} = $fcflags
+build.prop{fc.flags}[test_suite/subroutine] = $fcflags -O3
+build.prop{cc} = wrap_cc
+build.prop{cc.flags} = -O3
+build.prop{ar} = wrap_ar
+build.prop{dep.o.special} = hello_blockdata.o
diff --git a/test/repos/trunk/cfg/fcm2_delete_pp_file_inherit.cfg b/test/repos/trunk/cfg/fcm2_delete_pp_file_inherit.cfg
new file mode 100644
index 0000000..ef11840
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_delete_pp_file_inherit.cfg
@@ -0,0 +1,6 @@
+use = $RUN_DIR/fcm2_base
+
+extract.location{diff}[test_suite] = \
+ branches/dev/Share/modify_files_base \
+ branches/dev/Share/modify_pp_include \
+ branches/dev/Share/delete_pp_file
diff --git a/test/repos/trunk/cfg/fcm2_delete_ppinc_file.cfg b/test/repos/trunk/cfg/fcm2_delete_ppinc_file.cfg
new file mode 100644
index 0000000..ff4b539
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_delete_ppinc_file.cfg
@@ -0,0 +1,3 @@
+include = $HERE/fcm2_base.cfg
+
+extract.location{diff}[test_suite] = branches/dev/Share/delete_ppinc_file
diff --git a/test/repos/trunk/cfg/fcm2_delete_ppinc_file_inherit.cfg b/test/repos/trunk/cfg/fcm2_delete_ppinc_file_inherit.cfg
new file mode 100644
index 0000000..53bce12
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_delete_ppinc_file_inherit.cfg
@@ -0,0 +1,3 @@
+use = $RUN_DIR/fcm2_base
+
+extract.location{diff}[test_suite] = branches/dev/Share/delete_ppinc_file
diff --git a/test/repos/trunk/cfg/fcm2_delete_ppinc_file_inherit_force.cfg b/test/repos/trunk/cfg/fcm2_delete_ppinc_file_inherit_force.cfg
new file mode 100644
index 0000000..a637c8e
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_delete_ppinc_file_inherit_force.cfg
@@ -0,0 +1,5 @@
+use = $RUN_DIR/fcm2_base
+
+extract.location{diff}[test_suite] = branches/dev/Share/delete_ppinc_file
+
+preprocess.prop{fpp.defs}[test_suite/subroutine/hello_sub.F90] = HELLO_SUB DUMMY
diff --git a/test/repos/trunk/cfg/fcm2_dep_o.cfg b/test/repos/trunk/cfg/fcm2_dep_o.cfg
new file mode 100644
index 0000000..1952e47
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_dep_o.cfg
@@ -0,0 +1,5 @@
+include = $HERE/fcm2_base.cfg
+
+extract.location{diff}[test_suite] = branches/dev/Share/f77_dep
+
+build.prop{dep.o} [test_suite/program] = hello_sub.o
diff --git a/test/repos/trunk/cfg/fcm2_dep_o_all.cfg b/test/repos/trunk/cfg/fcm2_dep_o_all.cfg
new file mode 100644
index 0000000..8b0d5cd
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_dep_o_all.cfg
@@ -0,0 +1,5 @@
+include = $HERE/fcm2_base.cfg
+
+extract.location{diff}[test_suite] = branches/dev/Share/f77_dep
+
+build.prop{dep.o} = hello_sub.o
diff --git a/test/repos/trunk/cfg/fcm2_dep_o_invalid.cfg b/test/repos/trunk/cfg/fcm2_dep_o_invalid.cfg
new file mode 100644
index 0000000..87b4110
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_dep_o_invalid.cfg
@@ -0,0 +1,5 @@
+include = $HERE/fcm2_base.cfg
+
+extract.location{diff}[test_suite] = branches/dev/Share/f77_dep
+
+build.prop{dep.o} [test_suite/program] = invalid.o
diff --git a/test/repos/trunk/cfg/fcm2_duplicate_target.cfg b/test/repos/trunk/cfg/fcm2_duplicate_target.cfg
new file mode 100644
index 0000000..4264af6
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_duplicate_target.cfg
@@ -0,0 +1,4 @@
+include = $HERE/fcm2_base.cfg
+
+extract.location{diff}[test_suite] = branches/dev/Share/add_duplicate
+preprocess.prop{fpp.defs}[test_suite/subroutine/hello_sub2.F90] = HELLO_SUB
diff --git a/test/repos/trunk/cfg/fcm2_exclude_dependency.cfg b/test/repos/trunk/cfg/fcm2_exclude_dependency.cfg
new file mode 100644
index 0000000..03b740c
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_exclude_dependency.cfg
@@ -0,0 +1,3 @@
+include = $HERE/fcm2_base.cfg
+
+build.prop{no-dep.f.module} = hello_constants
diff --git a/test/repos/trunk/cfg/fcm2_exe_permissions.cfg b/test/repos/trunk/cfg/fcm2_exe_permissions.cfg
new file mode 100644
index 0000000..6b19e58
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_exe_permissions.cfg
@@ -0,0 +1,6 @@
+include = $HERE/fcm2_base_inc.cfg
+
+build.target = hello.sh test_suite/namelist/.etc
+build.target-rename = hello.exe:hello_world.exe
+
+extract.location{diff}[test_suite] = $BASE_DIR/work
diff --git a/test/repos/trunk/cfg/fcm2_exe_rename.cfg b/test/repos/trunk/cfg/fcm2_exe_rename.cfg
new file mode 100644
index 0000000..c36191f
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_exe_rename.cfg
@@ -0,0 +1,6 @@
+include = $HERE/fcm2_base_inc.cfg
+
+build.target = hello.sh test_suite/namelist/.etc
+build.target-rename = hello.exe:hello_world.exe
+
+extract.location{diff}[test_suite] = branches/dev/Share/exe_rename
diff --git a/test/repos/trunk/cfg/fcm2_extract_path_excl_no_ns.cfg b/test/repos/trunk/cfg/fcm2_extract_path_excl_no_ns.cfg
new file mode 100644
index 0000000..d2f8c4d
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_extract_path_excl_no_ns.cfg
@@ -0,0 +1,9 @@
+steps = extract preprocess build
+
+extract.ns = test_suite
+extract.path-excl = cfg
+extract.path-incl[test_suite] = cfg/fcm2_base.cfg
+
+build.target = hello.sh test_suite/namelist/.etc
+
+include = $HERE/fcm2_base_inc2.cfg
diff --git a/test/repos/trunk/cfg/fcm2_fc.cfg b/test/repos/trunk/cfg/fcm2_fc.cfg
new file mode 100644
index 0000000..cd03f67
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_fc.cfg
@@ -0,0 +1,3 @@
+include = $HERE/fcm2_base.cfg
+
+build.prop{fc} = wrap_fc2
diff --git a/test/repos/trunk/cfg/fcm2_fflags1.cfg b/test/repos/trunk/cfg/fcm2_fflags1.cfg
new file mode 100644
index 0000000..5d630e2
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_fflags1.cfg
@@ -0,0 +1,3 @@
+include = $HERE/fcm2_base.cfg
+
+build.prop{fc.flags}[test_suite] = $fcflags -O2 -g
diff --git a/test/repos/trunk/cfg/fcm2_fflags2.cfg b/test/repos/trunk/cfg/fcm2_fflags2.cfg
new file mode 100644
index 0000000..4bc74a2
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_fflags2.cfg
@@ -0,0 +1,3 @@
+include = $HERE/fcm2_base.cfg
+
+build.prop{fc.flags}[test_suite/subroutine/hello_sub.F90] = -O2 -g
diff --git a/test/repos/trunk/cfg/fcm2_fflags_inherit.cfg b/test/repos/trunk/cfg/fcm2_fflags_inherit.cfg
new file mode 100644
index 0000000..58094b2
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_fflags_inherit.cfg
@@ -0,0 +1,3 @@
+use = $RUN_DIR/fcm2_base
+
+build.prop{fc.flags}[test_suite/subroutine/hello_sub.F90] = -O2 -g
diff --git a/test/repos/trunk/cfg/fcm2_flag-output.cfg b/test/repos/trunk/cfg/fcm2_flag-output.cfg
new file mode 100644
index 0000000..11dfd31
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_flag-output.cfg
@@ -0,0 +1,3 @@
+include = $HERE/fcm2_base.cfg
+
+build.prop{fc.flag-output} = -o %s
diff --git a/test/repos/trunk/cfg/fcm2_inc_devnull.cfg b/test/repos/trunk/cfg/fcm2_inc_devnull.cfg
new file mode 100644
index 0000000..4c8d4c7
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_inc_devnull.cfg
@@ -0,0 +1,6 @@
+include = $HERE/fcm2_base_inc.cfg
+
+# The UM makes use of the following so we have to support it
+include = /dev/null
+
+build.target = hello.sh test_suite/namelist/.etc
diff --git a/test/repos/trunk/cfg/fcm2_inherit_invalid_path.cfg b/test/repos/trunk/cfg/fcm2_inherit_invalid_path.cfg
new file mode 100644
index 0000000..eebf917
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_inherit_invalid_path.cfg
@@ -0,0 +1 @@
+use = /invalid/path
diff --git a/test/repos/trunk/cfg/fcm2_inherit_redefine_fail.cfg b/test/repos/trunk/cfg/fcm2_inherit_redefine_fail.cfg
new file mode 100644
index 0000000..dc033be
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_inherit_redefine_fail.cfg
@@ -0,0 +1,5 @@
+use = $RUN_DIR/fcm2_base
+
+include = $HERE/fcm2_base.cfg
+extract.path-excl[test_suite] = cfg namelist
+build.target = hello.sh
diff --git a/test/repos/trunk/cfg/fcm2_inherit_redefine_ok.cfg b/test/repos/trunk/cfg/fcm2_inherit_redefine_ok.cfg
new file mode 100644
index 0000000..761e715
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_inherit_redefine_ok.cfg
@@ -0,0 +1,3 @@
+use = $RUN_DIR/fcm2_base
+
+include = $HERE/fcm2_base.cfg
diff --git a/test/repos/trunk/cfg/fcm2_invalid_base_url.cfg b/test/repos/trunk/cfg/fcm2_invalid_base_url.cfg
new file mode 100644
index 0000000..742a2b2
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_invalid_base_url.cfg
@@ -0,0 +1,4 @@
+include = $HERE/fcm2_base.cfg
+steps = extract
+
+extract.location{primary}[test_suite] = fcm:test_suite/invalid
diff --git a/test/repos/trunk/cfg/fcm2_invalid_branch_url.cfg b/test/repos/trunk/cfg/fcm2_invalid_branch_url.cfg
new file mode 100644
index 0000000..9159101
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_invalid_branch_url.cfg
@@ -0,0 +1,4 @@
+include = $HERE/fcm2_base.cfg
+steps = extract
+
+extract.location{diff}[test_suite] = branches/dev/Share/invalid
diff --git a/test/repos/trunk/cfg/fcm2_invalid_branch_url2.cfg b/test/repos/trunk/cfg/fcm2_invalid_branch_url2.cfg
new file mode 100644
index 0000000..9e0cb95
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_invalid_branch_url2.cfg
@@ -0,0 +1,4 @@
+include = $HERE/fcm2_base.cfg
+steps = extract
+
+extract.location{diff}[test_suite] = /invalid/path
diff --git a/test/repos/trunk/cfg/fcm2_invalid_inc.cfg b/test/repos/trunk/cfg/fcm2_invalid_inc.cfg
new file mode 100644
index 0000000..2992dd3
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_invalid_inc.cfg
@@ -0,0 +1 @@
+include = $HERE/invalid.cfg
diff --git a/test/repos/trunk/cfg/fcm2_invalid_label.cfg b/test/repos/trunk/cfg/fcm2_invalid_label.cfg
new file mode 100644
index 0000000..7825698
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_invalid_label.cfg
@@ -0,0 +1,3 @@
+include = $HERE/fcm2_base.cfg
+
+build.prep{fc.flags}[test_suite/subroutine/hello_sub.f90] = -O2
diff --git a/test/repos/trunk/cfg/fcm2_invalid_modifier.cfg b/test/repos/trunk/cfg/fcm2_invalid_modifier.cfg
new file mode 100644
index 0000000..595f7cc
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_invalid_modifier.cfg
@@ -0,0 +1,3 @@
+include = $HERE/fcm2_base.cfg
+
+build.prop{fc.flegs}[test_suite/subroutine/hello_sub.f90] = -O2
diff --git a/test/repos/trunk/cfg/fcm2_invalid_modifiers.cfg b/test/repos/trunk/cfg/fcm2_invalid_modifiers.cfg
new file mode 100644
index 0000000..50d8219
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_invalid_modifiers.cfg
@@ -0,0 +1,3 @@
+include = $HERE/fcm2_base.cfg
+
+build.prop{fc.flags cc.flags}[test_suite/subroutine/hello_sub.f90] = -O2
diff --git a/test/repos/trunk/cfg/fcm2_invalid_namespace.cfg b/test/repos/trunk/cfg/fcm2_invalid_namespace.cfg
new file mode 100644
index 0000000..e71873c
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_invalid_namespace.cfg
@@ -0,0 +1,3 @@
+include = $HERE/fcm2_base.cfg
+
+build.prop{fc.flags}[test_suite/invalid] = -O2
diff --git a/test/repos/trunk/cfg/fcm2_invalid_namespace2.cfg b/test/repos/trunk/cfg/fcm2_invalid_namespace2.cfg
new file mode 100644
index 0000000..8d82253
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_invalid_namespace2.cfg
@@ -0,0 +1,3 @@
+include = $HERE/fcm2_base.cfg
+
+preprocess.ns-excl[test_suite] = /
diff --git a/test/repos/trunk/cfg/fcm2_invalid_target.cfg b/test/repos/trunk/cfg/fcm2_invalid_target.cfg
new file mode 100644
index 0000000..cad9008
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_invalid_target.cfg
@@ -0,0 +1,3 @@
+include = $HERE/fcm2_base_inc.cfg
+
+build.target = hello.sh invalid.sh
diff --git a/test/repos/trunk/cfg/fcm2_invalid_variable.cfg b/test/repos/trunk/cfg/fcm2_invalid_variable.cfg
new file mode 100644
index 0000000..1e0c272
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_invalid_variable.cfg
@@ -0,0 +1,5 @@
+include = $HERE/fcm2_base.cfg
+
+build.prop{fc.flags}[test_suite/subroutine/hello_sub.f90] = $INVALID
+
+$INVALID = too late
diff --git a/test/repos/trunk/cfg/fcm2_library.cfg b/test/repos/trunk/cfg/fcm2_library.cfg
new file mode 100644
index 0000000..494206c
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_library.cfg
@@ -0,0 +1,3 @@
+include = $HERE/fcm2_base_inc.cfg
+
+build.target = test_suite/libo.a
diff --git a/test/repos/trunk/cfg/fcm2_library_rename.cfg b/test/repos/trunk/cfg/fcm2_library_rename.cfg
new file mode 100644
index 0000000..e3ee0d4
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_library_rename.cfg
@@ -0,0 +1,4 @@
+include = $HERE/fcm2_base_inc.cfg
+
+build.target = mylib.a
+build.target-rename = test_suite/module/libo.a:mylib.a
diff --git a/test/repos/trunk/cfg/fcm2_mirror.cfg b/test/repos/trunk/cfg/fcm2_mirror.cfg
new file mode 100644
index 0000000..c1bb8c0
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_mirror.cfg
@@ -0,0 +1,6 @@
+include = $HERE/fcm2_base.cfg
+
+steps = extract mirror
+
+mirror.target = localhost:${THIS_RUN_DIR}_mirror
+mirror.prop{config-file.steps} = preprocess build
diff --git a/test/repos/trunk/cfg/fcm2_mirror_after_pp.cfg b/test/repos/trunk/cfg/fcm2_mirror_after_pp.cfg
new file mode 100644
index 0000000..7b6cdb6
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_mirror_after_pp.cfg
@@ -0,0 +1,6 @@
+include = $HERE/fcm2_base.cfg
+
+steps = extract preprocess mirror
+
+mirror.target = localhost:${THIS_RUN_DIR}_mirror
+mirror.prop{config-file.steps} = build
diff --git a/test/repos/trunk/cfg/fcm2_mirror_inherit.cfg b/test/repos/trunk/cfg/fcm2_mirror_inherit.cfg
new file mode 100644
index 0000000..7391d71
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_mirror_inherit.cfg
@@ -0,0 +1,8 @@
+use = $RUN_DIR/fcm2_mirror
+
+extract.location{diff}[test_suite] = \
+ branches/dev/Share/modify_files_base \
+ branches/dev/Share/modify_files_merge1 \
+ branches/dev/Share/modify_files_merge2
+
+mirror.target = localhost:${THIS_RUN_DIR}_mirror
diff --git a/test/repos/trunk/cfg/fcm2_mirror_inherit_fflags.cfg b/test/repos/trunk/cfg/fcm2_mirror_inherit_fflags.cfg
new file mode 100644
index 0000000..5b3d776
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_mirror_inherit_fflags.cfg
@@ -0,0 +1,6 @@
+use = $RUN_DIR/fcm2_mirror
+
+mirror.target = localhost:${THIS_RUN_DIR}_mirror
+
+include = $HERE/fcm2_base_inc2.cfg
+build.prop{fc.flags}[test_suite] = $fcflags -O2 -g
diff --git a/test/repos/trunk/cfg/fcm2_mirror_inherit_notarget.cfg b/test/repos/trunk/cfg/fcm2_mirror_inherit_notarget.cfg
new file mode 100644
index 0000000..7fd45ca
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_mirror_inherit_notarget.cfg
@@ -0,0 +1,6 @@
+use = $RUN_DIR/fcm2_mirror
+
+extract.location{diff}[test_suite] = \
+ branches/dev/Share/modify_files_base \
+ branches/dev/Share/modify_files_merge1 \
+ branches/dev/Share/modify_files_merge2
diff --git a/test/repos/trunk/cfg/fcm2_modify_subroutine_inherit.cfg b/test/repos/trunk/cfg/fcm2_modify_subroutine_inherit.cfg
new file mode 100644
index 0000000..ddc5110
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_modify_subroutine_inherit.cfg
@@ -0,0 +1,3 @@
+use = $RUN_DIR/fcm2_base
+
+extract.location{diff}[test_suite] = branches/dev/Share/modify_subroutine
diff --git a/test/repos/trunk/cfg/fcm2_modify_subroutine_interface_inherit.cfg b/test/repos/trunk/cfg/fcm2_modify_subroutine_interface_inherit.cfg
new file mode 100644
index 0000000..559fae6
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_modify_subroutine_interface_inherit.cfg
@@ -0,0 +1,3 @@
+use = $RUN_DIR/fcm2_base
+
+extract.location{diff}[test_suite] = branches/dev/Share/modify_subroutine_interface
diff --git a/test/repos/trunk/cfg/fcm2_multi_inherit.cfg b/test/repos/trunk/cfg/fcm2_multi_inherit.cfg
new file mode 100644
index 0000000..06b437c
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_multi_inherit.cfg
@@ -0,0 +1,4 @@
+use = $RUN_DIR/fcm2_fflags_inherit
+use = $RUN_DIR/fcm2_modify_subroutine_inherit
+
+build.prop{fc.flags}[test_suite/module] = -O2 -g
diff --git a/test/repos/trunk/cfg/fcm2_multiple_build.cfg b/test/repos/trunk/cfg/fcm2_multiple_build.cfg
new file mode 100644
index 0000000..ef1dedd
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_multiple_build.cfg
@@ -0,0 +1,16 @@
+include = $HERE/fcm2_base.cfg
+
+step.class[build2] = build
+
+steps = extract preprocess build build2
+
+build2.prop{file-ext.script} = .pro
+build2.prop{fc} = wrap_fc
+build2.prop{fc.flags} = $fcflags
+build2.prop{fc.flags}[test_suite/subroutine] = $fcflags -O3
+build2.prop{cc} = wrap_cc
+build2.prop{cc.flags} = -O3
+build2.prop{ar} = wrap_ar
+build2.prop{dep.o.special}[test_suite/program] = hello_blockdata.o
+build2.prop{fc.defs}[test_suite/blockdata] = ODD
+build2.target = hello.sh
diff --git a/test/repos/trunk/cfg/fcm2_multiple_build_inherit.cfg b/test/repos/trunk/cfg/fcm2_multiple_build_inherit.cfg
new file mode 100644
index 0000000..d133685
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_multiple_build_inherit.cfg
@@ -0,0 +1,6 @@
+use = $RUN_DIR/fcm2_multiple_build
+
+extract.location{diff}[test_suite] = \
+ branches/dev/Share/modify_files_base \
+ branches/dev/Share/modify_files_merge1 \
+ branches/dev/Share/modify_files_merge2
diff --git a/test/repos/trunk/cfg/fcm2_multiple_pp-build.cfg b/test/repos/trunk/cfg/fcm2_multiple_pp-build.cfg
new file mode 100644
index 0000000..a237557
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_multiple_pp-build.cfg
@@ -0,0 +1,25 @@
+include = $HERE/fcm2_base.cfg
+
+step.class[preprocess2] = preprocess
+step.class[build2] = build
+
+steps = extract preprocess build preprocess2 build2
+
+preprocess2.prop{no-step-source} = preprocess
+preprocess2.ns-excl = / test_suite/subroutine/hello_c.c
+preprocess2.ns-incl = test_suite/subroutine test_suite/program
+preprocess2.prop{fpp} = wrap_pp
+preprocess2.prop{fpp.defs}[test_suite/subroutine/hello_sub.F90] = HELLO_SUB
+preprocess2.prop{fpp.defs}[test_suite/program/hello.F90] = CALL_HELLO_SUB
+
+build2.prop{no-step-source} = preprocess
+build2.prop{file-ext.script} = .pro
+build2.prop{fc} = wrap_fc
+build2.prop{fc.flags} = $fcflags
+build2.prop{fc.flags}[test_suite/subroutine] = $fcflags -O3
+build2.prop{cc} = wrap_cc
+build2.prop{cc.flags} = -O3
+build2.prop{ar} = wrap_ar
+build2.prop{dep.o.special}[test_suite/program] = hello_blockdata.o
+build2.prop{fc.defs}[test_suite/blockdata] = ODD
+build2.target = hello.sh
diff --git a/test/repos/trunk/cfg/fcm2_multiple_pp-build_inherit.cfg b/test/repos/trunk/cfg/fcm2_multiple_pp-build_inherit.cfg
new file mode 100644
index 0000000..07e5a4a
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_multiple_pp-build_inherit.cfg
@@ -0,0 +1,6 @@
+use = $RUN_DIR/fcm2_multiple_pp-build
+
+extract.location{diff}[test_suite] = \
+ branches/dev/Share/modify_files_base \
+ branches/dev/Share/modify_files_merge1 \
+ branches/dev/Share/modify_files_merge2
diff --git a/test/repos/trunk/cfg/fcm2_no_dep.cfg b/test/repos/trunk/cfg/fcm2_no_dep.cfg
new file mode 100644
index 0000000..4657ee6
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_no_dep.cfg
@@ -0,0 +1,3 @@
+include = $HERE/fcm2_base.cfg
+
+build.prop{no-dep.bin} = *
diff --git a/test/repos/trunk/cfg/fcm2_ns-dep_o.cfg b/test/repos/trunk/cfg/fcm2_ns-dep_o.cfg
new file mode 100644
index 0000000..c2de1f8
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_ns-dep_o.cfg
@@ -0,0 +1,5 @@
+include = $HERE/fcm2_base.cfg
+
+extract.location{diff}[test_suite] = branches/dev/Share/f77_dep
+
+build.prop{ns-dep.o} = test_suite
diff --git a/test/repos/trunk/cfg/fcm2_ns-dep_o_all.cfg b/test/repos/trunk/cfg/fcm2_ns-dep_o_all.cfg
new file mode 100644
index 0000000..1ce0686
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_ns-dep_o_all.cfg
@@ -0,0 +1,5 @@
+include = $HERE/fcm2_base.cfg
+
+extract.location{diff}[test_suite] = branches/dev/Share/f77_dep
+
+build.prop{ns-dep.o} = /
diff --git a/test/repos/trunk/cfg/fcm2_ns-dep_o_file.cfg b/test/repos/trunk/cfg/fcm2_ns-dep_o_file.cfg
new file mode 100644
index 0000000..68dd26f
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_ns-dep_o_file.cfg
@@ -0,0 +1,5 @@
+include = $HERE/fcm2_base.cfg
+
+extract.location{diff}[test_suite] = branches/dev/Share/f77_dep
+
+build.prop{ns-dep.o} = test_suite/subroutine/hello_sub.F90
diff --git a/test/repos/trunk/cfg/fcm2_ns-dep_o_invalid.cfg b/test/repos/trunk/cfg/fcm2_ns-dep_o_invalid.cfg
new file mode 100644
index 0000000..875790e
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_ns-dep_o_invalid.cfg
@@ -0,0 +1,5 @@
+include = $HERE/fcm2_base.cfg
+
+extract.location{diff}[test_suite] = branches/dev/Share/f77_dep
+
+build.prop{ns-dep.o} = invalid test_suite
diff --git a/test/repos/trunk/cfg/fcm2_ops.cfg b/test/repos/trunk/cfg/fcm2_ops.cfg
new file mode 100644
index 0000000..6ea59c9
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_ops.cfg
@@ -0,0 +1,82 @@
+steps = extract preprocess build
+
+extract.ns = ens gcom gen ops ops_admin um_admin
+extract.location[ops] = trunk at 19069
+extract.path-root[ops] = src
+extract.path-excl[ops] = config scripts/Ops_SatRad_IDL code/OpsMod_ODB
+
+extract.location[gen] = trunk at 3194
+extract.path-root[gen] = src/code
+
+extract.location[ops_admin] = trunk at 19069
+extract.path-root[ops_admin] = src/code
+
+extract.location[um_admin] = trunk at 11210
+extract.path-root[um_admin] = utilities
+extract.path-excl[um_admin] = /
+extract.path-incl[um_admin] = IBM_signal_hander
+
+extract.location[gcom] = trunk at 17285
+extract.path-root[gcom] = build
+extract.path-excl[gcom] = configs ext_scripts
+
+extract.location[ens] = trunk at 1460
+extract.path-root[ens] = forecast/code
+extract.path-excl[ens] = ext.cfg ibm.ext.cfg lapack_eigen
+
+preprocess.ns-excl = ops gen ops_admin um_admin
+preprocess.ns-incl = var/code/PF_MPP \
+ \ ops/code/OpsMod_Altimeter \
+ \ ops/code/OpsMod_OceanSound \
+ \ ops/code/OpsMod_Radar \
+ \ ops/code/OpsMod_RadarZ \
+ \ ops/code/OpsMod_SeaIce \
+ \ ops/code/OpsMod_SurfaceSST \
+ \ ops/code/OpsProg_ExtractAndProcess \
+ \ ops/code/Ops_RTTOV9/rttov9_parallel_ad.F90 \
+ \ ops/code/Ops_RTTOV9/rttov9_parallel_direct.F90 \
+ \ ops/code/Ops_RTTOV9/rttov9_parallel_k.F90 \
+ \ ops/code/Ops_RTTOV9/rttov9_parallel_tl.F90
+preprocess.prop{fpp} = wrap_pp
+preprocess.prop{fpp.defs}[gcom] = GC_VERSION="'3.4'" GC_DESCRIP="'MPP'" GC_BUILD_DATE="'17285'" MPI_SRC MPILIB_32B PREC_64B GC__FORTERRUNIT=0 GC__FLUSHUNIT6 MPI_BSEND_BUFFER_SIZE=2560000
+preprocess.prop{fpp.defs}[ens] = IBM
+preprocess.prop{fpp.defs}[ops/code/Ops_RTTOV9/rttov9_parallel_ad.F90] = _RTTOV_TSTRAD_TEMP RTTOV_ARCH_VECTOR _RTTOV_PARALLEL_AD
+preprocess.prop{fpp.defs}[ops/code/Ops_RTTOV9/rttov9_parallel_direct.F90] = _RTTOV_TSTRAD_TEMP RTTOV_ARCH_VECTOR _RTTOV_PARALLEL_DIRECT
+preprocess.prop{fpp.defs}[ops/code/Ops_RTTOV9/rttov9_parallel_k.F90] = _RTTOV_TSTRAD_TEMP RTTOV_ARCH_VECTOR _RTTOV_PARALLEL_K
+preprocess.prop{fpp.defs}[ops/code/Ops_RTTOV9/rttov9_parallel_tl.F90] = _RTTOV_TSTRAD_TEMP RTTOV_ARCH_VECTOR _RTTOV_PARALLEL_TL
+preprocess.prop{cpp.defs}[ens] = LOWERCASE
+
+$OPSDIR{?} = /home/h04/opsrc/ops0
+$mpich2 = $OPSDIR/mpi/mpich2-1.4-ukmo-v1/ifort-12
+build.target = OpsScr_Build EnsProg_ETKF.exe EnsProg_TrimObstore.exe
+build.prop{file-ext.script} = .sh
+build.prop{cc} = wrap_cc
+build.prop{cc.defs}[gen/GenMod_Platform] = UNDERSCORE LOWERCASE C_LONG_LONG_INT FRL8
+build.prop{cc.defs}[gen/UM_Platform] = VAROPSVER C_LOW_U LINUX LITTLE_END C_LONG_LONG_INT FRL8
+build.prop{cc.defs}[ops/code/MetDB_ClientServer] = hpux DEBUG LL64 UNDERSCORE
+build.prop{fc} = wrap_fc
+build.prop{fc.flags} = -implicitnone -stand f95 -warn all -warn nointerfaces -i8 -r8 -i-static
+build.prop{fc.flags}[gcom] = -implicitnone -stand f95 -warn all -i8 -r8 -i-static -warn none -I$mpich2/include
+build.prop{fc.flags}[ops/code/Ops_RTTOV9] = -implicitnone -stand f95 -warn all -warn nointerfaces -i-static -O3
+build.prop{fc.defs}[gen/GenMod_Utilities] = USE_FLUSH
+build.prop{fc.defs}[gen/UM_COEX] = VAROPSVER
+build.prop{fc.defs}[gen/UM_Platform] = VAROPSVER
+build.prop{fc.defs}[ops/code/MetDB_GRIB] = SX6
+build.prop{fc.defs}[ops/code/OpsMod_Extract] = LITTLE_END
+build.prop{fc.defs}[ops_admin/MetDB_Bufr] = BPATH
+build.prop{fc.defs}[ops/code/Ops_RTTOV9] = _RTTOV_TSTRAD_TEMP RTTOV_ARCH_VECTOR
+build.prop{fc.defs}[ops/code/Ops_RTTOV9/rttov9_parallel_ad.F90] =
+build.prop{fc.defs}[ops/code/Ops_RTTOV9/rttov9_parallel_direct.F90] =
+build.prop{fc.defs}[ops/code/Ops_RTTOV9/rttov9_parallel_k.F90] =
+build.prop{fc.defs}[ops/code/Ops_RTTOV9/rttov9_parallel_tl.F90] =
+build.prop{fc.flags-ld} = -i-static -L$mpich2/lib -lmpich -lmpl -lpthread
+build.prop{fc.flags-ld}[ens] = -i-static -L$mpich2/lib -lmpich -lmpl -lpthread -llapack
+build.prop{ns-dep.o}[ops/code/OpsProg_BackErrCreate] = gcom
+build.prop{ns-dep.o}[ops/code/OpsProg_ExtractAndProcess] = ops_admin/MetDB_BUFR_RETRIEVAL/source ops_admin/MetDB_Bufr ops_admin/lapack ops_admin/blas gcom
+build.prop{ns-dep.o}[ops/code/OpsProg_MOPS] = ops_admin/MetDB_BUFR_RETRIEVAL/source ops_admin/MetDB_Bufr gcom
+build.prop{ns-dep.o}[ops/code/OpsProg_KillRPC] = gcom
+build.prop{ns-dep.o}[ops/code/Ops_SatRad_Stats] = gcom
+build.prop{ns-dep.o}[ens/EnsProg_ETKF] = gcom
+build.prop{ns-dep.o}[ens/EnsProg_TrimObstore] = gcom
+build.prop{no-dep.f.module} = f90_unix_io xlfutility netcdf yomlun
+build.prop{no-dep.include} = mpif.h
diff --git a/test/repos/trunk/cfg/fcm2_postproc_hpc.cfg b/test/repos/trunk/cfg/fcm2_postproc_hpc.cfg
new file mode 100644
index 0000000..7bb1822
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_postproc_hpc.cfg
@@ -0,0 +1,171 @@
+steps = extract mirror
+
+mirror.target = $HPC:$THIS_RUN_DIR_HPC
+mirror.prop{config-file.steps} = preprocess build
+mirror.prop{rsync.flags} = -a --exclude='.*' --delete-excluded --timeout=900 --rsh='ssh -oBatchMode=yes' -v
+
+extract.ns = gen ops pp ppancil rst site_ext ver
+extract.location[pp] = trunk at 2728
+extract.path-excl[pp] = /
+extract.path-incl[pp] = CDP \
+ \ SCW \
+ \ cloud \
+ \ frasia \
+ \ get_metdb_obs \
+ \ model_processing \
+ \ moses-pdm-rfm \
+ \ precip \
+ \ precip_fcst \
+ \ pressure_wind \
+ \ product_gen \
+ \ scripts \
+ \ steps \
+ \ utilities \
+ \ verification \
+ \ visibility
+
+extract.location[ppancil] = trunk at 2524
+extract.path-excl[ppancil] = web
+
+extract.location{primary}[site_ext] = svn://fcm9/PostProc_svn/SiteExtract
+extract.location[site_ext] = trunk at 2481
+extract.path-root[site_ext] = FssMod_DMO
+
+extract.location[gen] = trunk at 3015
+extract.path-root[gen] = src/code
+extract.path-excl[gen] = GenMod_ModelIO
+
+extract.location{primary}[rst] = svn://fcm9/PostProc_svn/RoadTemp
+extract.location[rst] = trunk at 2416
+extract.path-excl[rst] = /
+extract.path-incl[rst] = src scripts
+
+extract.location[ver] = trunk at 4739
+extract.path-root[ver] = src/code
+extract.path-excl[ver] = /
+extract.path-incl[ver] = VerMod_FieldsIO VerMod_General VerMod_Grid
+
+extract.location[ops] = trunk at 18088
+extract.path-root[ops] = src/code
+extract.path-excl[ops] = /
+extract.path-incl[ops] = OpsMod_MOPS OpsMod_ObsInfo OpsMod_Constants
+
+preprocess.ns-excl = pp ppancil site_ext rst ops
+preprocess.ns-incl = pp/utilities pp/steps pp/get_metdb_obs/MetDB_source rst/src
+preprocess.prop{cpp} = xlC_r
+preprocess.prop{cpp.flags} = -E -C
+preprocess.prop{cpp.defs}[pp/get_metdb_obs/MetDB_source] = LOWERCASE L64 UNDERSCORE
+preprocess.prop{cpp.defs}[gen] = C_LOW_U FRL8 C_LONG_INT NEC VAROPSVER UTILIO UNDERSCORE LOWERCASE
+preprocess.prop{fpp.flags} = -E -P -traditional
+preprocess.prop{fpp.defs}[gen] = C_LOW_U FRL8 C_LONG_INT NEC VAROPSVER UTILIO UNDERSCORE UPPERCASE
+preprocess.prop{fpp.defs}[rst] = NEC
+preprocess.prop{no-dep.include} = gc_constants.h gc_kinds.h gc_options.h
+
+build.target = PPQYINTERP.ksh run_nae_pp.ksh run_qv_downscaling.ksh run_glo_pp.ksh run_EuroPP.ksh first_start.ksh PP4KOPER.ksh
+build.prop{cc} = xlC_r
+build.prop{fc} = xlf90_r
+build.prop{no-dep.f.module} = f90_unix_io
+build.prop{file-ext.script} = .ksh
+build.prop{dep.o}[pp/get_metdb_obs/get_metdb_obs.f90] = pout_i8.o
+build.prop{dep.o}[pp/precip/lightning_merge.f] = get_free_lun.o
+build.prop{dep.o}[pp/precip/mask_radar.f90] = nimrod_open.o nimrod_header.o
+build.prop{cc.flags-ld}[pp/steps] = -L/home/nwp/fr/ihab/fftw_opt/lib/ -lfftw3 -I/home/nwp/fr/ihab/fftw_opt/include/
+build.prop{cc.flags} = -qarch=pwr6 -qtune=pwr6 -O0 -qhot
+build.prop{cc.flags}[gen] = -qarch=pwr6 -qtune=pwr6 -O0 -qhot -qrealsize=8 -qintsize=8
+build.prop{cc.flags}[pp/get_metdb_obs/MetDB_source] =
+build.prop{cc.flags}[pp/steps] = -O0 -qarch=pwr6 -qtune=pwr6 -q64 -I/home/nwp/fr/ihab/fftw_opt/include/
+build.prop{fc.flags-ld}[rst] = -L/projects/um1/gcom/gcom3.2/meto_ibm_pwr6_serial/lib -lgcom -bnoquiet -L/projects/um1/lib -lsig -L/usr/lib -lmass -lessl -qsmp
+build.prop{fc.flags-ld}[pp/model_processing/format.f90] = -L/projects/um1/gcom/gcom3.2/meto_ibm_pwr6_serial/lib -lgcom
+build.prop{fc.flags} = -qarch=pwr6 -qtune=pwr6 -O0 -qextname
+build.prop{fc.flags}[rst ver ops] = -qarch=pwr6 -qtune=pwr6 -O0 -qfullpath -qextname -qrealsize=8 -qintsize=8
+build.prop{fc.flags}[gen] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qrealsize=8 -qintsize=8
+build.prop{fc.flags}[gen/UM_COEX] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qrealsize=8 -qintsize=8 -qfixed=132
+build.prop{fc.flags}[gen/UM_General] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qrealsize=8 -qintsize=8 -qfixed=132
+build.prop{fc.flags}[gen/UM_Platform] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qrealsize=8 -qintsize=8 -qfixed=132
+build.prop{fc.flags}[gen/UM_Platform/IOERROR.F90] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qrealsize=8 -qintsize=8
+build.prop{fc.flags}[pp/get_metdb_obs/get_metdb_obs.f90] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname
+build.prop{fc.flags}[pp/model_processing/get_um_info.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qfixed=132
+build.prop{fc.flags}[pp/moses-pdm-rfm] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qstrict
+build.prop{fc.flags}[pp/moses-pdm-rfm/daynumber.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qstrict -qfixed=132
+build.prop{fc.flags}[pp/moses-pdm-rfm/dlongrad.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qstrict -qfixed=132
+build.prop{fc.flags}[pp/moses-pdm-rfm/down_rad_calc.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qstrict -qfixed=132
+build.prop{fc.flags}[pp/moses-pdm-rfm/dsolrad.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qstrict -qfixed=132
+build.prop{fc.flags}[pp/moses-pdm-rfm/get_row_and_column.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qstrict -qfixed=132
+build.prop{fc.flags}[pp/moses-pdm-rfm/initialise_routing.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qstrict -qfixed=132
+build.prop{fc.flags}[pp/moses-pdm-rfm/morangstrom.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qstrict -qfixed=132
+build.prop{fc.flags}[pp/moses-pdm-rfm/morloc.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qstrict -qfixed=132
+build.prop{fc.flags}[pp/moses-pdm-rfm/moses_cloud_cover.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qstrict -qfixed=132
+build.prop{fc.flags}[pp/moses-pdm-rfm/moses_qs_from_t.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qstrict -qfixed=132
+build.prop{fc.flags}[pp/moses-pdm-rfm/nearest_real.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qstrict -qfixed=132
+build.prop{fc.flags}[pp/moses-pdm-rfm/nextpoint.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qstrict -qfixed=132
+build.prop{fc.flags}[pp/moses-pdm-rfm/nimrod_3d_idata_read.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qstrict -qfixed=132
+build.prop{fc.flags}[pp/moses-pdm-rfm/nimrod_3dextr_comp.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qstrict -qfixed=132
+build.prop{fc.flags}[pp/moses-pdm-rfm/nimrod_extr_comp.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qstrict -qfixed=132
+build.prop{fc.flags}[pp/moses-pdm-rfm/nimrod_extr_wind.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qstrict -qfixed=132
+build.prop{fc.flags}[pp/moses-pdm-rfm/nimrod_hdr_read.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qstrict -qfixed=132
+build.prop{fc.flags}[pp/moses-pdm-rfm/nimrod_idata_read.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qstrict -qfixed=132
+build.prop{fc.flags}[pp/moses-pdm-rfm/q_vp_from_t_td.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qstrict -qfixed=132
+build.prop{fc.flags}[pp/moses-pdm-rfm/read_ancil.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qstrict -qfixed=132
+build.prop{fc.flags}[pp/moses-pdm-rfm/regrid_real.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qstrict -qfixed=132
+build.prop{fc.flags}[pp/moses-pdm-rfm/route_runoff.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qstrict -qfixed=132
+build.prop{fc.flags}[pp/moses-pdm-rfm/routing.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qstrict -qfixed=132
+build.prop{fc.flags}[pp/moses-pdm-rfm/sam.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qstrict -qfixed=132
+build.prop{fc.flags}[pp/moses-pdm-rfm/ssdm.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qstrict -qfixed=132
+build.prop{fc.flags}[pp/moses-pdm-rfm/ssdm_var_generator.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qstrict -qfixed=132
+build.prop{fc.flags}[pp/moses-pdm-rfm/sun_angles.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qstrict -qfixed=132
+build.prop{fc.flags}[pp/moses-pdm-rfm/um_solpos.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qstrict -qfixed=132
+build.prop{fc.flags}[pp/moses-pdm-rfm/wavespeed.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qstrict -qfixed=132
+build.prop{fc.flags}[pp/precip/get_surface_obs.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qfixed=132
+build.prop{fc.flags}[pp/precip/lightning_forecast.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qfixed=132
+build.prop{fc.flags}[pp/precip/lightning_merge.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qfixed=132
+build.prop{fc.flags}[pp/precip/metar_to_synop_weather.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qfixed=132
+build.prop{fc.flags}[pp/precip/read_adv_fc.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qfixed=132
+build.prop{fc.flags}[pp/precip/read_rad.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qfixed=132
+build.prop{fc.flags}[pp/precip_fcst] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qfixed=132
+build.prop{fc.flags}[pp/precip_fcst/accmerge.f90] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname
+build.prop{fc.flags}[pp/precip_fcst/object_motion.f90] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname
+build.prop{fc.flags}[pp/precip_fcst/scale.f90] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname
+build.prop{fc.flags}[pp/precip_fcst/wind_forecast_precip.f90] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname
+build.prop{fc.flags}[pp/pressure_wind] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qfixed=132
+build.prop{fc.flags}[pp/pressure_wind/an_smear.f90] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname
+build.prop{fc.flags}[pp/pressure_wind/bilin_mdi.f90] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname
+build.prop{fc.flags}[pp/pressure_wind/convert_winduv.f90] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname
+build.prop{fc.flags}[pp/pressure_wind/gust_adjust.f90] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname
+build.prop{fc.flags}[pp/pressure_wind/gust_analysis.f90] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname
+build.prop{fc.flags}[pp/pressure_wind/pwindanal.f90] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname
+build.prop{fc.flags}[pp/utilities/beammap_ascii_to_nimrod.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qfixed=132
+build.prop{fc.flags}[pp/utilities/ccitt.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qfixed=132
+build.prop{fc.flags}[pp/utilities/datetime_c_to_i_secs.f90] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qfixed=132
+build.prop{fc.flags}[pp/utilities/def_head.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qfixed=132
+build.prop{fc.flags}[pp/utilities/domain_to_ng.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qfixed=132
+build.prop{fc.flags}[pp/utilities/european_observations_area.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qfixed=132
+build.prop{fc.flags}[pp/utilities/get_free_lun.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qfixed=132
+build.prop{fc.flags}[pp/utilities/icutout.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qfixed=132
+build.prop{fc.flags}[pp/utilities/ll_to_ng.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qfixed=132
+build.prop{fc.flags}[pp/utilities/ll_to_ps.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qfixed=132
+build.prop{fc.flags}[pp/utilities/locate_FCST_string.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qfixed=132
+build.prop{fc.flags}[pp/utilities/nearest.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qfixed=132
+build.prop{fc.flags}[pp/utilities/nearest_file.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qfixed=132
+build.prop{fc.flags}[pp/utilities/ng_to_ll.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qfixed=132
+build.prop{fc.flags}[pp/utilities/ng_to_ll_array.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qfixed=132
+build.prop{fc.flags}[pp/utilities/nimrod_i4read.f90] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qfixed=132
+build.prop{fc.flags}[pp/utilities/nimrod_open2.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qfixed=132
+build.prop{fc.flags}[pp/utilities/nimrod_open_i4read.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qfixed=132
+build.prop{fc.flags}[pp/utilities/nimrod_open_i4write.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qfixed=132
+build.prop{fc.flags}[pp/utilities/nimrod_regrid.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qfixed=132
+build.prop{fc.flags}[pp/utilities/observations_area_metdb.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qfixed=132
+build.prop{fc.flags}[pp/utilities/ps_to_ll.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qfixed=132
+build.prop{fc.flags}[pp/utilities/regrid.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qfixed=132
+build.prop{fc.flags}[pp/utilities/round_cycle_string.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qfixed=132
+build.prop{fc.flags}[pp/utilities/subtract_time.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qfixed=132
+build.prop{fc.flags}[pp/utilities/time_diff.f90] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qfixed=132
+build.prop{fc.flags}[pp/utilities/time_difference_prog.f90] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qfixed=132
+build.prop{fc.flags}[pp/utilities/total_accum.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qfixed=132
+build.prop{fc.flags}[pp/utilities/trim.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qfixed=132
+build.prop{fc.flags}[pp/utilities/zpdate.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qfixed=132
+build.prop{fc.flags}[pp/verification] = -qarch=pwr6 -qtune=pwr6 -O0 -qextname -qfixed=132
+build.prop{fc.flags}[rst/src] = -qarch=pwr6 -qtune=pwr6 -O0 -qfullpath -qextname -qrealsize=8 -qintsize=8 -qfixed=132
+build.prop{fc.flags}[rst/src/MORST_main.F] = -qarch=pwr6 -qtune=pwr6 -O0 -qfullpath -qextname -qrealsize=8 -qintsize=8 -qfixed=132 -qsmp=omp
+build.prop{fc.flags}[rst/src/ReadFrcData.f90] = -qarch=pwr6 -qtune=pwr6 -O0 -qstrict -qfullpath -qextname -qrealsize=8 -qintsize=8
+build.prop{fc.flags}[rst/src/output_status.f90] = -qarch=pwr6 -qtune=pwr6 -O0 -qfullpath -qextname
+build.prop{fc.flags}[rst/src/profile.f] = -qarch=pwr6 -qtune=pwr6 -O0 -qfullpath -qextname -qrealsize=8 -qintsize=8 -qfixed=132 -qsmp=auto
diff --git a/test/repos/trunk/cfg/fcm2_pp_change_blockdata.cfg b/test/repos/trunk/cfg/fcm2_pp_change_blockdata.cfg
new file mode 100644
index 0000000..d3c38ed
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_pp_change_blockdata.cfg
@@ -0,0 +1,3 @@
+include = $HERE/fcm2_base.cfg
+
+build.prop{fc.defs}[test_suite/blockdata] = ODD
diff --git a/test/repos/trunk/cfg/fcm2_pp_change_dependency.cfg b/test/repos/trunk/cfg/fcm2_pp_change_dependency.cfg
new file mode 100644
index 0000000..2712963
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_pp_change_dependency.cfg
@@ -0,0 +1,3 @@
+include = $HERE/fcm2_base.cfg
+
+preprocess.prop{fpp.defs}[test_suite/program/hello.F90] =
diff --git a/test/repos/trunk/cfg/fcm2_pp_change_include.cfg b/test/repos/trunk/cfg/fcm2_pp_change_include.cfg
new file mode 100644
index 0000000..4f9644c
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_pp_change_include.cfg
@@ -0,0 +1,3 @@
+include = $HERE/fcm2_base.cfg
+
+extract.location{diff}[test_suite] = branches/dev/Share/modify_pp_include
diff --git a/test/repos/trunk/cfg/fcm2_pp_change_include_inherit.cfg b/test/repos/trunk/cfg/fcm2_pp_change_include_inherit.cfg
new file mode 100644
index 0000000..a366279
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_pp_change_include_inherit.cfg
@@ -0,0 +1,3 @@
+use = $RUN_DIR/fcm2_base
+
+extract.location{diff}[test_suite] = branches/dev/Share/modify_pp_include
diff --git a/test/repos/trunk/cfg/fcm2_pp_empty_subroutine.cfg b/test/repos/trunk/cfg/fcm2_pp_empty_subroutine.cfg
new file mode 100644
index 0000000..5499b1c
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_pp_empty_subroutine.cfg
@@ -0,0 +1,3 @@
+include = $HERE/fcm2_base.cfg
+
+preprocess.prop{fpp.defs}[test_suite/subroutine/hello_sub.F90] =
diff --git a/test/repos/trunk/cfg/fcm2_pp_empty_subroutine_inherit.cfg b/test/repos/trunk/cfg/fcm2_pp_empty_subroutine_inherit.cfg
new file mode 100644
index 0000000..503fa5a
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_pp_empty_subroutine_inherit.cfg
@@ -0,0 +1,3 @@
+use = $RUN_DIR/fcm2_base
+
+preprocess.prop{fpp.defs}[test_suite/subroutine/hello_sub.F90] =
diff --git a/test/repos/trunk/cfg/fcm2_pp_empty_subroutine_inherit_force.cfg b/test/repos/trunk/cfg/fcm2_pp_empty_subroutine_inherit_force.cfg
new file mode 100644
index 0000000..59c2a3c
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_pp_empty_subroutine_inherit_force.cfg
@@ -0,0 +1,5 @@
+use = $RUN_DIR/fcm2_base
+
+preprocess.prop{fpp.defs}[test_suite/subroutine/hello_sub.F90] =
+
+build.prop{fc.flags}[test_suite/program] = -assume nosource_include -O2
diff --git a/test/repos/trunk/cfg/fcm2_revmatch_false.cfg b/test/repos/trunk/cfg/fcm2_revmatch_false.cfg
new file mode 100644
index 0000000..ed17bb0
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_revmatch_false.cfg
@@ -0,0 +1,7 @@
+include = $HERE/fcm2_base.cfg
+
+extract.location[test_suite] = trunk at 21
+extract.location{diff}[test_suite] = \
+ branches/dev/Share/modify_files_base at 21 \
+ branches/dev/Share/modify_files_merge1 at 21 \
+ branches/dev/Share/modify_files_merge2 at 21
diff --git a/test/repos/trunk/cfg/fcm2_single_file.cfg b/test/repos/trunk/cfg/fcm2_single_file.cfg
new file mode 100644
index 0000000..bb9270c
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_single_file.cfg
@@ -0,0 +1,3 @@
+include = $HERE/fcm2_base.cfg
+
+preprocess.prop{fpp.defs}[test_suite/program/hello.F90] = LOCAL_STRING
diff --git a/test/repos/trunk/cfg/fcm2_space_in_name.cfg b/test/repos/trunk/cfg/fcm2_space_in_name.cfg
new file mode 100644
index 0000000..4727f32
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_space_in_name.cfg
@@ -0,0 +1,5 @@
+include = $HERE/fcm2_base.cfg
+
+extract.location{diff}[test_suite] = branches/dev/Share/space_in_name
+
+build.prop{fc.defs}["test_suite/block data/hello blockdata.F90"] = ODD
diff --git a/test/repos/trunk/cfg/fcm2_sps.cfg b/test/repos/trunk/cfg/fcm2_sps.cfg
new file mode 100644
index 0000000..b6f3bda
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_sps.cfg
@@ -0,0 +1,46 @@
+steps = extract build
+
+extract.ns = gen sps
+extract.location[sps] = trunk at 4964
+extract.path-excl[sps] = doc src/config src/simim \
+ src/code/SpsMod_ModelIngest \
+ src/code/SpsProg_ConvertFieldsfile \
+ src/code/SpsTask_ConvectiveInitiation \
+ src/code/old \
+ src/scripts/Sps_System \
+ src/scripts/old
+
+extract.location[gen] = trunk at 3953
+extract.path-root[gen] = src/code
+extract.path-excl[gen] = /
+extract.path-incl[gen] = GenMod_UMConstants
+
+build.target = SpsScr_Install sps/data/Sps_Fire/.etc sps/data/coeffs/.etc sps/data/palettes/.etc \
+ sps/data/products/.etc sps/data/sad/.etc sps/data/slotstore/.etc
+build.target-rename = h5admin.exe:h5admin h5getatt.exe:h5getatt SpsProg_GetCoords.exe:sps_get_coords
+build.prop{fc} = wrap_fc
+$FOPT = -CB -traceback -u -convert big_endian
+$SPS_LIBDIR = /home/h04/cfsa/SPS/libraries/RHEL6
+build.prop{fc.flags} = $FOPT -I${SPS_LIBDIR}/grib_api/include
+build.prop{fc.flags}[gen/GenMod_UMConstants] = $FOPT -w -132
+build.prop{fc.flags}[sps/src/code/SpsMod_Utilities] = $FOPT -Duse_f90_unix=''
+build.prop{fc.flags}[sps/src/code/SpsTask_HDFReader] = $FOPT -auto -assume byterecl
+build.prop{fc.flags}[sps/src/code/SpsProg_ImageGrib] = $FOPT -auto -assume byterecl
+build.prop{fc.flags}[sps/src/code/SpsProg_GlobalComposite] = $FOPT -auto -assume byterecl
+build.prop{fc.flags}[sps/src/code/rttov10] = $FOPT -openmp
+build.prop{fc.flags}[sps/src/code/rttov10/main/rttov_locpat_k.F90] = $FOPT
+build.prop{cc} = wrap_cc
+build.prop{cc.flags} = -Wall -O2 -DLOWERCASE -I${SPS_LIBDIR}/hdf5/include
+build.prop{cc.flags}[sps/src/code/SpsMod_Image] = -Wall -DLOWERCASE -I${SPS_LIBDIR}/hdf5/include
+build.prop{fc.flags-ld} = -openmp -L${SPS_LIBDIR}/hdf5/lib -lhdf5 -lhdf5_hl \
+ \ -L${SPS_LIBDIR}/bufr_ifort -lbufr \
+ \ -L${SPS_LIBDIR}/grib_ifort -lgrib_ifort \
+ \ -L${SPS_LIBDIR}/g2lib -lg2 \
+ \ -L${SPS_LIBDIR}/grib_api/lib -lgrib_api_f90 -lgrib_api \
+ \ -L${SPS_LIBDIR}/jasper/lib -ljasper \
+ \ -L/usr/lib -llapack \
+ \ -ljpeg -lpng
+build.prop{dep.o}[sps/src/code/SpsMod_Store/SpsMod_Hdf5F90Support.c] = spsmod_storec.o
+build.prop{dep.include}[sps/src/code/SpsMod_Store/Sps_H5Fopen_auto.c] = Sps_H5Fopen_auto.h
+build.prop{no-dep.f.module} = grib_api
+build.prop{file-ext.script} = .ksh .pl .pm .pro
diff --git a/test/repos/trunk/cfg/fcm2_symbolic_link.cfg b/test/repos/trunk/cfg/fcm2_symbolic_link.cfg
new file mode 100644
index 0000000..237620b
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_symbolic_link.cfg
@@ -0,0 +1,3 @@
+include = $HERE/fcm2_base.cfg
+
+extract.location{diff}[test_suite] = branches/dev/Share/symbolic_link
diff --git a/test/repos/trunk/cfg/fcm2_um.cfg b/test/repos/trunk/cfg/fcm2_um.cfg
new file mode 100644
index 0000000..fb4b1b1
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_um.cfg
@@ -0,0 +1,36 @@
+steps = extract preprocess build
+
+extract.ns = um
+extract.location[um] = trunk at vn7.3
+extract.path-root[um] = src
+
+$keys_model = C_LONG_LONG_INT=c_long_long_int MPP=mpp C_LOW_U=c_low_u \
+ \ FRL8=frl8 LINUX=linux BUFRD_IO=bufrd_io LITTLE_END=little_end \
+ \ LINUX_INTEL_COMPILER=linux_intel_compiler CONTROL=control \
+ \ REPROD=reprod ATMOS=atmos GLOBAL=global A04_ALL=a04_all \
+ \ A01_3C=a01_3c A02_3C=a02_3c A03_8C=a03_8c A04_3D=a04_3d A05_4A=a05_4a \
+ \ A06_4A=a06_4a A08_7A=a08_7a A09_2A=a09_2a A10_2A=a10_2a A11_2A=a11_2a \
+ \ A12_2A=a12_2a A13_2A=a13_2a A14_1B=a14_1b A15_1A=a15_1a A16_1A=a16_1a \
+ \ A17_2B=a17_2b A18_0A=a18_0a A19_1A=a19_1a A25_0A=a25_0a A26_0A=a26_0a \
+ \ A30_1A=a30_1a A31_0A=a31_0a A32_1A=a32_1a A33_0A=a33_0a A34_0A=a34_0a \
+ \ A35_0A=a35_0a A38_0A=a38_0a A70_1C=a70_1c A71_1A=a71_1a C70_1A=c70_1a \
+ \ C72_0A=c72_0a C80_1A=c80_1a C82_1A=c82_1a C84_1A=c84_1a C92_2A=c92_2a \
+ \ C94_1A=c94_1a C95_2A=c95_2a C96_1C=c96_1c C97_3A=c97_3a
+preprocess.prop{fpp.defs} = $keys_model
+preprocess.prop{cpp.defs} = $keys_model
+preprocess.prop{fpp} = wrap_pp
+preprocess.prop{cpp} = wrap_mpicc
+preprocess.prop{cpp.flags} = -E
+preprocess.prop{fpp.flags} = -E -P -traditional -I /home/h04/opsrc/ops0/mpi/mpich2-1.4-ukmo-v1/ifort-12/include
+
+build.target = um.exe
+build.target-rename = flumeMain.exe:um.exe
+build.prop{cc} = wrap_cc
+build.prop{fc} = wrap_mpif90
+build.prop{fc.flags} = -i8 -r8 -w -I /home/h01/frum/gcom/gcom4.1/linux_ifort_mpich2/inc -O0
+build.prop{fc.flags-ld} = -L/home/h01/frum/gcom/gcom4.1/linux_ifort_mpich2/lib -lgcom -Wl,--noinhibit-exec -Vaxlib
+build.prop{dep.o.special} = blkdata.o
+build.prop{ns-dep.o} = um/control/c_code
+build.prop{no-dep.f.module} = netcdf mpl mod_prism_proto mod_prism_grids_writing mod_prism_def_partition_proto mod_prism_put_proto mod_prism_get_proto
+build.prop{no-dep.include} = netcdf.inc mpif.h
+build.prop{no-dep.bin}[um/script] = *
diff --git a/test/repos/trunk/cfg/fcm2_um77.cfg b/test/repos/trunk/cfg/fcm2_um77.cfg
new file mode 100644
index 0000000..f8342d0
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_um77.cfg
@@ -0,0 +1,74 @@
+step.class[preprocess-recon] = preprocess
+step.class[build-recon build-scripts] = build
+steps = extract build-scripts preprocess build preprocess-recon build-recon
+
+extract.ns = um
+extract.location[um] = trunk at vn7.7
+extract.path-root[um] = src
+extract.path-excl[um] = configs scm utility
+extract.path-incl[um] = utility/makebc utility/qxreconf
+
+build-scripts.prop{no-dep.bin} = *
+build-scripts.ns-excl = /
+build-scripts.ns-incl = um/script
+build-scripts.target = archfail autopp_tidyup getfile make_parexe.pl nextGenid \
+ OASIS3_ctl OASIS3_conf qscasedisp qscicerun qscicesetup qscombine \
+ qsexecute qsfinal qshistprint qshistreset qsmass qsmaster qsnemorun \
+ qsnemosetup NEMO_nl_ctl qspickup qsresubmit qsserver qssetup restartinfo \
+ submitchk UMScr_TopLevel qsmoose
+
+$keys_model = C_LONG_LONG_INT=c_long_long_int C_LOW_U=c_low_u \
+ \ FRL8=frl8 LINUX=linux BUFRD_IO=bufrd_io LITTLE_END=little_end \
+ \ LFS=lfs _LARGEFILE_SOURCE=_largefile_source _FILE_OFFSET_BITS=64 \
+ \ CONTROL=control ATMOS=atmos GLOBAL=global A04_ALL=a04_all \
+ \ A01_3C=a01_3c A02_3C=a02_3c A03_8C=a03_8c A04_3D=a04_3d A05_4A=a05_4a \
+ \ A06_4A=a06_4a A08_7A=a08_7a A09_2A=a09_2a A10_2A=a10_2a A11_0A=a11_0a \
+ \ A12_2A=a12_2a A13_2A=a13_2a A14_0A=a14_0a A15_1A=a15_1a A16_1A=a16_1a \
+ \ A17_0A=a17_0a A18_2A=a18_2a A19_1A=a19_1a A25_0A=a25_0a A26_0A=a26_0a \
+ \ A30_1A=a30_1a A31_0A=a31_0a A32_1A=a32_1a A33_0A=a33_0a A34_0A=a34_0a \
+ \ A35_0A=a35_0a A36_0A=a36_0a A37_0A=a37_0a A38_0A=a38_0a A39_0A=a39_0a \
+ \ A70_1C=a70_1c A71_1A=a71_1a C70_1A=c70_1a C72_0A=c72_0a C80_1A=c80_1a \
+ \ C82_1A=c82_1a C84_1A=c84_1a C92_2A=c92_2a C94_1A=c94_1a C95_2A=c95_2a \
+ \ C96_1C=c96_1c C97_3A=c97_3a
+preprocess.prop{fpp.defs} = $keys_model
+preprocess.prop{cpp.defs} = $keys_model
+preprocess.prop{fpp} = wrap_pp
+preprocess.prop{cpp} = wrap_mpicc
+preprocess.prop{cpp.flags} = -E
+preprocess.prop{fpp.flags} = -E -P -traditional -I /home/h04/opsrc/ops0/mpi/mpich2-1.4-ukmo-v1/ifort-12/include
+preprocess.ns-excl = um/script um/utility/qxreconf
+
+build.target = um.exe
+build.target-rename = flumeMain.exe:um.exe
+build.prop{cc} = wrap_cc
+build.prop{fc} = wrap_mpif90
+build.prop{fc.flags} = -i8 -r8 -w -I /home/h01/frum/gcom/gcom4.1/linux_ifort_mpich2/inc -O0
+build.prop{fc.flags-ld} = -L/home/h01/frum/gcom/gcom4.1/linux_ifort_mpich2/lib -lgcom -Vaxlib
+build.prop{dep.o.special} = blkdata.o
+build.prop{ns-dep.o} = um/control/c_code
+build.prop{no-dep.f.module} = mpl
+build.ns-excl = um/script um/utility/qxreconf
+
+$keys_recon = C_LONG_LONG_INT=c_long_long_int C_LOW_U=c_low_u \
+ \ FRL8=frl8 LINUX=linux BUFRD_IO=bufrd_io LITTLE_END=little_end \
+ \ LFS=lfs _LARGEFILE_SOURCE=_largefile_source _FILE_OFFSET_BITS=64 \
+ \ RECON=recon
+preprocess-recon.prop{no-step-source} = preprocess
+preprocess-recon.prop{fpp.defs} = $keys_recon
+preprocess-recon.prop{cpp.defs} = $keys_recon
+preprocess-recon.prop{fpp} = wrap_pp
+preprocess-recon.prop{cpp} = wrap_mpicc
+preprocess-recon.prop{cpp.flags} = -E
+preprocess-recon.prop{fpp.flags} = -E -P -traditional -I /home/h04/opsrc/ops0/mpi/mpich2-1.0.8p1-ukmo-v2/ifort-10/include
+preprocess-recon.ns-excl = um/script um/utility/makebc
+
+build-recon.prop{no-step-source} = preprocess
+build-recon.target = qxreconf
+build-recon.target-rename = reconfigure.exe:qxreconf
+build-recon.prop{cc} = wrap_cc
+build-recon.prop{fc} = wrap_mpif90
+build-recon.prop{fc.flags} = -i8 -r8 -w -I /home/h01/frum/gcom/gcom4.1/linux_ifort_mpich2/inc -O0
+build-recon.prop{fc.flags-ld} = -L/home/h01/frum/gcom/gcom4.1/linux_ifort_mpich2/lib -lgcom -Vaxlib
+build-recon.prop{ns-dep.o} = um/control/c_code
+build-recon.prop{no-dep.f.module} = mpl
+build-recon.ns-excl = um/script um/utility/makebc
diff --git a/test/repos/trunk/cfg/fcm2_um77_hpc.cfg b/test/repos/trunk/cfg/fcm2_um77_hpc.cfg
new file mode 100644
index 0000000..3bc8ffc
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_um77_hpc.cfg
@@ -0,0 +1,89 @@
+step.class[preprocess-recon] = preprocess
+step.class[build-recon build-scripts] = build
+steps = extract mirror
+
+mirror.target = $HPC:$THIS_RUN_DIR_HPC
+mirror.prop{config-file.steps} = build-scripts preprocess build preprocess-recon build-recon
+
+extract.ns = um
+extract.location[um] = trunk at vn7.7
+extract.path-root[um] = src
+extract.path-excl[um] = configs scm utility
+extract.path-incl[um] = utility/makebc utility/qxreconf
+
+build-scripts.prop{no-dep.bin} = *
+build-scripts.ns-excl = /
+build-scripts.ns-incl = um/script
+build-scripts.target = archfail autopp_tidyup getfile make_parexe.pl nextGenid \
+ OASIS3_ctl OASIS3_conf qscasedisp qscicerun qscicesetup qscombine \
+ qsexecute qsfinal qshistprint qshistreset qsmass qsmaster qsnemorun \
+ qsnemosetup NEMO_nl_ctl qspickup qsresubmit qsserver qssetup restartinfo \
+ submitchk UMScr_TopLevel qsmoose
+
+$keys_model = C_LONG_INT=c_long_int C_LOW_U=c_low_u FRL8=frl8 \
+ \ BUFRD_IO=bufrd_io VECTLIB=vectlib IBM=ibm C98_1A=c98_1a MO_GRIB=mo_grib \
+ \ CONTROL=control ATMOS=atmos \
+ \ GLOBAL=global A04_ALL=a04_all A01_3C=a01_3c A02_3C=a02_3c \
+ \ A03_8C=a03_8c A04_3D=a04_3d A05_4A=a05_4a A06_4A=a06_4a A08_7A=a08_7a \
+ \ A09_2A=a09_2a A10_2A=a10_2a A11_0A=a11_0a A12_2A=a12_2a A13_2A=a13_2a \
+ \ A14_0A=a14_0a A15_1A=a15_1a A16_1A=a16_1a A17_0A=a17_0a A18_2A=a18_2a \
+ \ A19_1A=a19_1a A25_0A=a25_0a A26_0A=a26_0a A30_1A=a30_1a A31_0A=a31_0a \
+ \ A32_1A=a32_1a A33_0A=a33_0a A34_0A=a34_0a A35_0A=a35_0a A36_0A=a36_0a \
+ \ A37_0A=a37_0a A38_0A=a38_0a A39_0A=a39_0a A70_1C=a70_1c A71_1A=a71_1a \
+ \ C70_1A=c70_1a C72_0A=c72_0a C80_1A=c80_1a \
+ \ C82_1A=c82_1a C84_1A=c84_1a C92_2A=c92_2a C94_1A=c94_1a C95_2A=c95_2a \
+ \ C96_1C=c96_1c C97_3A=c97_3a
+preprocess.prop{fpp.defs} = $keys_model
+preprocess.prop{cpp.defs} = $keys_model
+preprocess.prop{fpp} = cpp
+preprocess.prop{cpp} = xlc
+preprocess.prop{cpp.flags} = -E -C
+preprocess.prop{fpp.flags} = -E -P -traditional
+preprocess.ns-excl = um/script um/utility/qxreconf
+
+build.target = um.exe
+build.target-rename = flumeMain.exe:um.exe
+build.prop{cc} = xlc_r
+build.prop{fc} = mpxlf90_r
+$flags_base_model = -qrealsize=8 -qintsize=8 -qextname -qsuffix=f=f90 \
+ \ -qarch=pwr6 -qtune=pwr6 -qxflag=p6div -NS32768 -g -O0 \
+ \ -qstrict -I/projects/um1/gcom/gcom3.6/meto_ibm_pwr6_mpp/inc \
+ \ -I/projects/um1/lib/netcdf3.20090102/include
+build.prop{fc.flags} = $flags_base_model
+build.prop{fc.flags}[um/io_services] = $flags_base_model -qsmp=omp
+build.prop{fc.flags-ld} = -lmass -lmassvp6 -qsmp=omp \
+ \ -L/projects/um1/gcom/gcom3.6/meto_ibm_pwr6_mpp/lib -lgcom \
+ \ -L/projects/um1/lib/netcdf3.20090102/lib64 -lnetcdf \
+ \ -L/projects/um1/lib -lsig -lgrib
+build.prop{dep.o.special} = blkdata.o
+build.prop{ns-dep.o} = um/control/c_code
+build.prop{dep.o}[um/control/c_code] = print_from_c.o
+build.prop{no-dep.f.module} = mpl
+build.ns-excl = um/script um/utility/qxreconf
+
+$keys_recon = C_LONG_INT=c_long_int C_LOW_U=c_low_u FRL8=frl8 \
+ \ BUFRD_IO=bufrd_io VECTLIB=vectlib IBM=ibm C98_1A=c98_1a MO_GRIB=mo_grib \
+ \ RECON=recon
+preprocess-recon.prop{no-step-source} = preprocess
+preprocess-recon.prop{fpp.defs} = $keys_recon
+preprocess-recon.prop{cpp.defs} = $keys_recon
+preprocess-recon.prop{fpp} = cpp
+preprocess-recon.prop{cpp} = xlc
+preprocess-recon.prop{cpp.flags} = -E -C
+preprocess-recon.prop{fpp.flags} = -E -P -traditional
+preprocess-recon.ns-excl = um/script um/utility/makebc
+
+build-recon.prop{no-step-source} = preprocess
+build-recon.target = qxreconf
+build-recon.target-rename = reconfigure.exe:qxreconf
+build-recon.prop{cc} = xlc_r
+build-recon.prop{fc} = mpxlf90_r
+build-recon.prop{fc.flags} = $flags_base_model
+build-recon.prop{fc.flags-ld} = -lmass -lmassvp6 -qsmp=omp \
+ \ -L/projects/um1/gcom/gcom3.6/meto_ibm_pwr6_mpp/lib -lgcom \
+ \ -L/projects/um1/lib/netcdf3.20090102/lib64 -lnetcdf \
+ \ -L/projects/um1/lib -lsig -lgrib
+build-recon.prop{ns-dep.o} = um/control/c_code
+build-recon.prop{dep.o}[um/control/c_code] = print_from_c.o
+build-recon.prop{no-dep.f.module} = mpl
+build-recon.ns-excl = um/script um/utility/makebc
diff --git a/test/repos/trunk/cfg/fcm2_um77_inherit.cfg b/test/repos/trunk/cfg/fcm2_um77_inherit.cfg
new file mode 100644
index 0000000..fabdf74
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_um77_inherit.cfg
@@ -0,0 +1,15 @@
+use = $RUN_DIR/fcm2_um77
+
+extract.location{diff}[um] = \
+ branches/dev/frps/VN7.7_qpos_opt at 28576 \
+ branches/dev/frpe/VN7.7_extra_ts at 28576 \
+ branches/dev/hadco/VN7.7_incrCLO at 25591 \
+ branches/dev/frml/VN7.7_new_pmsl at 26275 \
+ branches/dev/frhi/VN7.7_bugfix at 27020 \
+ branches/dev/frgr/VN7.7_JULES_fixes at 26312 \
+ branches/dev/haddb/VN7.7_JULES_decpl at 27034 \
+ branches/dev/frme/VN7.7_cloud_scheme_bugfixes at 26531 \
+ branches/dev/frwm/VN7.7_simple_conv_diag at 26841 \
+ branches/dev/frjw/VN7.7_mphys_pert_sens at 26510 \
+ branches/dev/frjw/VN7.7_microphys_iterations_plumbing at 27035 \
+ branches/dev/haddb/VN7.7_CoastConvG at 27374
diff --git a/test/repos/trunk/cfg/fcm2_um77_inherit_hpc.cfg b/test/repos/trunk/cfg/fcm2_um77_inherit_hpc.cfg
new file mode 100644
index 0000000..8354b82
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_um77_inherit_hpc.cfg
@@ -0,0 +1,17 @@
+use = $RUN_DIR/fcm2_um77_hpc
+
+extract.location{diff}[um] = \
+ branches/dev/frps/VN7.7_qpos_opt at 28576 \
+ branches/dev/frpe/VN7.7_extra_ts at 28576 \
+ branches/dev/hadco/VN7.7_incrCLO at 25591 \
+ branches/dev/frml/VN7.7_new_pmsl at 26275 \
+ branches/dev/frhi/VN7.7_bugfix at 27020 \
+ branches/dev/frgr/VN7.7_JULES_fixes at 26312 \
+ branches/dev/haddb/VN7.7_JULES_decpl at 27034 \
+ branches/dev/frme/VN7.7_cloud_scheme_bugfixes at 26531 \
+ branches/dev/frwm/VN7.7_simple_conv_diag at 26841 \
+ branches/dev/frjw/VN7.7_mphys_pert_sens at 26510 \
+ branches/dev/frjw/VN7.7_microphys_iterations_plumbing at 27035 \
+ branches/dev/haddb/VN7.7_CoastConvG at 27374
+
+mirror.target = $HPC:$THIS_RUN_DIR_HPC
diff --git a/test/repos/trunk/cfg/fcm2_um_hpc.cfg b/test/repos/trunk/cfg/fcm2_um_hpc.cfg
new file mode 100644
index 0000000..5f98ed6
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_um_hpc.cfg
@@ -0,0 +1,42 @@
+steps = extract mirror
+
+mirror.target = $HPC:$THIS_RUN_DIR_HPC
+mirror.prop{config-file.steps} = preprocess build
+
+extract.ns = um
+extract.location[um] = trunk at vn7.3
+extract.path-root[um] = src
+
+$keys_model = C_LONG_INT=c_long_int MPP=mpp C_LOW_U=c_low_u \
+ \ FRL8=frl8 BUFRD_IO=bufrd_io VECTLIB=vectlib IBM=ibm CONTROL=control \
+ \ REPROD=reprod MPP=mpp ATMOS=atmos GLOBAL=global A04_ALL=a04_all \
+ \ A01_3C=a01_3c A02_3C=a02_3c A03_8C=a03_8c A04_3D=a04_3d A05_4A=a05_4a \
+ \ A06_4A=a06_4a A08_7A=a08_7a A09_2A=a09_2a A10_2A=a10_2a A11_2A=a11_2a \
+ \ A12_2A=a12_2a A13_2A=a13_2a A14_1B=a14_1b A15_1A=a15_1a A16_1A=a16_1a \
+ \ A17_2B=a17_2b A18_0A=a18_0a A19_1A=a19_1a A25_0A=a25_0a A26_0A=a26_0a \
+ \ A30_1A=a30_1a A31_0A=a31_0a A32_1A=a32_1a A33_0A=a33_0a A34_0A=a34_0a \
+ \ A35_0A=a35_0a A38_0A=a38_0a A70_1C=a70_1c A71_1A=a71_1a C70_1A=c70_1a \
+ \ C72_0A=c72_0a C80_1A=c80_1a C82_1A=c82_1a C84_1A=c84_1a C92_2A=c92_2a \
+ \ C94_1A=c94_1a C95_2A=c95_2a C96_1C=c96_1c C97_3A=c97_3a
+preprocess.prop{fpp.defs} = $keys_model
+preprocess.prop{cpp.defs} = $keys_model
+preprocess.prop{fpp} = cpp
+preprocess.prop{cpp} = xlc
+preprocess.prop{cpp.flags} = -E -C
+preprocess.prop{fpp.flags} = -E -P -traditional
+
+build.target = um.exe
+build.target-rename = flumeMain.exe:um.exe
+build.prop{cc} = xlc_r
+build.prop{fc} = mpxlf90_r
+build.prop{fc.flags} = -I/projects/um1/gcom/gcom3.3/meto_ibm_pwr6_mpp/inc -I/projects/um1/lib/netcdf3.20090102/include -qextname -qsuffix=f=f90 -qarch=pwr6 -qtune=pwr6 -qrealsize=8 -qintsize=8 -NS32768 -O0
+build.prop{fc.flags}[um/atmosphere/dynamics_advection/eta_vert_weights_e.F90] = -qextname -qsuffix=f=f90 -qarch=pwr6 -qtune=pwr6 -qrealsize=8 -qintsize=8 -O0 -NS32768
+build.prop{fc.flags}[um/control/top_level/atm_step.F90] = -qextname -qsuffix=f=f90 -qarch=pwr6 -qtune=pwr6 -qrealsize=8 -qintsize=8 -O0 -NS32768
+build.prop{fc.flags}[um/control/top_level/u_model.F90] = -qextname -qsuffix=f=f90 -qarch=pwr6 -qtune=pwr6 -qrealsize=8 -qintsize=8 -O0 -NS32768
+build.prop{fc.flags-ld} = -lmass -lmassvp6 -L/projects/um1/gcom/gcom3.3/meto_ibm_pwr6_mpp/lib -lgcom -L/projects/um1/lib -lgrib -lsig -L/projects/um1/lib/netcdf3.20090102/lib64 -lnetcdf
+build.prop{dep.o.special} = blkdata.o
+build.prop{ns-dep.o} = um/control/c_code
+build.prop{dep.o}[um/control/c_code] = print_from_c.o
+build.prop{no-dep.f.module} = netcdf mpl mod_prism_proto mod_prism_grids_writing mod_prism_def_partition_proto mod_prism_put_proto mod_prism_get_proto
+build.prop{no-dep.include} = netcdf.inc mpif.h
+build.prop{no-dep.bin}[um/script] = *
diff --git a/test/repos/trunk/cfg/fcm2_um_inherit.cfg b/test/repos/trunk/cfg/fcm2_um_inherit.cfg
new file mode 100644
index 0000000..9ecf20e
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_um_inherit.cfg
@@ -0,0 +1,17 @@
+use = $RUN_DIR/fcm2_um
+
+extract.location{diff}[um] = \
+ branches/dev/Share/VN7.3_hg3_dust_443 at 11858 \
+ branches/dev/Share/VN7.3_hg3_ccw_precip at 11857 \
+ branches/dev/hadco/VN7.3_HG3_porting_lsp_fixes at 12029 \
+ branches/dev/hadco/VN7.3_pc2_qcl_gt_tiny at 12142 \
+ branches/dev/hadas/VN7.3_w_CAPE_diag at 12012 \
+ branches/dev/frlk/VN7.3_BLLEVS_fixes at 13938 \
+ branches/dev/frwm/VN7.3_Cu_Diag_Low_LCL at 12011 \
+ branches/dev/frwm/VN7.3_LimitCnvParcPert at 12041 \
+ branches/dev/hadip/VN7.3_ilp_moose at 13314 \
+ branches/dev/hadco/VN7.3_temp_fix_solver at 12740 \
+ branches/dev/hadng/VN7.3_wetlands_rothc at 12603 \
+ branches/dev/frtg/VN7.3_reconf_extern_ancil at 13063 \
+ branches/dev/frma/VN7.3_rad_dev at 12732 \
+ branches/dev/frlk/VN7.3_melt_fix at 13822
diff --git a/test/repos/trunk/cfg/fcm2_um_inherit_hpc.cfg b/test/repos/trunk/cfg/fcm2_um_inherit_hpc.cfg
new file mode 100644
index 0000000..6a0e5fe
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_um_inherit_hpc.cfg
@@ -0,0 +1,19 @@
+use = $RUN_DIR/fcm2_um_hpc
+
+extract.location{diff}[um] = \
+ branches/dev/Share/VN7.3_hg3_dust_443 at 11858 \
+ branches/dev/Share/VN7.3_hg3_ccw_precip at 11857 \
+ branches/dev/hadco/VN7.3_HG3_porting_lsp_fixes at 12029 \
+ branches/dev/hadco/VN7.3_pc2_qcl_gt_tiny at 12142 \
+ branches/dev/hadas/VN7.3_w_CAPE_diag at 12012 \
+ branches/dev/frlk/VN7.3_BLLEVS_fixes at 13938 \
+ branches/dev/frwm/VN7.3_Cu_Diag_Low_LCL at 12011 \
+ branches/dev/frwm/VN7.3_LimitCnvParcPert at 12041 \
+ branches/dev/hadip/VN7.3_ilp_moose at 13314 \
+ branches/dev/hadco/VN7.3_temp_fix_solver at 12740 \
+ branches/dev/hadng/VN7.3_wetlands_rothc at 12603 \
+ branches/dev/frtg/VN7.3_reconf_extern_ancil at 13063 \
+ branches/dev/frma/VN7.3_rad_dev at 12732 \
+ branches/dev/frlk/VN7.3_melt_fix at 13822
+
+mirror.target = $HPC:$THIS_RUN_DIR_HPC
diff --git a/test/repos/trunk/cfg/fcm2_var.cfg b/test/repos/trunk/cfg/fcm2_var.cfg
new file mode 100644
index 0000000..21b0fa4
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_var.cfg
@@ -0,0 +1,80 @@
+steps = extract preprocess build
+
+extract.ns = gcom gen ops var var_admin
+extract.location[var] = trunk at 14844
+extract.path-root[var] = src
+extract.path-excl[var] = config scripts
+extract.path-incl[var] = scripts/Var_Scripts
+
+extract.location[ops] = trunk at 18341
+extract.path-root[ops] = src/code
+extract.path-excl[ops] = /
+extract.path-incl[ops] = OpsMod_Constants \
+ \ OpsMod_Control OpsMod_GeoIR \
+ \ OpsMod_ObsInfo \
+ \ OpsMod_RTTOV \
+ \ OpsMod_Sort \
+ \ OpsMod_Utilities \
+ \ OpsMod_Varobs \
+ \ OpsMod_VerticalInterp \
+ \ OpsMod_VisControl \
+ \ OpsProg_RTTOV9 \
+ \ Ops_AIRS_1DVar \
+ \ Ops_AIRS_Utilities \
+ \ Ops_RTTOV7 \
+ \ Ops_RTTOV7_RTTOVCLD \
+ \ Ops_RTTOV9 \
+ \ Ops_SatRad_Info \
+ \ Ops_SatRad_Process \
+ \ Ops_SatRad_SetUp \
+ \ Ops_SatRad_Utilities
+
+extract.location[gen] = trunk at 3073
+extract.path-root[gen] = src/code
+
+extract.location[var_admin] = trunk at 14851
+extract.path-root[var_admin] = src/code
+
+extract.location[gcom] = branches/dev/ibmjb/r12957_2194_ralltoalle_out_of_order at 15824
+extract.path-root[gcom] = build
+extract.path-excl[gcom] = configs ext_scripts
+
+preprocess.ns-excl = var ops gen var_admin
+preprocess.ns-incl = var/code/PF_MPP \
+ \ ops/Ops_RTTOV9/rttov9_parallel_ad.F90 \
+ \ ops/Ops_RTTOV9/rttov9_parallel_direct.F90 \
+ \ ops/Ops_RTTOV9/rttov9_parallel_k.F90 \
+ \ ops/Ops_RTTOV9/rttov9_parallel_tl.F90
+preprocess.prop{fpp} = wrap_pp
+preprocess.prop{fpp.defs} = IFORT_CDIRS
+preprocess.prop{fpp.defs}[gcom] = GC_VERSION="'3.4+'" GC_BUILD_DATE="'15824'" PREC_64B GC__FLUSHUNIT6 GC__FORTERRUNIT=0 GC_DESCRIP="'MPP'" MPI_SRC MPILIB_32B
+preprocess.prop{fpp.defs}[ops/Ops_RTTOV9/rttov9_parallel_ad.F90] = _RTTOV_PARALLEL_AD
+preprocess.prop{fpp.defs}[ops/Ops_RTTOV9/rttov9_parallel_direct.F90] = _RTTOV_PARALLEL_DIRECT
+preprocess.prop{fpp.defs}[ops/Ops_RTTOV9/rttov9_parallel_k.F90] = _RTTOV_PARALLEL_K
+preprocess.prop{fpp.defs}[ops/Ops_RTTOV9/rttov9_parallel_tl.F90] = _RTTOV_PARALLEL_TL
+
+$OPSDIR{?} = /home/h04/opsrc/ops0
+$mpich2 = $OPSDIR/mpi/mpich2-1.4-ukmo-v1/ifort-12
+build.target = VarScr_HelpCompile
+build.prop{cc} = wrap_cc
+build.prop{cc.defs}[gen/GenMod_Platform] = LOWERCASE UNDERSCORE FRL8 C_LONG_LONG_INT
+build.prop{cc.defs}[gen/UM_Platform] = VAROPSVER C_LOW_U FRL8 C_LONG_LONG_INT LINUX LITTLE_END
+build.prop{fc} = wrap_fc
+build.prop{fc.flags} = -implicitnone -integer_size 64 -real_size 64 -ftrapuv
+build.prop{fc.flags}[gcom] = -implicitnone -integer_size 64 -real_size 64 -ftrapuv -warn none
+build.prop{fc.flags}[gcom/mpl/mpl.F90] = -implicitnone -integer_size 64 -real_size 64 -ftrapuv -warn none -I$mpich2/include
+build.prop{fc.flags}[gen] = -implicitnone -integer_size 64 -real_size 64 -ftrapuv -warn noerrors
+build.prop{fc.flags}[ops/Ops_RTTOV9] = -implicitnone -integer_size 64 -real_size 64 -ftrapuv -warn none
+build.prop{fc.flags}[var_admin] = -implicitnone -integer_size 64 -real_size 64 -ftrapuv -warn none
+build.prop{fc.flags}[var/code/PF_Interpolation/Cubic_Lagrange_Adj.F90] = -implicitnone -integer_size 64 -real_size 64 -ftrapuv -Wp,-P
+build.prop{fc.flags}[var/code/PF_MPP] = -implicitnone -integer_size 64 -real_size 64 -ftrapuv -warn noerrors
+build.prop{fc.flags}[var/code/VarProg_UMFileUtils] = -implicitnone -integer_size 64 -real_size 64
+build.prop{fc.defs}[var ops gen var_admin] = IFORT_CDIRS
+build.prop{fc.defs}[var/code/PF_MPP] =
+build.prop{fc.defs}[gen/GenMod_Control] = GCOMHEADERS
+build.prop{fc.defs}[gen/GenMod_Utilities/Gen_FlushUnit.F90] = USE_FLUSH
+build.prop{fc.defs}[gen/UM_COEX gen/UM_Platform] = VAROPSVER
+build.prop{fc.flags-ld} = -L$mpich2/lib -lmpich -lmpl -lpthread
+build.prop{ns-dep.o}[var] = gcom var_admin/VarMod_Lapack var_admin/VarMod_Blas
+build.prop{no-dep.f.module} = f90_unix_io xlfutility
+build.prop{no-dep.include} = mpif.h
diff --git a/test/repos/trunk/cfg/fcm2_var_hpc.cfg b/test/repos/trunk/cfg/fcm2_var_hpc.cfg
new file mode 100644
index 0000000..8148207
--- /dev/null
+++ b/test/repos/trunk/cfg/fcm2_var_hpc.cfg
@@ -0,0 +1,87 @@
+steps = extract mirror
+
+mirror.target = $HPC:$THIS_RUN_DIR_HPC
+mirror.prop{config-file.steps} = preprocess build
+
+extract.ns = gcom gen ops var var_admin
+extract.location[var] = trunk at 14844
+extract.path-root[var] = src
+extract.path-excl[var] = config scripts
+extract.path-incl[var] = scripts/Var_Scripts
+
+extract.location[ops] = trunk at 18341
+extract.path-root[ops] = src/code
+extract.path-excl[ops] = /
+extract.path-incl[ops] = OpsMod_Constants \
+ \ OpsMod_Control OpsMod_GeoIR \
+ \ OpsMod_ObsInfo \
+ \ OpsMod_RTTOV \
+ \ OpsMod_Sort \
+ \ OpsMod_Utilities \
+ \ OpsMod_Varobs \
+ \ OpsMod_VerticalInterp \
+ \ OpsMod_VisControl \
+ \ OpsProg_RTTOV9 \
+ \ Ops_AIRS_1DVar \
+ \ Ops_AIRS_Utilities \
+ \ Ops_RTTOV7 \
+ \ Ops_RTTOV7_RTTOVCLD \
+ \ Ops_RTTOV9 \
+ \ Ops_SatRad_Info \
+ \ Ops_SatRad_Process \
+ \ Ops_SatRad_SetUp \
+ \ Ops_SatRad_Utilities
+
+extract.location[gen] = trunk at 3073
+extract.path-root[gen] = src/code
+
+extract.location[var_admin] = trunk at 14851
+extract.path-root[var_admin] = src/code
+
+extract.location[gcom] = branches/dev/ibmjb/r12957_2194_ralltoalle_out_of_order at 15824
+extract.path-root[gcom] = build
+extract.path-excl[gcom] = configs ext_scripts
+
+preprocess.ns-excl = var ops gen var_admin
+preprocess.ns-incl = ops/Ops_RTTOV9/rttov9_parallel_ad.F90 \
+ \ ops/Ops_RTTOV9/rttov9_parallel_direct.F90 \
+ \ ops/Ops_RTTOV9/rttov9_parallel_k.F90 \
+ \ ops/Ops_RTTOV9/rttov9_parallel_tl.F90
+preprocess.prop{cpp.flags} = -C
+preprocess.prop{cpp.defs} = LOWERCASE
+preprocess.prop{fpp.defs}[gcom] = GC_VERSION="'3.4+'" GC_BUILD_DATE="'15824'" PREC_64B GC__FLUSHUNIT6 GC__FORTERRUNIT=0 MPI_SRC MPILIB_32B GC_DESCRIP="'MPP'" MPI_BSEND_BUFFER_SIZE=10240000 IBM
+preprocess.prop{fpp.defs}[ops/Ops_RTTOV9/rttov9_parallel_ad.F90] = _RTTOV_PARALLEL_AD
+preprocess.prop{fpp.defs}[ops/Ops_RTTOV9/rttov9_parallel_direct.F90] = _RTTOV_PARALLEL_DIRECT
+preprocess.prop{fpp.defs}[ops/Ops_RTTOV9/rttov9_parallel_k.F90] = _RTTOV_PARALLEL_K
+preprocess.prop{fpp.defs}[ops/Ops_RTTOV9/rttov9_parallel_tl.F90] = _RTTOV_PARALLEL_TL
+
+build.target = VarScr_HelpCompile
+build.prop{cc} = xlc
+build.prop{cc.defs}[gen/GenMod_Platform] = LOWERCASE FRL8 C_LONG_INT
+build.prop{cc.defs}[gen/UM_Platform] = VAROPSVER C_LOW FRL8 C_LONG_INT
+build.prop{fc} = mpxlf95_r
+build.prop{fc.flags} = -O0 -qrealsize=8 -qintsize=8 -g -qfullpath -qessl -qarch=pwr6 -qtune=pwr6 -qmaxmem=-1
+build.prop{fc.flags}[var/code/PFMod_Model] = -O0 -qrealsize=8 -qintsize=8 -g -qfullpath -qessl -qarch=pwr6 -qtune=pwr6 -qmaxmem=-1 -NS1024
+build.prop{fc.flags}[gcom/gc] = -O0 -qrealsize=8 -qintsize=8 -g -qfullpath -qessl -qarch=pwr6 -qtune=pwr6 -qmaxmem=-1 -qfixed
+build.prop{fc.flags}[gcom/gcg] = -O0 -qrealsize=8 -qintsize=8 -g -qfullpath -qessl -qarch=pwr6 -qtune=pwr6 -qmaxmem=-1 -qfixed
+build.prop{fc.flags}[gen/GenMod_Reporting/GenMod_Reporting.F90] = -O0 -qrealsize=8 -qintsize=8 -g -qfullpath -qessl -qarch=pwr6 -qtune=pwr6 -qmaxmem=-1 -WF,-qfpp
+build.prop{fc.flags}[gen/GenMod_Utilities/Gen_FlushUnit.F90] = -O0 -qrealsize=8 -qintsize=8 -g -qfullpath -qessl -qarch=pwr6 -qtune=pwr6 -qmaxmem=-1 -WF,-qfpp
+build.prop{fc.flags}[gen/UM_COEX] = -O0 -qrealsize=8 -qintsize=8 -g -qfullpath -qessl -qarch=pwr6 -qtune=pwr6 -qmaxmem=-1 -qfixed -WF,-qfpp
+build.prop{fc.flags}[gen/UM_General] = -O0 -qrealsize=8 -qintsize=8 -g -qfullpath -qessl -qarch=pwr6 -qtune=pwr6 -qmaxmem=-1 -qfixed
+build.prop{fc.flags}[gen/UM_Platform] = -O0 -qrealsize=8 -qintsize=8 -g -qfullpath -qessl -qarch=pwr6 -qtune=pwr6 -qmaxmem=-1 -qfixed -WF,-qfpp
+build.prop{fc.flags}[gen/UM_Platform/IOERROR.F90] = -O0 -qrealsize=8 -qintsize=8 -g -qfullpath -qessl -qarch=pwr6 -qtune=pwr6 -qmaxmem=-1 -qfixed -WF,-qfpp -qfree=f90
+build.prop{fc.flags}[var/code/VarMod_CovVertical] = -O0 -qrealsize=8 -qintsize=8 -g -qfullpath -qessl -qarch=pwr6 -qtune=pwr6 -qmaxmem=-1 -qnoessl
+build.prop{fc.flags}[var/code/VarMod_CovVerticalData] = -O0 -qrealsize=8 -qintsize=8 -g -qfullpath -qessl -qarch=pwr6 -qtune=pwr6 -qmaxmem=-1 -qnoessl
+build.prop{fc.flags}[var_admin] = -O0 -qrealsize=8 -qintsize=8 -g -qfullpath -qessl -qarch=pwr6 -qtune=pwr6 -qmaxmem=-1 -qfixed -qrealsize=4 -qstrict
+build.prop{fc.flag-define} = -WF,-D%s
+build.prop{fc.defs}[gen/GenMod_Control] = GCOMHEADERS
+build.prop{fc.defs}[gen/GenMod_Control/Gen_SetupControl.F90] = USE_CUSTOM_SIGNAL_HANDLER AIX
+build.prop{fc.defs}[gen/GenMod_Reporting/GenMod_Reporting.F90] = GEN_LEN_ERROR_OUT=134
+build.prop{fc.defs}[gen/GenMod_Utilities/Gen_FlushUnit.F90] = USE_FLUSH AIX
+build.prop{fc.defs}[gen/UM_COEX gen/UM_Platform] = VAROPSVER
+build.prop{fc.defs}[ops/Ops_RTTOV9] = RTTOV_ARCH_VECTOR
+build.prop{fc.defs}[ops/Ops_RTTOV9/rttov9_parkind1.F90] = DEFAULT_INTEGER_32BIT
+build.prop{fc.flags-ld} = -L/projects/um1/lib -lsig -lessl -lmassvp6 -lmass
+build.prop{ns-dep.o}[var] = gcom var_admin/VarMod_Lapack var_admin/VarMod_Blas
+build.prop{no-dep.f.module} = f90_unix_io xlfutility
+build.prop{no-dep.include} = mpif.h
diff --git a/test/repos/trunk/module/hello_constants.f90 b/test/repos/trunk/module/hello_constants.f90
new file mode 100644
index 0000000..b8237b9
--- /dev/null
+++ b/test/repos/trunk/module/hello_constants.f90
@@ -0,0 +1,5 @@
+MODULE Hello_Constants
+
+INCLUDE 'hello_constants_dummy.inc'
+
+END MODULE Hello_Constants
diff --git a/test/repos/trunk/module/hello_constants.inc b/test/repos/trunk/module/hello_constants.inc
new file mode 100644
index 0000000..ae26a9b
--- /dev/null
+++ b/test/repos/trunk/module/hello_constants.inc
@@ -0,0 +1 @@
+CHARACTER (LEN=80), PARAMETER :: hello_string = 'Hello Earth!'
diff --git a/test/repos/trunk/module/hello_constants_dummy.inc b/test/repos/trunk/module/hello_constants_dummy.inc
new file mode 100644
index 0000000..06f117b
--- /dev/null
+++ b/test/repos/trunk/module/hello_constants_dummy.inc
@@ -0,0 +1 @@
+INCLUDE 'hello_constants.inc'
diff --git a/test/repos/trunk/namelist/namelist.NL b/test/repos/trunk/namelist/namelist.NL
new file mode 100644
index 0000000..8210d58
--- /dev/null
+++ b/test/repos/trunk/namelist/namelist.NL
@@ -0,0 +1,3 @@
+&TestNL
+ test = ,
+/
diff --git a/test/repos/trunk/pro/hello.pro b/test/repos/trunk/pro/hello.pro
new file mode 100644
index 0000000..bc880e8
--- /dev/null
+++ b/test/repos/trunk/pro/hello.pro
@@ -0,0 +1,2 @@
+PRO HELLO
+END
diff --git a/test/repos/trunk/pro/plot.pro b/test/repos/trunk/pro/plot.pro
new file mode 100644
index 0000000..5896f2b
--- /dev/null
+++ b/test/repos/trunk/pro/plot.pro
@@ -0,0 +1,3 @@
+PRO PLOT
+; Calls : hello.pro
+END
diff --git a/test/repos/trunk/program/hello.F90 b/test/repos/trunk/program/hello.F90
new file mode 100644
index 0000000..87f1a31
--- /dev/null
+++ b/test/repos/trunk/program/hello.F90
@@ -0,0 +1,26 @@
+PROGRAM Hello
+
+#if !defined(LOCAL_STRING)
+USE Hello_Constants, ONLY: hello_string
+#endif
+
+IMPLICIT NONE
+
+#if defined(LOCAL_STRING)
+CHARACTER (LEN=80), PARAMETER :: hello_string = 'Hello Mother Earth!'
+#endif
+
+INTEGER :: integer_arg = 1234
+
+#if defined(CALL_HELLO_SUB)
+INCLUDE 'hello_sub.interface'
+#endif
+
+CHARACTER (LEN=*), PARAMETER :: this = 'Hello'
+
+WRITE (*, '(A)') this // ': ' // TRIM (hello_string)
+#if defined(CALL_HELLO_SUB)
+CALL Hello_Sub (integer_arg)
+#endif
+
+END PROGRAM Hello
diff --git a/test/repos/trunk/script/hello.sh b/test/repos/trunk/script/hello.sh
new file mode 100755
index 0000000..3574568
--- /dev/null
+++ b/test/repos/trunk/script/hello.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/ksh
+# Calls : hello.exe
+# Calls : plot.pro
+
+hello.exe
diff --git a/test/repos/trunk/subroutine/hello_c.c b/test/repos/trunk/subroutine/hello_c.c
new file mode 100644
index 0000000..45ca182
--- /dev/null
+++ b/test/repos/trunk/subroutine/hello_c.c
@@ -0,0 +1,5 @@
+#include <stdio.h>
+
+void hello_c_ () {
+ printf ("%s\n", "Hello_C: Hello Earth!");
+}
diff --git a/test/repos/trunk/subroutine/hello_sub.F90 b/test/repos/trunk/subroutine/hello_sub.F90
new file mode 100644
index 0000000..b7d7da6
--- /dev/null
+++ b/test/repos/trunk/subroutine/hello_sub.F90
@@ -0,0 +1,24 @@
+#if defined(HELLO_SUB)
+SUBROUTINE Hello_Sub (integer_arg)
+
+USE Hello_Constants, ONLY: hello_string
+
+IMPLICIT NONE
+
+CHARACTER (LEN=*), PARAMETER :: this = 'Hello_Sub'
+INTEGER :: integer_arg
+INTEGER :: integer_common
+COMMON /general/integer_common
+
+! DEPENDS ON: hello_c.o
+EXTERNAL Hello_C
+
+#include "hello_sub_dummy.h"
+
+WRITE (*, '(A,I0)') this // ': integer (arg): ', integer_arg
+WRITE (*, '(A,I0)') this // ': integer (common): ', integer_common
+
+CALL Hello_C ()
+
+END SUBROUTINE Hello_Sub
+#endif
diff --git a/test/repos/trunk/subroutine/hello_sub.h b/test/repos/trunk/subroutine/hello_sub.h
new file mode 100644
index 0000000..36fd211
--- /dev/null
+++ b/test/repos/trunk/subroutine/hello_sub.h
@@ -0,0 +1 @@
+WRITE (*, '(A)') this // ': ' // TRIM (hello_string)
diff --git a/test/repos/trunk/subroutine/hello_sub_dummy.h b/test/repos/trunk/subroutine/hello_sub_dummy.h
new file mode 100644
index 0000000..591744b
--- /dev/null
+++ b/test/repos/trunk/subroutine/hello_sub_dummy.h
@@ -0,0 +1 @@
+#include "hello_sub.h"
diff --git a/test/run_tests b/test/run_tests
new file mode 100755
index 0000000..60cbbcc
--- /dev/null
+++ b/test/run_tests
@@ -0,0 +1,259 @@
+#!/bin/ksh
+set -u
+trap "echo Received signal ERR; exit 1" ERR
+trap "echo Received signal TERM; exit 1" TERM
+
+export MY_BIN=$(cd $(dirname $0) && pwd)
+PATH=$MY_BIN:$PATH
+
+export DEBUG=false
+FCM1=true
+FCM2=true
+while getopts ":c:t:flradgh12" opt
+do
+ case $opt in
+ c ) CONTROL_URL=$OPTARG ;;
+ t ) TEST_URL=$OPTARG ;;
+ f ) FUNC_TESTS=true ;;
+ l ) LOCAL_TESTS=true ;;
+ r ) REMOTE_TESTS=true ;;
+ a ) FUNC_TESTS=true
+ LOCAL_TESTS=true
+ REMOTE_TESTS=true ;;
+ d ) DELETE=true ;;
+ g ) DEBUG=true ;;
+ h ) HELP=true ;;
+ 1 ) FCM2=false ;;
+ 2 ) FCM1=false ;;
+ \? ) echo "Invalid option"
+ HELP=true
+ break ;;
+ esac
+done
+if [[ $# != $(($OPTIND - 1)) ]]; then
+ echo "Invalid argument"
+ HELP=true
+fi
+
+if [[ ${HELP:-} == true ]]; then
+ echo 'Usage: run_tests [options]'
+ echo 'Valid options:'
+ echo '-c URL'
+ echo ' Generate the control results using the version of FCM at "URL"'
+ echo '-t URL'
+ echo ' Generate the test results using the version of FCM at "URL"'
+ echo '-f'
+ echo ' Perform the functional tests'
+ echo '-l'
+ echo ' Perform the local performance tests'
+ echo '-r'
+ echo ' Perform the remote performance tests'
+ echo '-a'
+ echo ' Perform all the tests (equivalent to -flr)'
+ echo '-d'
+ echo ' Remove any previous test results before starting'
+ echo '-g'
+ echo ' Output additional diagnostics'
+ echo '-h'
+ echo ' Print this help message'
+ echo '-1'
+ echo ' Only run the FCM1 tests'
+ echo '-2'
+ echo ' Only run the FCM2 tests'
+ exit 1
+fi
+
+if [[ -n "${CONTROL_URL:-}" ]]; then
+ TYPES="control"
+fi
+if [[ -n "${TEST_URL:-}" ]]; then
+ TYPES="${TYPES:-} test"
+fi
+if [[ -z "${TYPES:-}" ]]; then
+ echo "Either a control or a test URL must be specified"
+ exit 1
+fi
+
+if [[ ${REMOTE_TESTS:-} == true ]]; then
+ export HPC=$(rose host-select -q hpc)
+ export BASE_DIR_HPC=$(ssh $HPC 'echo $PWD')/working/fcm_test_suite
+fi
+
+export BASE_DIR=$LOCALTEMP/fcm_test_suite
+if [[ ${DELETE:-} == true ]]; then
+ if $DEBUG; then
+ echo "Removing any previous test directory ..."
+ fi
+ rm -rf $BASE_DIR
+ if [[ ${REMOTE_TESTS:-} == true ]]; then
+ ssh $HPC "rm -rf $BASE_DIR_HPC"
+ fi
+fi
+mkdir -p $BASE_DIR
+
+export REPOS_DIR=$BASE_DIR/test_svn
+export REPOS_URL="file://$REPOS_DIR"
+if [[ ! -d $REPOS_DIR ]]; then
+ echo "$(date): Creating test repository ..."
+ $MY_BIN/create_repos > $BASE_DIR/repos.stdout 2> $BASE_DIR/repos.stderr
+fi
+
+cp $MY_BIN/compare_*_fcm* $BASE_DIR
+PATH_BASE=$MY_BIN/wrapper_scripts:$PATH:~opsrc/ops0/mpi/mpich2-1.4-ukmo-v1/ifort-12/bin
+
+trap "" ERR
+export TEST
+export TYPE
+for TYPE in $TYPES
+do
+ if [[ $TYPE == test ]]; then
+ URL=$TEST_URL
+ else
+ URL=$CONTROL_URL
+ fi
+ REV=$(git describe $URL) || exit $RC
+ echo "FCM version to be used: $REV"
+
+ export RUN_DIR=$BASE_DIR/$TYPE
+ rm -rf $RUN_DIR
+ mkdir $RUN_DIR
+
+ if $DEBUG; then
+ echo "Creating local copy of FCM ..."
+ fi
+ (cd $MY_BIN/.. && git archive --format=tar --prefix=fcm/ $REV) | (cd $RUN_DIR && tar xf -)
+ if [[ -a $RUN_DIR/fcm/etc/fcm.cfg ]]; then
+ echo "set::url::test_suite $REPOS_URL" >>$RUN_DIR/fcm/etc/fcm.cfg
+ else
+ echo "location{primary}[test_suite] = $REPOS_URL" >>$RUN_DIR/fcm/etc/fcm/keyword.cfg
+ fi
+ export PATH=$RUN_DIR/fcm/bin:$PATH_BASE
+
+ if [[ ${FUNC_TESTS:-} == true ]]; then
+ . $MY_BIN/tests_functional.list
+ export COMPARE_TIMES=false
+ let failed=0
+ if [[ $FCM1 == true ]]; then
+ for TEST in $TESTS_FCM1
+ do
+ $MY_BIN/perform_test_fcm1
+ if [[ $? != 0 ]]; then
+ let failed=failed+1
+ fi
+ done
+ fi
+ if [[ $FCM2 == true ]]; then
+ for TEST in $TESTS_FCM2
+ do
+ $MY_BIN/perform_test_fcm2
+ if [[ $? != 0 ]]; then
+ let failed=failed+1
+ fi
+ done
+ fi
+
+ echo "$(date): Functional tests finished"
+ if [[ $failed == 0 ]]; then
+ echo "SUMMARY: All functional tests succeeded"
+ else
+ echo "SUMMARY: $failed functional tests failed"
+ fi
+ fi
+
+ if [[ ${LOCAL_TESTS:-} == true ]]; then
+ . $MY_BIN/tests_perf_local.list
+ export COMPARE_TIMES=true
+ export run_1=no
+ export run_2=no
+ let failed=0
+ if [[ $FCM1 == true ]]; then
+ for TEST in $TESTS_FCM1
+ do
+ $MY_BIN/perform_test_fcm1
+ if [[ $? != 0 ]]; then
+ let failed=failed+1
+ fi
+ done
+ fi
+ if [[ $FCM2 == true ]]; then
+ for TEST in $TESTS_FCM2
+ do
+ $MY_BIN/perform_test_fcm2
+ if [[ $? != 0 ]]; then
+ let failed=failed+1
+ fi
+ done
+ fi
+ unset run_1 run_2
+
+ echo "$(date): Local performance tests finished"
+ if [[ $failed == 0 ]]; then
+ echo "SUMMARY: All local performance tests succeeded"
+ else
+ echo "SUMMARY: $failed local performance tests failed"
+ fi
+ fi
+
+ if [[ ${REMOTE_TESTS:-} == true ]]; then
+ if $DEBUG; then
+ echo "Copying files to HPC platform ..."
+ fi
+ export RUN_DIR_HPC=$BASE_DIR_HPC/$TYPE
+ ssh $HPC "rm -rf $RUN_DIR_HPC"
+ ssh $HPC "mkdir -p $RUN_DIR_HPC"
+ rsync -a --rsh="ssh" $RUN_DIR/fcm $HPC:$RUN_DIR_HPC
+ rsync -a --rsh="ssh" compare_*_fcm* report_hpc_results $HPC:$BASE_DIR_HPC
+
+ BATCH_SCRIPT_NAME=hpc_batch.sh
+ export BATCH_DIRS_NAME=hpc_dirs.sh
+ export BATCH_SCRIPT=$RUN_DIR/$BATCH_SCRIPT_NAME
+ $MY_BIN/create_hpc_batch_script
+ export BATCH_DIRS=$RUN_DIR/$BATCH_DIRS_NAME
+
+ . $MY_BIN/tests_perf_remote.list
+ export COMPARE_TIMES=true
+ export mirror=remote
+ let failed=0
+ echo 'TESTS_FCM1="' >$BATCH_DIRS
+ if [[ $FCM1 == true ]]; then
+ for TEST in $TESTS_FCM1
+ do
+ $MY_BIN/perform_test_fcm1
+ if [[ $? != 0 ]]; then
+ let failed=failed+1
+ else
+ SUBMIT_REMOTE=true
+ fi
+ done
+ fi
+ echo '"' >>$BATCH_DIRS
+ echo 'TESTS_FCM2="' >>$BATCH_DIRS
+ if [[ $FCM2 == true ]]; then
+ export NPROC=6
+ for TEST in $TESTS_FCM2
+ do
+ $MY_BIN/perform_test_fcm2
+ if [[ $? != 0 ]]; then
+ let failed=failed+1
+ else
+ SUBMIT_REMOTE=true
+ fi
+ done
+ fi
+ echo '"' >>$BATCH_DIRS
+ unset mirror NPROC
+
+ if [[ ${SUBMIT_REMOTE:-} == true ]]; then
+ echo "$(date): Submitting HPC build job ..."
+ rsync -a --rsh="ssh" $BATCH_SCRIPT $BATCH_DIRS $HPC:$RUN_DIR_HPC
+ ssh $HPC "llsubmit $RUN_DIR_HPC/$BATCH_SCRIPT_NAME"
+ fi
+
+ echo "$(date): HPC performance tests finished"
+ if [[ $failed == 0 ]]; then
+ echo "SUMMARY: All HPC performance tests succeeded"
+ else
+ echo "SUMMARY: $failed HPC performance tests failed"
+ fi
+ fi
+done
diff --git a/test/test_config/fcm1_add_directory b/test/test_config/fcm1_add_directory
new file mode 100755
index 0000000..06b4090
--- /dev/null
+++ b/test/test_config/fcm1_add_directory
@@ -0,0 +1,3 @@
+build_1=fail_known
+
+# This case fails since the extract system does not detect the new directory.
diff --git a/test/test_config/fcm1_add_directory_expsrc b/test/test_config/fcm1_add_directory_expsrc
new file mode 100755
index 0000000..64b9edb
--- /dev/null
+++ b/test/test_config/fcm1_add_directory_expsrc
@@ -0,0 +1,2 @@
+# Although this case works, the output from the extract system does not correctly
+# identify the file as added from branch1 (unlike in fcm1_add_file).
diff --git a/test/test_config/fcm1_branches_clash b/test/test_config/fcm1_branches_clash
new file mode 100755
index 0000000..c00e841
--- /dev/null
+++ b/test/test_config/fcm1_branches_clash
@@ -0,0 +1 @@
+extract_1=fail
diff --git a/test/test_config/fcm1_branches_merge_conflict_fail b/test/test_config/fcm1_branches_merge_conflict_fail
new file mode 100755
index 0000000..c00e841
--- /dev/null
+++ b/test/test_config/fcm1_branches_merge_conflict_fail
@@ -0,0 +1 @@
+extract_1=fail
diff --git a/test/test_config/fcm1_branches_merge_incremental b/test/test_config/fcm1_branches_merge_incremental
new file mode 100755
index 0000000..0cb29ab
--- /dev/null
+++ b/test/test_config/fcm1_branches_merge_incremental
@@ -0,0 +1 @@
+cfg_name="fcm1_base fcm1_branches_merge"
diff --git a/test/test_config/fcm1_branches_merge_inherit_wrong_include b/test/test_config/fcm1_branches_merge_inherit_wrong_include
new file mode 100755
index 0000000..e5257e8
--- /dev/null
+++ b/test/test_config/fcm1_branches_merge_inherit_wrong_include
@@ -0,0 +1,2 @@
+# The wrong include file gets used as demonstrated by the output from running the program.
+# See ticket:206
diff --git a/test/test_config/fcm1_branches_merge_wcopies b/test/test_config/fcm1_branches_merge_wcopies
new file mode 100755
index 0000000..92f75b3
--- /dev/null
+++ b/test/test_config/fcm1_branches_merge_wcopies
@@ -0,0 +1,4 @@
+mkdir -p $BASE_DIR/work
+svn co -q $REPOS_URL/branches/dev/Share/modify_files_base $BASE_DIR/work/b1
+svn co -q $REPOS_URL/branches/dev/Share/modify_files_merge1 $BASE_DIR/work/b2
+svn co -q $REPOS_URL/branches/dev/Share/modify_files_merge2 $BASE_DIR/work/b3
diff --git a/test/test_config/fcm1_branches_merge_wcopy b/test/test_config/fcm1_branches_merge_wcopy
new file mode 100755
index 0000000..48d6a7a
--- /dev/null
+++ b/test/test_config/fcm1_branches_merge_wcopy
@@ -0,0 +1 @@
+svn co -q $REPOS_URL/branches/dev/Share/modify_files_merge2 $BASE_DIR/work
diff --git a/test/test_config/fcm1_cflags_incremental b/test/test_config/fcm1_cflags_incremental
new file mode 100755
index 0000000..7ee8662
--- /dev/null
+++ b/test/test_config/fcm1_cflags_incremental
@@ -0,0 +1 @@
+cfg_name="fcm1_base fcm1_cflags fcm1_base"
diff --git a/test/test_config/fcm1_change_src_type_incremental b/test/test_config/fcm1_change_src_type_incremental
new file mode 100755
index 0000000..4ae350d
--- /dev/null
+++ b/test/test_config/fcm1_change_src_type_incremental
@@ -0,0 +1,5 @@
+cfg_name="fcm1_change_src_type fcm1_change_src_type"
+
+# This test was designed to demonstrate the problem raised in ticket:345.
+# The problem (now fixed) was that some files got rebuilt unnecessarily
+# in the incremental build.
diff --git a/test/test_config/fcm1_delete_directory b/test/test_config/fcm1_delete_directory
new file mode 100755
index 0000000..641513a
--- /dev/null
+++ b/test/test_config/fcm1_delete_directory
@@ -0,0 +1,3 @@
+extract_1=fail_known
+
+# This case fails since the extract system does not handle deleted directories.
diff --git a/test/test_config/fcm1_delete_directory_inherit b/test/test_config/fcm1_delete_directory_inherit
new file mode 100755
index 0000000..641513a
--- /dev/null
+++ b/test/test_config/fcm1_delete_directory_inherit
@@ -0,0 +1,3 @@
+extract_1=fail_known
+
+# This case fails since the extract system does not handle deleted directories.
diff --git a/test/test_config/fcm1_delete_file b/test/test_config/fcm1_delete_file
new file mode 100755
index 0000000..7e1fa29
--- /dev/null
+++ b/test/test_config/fcm1_delete_file
@@ -0,0 +1 @@
+build_1=fail
diff --git a/test/test_config/fcm1_delete_file_inherit b/test/test_config/fcm1_delete_file_inherit
new file mode 100755
index 0000000..7e1fa29
--- /dev/null
+++ b/test/test_config/fcm1_delete_file_inherit
@@ -0,0 +1 @@
+build_1=fail
diff --git a/test/test_config/fcm1_delete_inc_file b/test/test_config/fcm1_delete_inc_file
new file mode 100755
index 0000000..7e1fa29
--- /dev/null
+++ b/test/test_config/fcm1_delete_inc_file
@@ -0,0 +1 @@
+build_1=fail
diff --git a/test/test_config/fcm1_delete_inc_file_inherit b/test/test_config/fcm1_delete_inc_file_inherit
new file mode 100755
index 0000000..6f4f0ee
--- /dev/null
+++ b/test/test_config/fcm1_delete_inc_file_inherit
@@ -0,0 +1,4 @@
+build_1=succeed_known
+
+# The build succeeds when it should, in fact, fail.
+# See ticket:335
diff --git a/test/test_config/fcm1_delete_inc_file_inherit_force b/test/test_config/fcm1_delete_inc_file_inherit_force
new file mode 100755
index 0000000..6f4f0ee
--- /dev/null
+++ b/test/test_config/fcm1_delete_inc_file_inherit_force
@@ -0,0 +1,4 @@
+build_1=succeed_known
+
+# The build succeeds when it should, in fact, fail.
+# See ticket:335
diff --git a/test/test_config/fcm1_delete_pp_file b/test/test_config/fcm1_delete_pp_file
new file mode 100755
index 0000000..7e1fa29
--- /dev/null
+++ b/test/test_config/fcm1_delete_pp_file
@@ -0,0 +1 @@
+build_1=fail
diff --git a/test/test_config/fcm1_delete_pp_file_inherit b/test/test_config/fcm1_delete_pp_file_inherit
new file mode 100755
index 0000000..7e1fa29
--- /dev/null
+++ b/test/test_config/fcm1_delete_pp_file_inherit
@@ -0,0 +1 @@
+build_1=fail
diff --git a/test/test_config/fcm1_delete_ppinc_file b/test/test_config/fcm1_delete_ppinc_file
new file mode 100755
index 0000000..7e1fa29
--- /dev/null
+++ b/test/test_config/fcm1_delete_ppinc_file
@@ -0,0 +1 @@
+build_1=fail
diff --git a/test/test_config/fcm1_delete_ppinc_file_inherit b/test/test_config/fcm1_delete_ppinc_file_inherit
new file mode 100755
index 0000000..6f4f0ee
--- /dev/null
+++ b/test/test_config/fcm1_delete_ppinc_file_inherit
@@ -0,0 +1,4 @@
+build_1=succeed_known
+
+# The build succeeds when it should, in fact, fail.
+# See ticket:335
diff --git a/test/test_config/fcm1_delete_ppinc_file_inherit_force b/test/test_config/fcm1_delete_ppinc_file_inherit_force
new file mode 100755
index 0000000..6f4f0ee
--- /dev/null
+++ b/test/test_config/fcm1_delete_ppinc_file_inherit_force
@@ -0,0 +1,4 @@
+build_1=succeed_known
+
+# The build succeeds when it should, in fact, fail.
+# See ticket:335
diff --git a/test/test_config/fcm1_duplicate_target b/test/test_config/fcm1_duplicate_target
new file mode 100755
index 0000000..92a1bf3
--- /dev/null
+++ b/test/test_config/fcm1_duplicate_target
@@ -0,0 +1,4 @@
+build_1=succeed_known
+
+# The build succeeds when it should, in fact, fail.
+# See ticket:205
diff --git a/test/test_config/fcm1_exclude_dependency b/test/test_config/fcm1_exclude_dependency
new file mode 100755
index 0000000..7e1fa29
--- /dev/null
+++ b/test/test_config/fcm1_exclude_dependency
@@ -0,0 +1 @@
+build_1=fail
diff --git a/test/test_config/fcm1_exe_permissions b/test/test_config/fcm1_exe_permissions
new file mode 100755
index 0000000..8d98fae
--- /dev/null
+++ b/test/test_config/fcm1_exe_permissions
@@ -0,0 +1,3 @@
+svn co -q $REPOS_URL/branches/dev/Share/exe_rename $BASE_DIR/work
+chmod 644 $BASE_DIR/work/script/hello.sh
+run_1=fail
diff --git a/test/test_config/fcm1_exe_rename_incremental b/test/test_config/fcm1_exe_rename_incremental
new file mode 100755
index 0000000..7d5e5ef
--- /dev/null
+++ b/test/test_config/fcm1_exe_rename_incremental
@@ -0,0 +1 @@
+cfg_name="fcm1_base fcm1_exe_rename"
diff --git a/test/test_config/fcm1_fc_incremental b/test/test_config/fcm1_fc_incremental
new file mode 100755
index 0000000..5b1a7ba
--- /dev/null
+++ b/test/test_config/fcm1_fc_incremental
@@ -0,0 +1 @@
+cfg_name="fcm1_base fcm1_fc fcm1_base"
diff --git a/test/test_config/fcm1_fflags_incremental b/test/test_config/fcm1_fflags_incremental
new file mode 100755
index 0000000..85948ba
--- /dev/null
+++ b/test/test_config/fcm1_fflags_incremental
@@ -0,0 +1 @@
+cfg_name="fcm1_base fcm1_fflags1 fcm1_fflags2 fcm1_base"
diff --git a/test/test_config/fcm1_inc_devnull b/test/test_config/fcm1_inc_devnull
new file mode 100755
index 0000000..e66242c
--- /dev/null
+++ b/test/test_config/fcm1_inc_devnull
@@ -0,0 +1 @@
+run_1=no
diff --git a/test/test_config/fcm1_inherit_invalid_path b/test/test_config/fcm1_inherit_invalid_path
new file mode 100755
index 0000000..c88bb32
--- /dev/null
+++ b/test/test_config/fcm1_inherit_invalid_path
@@ -0,0 +1,3 @@
+extract_1=fail
+
+# Poor error message using FCM 1
diff --git a/test/test_config/fcm1_invalid_base_url b/test/test_config/fcm1_invalid_base_url
new file mode 100755
index 0000000..c00e841
--- /dev/null
+++ b/test/test_config/fcm1_invalid_base_url
@@ -0,0 +1 @@
+extract_1=fail
diff --git a/test/test_config/fcm1_invalid_branch_url b/test/test_config/fcm1_invalid_branch_url
new file mode 100755
index 0000000..c00e841
--- /dev/null
+++ b/test/test_config/fcm1_invalid_branch_url
@@ -0,0 +1 @@
+extract_1=fail
diff --git a/test/test_config/fcm1_invalid_inc b/test/test_config/fcm1_invalid_inc
new file mode 100755
index 0000000..c00e841
--- /dev/null
+++ b/test/test_config/fcm1_invalid_inc
@@ -0,0 +1 @@
+extract_1=fail
diff --git a/test/test_config/fcm1_invalid_namespace b/test/test_config/fcm1_invalid_namespace
new file mode 100755
index 0000000..7e1fa29
--- /dev/null
+++ b/test/test_config/fcm1_invalid_namespace
@@ -0,0 +1 @@
+build_1=fail
diff --git a/test/test_config/fcm1_invalid_variable b/test/test_config/fcm1_invalid_variable
new file mode 100755
index 0000000..7e1fa29
--- /dev/null
+++ b/test/test_config/fcm1_invalid_variable
@@ -0,0 +1 @@
+build_1=fail
diff --git a/test/test_config/fcm1_ld_incremental b/test/test_config/fcm1_ld_incremental
new file mode 100755
index 0000000..0ccf106
--- /dev/null
+++ b/test/test_config/fcm1_ld_incremental
@@ -0,0 +1 @@
+cfg_name="fcm1_base fcm1_ld fcm1_base"
diff --git a/test/test_config/fcm1_library b/test/test_config/fcm1_library
new file mode 100755
index 0000000..e66242c
--- /dev/null
+++ b/test/test_config/fcm1_library
@@ -0,0 +1 @@
+run_1=no
diff --git a/test/test_config/fcm1_library_rename b/test/test_config/fcm1_library_rename
new file mode 100755
index 0000000..e66242c
--- /dev/null
+++ b/test/test_config/fcm1_library_rename
@@ -0,0 +1 @@
+run_1=no
diff --git a/test/test_config/fcm1_mirror b/test/test_config/fcm1_mirror
new file mode 100755
index 0000000..830e04f
--- /dev/null
+++ b/test/test_config/fcm1_mirror
@@ -0,0 +1 @@
+mirror=local
diff --git a/test/test_config/fcm1_mirror_inherit b/test/test_config/fcm1_mirror_inherit
new file mode 100755
index 0000000..830e04f
--- /dev/null
+++ b/test/test_config/fcm1_mirror_inherit
@@ -0,0 +1 @@
+mirror=local
diff --git a/test/test_config/fcm1_no_dep b/test/test_config/fcm1_no_dep
new file mode 100755
index 0000000..66a8cf1
--- /dev/null
+++ b/test/test_config/fcm1_no_dep
@@ -0,0 +1 @@
+run_1=fail
diff --git a/test/test_config/fcm1_ops_parallel b/test/test_config/fcm1_ops_parallel
new file mode 100755
index 0000000..5f54981
--- /dev/null
+++ b/test/test_config/fcm1_ops_parallel
@@ -0,0 +1,2 @@
+cfg_name=fcm1_ops
+NPROC=6
diff --git a/test/test_config/fcm1_pp_change_include_inherit b/test/test_config/fcm1_pp_change_include_inherit
new file mode 100755
index 0000000..286a6c6
--- /dev/null
+++ b/test/test_config/fcm1_pp_change_include_inherit
@@ -0,0 +1,2 @@
+# The wrong include file gets used as demonstrated by the lack of anything being compiled.
+# See ticket:206
diff --git a/test/test_config/fcm1_pp_change_keys_incremental b/test/test_config/fcm1_pp_change_keys_incremental
new file mode 100755
index 0000000..9093515
--- /dev/null
+++ b/test/test_config/fcm1_pp_change_keys_incremental
@@ -0,0 +1 @@
+cfg_name="fcm1_pp_change_dependency fcm1_base fcm1_pp_change_blockdata"
diff --git a/test/test_config/fcm1_pp_empty_subroutine b/test/test_config/fcm1_pp_empty_subroutine
new file mode 100755
index 0000000..7e1fa29
--- /dev/null
+++ b/test/test_config/fcm1_pp_empty_subroutine
@@ -0,0 +1 @@
+build_1=fail
diff --git a/test/test_config/fcm1_pp_empty_subroutine_inherit b/test/test_config/fcm1_pp_empty_subroutine_inherit
new file mode 100755
index 0000000..605d5de
--- /dev/null
+++ b/test/test_config/fcm1_pp_empty_subroutine_inherit
@@ -0,0 +1,3 @@
+build_1=succeed_known
+
+# This case does not fail as expected (see #335)
diff --git a/test/test_config/fcm1_pp_empty_subroutine_inherit_force b/test/test_config/fcm1_pp_empty_subroutine_inherit_force
new file mode 100755
index 0000000..7f69c0a
--- /dev/null
+++ b/test/test_config/fcm1_pp_empty_subroutine_inherit_force
@@ -0,0 +1,5 @@
+build_1=fail
+
+# This case does fail as expected.
+# However, the failure is at the link stage whereas it ought to fail
+# due to a missing dependency (see #335)
diff --git a/test/test_config/fcm1_revmatch_true b/test/test_config/fcm1_revmatch_true
new file mode 100755
index 0000000..c00e841
--- /dev/null
+++ b/test/test_config/fcm1_revmatch_true
@@ -0,0 +1 @@
+extract_1=fail
diff --git a/test/test_config/fcm1_sps_parallel b/test/test_config/fcm1_sps_parallel
new file mode 100755
index 0000000..5230915
--- /dev/null
+++ b/test/test_config/fcm1_sps_parallel
@@ -0,0 +1,2 @@
+cfg_name=fcm1_sps
+NPROC=6
diff --git a/test/test_config/fcm1_um b/test/test_config/fcm1_um
new file mode 100755
index 0000000..73fd90d
--- /dev/null
+++ b/test/test_config/fcm1_um
@@ -0,0 +1,2 @@
+cfg_name="fcm1_um fcm1_um"
+NPROC=6
diff --git a/test/test_config/fcm1_um_inherit b/test/test_config/fcm1_um_inherit
new file mode 100755
index 0000000..6b9008c
--- /dev/null
+++ b/test/test_config/fcm1_um_inherit
@@ -0,0 +1,2 @@
+cfg_name="fcm1_um_inherit fcm1_um_inherit"
+NPROC=6
diff --git a/test/test_config/fcm1_var_parallel b/test/test_config/fcm1_var_parallel
new file mode 100755
index 0000000..e009b76
--- /dev/null
+++ b/test/test_config/fcm1_var_parallel
@@ -0,0 +1,2 @@
+cfg_name=fcm1_var
+NPROC=6
diff --git a/test/test_config/fcm2_branches_clash b/test/test_config/fcm2_branches_clash
new file mode 100755
index 0000000..6ab2ba3
--- /dev/null
+++ b/test/test_config/fcm2_branches_clash
@@ -0,0 +1 @@
+make_1=fail
diff --git a/test/test_config/fcm2_branches_merge_incremental b/test/test_config/fcm2_branches_merge_incremental
new file mode 100755
index 0000000..bf5bef3
--- /dev/null
+++ b/test/test_config/fcm2_branches_merge_incremental
@@ -0,0 +1 @@
+cfg_name="fcm2_base fcm2_branches_merge"
diff --git a/test/test_config/fcm2_branches_merge_inherit_wrong_include b/test/test_config/fcm2_branches_merge_inherit_wrong_include
new file mode 100755
index 0000000..e5257e8
--- /dev/null
+++ b/test/test_config/fcm2_branches_merge_inherit_wrong_include
@@ -0,0 +1,2 @@
+# The wrong include file gets used as demonstrated by the output from running the program.
+# See ticket:206
diff --git a/test/test_config/fcm2_branches_merge_wcopies b/test/test_config/fcm2_branches_merge_wcopies
new file mode 100755
index 0000000..92f75b3
--- /dev/null
+++ b/test/test_config/fcm2_branches_merge_wcopies
@@ -0,0 +1,4 @@
+mkdir -p $BASE_DIR/work
+svn co -q $REPOS_URL/branches/dev/Share/modify_files_base $BASE_DIR/work/b1
+svn co -q $REPOS_URL/branches/dev/Share/modify_files_merge1 $BASE_DIR/work/b2
+svn co -q $REPOS_URL/branches/dev/Share/modify_files_merge2 $BASE_DIR/work/b3
diff --git a/test/test_config/fcm2_branches_merge_wcopy b/test/test_config/fcm2_branches_merge_wcopy
new file mode 100755
index 0000000..48d6a7a
--- /dev/null
+++ b/test/test_config/fcm2_branches_merge_wcopy
@@ -0,0 +1 @@
+svn co -q $REPOS_URL/branches/dev/Share/modify_files_merge2 $BASE_DIR/work
diff --git a/test/test_config/fcm2_cflags_incremental b/test/test_config/fcm2_cflags_incremental
new file mode 100755
index 0000000..e2acddb
--- /dev/null
+++ b/test/test_config/fcm2_cflags_incremental
@@ -0,0 +1 @@
+cfg_name="fcm2_base fcm2_cflags fcm2_base"
diff --git a/test/test_config/fcm2_cyclic_dep_fail b/test/test_config/fcm2_cyclic_dep_fail
new file mode 100755
index 0000000..6ab2ba3
--- /dev/null
+++ b/test/test_config/fcm2_cyclic_dep_fail
@@ -0,0 +1 @@
+make_1=fail
diff --git a/test/test_config/fcm2_delete_file b/test/test_config/fcm2_delete_file
new file mode 100755
index 0000000..0eadb74
--- /dev/null
+++ b/test/test_config/fcm2_delete_file
@@ -0,0 +1,2 @@
+make_1=fail
+compare_fcm1=false
diff --git a/test/test_config/fcm2_delete_file_inherit b/test/test_config/fcm2_delete_file_inherit
new file mode 100755
index 0000000..0eadb74
--- /dev/null
+++ b/test/test_config/fcm2_delete_file_inherit
@@ -0,0 +1,2 @@
+make_1=fail
+compare_fcm1=false
diff --git a/test/test_config/fcm2_delete_inc_file b/test/test_config/fcm2_delete_inc_file
new file mode 100755
index 0000000..6ab2ba3
--- /dev/null
+++ b/test/test_config/fcm2_delete_inc_file
@@ -0,0 +1 @@
+make_1=fail
diff --git a/test/test_config/fcm2_delete_inc_file_inherit b/test/test_config/fcm2_delete_inc_file_inherit
new file mode 100755
index 0000000..6ab2ba3
--- /dev/null
+++ b/test/test_config/fcm2_delete_inc_file_inherit
@@ -0,0 +1 @@
+make_1=fail
diff --git a/test/test_config/fcm2_delete_inc_file_inherit_force b/test/test_config/fcm2_delete_inc_file_inherit_force
new file mode 100755
index 0000000..0eadb74
--- /dev/null
+++ b/test/test_config/fcm2_delete_inc_file_inherit_force
@@ -0,0 +1,2 @@
+make_1=fail
+compare_fcm1=false
diff --git a/test/test_config/fcm2_delete_pp_file b/test/test_config/fcm2_delete_pp_file
new file mode 100755
index 0000000..0eadb74
--- /dev/null
+++ b/test/test_config/fcm2_delete_pp_file
@@ -0,0 +1,2 @@
+make_1=fail
+compare_fcm1=false
diff --git a/test/test_config/fcm2_delete_pp_file_inherit b/test/test_config/fcm2_delete_pp_file_inherit
new file mode 100755
index 0000000..0eadb74
--- /dev/null
+++ b/test/test_config/fcm2_delete_pp_file_inherit
@@ -0,0 +1,2 @@
+make_1=fail
+compare_fcm1=false
diff --git a/test/test_config/fcm2_delete_ppinc_file b/test/test_config/fcm2_delete_ppinc_file
new file mode 100755
index 0000000..0eadb74
--- /dev/null
+++ b/test/test_config/fcm2_delete_ppinc_file
@@ -0,0 +1,2 @@
+make_1=fail
+compare_fcm1=false
diff --git a/test/test_config/fcm2_delete_ppinc_file_inherit b/test/test_config/fcm2_delete_ppinc_file_inherit
new file mode 100755
index 0000000..6ab2ba3
--- /dev/null
+++ b/test/test_config/fcm2_delete_ppinc_file_inherit
@@ -0,0 +1 @@
+make_1=fail
diff --git a/test/test_config/fcm2_delete_ppinc_file_inherit_force b/test/test_config/fcm2_delete_ppinc_file_inherit_force
new file mode 100755
index 0000000..0eadb74
--- /dev/null
+++ b/test/test_config/fcm2_delete_ppinc_file_inherit_force
@@ -0,0 +1,2 @@
+make_1=fail
+compare_fcm1=false
diff --git a/test/test_config/fcm2_dep_o_invalid b/test/test_config/fcm2_dep_o_invalid
new file mode 100755
index 0000000..6ab2ba3
--- /dev/null
+++ b/test/test_config/fcm2_dep_o_invalid
@@ -0,0 +1 @@
+make_1=fail
diff --git a/test/test_config/fcm2_duplicate_target b/test/test_config/fcm2_duplicate_target
new file mode 100755
index 0000000..0eadb74
--- /dev/null
+++ b/test/test_config/fcm2_duplicate_target
@@ -0,0 +1,2 @@
+make_1=fail
+compare_fcm1=false
diff --git a/test/test_config/fcm2_exclude_dependency b/test/test_config/fcm2_exclude_dependency
new file mode 100755
index 0000000..0eadb74
--- /dev/null
+++ b/test/test_config/fcm2_exclude_dependency
@@ -0,0 +1,2 @@
+make_1=fail
+compare_fcm1=false
diff --git a/test/test_config/fcm2_exe_permissions b/test/test_config/fcm2_exe_permissions
new file mode 100755
index 0000000..8d98fae
--- /dev/null
+++ b/test/test_config/fcm2_exe_permissions
@@ -0,0 +1,3 @@
+svn co -q $REPOS_URL/branches/dev/Share/exe_rename $BASE_DIR/work
+chmod 644 $BASE_DIR/work/script/hello.sh
+run_1=fail
diff --git a/test/test_config/fcm2_exe_rename_incremental b/test/test_config/fcm2_exe_rename_incremental
new file mode 100755
index 0000000..2af2b7e
--- /dev/null
+++ b/test/test_config/fcm2_exe_rename_incremental
@@ -0,0 +1 @@
+cfg_name="fcm2_base fcm2_exe_rename"
diff --git a/test/test_config/fcm2_fc_incremental b/test/test_config/fcm2_fc_incremental
new file mode 100755
index 0000000..e45691c
--- /dev/null
+++ b/test/test_config/fcm2_fc_incremental
@@ -0,0 +1 @@
+cfg_name="fcm2_base fcm2_fc fcm2_base"
diff --git a/test/test_config/fcm2_fflags_incremental b/test/test_config/fcm2_fflags_incremental
new file mode 100755
index 0000000..39caa04
--- /dev/null
+++ b/test/test_config/fcm2_fflags_incremental
@@ -0,0 +1,2 @@
+cfg_name="fcm2_base fcm2_fflags1 fcm2_fflags2 fcm2_base"
+compare_fcm1=false
diff --git a/test/test_config/fcm2_inc_devnull b/test/test_config/fcm2_inc_devnull
new file mode 100755
index 0000000..0eadb74
--- /dev/null
+++ b/test/test_config/fcm2_inc_devnull
@@ -0,0 +1,2 @@
+make_1=fail
+compare_fcm1=false
diff --git a/test/test_config/fcm2_inherit_invalid_path b/test/test_config/fcm2_inherit_invalid_path
new file mode 100755
index 0000000..6ab2ba3
--- /dev/null
+++ b/test/test_config/fcm2_inherit_invalid_path
@@ -0,0 +1 @@
+make_1=fail
diff --git a/test/test_config/fcm2_inherit_redefine_fail b/test/test_config/fcm2_inherit_redefine_fail
new file mode 100755
index 0000000..6ab2ba3
--- /dev/null
+++ b/test/test_config/fcm2_inherit_redefine_fail
@@ -0,0 +1 @@
+make_1=fail
diff --git a/test/test_config/fcm2_invalid_base_url b/test/test_config/fcm2_invalid_base_url
new file mode 100755
index 0000000..6ab2ba3
--- /dev/null
+++ b/test/test_config/fcm2_invalid_base_url
@@ -0,0 +1 @@
+make_1=fail
diff --git a/test/test_config/fcm2_invalid_branch_url b/test/test_config/fcm2_invalid_branch_url
new file mode 100755
index 0000000..6ab2ba3
--- /dev/null
+++ b/test/test_config/fcm2_invalid_branch_url
@@ -0,0 +1 @@
+make_1=fail
diff --git a/test/test_config/fcm2_invalid_branch_url2 b/test/test_config/fcm2_invalid_branch_url2
new file mode 100755
index 0000000..6ab2ba3
--- /dev/null
+++ b/test/test_config/fcm2_invalid_branch_url2
@@ -0,0 +1 @@
+make_1=fail
diff --git a/test/test_config/fcm2_invalid_inc b/test/test_config/fcm2_invalid_inc
new file mode 100755
index 0000000..6ab2ba3
--- /dev/null
+++ b/test/test_config/fcm2_invalid_inc
@@ -0,0 +1 @@
+make_1=fail
diff --git a/test/test_config/fcm2_invalid_label b/test/test_config/fcm2_invalid_label
new file mode 100755
index 0000000..6ab2ba3
--- /dev/null
+++ b/test/test_config/fcm2_invalid_label
@@ -0,0 +1 @@
+make_1=fail
diff --git a/test/test_config/fcm2_invalid_modifier b/test/test_config/fcm2_invalid_modifier
new file mode 100755
index 0000000..6ab2ba3
--- /dev/null
+++ b/test/test_config/fcm2_invalid_modifier
@@ -0,0 +1 @@
+make_1=fail
diff --git a/test/test_config/fcm2_invalid_modifiers b/test/test_config/fcm2_invalid_modifiers
new file mode 100755
index 0000000..6ab2ba3
--- /dev/null
+++ b/test/test_config/fcm2_invalid_modifiers
@@ -0,0 +1 @@
+make_1=fail
diff --git a/test/test_config/fcm2_invalid_namespace b/test/test_config/fcm2_invalid_namespace
new file mode 100755
index 0000000..0eadb74
--- /dev/null
+++ b/test/test_config/fcm2_invalid_namespace
@@ -0,0 +1,2 @@
+make_1=fail
+compare_fcm1=false
diff --git a/test/test_config/fcm2_invalid_namespace2 b/test/test_config/fcm2_invalid_namespace2
new file mode 100755
index 0000000..6ab2ba3
--- /dev/null
+++ b/test/test_config/fcm2_invalid_namespace2
@@ -0,0 +1 @@
+make_1=fail
diff --git a/test/test_config/fcm2_invalid_target b/test/test_config/fcm2_invalid_target
new file mode 100755
index 0000000..6ab2ba3
--- /dev/null
+++ b/test/test_config/fcm2_invalid_target
@@ -0,0 +1 @@
+make_1=fail
diff --git a/test/test_config/fcm2_invalid_variable b/test/test_config/fcm2_invalid_variable
new file mode 100755
index 0000000..0eadb74
--- /dev/null
+++ b/test/test_config/fcm2_invalid_variable
@@ -0,0 +1,2 @@
+make_1=fail
+compare_fcm1=false
diff --git a/test/test_config/fcm2_library b/test/test_config/fcm2_library
new file mode 100755
index 0000000..1a91d42
--- /dev/null
+++ b/test/test_config/fcm2_library
@@ -0,0 +1,2 @@
+run_1=no
+compare_fcm1=false
diff --git a/test/test_config/fcm2_library_rename b/test/test_config/fcm2_library_rename
new file mode 100755
index 0000000..e66242c
--- /dev/null
+++ b/test/test_config/fcm2_library_rename
@@ -0,0 +1 @@
+run_1=no
diff --git a/test/test_config/fcm2_mirror b/test/test_config/fcm2_mirror
new file mode 100755
index 0000000..830e04f
--- /dev/null
+++ b/test/test_config/fcm2_mirror
@@ -0,0 +1 @@
+mirror=local
diff --git a/test/test_config/fcm2_mirror_after_pp b/test/test_config/fcm2_mirror_after_pp
new file mode 100755
index 0000000..9af7e0d
--- /dev/null
+++ b/test/test_config/fcm2_mirror_after_pp
@@ -0,0 +1,2 @@
+mirror=local
+make_1=fail
diff --git a/test/test_config/fcm2_mirror_inherit b/test/test_config/fcm2_mirror_inherit
new file mode 100755
index 0000000..830e04f
--- /dev/null
+++ b/test/test_config/fcm2_mirror_inherit
@@ -0,0 +1 @@
+mirror=local
diff --git a/test/test_config/fcm2_mirror_inherit_fflags b/test/test_config/fcm2_mirror_inherit_fflags
new file mode 100755
index 0000000..830e04f
--- /dev/null
+++ b/test/test_config/fcm2_mirror_inherit_fflags
@@ -0,0 +1 @@
+mirror=local
diff --git a/test/test_config/fcm2_mirror_inherit_notarget b/test/test_config/fcm2_mirror_inherit_notarget
new file mode 100755
index 0000000..6ab2ba3
--- /dev/null
+++ b/test/test_config/fcm2_mirror_inherit_notarget
@@ -0,0 +1 @@
+make_1=fail
diff --git a/test/test_config/fcm2_modify_subroutine_inherit b/test/test_config/fcm2_modify_subroutine_inherit
new file mode 100755
index 0000000..5db5e06
--- /dev/null
+++ b/test/test_config/fcm2_modify_subroutine_inherit
@@ -0,0 +1 @@
+compare_fcm1=false
diff --git a/test/test_config/fcm2_multi_inherit b/test/test_config/fcm2_multi_inherit
new file mode 100755
index 0000000..5db5e06
--- /dev/null
+++ b/test/test_config/fcm2_multi_inherit
@@ -0,0 +1 @@
+compare_fcm1=false
diff --git a/test/test_config/fcm2_no_dep b/test/test_config/fcm2_no_dep
new file mode 100755
index 0000000..66a8cf1
--- /dev/null
+++ b/test/test_config/fcm2_no_dep
@@ -0,0 +1 @@
+run_1=fail
diff --git a/test/test_config/fcm2_ns-dep_o_invalid b/test/test_config/fcm2_ns-dep_o_invalid
new file mode 100755
index 0000000..6ab2ba3
--- /dev/null
+++ b/test/test_config/fcm2_ns-dep_o_invalid
@@ -0,0 +1 @@
+make_1=fail
diff --git a/test/test_config/fcm2_ops_parallel b/test/test_config/fcm2_ops_parallel
new file mode 100755
index 0000000..70a5501
--- /dev/null
+++ b/test/test_config/fcm2_ops_parallel
@@ -0,0 +1,2 @@
+cfg_name=fcm2_ops
+NPROC=6
diff --git a/test/test_config/fcm2_override_variable b/test/test_config/fcm2_override_variable
new file mode 100755
index 0000000..7dad7b7
--- /dev/null
+++ b/test/test_config/fcm2_override_variable
@@ -0,0 +1,2 @@
+cfg_name="fcm2_base"
+export fcflags="-assume nosource_include -g"
diff --git a/test/test_config/fcm2_pp_change_include_inherit b/test/test_config/fcm2_pp_change_include_inherit
new file mode 100755
index 0000000..c15edb9
--- /dev/null
+++ b/test/test_config/fcm2_pp_change_include_inherit
@@ -0,0 +1,4 @@
+compare_fcm1=false
+
+# The wrong include file gets used as demonstrated by the lack of anything being compiled.
+# See ticket:206
diff --git a/test/test_config/fcm2_pp_change_keys_incremental b/test/test_config/fcm2_pp_change_keys_incremental
new file mode 100755
index 0000000..60991cc
--- /dev/null
+++ b/test/test_config/fcm2_pp_change_keys_incremental
@@ -0,0 +1 @@
+cfg_name="fcm2_pp_change_dependency fcm2_base fcm2_pp_change_blockdata"
diff --git a/test/test_config/fcm2_pp_empty_subroutine b/test/test_config/fcm2_pp_empty_subroutine
new file mode 100755
index 0000000..0eadb74
--- /dev/null
+++ b/test/test_config/fcm2_pp_empty_subroutine
@@ -0,0 +1,2 @@
+make_1=fail
+compare_fcm1=false
diff --git a/test/test_config/fcm2_pp_empty_subroutine_inherit b/test/test_config/fcm2_pp_empty_subroutine_inherit
new file mode 100755
index 0000000..6ab2ba3
--- /dev/null
+++ b/test/test_config/fcm2_pp_empty_subroutine_inherit
@@ -0,0 +1 @@
+make_1=fail
diff --git a/test/test_config/fcm2_pp_empty_subroutine_inherit_force b/test/test_config/fcm2_pp_empty_subroutine_inherit_force
new file mode 100755
index 0000000..0eadb74
--- /dev/null
+++ b/test/test_config/fcm2_pp_empty_subroutine_inherit_force
@@ -0,0 +1,2 @@
+make_1=fail
+compare_fcm1=false
diff --git a/test/test_config/fcm2_sps_parallel b/test/test_config/fcm2_sps_parallel
new file mode 100755
index 0000000..8a431f5
--- /dev/null
+++ b/test/test_config/fcm2_sps_parallel
@@ -0,0 +1,2 @@
+cfg_name=fcm2_sps
+NPROC=6
diff --git a/test/test_config/fcm2_um b/test/test_config/fcm2_um
new file mode 100755
index 0000000..8ad1be3
--- /dev/null
+++ b/test/test_config/fcm2_um
@@ -0,0 +1,2 @@
+cfg_name="fcm2_um fcm2_um"
+NPROC=6
diff --git a/test/test_config/fcm2_um77 b/test/test_config/fcm2_um77
new file mode 100755
index 0000000..f5d65e4
--- /dev/null
+++ b/test/test_config/fcm2_um77
@@ -0,0 +1,2 @@
+cfg_name="fcm2_um77 fcm2_um77"
+NPROC=6
diff --git a/test/test_config/fcm2_um77_inherit b/test/test_config/fcm2_um77_inherit
new file mode 100755
index 0000000..9ebe967
--- /dev/null
+++ b/test/test_config/fcm2_um77_inherit
@@ -0,0 +1,2 @@
+cfg_name="fcm2_um77_inherit fcm2_um77_inherit"
+NPROC=6
diff --git a/test/test_config/fcm2_um_inherit b/test/test_config/fcm2_um_inherit
new file mode 100755
index 0000000..2863cbb
--- /dev/null
+++ b/test/test_config/fcm2_um_inherit
@@ -0,0 +1,2 @@
+cfg_name="fcm2_um_inherit fcm2_um_inherit"
+NPROC=6
diff --git a/test/test_config/fcm2_var_parallel b/test/test_config/fcm2_var_parallel
new file mode 100755
index 0000000..0a15ac1
--- /dev/null
+++ b/test/test_config/fcm2_var_parallel
@@ -0,0 +1,2 @@
+cfg_name=fcm2_var
+NPROC=6
diff --git a/test/test_include/inc/fortran.inc b/test/test_include/inc/fortran.inc
new file mode 100644
index 0000000..5a1692c
--- /dev/null
+++ b/test/test_include/inc/fortran.inc
@@ -0,0 +1 @@
+WRITE (*, '(A)') 'PASSED: correct include file'
diff --git a/test/test_include/prog/fortran.inc b/test/test_include/prog/fortran.inc
new file mode 100644
index 0000000..b1d5733
--- /dev/null
+++ b/test/test_include/prog/fortran.inc
@@ -0,0 +1 @@
+WRITE (*, '(A)') 'FAILED: wrong include file'
diff --git a/test/test_include/prog/test_fortran_inc.f90 b/test/test_include/prog/test_fortran_inc.f90
new file mode 100644
index 0000000..0fc3e5b
--- /dev/null
+++ b/test/test_include/prog/test_fortran_inc.f90
@@ -0,0 +1,5 @@
+PROGRAM Test
+
+INCLUDE 'fortran.inc'
+
+END PROGRAM Test
diff --git a/test/test_include/prog/test_prepro_inc.F90 b/test/test_include/prog/test_prepro_inc.F90
new file mode 100644
index 0000000..606651f
--- /dev/null
+++ b/test/test_include/prog/test_prepro_inc.F90
@@ -0,0 +1,5 @@
+PROGRAM Test
+
+#include "fortran.inc"
+
+END PROGRAM Test
diff --git a/test/test_include/test.sh b/test/test_include/test.sh
new file mode 100755
index 0000000..69fd9da
--- /dev/null
+++ b/test/test_include/test.sh
@@ -0,0 +1,38 @@
+#!/bin/sh
+
+echo "### Fortran compiler tests"
+for fc in \
+ "ifort" \
+ "ifort -assume nosource_include" \
+ "gfortran" \
+ "gfortran -I-"
+do
+ echo
+ echo "Compiler: $fc"
+ echo "Fortran include test:"
+ $fc -o test.o -I$PWD/inc -c prog/test_fortran_inc.f90
+ $fc -o test.exe test.o
+ test.exe
+ rm test.exe test.o
+ echo "CPP include test:"
+ $fc -o test.o -I$PWD/inc -c prog/test_prepro_inc.F90
+ $fc -o test.exe test.o
+ test.exe
+ rm test.exe test.o
+done
+
+echo
+echo "### Preprocessor tests"
+fc=gfortran
+for cpp in \
+ "cpp -P -traditional" \
+ "cpp -P -traditional -I-"
+do
+ echo
+ echo "Pre-processor: $cpp"
+ $cpp -I$PWD/inc prog/test_prepro_inc.F90 >tmp.f90
+ $fc -o test.o -I$PWD/inc -c tmp.f90
+ $fc -o test.exe test.o
+ test.exe
+ rm test.exe test.o tmp.f90
+done
diff --git a/test/tests_functional.list b/test/tests_functional.list
new file mode 100755
index 0000000..7e79dba
--- /dev/null
+++ b/test/tests_functional.list
@@ -0,0 +1,158 @@
+TESTS_FCM1="
+fcm1_base
+fcm1_add_directory
+fcm1_add_directory_expsrc
+fcm1_add_file
+fcm1_add_file_inherit
+fcm1_branches_clash
+fcm1_branches_merge
+fcm1_branches_merge_conflict_fail
+fcm1_branches_merge_conflict_override
+fcm1_branches_merge_incremental
+fcm1_branches_merge_inherit
+fcm1_branches_merge_inherit_wrong_include
+fcm1_branches_merge_wcopy
+fcm1_branches_merge_wcopies
+fcm1_cflags_incremental
+fcm1_change_src_type_incremental
+fcm1_delete_directory
+fcm1_delete_directory_inherit
+fcm1_delete_file
+fcm1_delete_file_inherit
+fcm1_delete_inc_file
+fcm1_delete_inc_file_inherit
+fcm1_delete_inc_file_inherit_force
+fcm1_delete_pp_file
+fcm1_delete_pp_file_inherit
+fcm1_delete_ppinc_file
+fcm1_delete_ppinc_file_inherit
+fcm1_delete_ppinc_file_inherit_force
+fcm1_duplicate_target
+fcm1_exclude_dependency
+fcm1_exe_permissions
+fcm1_exe_rename
+fcm1_exe_rename_incremental
+fcm1_fc_incremental
+fcm1_fflags_incremental
+fcm1_fflags_inherit
+fcm1_inc_devnull
+fcm1_inherit_invalid_path
+fcm1_inherit_target
+fcm1_invalid_base_url
+fcm1_invalid_branch_url
+fcm1_invalid_inc
+fcm1_invalid_namespace
+fcm1_invalid_variable
+fcm1_ld_incremental
+fcm1_library
+fcm1_library_rename
+fcm1_mirror
+fcm1_mirror_inherit
+fcm1_modify_subroutine_inherit
+fcm1_modify_subroutine_interface_inherit
+fcm1_multi_inherit
+fcm1_no_dep
+fcm1_pp_change_include
+fcm1_pp_change_include_inherit
+fcm1_pp_change_keys_incremental
+fcm1_pp_empty_subroutine
+fcm1_pp_empty_subroutine_inherit
+fcm1_pp_empty_subroutine_inherit_force
+fcm1_revmatch_false
+fcm1_revmatch_true
+fcm1_suite
+fcm1_symbolic_link
+"
+
+TESTS_FCM2="
+fcm2_base
+fcm2_add_directory_expsrc
+fcm2_add_file
+fcm2_add_file_inherit
+fcm2_branches_clash
+fcm2_branches_merge
+fcm2_branches_merge_duplicate
+fcm2_branches_merge_incremental
+fcm2_branches_merge_inherit
+fcm2_branches_merge_inherit_wrong_include
+fcm2_branches_merge_wcopy
+fcm2_branches_merge_wcopies
+fcm2_cflags_incremental
+fcm2_change_variable
+fcm2_cyclic_dep_fail
+fcm2_cyclic_dep_ok
+fcm2_delete_directory
+fcm2_delete_directory_inherit
+fcm2_delete_file
+fcm2_delete_file_inherit
+fcm2_delete_inc_file
+fcm2_delete_inc_file_inherit
+fcm2_delete_inc_file_inherit_force
+fcm2_delete_pp_file
+fcm2_delete_pp_file_inherit
+fcm2_delete_ppinc_file
+fcm2_delete_ppinc_file_inherit
+fcm2_delete_ppinc_file_inherit_force
+fcm2_dep_o
+fcm2_dep_o_all
+fcm2_dep_o_invalid
+fcm2_duplicate_target
+fcm2_exclude_dependency
+fcm2_extract_path_excl_no_ns
+fcm2_exe_permissions
+fcm2_exe_rename
+fcm2_exe_rename_incremental
+fcm2_fc_incremental
+fcm2_fflags_incremental
+fcm2_fflags_inherit
+fcm2_flag-output
+fcm2_inc_devnull
+fcm2_inherit_invalid_path
+fcm2_inherit_redefine_fail
+fcm2_inherit_redefine_ok
+fcm2_invalid_base_url
+fcm2_invalid_branch_url
+fcm2_invalid_branch_url2
+fcm2_invalid_inc
+fcm2_invalid_label
+fcm2_invalid_modifier
+fcm2_invalid_modifiers
+fcm2_invalid_namespace
+fcm2_invalid_namespace2
+fcm2_invalid_target
+fcm2_invalid_variable
+fcm2_library
+fcm2_library_rename
+fcm2_mirror
+fcm2_mirror_after_pp
+fcm2_mirror_inherit
+fcm2_mirror_inherit_notarget
+fcm2_mirror_inherit_fflags
+fcm2_modify_subroutine_inherit
+fcm2_modify_subroutine_interface_inherit
+fcm2_multi_inherit
+fcm2_multiple_build
+fcm2_multiple_build_inherit
+fcm2_multiple_pp-build
+fcm2_multiple_pp-build_inherit
+fcm2_no_dep
+fcm2_ns-dep_o
+fcm2_ns-dep_o_all
+fcm2_ns-dep_o_file
+fcm2_ns-dep_o_invalid
+fcm2_override_variable
+fcm2_pp_change_include
+fcm2_pp_change_include_inherit
+fcm2_pp_change_keys_incremental
+fcm2_pp_empty_subroutine
+fcm2_pp_empty_subroutine_inherit
+fcm2_pp_empty_subroutine_inherit_force
+fcm2_revmatch_false
+fcm2_single_file
+fcm2_space_in_name
+fcm2_symbolic_link
+"
+
+# Remove any commented out tests
+TESTS_FCM1=$(echo $TESTS_FCM1 | sed 's/#[^ ]*//g')
+TESTS_FCM2=$(echo $TESTS_FCM2 | sed 's/#[^ ]*//g')
diff --git a/test/tests_perf_local.list b/test/tests_perf_local.list
new file mode 100755
index 0000000..f1fcfbf
--- /dev/null
+++ b/test/tests_perf_local.list
@@ -0,0 +1,23 @@
+TESTS_FCM1="
+fcm1_var
+fcm1_var_parallel
+fcm1_ops_parallel
+fcm1_sps_parallel
+fcm1_um
+fcm1_um_inherit
+"
+
+TESTS_FCM2="
+fcm2_var
+fcm2_var_parallel
+fcm2_ops_parallel
+fcm2_sps_parallel
+fcm2_um
+fcm2_um_inherit
+fcm2_um77
+fcm2_um77_inherit
+"
+
+# Remove any commented out tests
+TESTS_FCM1=$(echo $TESTS_FCM1 | sed 's/#[^ ]*//g')
+TESTS_FCM2=$(echo $TESTS_FCM2 | sed 's/#[^ ]*//g')
diff --git a/test/tests_perf_remote.list b/test/tests_perf_remote.list
new file mode 100755
index 0000000..dcd8e7a
--- /dev/null
+++ b/test/tests_perf_remote.list
@@ -0,0 +1,19 @@
+TESTS_FCM1="
+fcm1_var_hpc
+fcm1_postproc_hpc
+fcm1_um_hpc
+fcm1_um_inherit_hpc
+"
+
+TESTS_FCM2="
+fcm2_var_hpc
+fcm2_postproc_hpc
+fcm2_um_hpc
+fcm2_um_inherit_hpc
+fcm2_um77_hpc
+fcm2_um77_inherit_hpc
+"
+
+# Remove any commented out tests
+TESTS_FCM1=$(echo $TESTS_FCM1 | sed 's/#[^ ]*//g')
+TESTS_FCM2=$(echo $TESTS_FCM2 | sed 's/#[^ ]*//g')
diff --git a/test/wrapper_scripts/wrap_ar b/test/wrapper_scripts/wrap_ar
new file mode 100755
index 0000000..04cf798
--- /dev/null
+++ b/test/wrapper_scripts/wrap_ar
@@ -0,0 +1,2 @@
+echo "wrap_ar $@" | sed "s#$RUN_DIR##g" | sed "s#/var/tmp/[^/]*/lib#/lib#" >>$command_file
+ar "$@"
diff --git a/test/wrapper_scripts/wrap_cc b/test/wrapper_scripts/wrap_cc
new file mode 100755
index 0000000..82898ed
--- /dev/null
+++ b/test/wrapper_scripts/wrap_cc
@@ -0,0 +1,2 @@
+echo "wrap_cc $@" | sed "s#$RUN_DIR##g" | sed "s#-I\./#-I#g" >>$command_file
+gcc "$@"
diff --git a/test/wrapper_scripts/wrap_fc b/test/wrapper_scripts/wrap_fc
new file mode 100755
index 0000000..e77e4ff
--- /dev/null
+++ b/test/wrapper_scripts/wrap_fc
@@ -0,0 +1,2 @@
+echo "wrap_fc $@" | sed "s#$RUN_DIR##g" | sed "s#-I\./#-I#g" | sed "s#-L/var/tmp/[^ ]* #-Llib #" >>$command_file
+ifort "$@"
diff --git a/test/wrapper_scripts/wrap_fc2 b/test/wrapper_scripts/wrap_fc2
new file mode 100755
index 0000000..4573377
--- /dev/null
+++ b/test/wrapper_scripts/wrap_fc2
@@ -0,0 +1,2 @@
+echo "wrap_fc2 $@" | sed "s#$RUN_DIR##g" | sed "s#-I\./#-I#g" | sed "s#-L/var/tmp/[^ ]* #-Llib #" >>$command_file
+ifort "$@"
diff --git a/test/wrapper_scripts/wrap_ld b/test/wrapper_scripts/wrap_ld
new file mode 100755
index 0000000..585fbde
--- /dev/null
+++ b/test/wrapper_scripts/wrap_ld
@@ -0,0 +1,2 @@
+echo "wrap_ld $@" | sed "s#$RUN_DIR##g" >>$command_file
+ifort "$@"
diff --git a/test/wrapper_scripts/wrap_ld2 b/test/wrapper_scripts/wrap_ld2
new file mode 100755
index 0000000..155c9b9
--- /dev/null
+++ b/test/wrapper_scripts/wrap_ld2
@@ -0,0 +1,2 @@
+echo "wrap_ld2 $@" | sed "s#$RUN_DIR##g" >>$command_file
+ifort "$@"
diff --git a/test/wrapper_scripts/wrap_mpicc b/test/wrapper_scripts/wrap_mpicc
new file mode 100755
index 0000000..90330cb
--- /dev/null
+++ b/test/wrapper_scripts/wrap_mpicc
@@ -0,0 +1,2 @@
+echo "wrap_mpicc $@" | sed "s#$RUN_DIR##g" | sed "s#-I\./#-I#g" >>$command_file
+mpicc "$@"
diff --git a/test/wrapper_scripts/wrap_mpif90 b/test/wrapper_scripts/wrap_mpif90
new file mode 100755
index 0000000..202c197
--- /dev/null
+++ b/test/wrapper_scripts/wrap_mpif90
@@ -0,0 +1,2 @@
+echo "wrap_mpif90 $@" | sed "s#$RUN_DIR##g" | sed "s#-I\./#-I#g" | sed "s#-L/var/tmp/[^ ]* #-Llib #" >>$command_file
+mpif90 "$@"
diff --git a/test/wrapper_scripts/wrap_pp b/test/wrapper_scripts/wrap_pp
new file mode 100755
index 0000000..0ece941
--- /dev/null
+++ b/test/wrapper_scripts/wrap_pp
@@ -0,0 +1,2 @@
+echo "wrap_pp $@" | sed "s#$RUN_DIR##g" | sed "s#-I\./#-I#g" >>$command_file
+cpp "$@"
diff --git a/tutorial/README b/tutorial/README
new file mode 100644
index 0000000..a22e5b3
--- /dev/null
+++ b/tutorial/README
@@ -0,0 +1,14 @@
+This directory contains the files necessary to set up a Subversion repository
+for the FCM tutorial.
+
+For example (in bash/ksh):
+
+(shell)$ cd fcm-release/tutorial/ # to this directory
+(shell)$ ./fcm-tutorial-repos-create /path/to/tutorial/repos
+
+The repository should be configured to allow users write access. You may find
+it easiest to simply allow anonymous access.
+
+A Trac environment should be configured to be associated with the tutorial
+repository. You then need to allow users write access. You may find it easiest
+to set up a number of guest accounts for this purpose.
diff --git a/tutorial/fcm-tutorial-repos-create b/tutorial/fcm-tutorial-repos-create
new file mode 100755
index 0000000..93996d8
--- /dev/null
+++ b/tutorial/fcm-tutorial-repos-create
@@ -0,0 +1,80 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+set -eu
+if (($# < 1)); then
+ echo "Usage: $(basename $0) DEST" >&2
+ exit 1
+fi
+DEST=$1
+if [[ -e $DEST ]]; then
+ echo "$DEST: destination already exists." >&2
+ exit 1
+fi
+THIS_HOME=$(cd $(dirname $0) && pwd)
+WORK_DIR=
+function FINALLY() {
+ trap '' ERR
+ trap '' EXIT
+ cd ~
+ if [[ -n $WORK_DIR ]]; then
+ rm -rf $WORK_DIR
+ fi
+}
+#-------------------------------------------------------------------------------
+function rsyncs() {
+ rsync -a --exclude=".svn" --checksum "$@"
+}
+#-------------------------------------------------------------------------------
+
+WORK_DIR=$(mktemp -d)
+trap FINALLY ERR
+trap FINALLY EXIT
+cd $WORK_DIR
+
+svnadmin create repos
+REPOS_URL=file://$PWD/repos
+svn checkout -q $REPOS_URL working-copy
+mkdir -p working-copy/tutorial/{trunk,branches,tags}
+
+# r1
+rsyncs $THIS_HOME/trunk-r1/* working-copy/tutorial/trunk/
+svn add -q working-copy/tutorial
+svn commit -q -m'tutorial: initial import.' working-copy
+svn update -q working-copy
+
+# r2
+TRUNK_SRC=working-copy/tutorial/trunk/src
+svn move -q $TRUNK_SRC/module/hello_num.f90 $TRUNK_SRC/module/hello_number.f90
+sed -i 's/Hello World/Hello Earth/' $TRUNK_SRC/module/hello_constants.f90
+sed -i 's/Hello World/Hello Earth/' $TRUNK_SRC/subroutine/hello_c.c
+svn commit -q -m'tutorial: World=Earth, and correct module name.' working-copy
+svn update -q working-copy
+
+rsyncs $THIS_HOME/hooks/* repos/hooks/
+curl -o repos/hooks/svnperms.py \
+ http://svn.apache.org/repos/asf/subversion/trunk/tools/hook-scripts/svnperms.py
+chmod +x repos/hooks/svnperms.py
+cat >repos/hooks/svnperms.conf <<__SVNPERMS_CONF__
+[$(basename $DEST)]
+tutorial/branches/[^/]+/.* = *(add,remove,update)
+__SVNPERMS_CONF__
+mkdir -p $(dirname $DEST)
+svnadmin hotcopy repos $DEST
+echo "$DEST: tutorial repository created."
diff --git a/tutorial/hooks/pre-commit b/tutorial/hooks/pre-commit
new file mode 100755
index 0000000..0270e90
--- /dev/null
+++ b/tutorial/hooks/pre-commit
@@ -0,0 +1,7 @@
+#!/bin/bash
+set -eu
+REPOS=$1
+TXN=$2
+SCRIPT=$REPOS/hooks/svnperms.py
+CONFIG=$REPOS/hooks/svnperms.conf
+$SCRIPT -r $REPOS -t $TXN -f $CONFIG
diff --git a/tutorial/trunk-r1/doc/hello.html b/tutorial/trunk-r1/doc/hello.html
new file mode 100644
index 0000000..b779e01
--- /dev/null
+++ b/tutorial/trunk-r1/doc/hello.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN">
+
+<html>
+<head>
+ <title>Hello</title>
+ <meta name="author" content="FCM Team">
+ <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
+</head>
+
+<body>
+ <h1>Hello</h1>
+ <p>Hello from the FCM team!</p>
+</body>
+</html>
diff --git a/tutorial/trunk-r1/fcm-make.cfg b/tutorial/trunk-r1/fcm-make.cfg
new file mode 100644
index 0000000..4251ac0
--- /dev/null
+++ b/tutorial/trunk-r1/fcm-make.cfg
@@ -0,0 +1,5 @@
+steps = extract build
+extract.ns = tutorial
+extract.location[tutorial] = $HERE
+extract.path-root[tutorial] = src
+build.target{task} = link
diff --git a/tutorial/trunk-r1/src/module/hello_constants.f90 b/tutorial/trunk-r1/src/module/hello_constants.f90
new file mode 100644
index 0000000..785961a
--- /dev/null
+++ b/tutorial/trunk-r1/src/module/hello_constants.f90
@@ -0,0 +1,3 @@
+MODULE hello_constants
+CHARACTER(*), PARAMETER :: hello_string='Hello World!'
+END MODULE hello_constants
diff --git a/tutorial/trunk-r1/src/module/hello_num.f90 b/tutorial/trunk-r1/src/module/hello_num.f90
new file mode 100644
index 0000000..fc59ee8
--- /dev/null
+++ b/tutorial/trunk-r1/src/module/hello_num.f90
@@ -0,0 +1,17 @@
+MODULE hello_number
+
+IMPLICIT NONE
+
+PRIVATE
+INTEGER, PARAMETER :: i=0
+INTEGER, PARAMETER :: huge_number=HUGE(i)
+
+PUBLIC hello_huge_number
+
+CONTAINS
+SUBROUTINE hello_huge_number()
+CHARACTER(LEN=*), PARAMETER :: this='hello_huge_number'
+WRITE(*, '(A,I0)') this // ': maximum integer: ', huge_number
+END SUBROUTINE hello_huge_number
+
+END MODULE hello_number
diff --git a/tutorial/trunk-r1/src/program/hello.f90 b/tutorial/trunk-r1/src/program/hello.f90
new file mode 100644
index 0000000..c44289a
--- /dev/null
+++ b/tutorial/trunk-r1/src/program/hello.f90
@@ -0,0 +1,12 @@
+PROGRAM hello
+
+USE Hello_Constants, ONLY: hello_string
+
+IMPLICIT NONE
+INCLUDE 'hello_sub.interface'
+CHARACTER(*), PARAMETER :: this='hello'
+
+WRITE(*, '(A)') this // ': ' // TRIM(hello_string)
+CALL Hello_Sub()
+
+END PROGRAM hello
diff --git a/tutorial/trunk-r1/src/subroutine/hello_c.c b/tutorial/trunk-r1/src/subroutine/hello_c.c
new file mode 100644
index 0000000..711dd87
--- /dev/null
+++ b/tutorial/trunk-r1/src/subroutine/hello_c.c
@@ -0,0 +1,5 @@
+#include <stdio.h>
+
+void hello_c_() {
+ printf ("%s\n", "hello_c: Hello World!");
+}
diff --git a/tutorial/trunk-r1/src/subroutine/hello_sub.f90 b/tutorial/trunk-r1/src/subroutine/hello_sub.f90
new file mode 100644
index 0000000..ba122d7
--- /dev/null
+++ b/tutorial/trunk-r1/src/subroutine/hello_sub.f90
@@ -0,0 +1,15 @@
+SUBROUTINE hello_sub
+
+USE hello_constants, ONLY: hello_string
+USE hello_number, ONLY: hello_huge_number
+
+IMPLICIT NONE
+CHARACTER(*), PARAMETER :: this = 'hello_sub'
+! DEPENDS ON: hello_c.o
+EXTERNAL hello_c
+
+WRITE(*, '(A)') this // ': ' // TRIM(hello_string)
+CALL hello_huge_number()
+CALL hello_c()
+
+END SUBROUTINE hello_sub
diff --git a/usr/bin/fcm b/usr/bin/fcm
new file mode 100755
index 0000000..665112b
--- /dev/null
+++ b/usr/bin/fcm
@@ -0,0 +1,31 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# (C) British Crown Copyright 2006-14 Met Office.
+#
+# This file is part of FCM, tools for managing and building source code.
+#
+# FCM 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# FCM 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 FCM. If not, see <http://www.gnu.org/licenses/>.
+#-------------------------------------------------------------------------------
+# Wrapper for "fcm":
+# * Should be installed at a location like "/usr/bin".
+# * Modify the default value for FCM_HOME_ROOT to suit your site.
+#-------------------------------------------------------------------------------
+if [[ -z ${FCM_HOME:-} ]]; then
+ FCM_HOME_ROOT=${FCM_HOME_ROOT:-/opt}
+ FCM_HOME=$FCM_HOME_ROOT/fcm
+ if [[ -n ${FCM_VERSION:-} && -d $FCM_HOME_ROOT/fcm-$FCM_VERSION ]]; then
+ FCM_HOME=$FCM_HOME_ROOT/fcm-$FCM_VERSION
+ fi
+fi
+exec $FCM_HOME/bin/$(basename $0) "$@"
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/debian-science/packages/fcm.git
More information about the debian-science-commits
mailing list