123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721 |
- # ------------------------------------------------------------------------------
- # NAME
- # Fcm::Cm
- #
- # DESCRIPTION
- # This module contains the FCM code management functionalities and wrappers
- # to Subversion commands.
- #
- # COPYRIGHT
- # (C) Crown copyright Met Office. All rights reserved.
- # For further details please refer to the file COPYRIGHT.txt
- # which you should have received as part of this distribution.
- # ------------------------------------------------------------------------------
- use strict;
- use warnings;
- package Fcm::Cm;
- use base qw{Exporter};
- our @EXPORT_OK = qw(cli cm_check_missing cm_check_unknown cm_switch cm_update);
- use Cwd qw{cwd};
- use Getopt::Long qw{GetOptions :config bundling};
- use Fcm::CLI::Exception;
- use Fcm::Config;
- use Fcm::CmBranch;
- use Fcm::CmUrl;
- use Fcm::Keyword;
- use Fcm::Util qw{
- get_url_of_wc
- get_url_peg_of_wc
- get_wct
- is_url
- is_wc
- run_command
- tidy_url
- };
- use File::Basename qw{basename dirname};
- use File::Path qw{mkpath rmtree};
- use File::Spec;
- use File::Temp qw{tempfile};
- use Pod::Usage qw{pod2usage};
- # ------------------------------------------------------------------------------
- # 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 => "Performing merge ...\n",
- MERGE_CF => "About to merge in changes from %s compared with %s\n",
- MERGE_CI => "The following is added to the commit message file:\n%s",
- MERGE_DRY => "This merge will result in the following change:\n",
- MERGE_REVS => "Merge(s) available 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 => "Status of the target working copy(ies):\n%s",
- );
- # 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 usage",
- 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",
- );
- # 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{Are you sure you want to continue?},
- MERGE => qq{Would you like to go ahead with the merge?},
- MERGE_REV => qq{Please enter the revision you wish to merge from},
- 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 };
- # List of exception handlers [$class, CODE->($function, $e)]
- our @CLI_EXCEPTION_HANDLERS = (
- ['Fcm::CLI::Exception', \&_cli_e_handler_of_cli_exception],
- ['Fcm::Cm::Exception' , \&_cli_e_handler_of_cm_exception],
- ['Fcm::Cm::Abort' , \&_cli_e_handler_of_cm_abort],
- );
- # Event handlers
- our %CLI_HANDLER_OF = (
- 'WC_STATUS' => \&_cli_handler_of_wc_status,
- 'WC_STATUS_PATH' => \&_cli_handler_of_wc_status_path,
- );
- # Handlers of sub-commands
- our %CLI_IMPL_OF = (
- 'add' => \&_cli_command_add,
- 'branch' => \&cm_branch,
- 'commit' => \&cm_commit,
- 'conflicts' => \&cm_conflicts,
- 'checkout' => \&_cli_command_checkout,
- 'delete' => \&_cli_command_delete,
- 'diff' => \&cm_diff,
- 'merge' => \&cm_merge,
- 'mkpatch' => \&cm_mkpatch,
- 'switch' => \&_cli_command_switch,
- 'update' => \&_cli_command_update,
- );
- # List of overridden subcommands that need to display "svn help"
- our %CLI_MORE_HELP_FOR = map {($_, 1)} qw{add diff delete switch update};
- # The preferred name of subcommand aliases
- our %CLI_PREFERRED_NAME_OF = (
- 'ann' => 'blame',
- 'annotate' => 'blame',
- 'br' => 'branch',
- 'ci' => 'commit',
- 'cf' => 'conflicts',
- 'co' => 'checkout',
- 'cp' => 'copy',
- 'del' => 'delete',
- 'di' => 'diff',
- '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',
- 'remove' => 'delete',
- 'ren' => 'move',
- 'rename' => 'move',
- 'rm' => 'delete',
- 'sw' => 'switch',
- 'up' => 'update',
- );
- # List of subcommands that accept URL inputs
- our %CLI_SUBCOMMAND_URL = map {($_, 1)} qw{
- blame
- branch
- cat
- checkout
- copy
- delete
- diff
- export
- import
- info
- list
- lock
- log
- merge
- mkdir
- mkpatch
- move
- propdel
- propedit
- propget
- proplist
- propset
- switch
- unlock
- };
- # List of subcommands that accept revision inputs
- our %CLI_SUBCOMMAND_REV = map {($_, 1)} qw{
- blame
- branch
- cat
- checkout
- copy
- diff
- export
- info
- list
- log
- merge
- mkpatch
- move
- propdel
- propedit
- propget
- proplist
- propset
- switch
- };
- # 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 = (
- MISSING => sub {substr($_[0], 0, 1) eq '!'},
- MODIFIED => sub {substr($_[0], 0, 6) =~ qr{\S}xms},
- OUT_OF_DATE => sub {substr($_[0], 7, 1) eq '*'},
- UNKNOWN => sub {substr($_[0], 0, 1) eq '?'},
- );
- # ------------------------------------------------------------------------------
- # Entry function for the FCM code management CLI. Calls the relevant FCM code
- # management function or SVN command based on $function.
- sub cli {
- my ($function, @args) = @_;
- if (exists($CLI_PREFERRED_NAME_OF{$function})) {
- $function = $CLI_PREFERRED_NAME_OF{$function};
- }
- if (grep {$_ eq '-h' || $_ eq '--help'} @args) {
- return _cli_help($function, 'NOEXIT');
- }
- if (exists($CLI_SUBCOMMAND_URL{$function})) {
- _cli_keyword_expand_url(\@args);
- }
- if (exists($CLI_SUBCOMMAND_REV{$function})) {
- _cli_keyword_expand_rev(\@args);
- }
- if (exists($CLI_IMPL_OF{$function})) {
- eval {
- local(@ARGV) = @args;
- return $CLI_IMPL_OF{$function}->(@args);
- };
- if ($@) {
- my $e = $@;
- for (@CLI_EXCEPTION_HANDLERS) {
- my ($class, $handler) = @{$_};
- if ($class->caught($e)) {
- return $handler->($function, $e);
- }
- }
- die($e);
- }
- }
- else {
- return _svn($function, @args);
- }
- }
- # ------------------------------------------------------------------------------
- # SYNOPSIS
- # &Fcm::Cm::cm_branch ();
- #
- # DESCRIPTION
- # This is a FCM command to check information, create or delete a branch in
- # a Subversion repository.
- # ------------------------------------------------------------------------------
- sub cm_branch {
- # Process command line options
- # ----------------------------------------------------------------------------
- my (
- $info,
- $delete,
- $create,
- $list,
- $branch_of_branch,
- $name,
- $non_interactive,
- $password,
- $rev,
- $rev_flag,
- $show_all,
- $show_children,
- $show_other,
- $show_siblings,
- $svn_non_interactive,
- @tickets,
- $type,
- @userlist,
- $verbose,
- );
- my $rc = GetOptions(
- 'info|i' => \$info,
- 'delete|d' => \$delete,
- 'create|c' => \$create,
- 'list|l' => \$list,
- 'branch-of-branch' => \$branch_of_branch,
- 'name|n=s' => \$name,
- 'non-interactive' => \$non_interactive,
- 'password=s' => \$password,
- 'revision|r=s' => \$rev,
- 'rev-flag=s' => \$rev_flag,
- 'show-all|a' => \$show_all,
- 'show-children' => \$show_children,
- 'show-other' => \$show_other,
- 'show-siblings' => \$show_siblings,
- 'svn-non-interactive' => \$svn_non_interactive,
- 'ticket|k=s' => \@tickets,
- 'type|t=s' => \$type,
- 'user|u=s' => \@userlist,
- 'verbose|v' => \$verbose,
- );
- if (!$rc) {
- _cli_err();
- }
- my $num_options = 0;
- $num_options++ if defined $info;
- $num_options++ if defined $delete;
- $num_options++ if defined $create;
- $num_options++ if defined $list;
- if ($num_options > 1) {
- _cli_err();
- }
- # Get URL of repository or branch
- # ----------------------------------------------------------------------------
- my $url;
- if ($ARGV[0]) {
- $url = Fcm::CmUrl->new (URL => $ARGV[0]);
- if (not $url->is_url) {
- # An argument is specified and is not a URL
- # Assume that it is a path with a working copy
- if (&is_wc ($ARGV[0])) {
- $url = Fcm::CmUrl->new (URL => &get_url_of_wc ($ARGV[0]));
- } else {
- return _cm_err(Fcm::Cm::Exception->INVALID_WC, $ARGV[0]);
- }
- }
- } else {
- # An argument is not specified
- # Assume that the current directory is a working copy
- if (&is_wc ()) {
- $url = Fcm::CmUrl->new (URL => &get_url_of_wc ());
- } else {
- return _cm_err(Fcm::Cm::Exception->INVALID_TARGET, '.');
- }
- }
- # Ensure $url->url_peg is a URL of a standard FCM project
- if (!$url->project_url()) {
- return _cm_err(Fcm::Cm::Exception->INVALID_PROJECT, $url->url_peg());
- }
- if ($create) {
- # The --create option is specified, create a branch
- # --------------------------------------------------------------------------
- # Check branch type flags
- if ($type) {
- $type = uc ($type);
- if ($type =~ /^(USER|SHARE)$/) {
- $type = 'DEV' . $Fcm::Config::DELIMITER . $1;
- } elsif ($type =~ /^(CONFIG|REL)$/) {
- $type = 'PKG' . $Fcm::Config::DELIMITER . $1;
- } elsif ($type =~ /^(DEV|TEST|PKG)$/) {
- $type = $1 . $Fcm::Config::DELIMITER . 'USER';
- } elsif ($type !~ /^(?:DEV|TEST|PKG)$Fcm::Config::DELIMITER(?:USER|SHARE)$/
- and $type !~ /^PKG$Fcm::Config::DELIMITER(?:CONFIG|REL)/) {
- _cli_err('CLI_OPT_ARG', 'type', $type);
- }
- } else {
- $type = 'DEV' . $Fcm::Config::DELIMITER . 'USER';
- }
- # Check branch name
- if (!$name) {
- _cli_err('CLI_OPT_WITH_OPT', 'name', 'create');
- }
- if ($name !~ qr{\A[\w.-]+\z}xms) {
- _cli_err('CLI_OPT_ARG', 'name', $name);
- }
- # Check revision flag is valid
- if ($rev_flag) {
- $rev_flag = uc ($rev_flag);
- if ($rev_flag !~ qr{\A (?:NORMAL|NUMBER|NONE) \z}xms) {
- _cli_err('CLI_OPT_ARG', 'rev-flag', $rev_flag);
- }
- } else {
- $rev_flag = 'NORMAL';
- }
- # Handle multiple tickets
- @tickets = split (
- /$Fcm::Config::DELIMITER_LIST/,
- join ($Fcm::Config::DELIMITER_LIST, @tickets)
- );
- s/^#// for (@tickets);
- @tickets = sort {$a <=> $b} @tickets;
- # Determine whether to create a branch of a branch
- $url->branch ('trunk') unless $branch_of_branch;
- # Create the branch
- my $branch = Fcm::CmBranch->new;
- $branch->create (
- SRC => $url,
- TYPE => $type,
- NAME => $name,
- PASSWORD => $password,
- REV_FLAG => $rev_flag,
- TICKET => \@tickets,
- REV => $rev,
- NON_INTERACTIVE => $non_interactive,
- SVN_NON_INTERACTIVE => $svn_non_interactive,
- );
- } elsif ($list) {
- # The option --list is specified
- # List branches owned by current or specified users
- # --------------------------------------------------------------------------
- # Get URL of the project "branches/" sub-directory
- $url->subdir ('');
- $url->branch ('');
- my @branches = $url->branch_list($rev);
- if (!$show_all) {
- @userlist = split(qr{:}xms, join(q{:}, @userlist));
- if (!@userlist) {
- @userlist = (Fcm::Config->instance()->user_id());
- }
- my %filter = map {($_, 1)} @userlist;
- @branches = grep {
- $filter{Fcm::CmBranch->new(URL => $_)->branch_owner()}
- } @branches
- }
- # Output, number of branches found
- $CLI_MESSAGE->(
- 'BRANCH_LIST',
- $url->project_url_peg(),
- $rev ? "r$rev" : 'HEAD',
- scalar(@branches),
- ($show_all ? '[--show-all]' : join(q{, }, sort(@userlist))),
- );
- if (@branches) {
- # Output the URL of each branch
- if (not $verbose) {
- my $project = $url->project_url;
- @branches = map {Fcm::Keyword::unexpand($_)} @branches;
- }
- @branches = map {$_ . "\n"} sort @branches;
- $CLI_MESSAGE->(q{}, join(q{}, @branches));
- } else {
- # No branch found, exit with an error code
- return;
- }
- } else {
- # The option --info or --delete is specified
- # Report branch information (and/or delete a branch)
- # --------------------------------------------------------------------------
- # Set verbose level
- Fcm::Config->instance()->verbose ($verbose ? 1 : 0);
- # Set up the branch, report any error
- my $branch = Fcm::CmBranch->new (URL => $url->url_peg);
- if (!$branch->branch()) {
- return _cm_err(Fcm::Cm::Exception->INVALID_BRANCH, $branch->url_peg());
- }
- if (!$branch->url_exists()) {
- return _cm_err(Fcm::Cm::Exception->NOT_EXIST, $branch->url_peg());
- }
- # Remove the sub-directory part of the URL
- $branch->subdir ('');
- # Report branch info
- $branch->display_info (
- SHOW_CHILDREN => ($show_all || $show_children),
- SHOW_OTHER => ($show_all || $show_other ),
- SHOW_SIBLINGS => ($show_all || $show_siblings),
- );
- # Delete branch if --delete is specified
- $branch->del (
- PASSWORD => $password,
- NON_INTERACTIVE => $non_interactive,
- SVN_NON_INTERACTIVE => $svn_non_interactive,
- ) if $delete;
- }
- }
- # ------------------------------------------------------------------------------
- # SYNOPSIS
- # &Fcm::Cm::cm_commit ();
- #
- # DESCRIPTION
- # This is a FCM wrapper to the "svn commit" command.
- # ------------------------------------------------------------------------------
- sub cm_commit {
- my ($dry_run, $svn_non_interactive, $password);
- my $rc = GetOptions(
- 'dry-run' => \$dry_run,
- 'svn-non-interactive' => \$svn_non_interactive,
- 'password=s' => \$password,
- );
- if (!$rc) {
- _cli_err();
- }
- # The remaining argument is the path to a working copy
- my ($path) = @ARGV;
- if ($path) {
- if (!-e $path) {
- return _cm_err(Fcm::Cm::Exception->NOT_EXIST, $path);
- }
- } else {
- # No argument specified, use current working directory
- $path = cwd ();
- }
- # Make sure we are in a working copy
- if (!is_wc($path)) {
- return _cm_err(Fcm::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 = &get_wct ($path);
- if ($dir ne cwd ()) {
- chdir($dir) || return _cm_err(Fcm::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);
- unless (defined $dry_run) {
- my (@conflict, @missing, @outdate);
- for (@status) {
- if (/^C/) {
- push @conflict, $_;
- next;
- }
- if (/^!/) {
- push @missing, $_;
- next;
- }
- if (/^.{7}\*/) {
- push @outdate, $_;
- next;
- }
- # 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)
- next unless /^A.{7} *\d+ +(.*)/;
- my $file = $1;
- next unless -f $file;
- my ($command, @arguments)
- = (-x $file && !-l $file) ? ('propset', '*') : ('propdel');
- run_command(['svn', $command, qw{-q svn:executable}, @arguments, $file]);
- }
- # Abort commit if files are in conflict, missing, or out of date
- if (@conflict or @missing or @outdate) {
- for (
- ['ST_CONFLICT' , \@conflict],
- ['ST_MISSING' , \@missing ],
- ['ST_OUT_OF_DATE', \@outdate ],
- ) {
- my ($key, $array_ref) = @{$_};
- if (@{$array_ref}) {
- $CLI_MESSAGE->($key, join(q{}, @{$array_ref}));
- }
- }
- return _cm_abort(Fcm::Cm::Abort->FAIL);
- }
- }
- # Read in any existing message
- my $ci_mesg = Fcm::CmCommitMessage->new;
- $ci_mesg->read_file;
- # Execute "svn status" for a list of changed items
- @status = grep !/^\?/, _svn_status_get();
- # Abort if there is no change in the working copy
- if (!@status) {
- return _cm_abort(Fcm::Cm::Abort->NULL);
- }
- # Get associated URL of current working copy
- my $url = Fcm::CmUrl->new (URL => &get_url_of_wc ());
- # Include URL, or project, branch and sub-directory info in @status
- unshift @status, "\n";
- if ($url->project and $url->branch) {
- unshift @status, (
- '[Project: ' . $url->project . ']' . "\n",
- '[Branch : ' . $url->branch . ']' . "\n",
- '[Sub-dir: ' . ($url->subdir ? $url->subdir : '<top>') . ']' . "\n",
- );
- } else {
- unshift @status, '[URL: ' . $url->url . ']' . "\n";
- }
- # Use a temporary file to store the final commit log message
- $ci_mesg->ignore_mesg (\@status);
- my $logfile = $ci_mesg->edit_file (TEMP => 1);
- # Check with the user to see if he/she wants to go ahead
- my $reply = 'n';
- if (!defined($dry_run)) {
- # Add extra warning for trunk commit
- my @prompt_args;
- my $user = Fcm::Config->instance()->user_id();
- if ($url->is_trunk()) {
- @prompt_args = ('CI_TRUNK');
- }
- elsif ($user && $url->is_branch() && $url->branch_owner() ne $user) {
- if (exists $Fcm::CmUrl::owner_keywords{$url->branch_owner}) {
- @prompt_args = (
- 'CI_BRANCH_SHARED',
- uc($Fcm::CmUrl::owner_keywords{$url->branch_owner()}),
- );
- }
- else {
- @prompt_args = ('CI_BRANCH_USER');
- }
- }
- else {
- @prompt_args = ('CI');
- }
- $reply = $CLI_PROMPT->('commit', @prompt_args);
- }
- if ($reply eq 'y') {
- # Commit the change if user replies "y" for "yes"
- my @command = (
- qw/svn commit -F/, $logfile,
- ($svn_non_interactive ? '--non-interactive' : ()),
- (defined $password ? ('--password', $password) : ()),
- );
- my $rc;
- &run_command (\@command, RC => \$rc, ERROR => 'warn');
- if ($rc) {
- # Commit failed
- # Write temporary commit log content to commit log message file
- $ci_mesg->write_file;
- # Fail the command
- return _cm_abort(Fcm::Cm::Abort->FAIL);
- }
- # Remove commit message file
- unlink $ci_mesg->file;
- # Update the working copy
- $CLI_MESSAGE->(q{}, join(q{}, _svn_update()));
- } else {
- $ci_mesg->write_file;
- if (!$dry_run) {
- return _cm_abort();
- }
- }
- return;
- }
- # ------------------------------------------------------------------------------
- # SYNOPSIS
- # &Fcm::Cm::cm_conflicts ();
- #
- # DESCRIPTION
- # This is a FCM command for resolving conflicts within working copy using a
- # graphical merge tool.
- # ------------------------------------------------------------------------------
- sub cm_conflicts {
- # Path to the working copy
- my $path = $ARGV[0];
- $path = cwd () if not $path;
- # Check for any files with conflicts
- my @status = grep /^C.{4} *(.*)/, &run_command (
- [qw/svn st/, ($path eq cwd () ? () : $path)], METHOD => 'qx',
- );
- my @files = map {m/^C.{4} *(.*)/; $1} @status;
- # Save current working directory
- my $topdir = cwd ();
- # Set up environment for graphical merge
- # Use environment variable if set, otherwise use default setting
- local(%ENV) = %ENV;
- $ENV{FCM_GRAPHIC_MERGE}
- ||= Fcm::Config->instance()->setting (qw/TOOL GRAPHIC_MERGE/);
- FILE:
- for my $file (@files) {
- # Print name of file in conflicts
- $CLI_MESSAGE->('CF', $file);
- # Determine directory and base name of file in conflicts
- my $base = basename $file;
- my $dir = dirname $file;
- # Change to container directory of file in conflicts
- chdir(File::Spec->catfile($topdir, $dir))
- || return _cm_err(Fcm::Cm::Exception->CHDIR, $dir);
- # Use "svn info" to determine conflict marker files
- my @info = &run_command ([qw/svn info/, $base], METHOD => 'qx');
- # Ignore if $base is a binary file
- if (-B $base) {
- $CLI_MESSAGE->('CF_BINARY', $base);
- next FILE;
- }
- # Get conflicts markers files
- my ($older, $mine, $yours);
- for (@info) {
- $older = $1 if (/^Conflict Previous Base File: (.*)/);
- $mine = $1 if (/^Conflict Previous Working File: (.*)/);
- $yours = $1 if (/^Conflict Current Base File: (.*)/);
- }
- if (-f $base and (stat $base)[9] > (stat $mine)[9] + 1) {
- # If $base is newer (by more than a second), it may contain saved changes
- if ($CLI_PROMPT->('conflicts', 'CF_OVERWRITE', $base) ne 'y') {
- next FILE;
- }
- }
- # Launch graphic merge tool
- my $rc;
- my $command = [qw/fcm_graphic_merge/, $base, $mine, $older, $yours];
- # $rc == 0: all conflicts resovled
- # $rc == 1: some conflicts not resolved
- # $rc == 2: trouble
- eval {
- run_command($command, RC => \$rc);
- };
- if ($@) {
- if (!defined($rc) || $rc > 1) {
- die($@);
- }
- }
- next FILE if $rc;
- # Prompt user to run "svn resolved" on the file
- if ($CLI_PROMPT->('conflicts', 'RUN_SVN_COMMAND', 'resolved') eq 'y') {
- run_command([qw{svn resolved}, $base]);
- }
- }
- }
- # ------------------------------------------------------------------------------
- # SYNOPSIS
- # &Fcm::Cm::cm_diff ();
- #
- # DESCRIPTION
- # This is a wrapper to "svn diff". It adds two extra functionalities. The
- # first one allows the command to show differences relative to the base of
- # the branch. The second one allows differences to be displayed via a
- # graphical tool.
- # ------------------------------------------------------------------------------
- sub cm_diff {
- # Set up environment for graphical diff
- # Use environment variable if set, otherwise use default setting
- local(%ENV) = %ENV;
- $ENV{FCM_GRAPHIC_DIFF}
- ||= Fcm::Config->instance()->setting(qw/TOOL GRAPHIC_DIFF/);
- # Check for the --branch options
- # ----------------------------------------------------------------------------
- my $branch = grep {$_ eq '-b' or $_ eq '--branch'} @ARGV;
- if (not $branch) {
- # The --branch option not specified, just call "svn diff"
- # Convert the --graphical to qw/--diff-cmd fcm_graphical_diff/
- # Convert the --summarise to --summarize
- @ARGV = map {
- my @return;
- if ($_ eq '-g' or $_ eq '--graphical') {
- @return = (qw/--diff-cmd fcm_graphic_diff/)
- } elsif ($_ eq '--summarise') {
- @return = ('--summarize');
- } else {
- @return = ($_);
- }
- @return;
- } @ARGV;
- # Execute the command
- return _svn('diff', @ARGV);
- }
- # The --branch option is specified
- # ----------------------------------------------------------------------------
- # Determine whether the --graphical option is specified,
- # if so set the appropriate command
- # ----------------------------------------------------------------------------
- my ($diff_cmd, $extensions, $graphical, $summarise, $trac, $wiki);
- my $rc = GetOptions (
- 'b|branch' => \$branch,
- 'diff-cmd=s' => \$diff_cmd,
- 'x|extensions=s' => \$extensions,
- 'g|graphical' => \$graphical,
- 'summarise|summarize' => \$summarise,
- 't|trac' => \$trac,
- 'wiki' => \$wiki,
- );
- if (!$rc) {
- _cli_err();
- }
- my @diff_cmd = ();
- if ($graphical) {
- @diff_cmd = (qw/--diff-cmd fcm_graphic_diff/);
- } elsif ($diff_cmd) {
- @diff_cmd = ('--diff-cmd', $diff_cmd);
- push @diff_cmd, '--extensions', split (/\s+/, $extensions) if $extensions;
- }
- # The remaining argument should either be a URL or a PATH
- my ($url_arg, $path_arg);
- if (@ARGV) {
- my $arg = Fcm::CmUrl->new (URL => $ARGV[0]);
- if ($arg->is_url) {
- $url_arg = $ARGV[0];
- } else {
- $path_arg = $ARGV[0];
- }
- }
- # Get repository and branch information
- # ----------------------------------------------------------------------------
- my ($url, $path);
- if (defined $url_arg) {
- # If a URL is specified, get repository and branch information from it
- $url = Fcm::CmBranch->new (URL => $url_arg);
- } else {
- # Get repository and branch information from the specified path or the
- # current directory if it is a working copy
- $path = $path_arg ? $path_arg : cwd ();
- if (!is_wc($path)) {
- return _cm_err(Fcm::Cm::Exception->INVALID_WC, $path);
- }
- $url = Fcm::CmBranch->new (URL => &get_url_peg_of_wc ($path));
- }
- # Check that URL is a standard FCM branch
- if (!$url->is_branch()) {
- return _cm_err(Fcm::Cm::Exception->INVALID_BRANCH, $url->url_peg());
- }
- # Save and remove sub-directory part of the URL
- my $subdir = $url->subdir ();
- $url->subdir ('');
- # Check that $url exists
- if (!$url->url_exists()) {
- return _cm_err(Fcm::Cm::Exception->INVALID_URL, $url->url_peg());
- }
- # Compare current branch with its parent
- # ----------------------------------------------------------------------------
- my $parent = Fcm::CmBranch->new (URL => $url->parent->url);
- $parent->pegrev ($url->pegrev) if $url->pegrev;
- if (!$parent->url_exists()) {
- return _cm_err(
- Fcm::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
- $url->subdir ($subdir);
- $base->subdir ($subdir);
- # Ensure the branch URL has a peg revision
- $url->pegrev ($url->svninfo (FLAG => 'Last Changed Rev')) if not $url->pegrev;
- if ($trac or $wiki) {
- # Trac/wiki
- # --------------------------------------------------------------------------
- if (!$url_arg && _svn_status_get([$path_arg ? $path_arg : q{.}])) {
- $CLI_MESSAGE->('ST_IN_TRAC_DIFF', ($path_arg ? $path_arg : q{.}));
- }
- # Trac wiki syntax
- my $wiki_syntax = 'diff:' . $base->path_peg . '//' . $url->path_peg;
- if ($wiki) {
- # Print Trac wiki syntax only
- $CLI_MESSAGE->(q{}, "$wiki_syntax\n");
- } else { # if $trac
- # Use Trac to view "diff"
- my $browser = Fcm::Config->instance()->setting(qw/WEB_BROWSER/);
- $browser ||= 'firefox';
- my $trac_url = Fcm::Keyword::get_browser_url($url->project_url());
- $trac_url =~ s{/intertrac/.*$}{/intertrac/$wiki_syntax}xms;
- &run_command ([$browser, $trac_url], METHOD => 'exec', PRINT => 1);
- }
- } else {
- # Execute the "diff" command
- # --------------------------------------------------------------------------
- my @command = (
- qw/svn diff/, @diff_cmd,
- ($summarise ? ('--summarize') : ()),
- '--old', $base->url_peg,
- '--new', ($url_arg ? $url->url_peg : ($path_arg ? $path_arg : '.')),
- );
- &run_command (\@command, PRINT => 1);
- }
- }
- # ------------------------------------------------------------------------------
- # SYNOPSIS
- # &Fcm::Cm::cm_merge ();
- #
- # DESCRIPTION
- # This is a wrapper to "svn merge".
- # ------------------------------------------------------------------------------
- sub cm_merge {
- # Options
- # ----------------------------------------------------------------------------
- my ($custom, $dry_run, $non_interactive, $reverse, $rev, $verbose);
- my $rc = GetOptions(
- 'custom' => \$custom,
- 'dry-run' => \$dry_run,
- 'non-interactive' => \$non_interactive,
- 'reverse' => \$reverse,
- 'revision|r=s' => \$rev,
- 'verbose|v' => \$verbose,
- );
- if (!$rc) {
- _cli_err();
- }
- # Find out the URL of the working copy
- # ----------------------------------------------------------------------------
- my ($target, $wct);
- if (&is_wc ()) {
- $wct = &get_wct ();
- if ($wct ne cwd ()) {
- chdir($wct) || return _cm_err(Fcm::Cm::Exception->CHDIR, $wct);
- $CLI_MESSAGE->('CHDIR_WCT', $wct);
- }
- $target = Fcm::CmBranch->new (URL => &get_url_of_wc ($wct));
- } else {
- return _cm_err(Fcm::Cm::Exception->INVALID_WC, '.');
- }
- if (!$target->url_exists()) {
- return _cm_err(Fcm::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;
- $target->subdir ('') if $subdir;
- # Check for any local modifications
- # ----------------------------------------------------------------------------
- if (!$dry_run && !$non_interactive) {
- _svn_status_checker('merge', 'MODIFIED', $CLI_HANDLER_OF{WC_STATUS})->();
- }
- # Determine the SOURCE URL
- # ----------------------------------------------------------------------------
- my $source;
- if ($reverse) {
- # Reverse merge, the SOURCE is the the working copy URL
- $source = Fcm::CmBranch->new (URL => $target->url);
- } else {
- # Automatic/custom merge, argument 1 is the SOURCE of the merge
- my $source_url = shift (@ARGV);
- if (!$source_url) {
- _cli_err('CLI_MERGE_ARG1');
- }
- $source = _cm_get_source($source_url, $target);
- }
- # Parse the revision option
- # ----------------------------------------------------------------------------
- if ($reverse && !$rev) {
- _cli_err('CLI_OPT_WITH_OPT', 'revision', 'reverse');
- }
- my @revs = (($reverse || $custom) && $rev ? split(qr{:}xms, $rev) : ());
- # Determine the merge delta and the commit log message
- # ----------------------------------------------------------------------------
- my (@delta, $mesg);
- my $separator = '-' x 80 . "\n";
- if ($reverse) {
- # Reverse merge
- # --------------------------------------------------------------------------
- if (@revs == 1) {
- $revs[1] = ($revs[0] - 1);
- } else {
- @revs = sort {$b <=> $a} @revs;
- }
- $source->pegrev ($source->svninfo (FLAG => 'Last Changed Rev'))
- unless $source->pegrev;
- $source->subdir ($subdir);
- # "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 ($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->pegrev ($source->svninfo (FLAG => 'Last Changed Rev'))
- unless $source->pegrev;
- $source->subdir ($subdir);
- $target->subdir ($subdir);
- # "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 (@ARGV);
- if (!$source2_url) {
- _cli_err('CLI_MERGE_ARG2');
- }
- my $source2 = _cm_get_source($source2_url, $target);
- $source->pegrev ($source->svninfo (FLAG => 'Last Changed Rev'))
- unless $source->pegrev;
- $source2->pegrev ($source2->svninfo (FLAG => 'Last Changed Rev'))
- unless $source2->pegrev;
- $source->subdir ($subdir);
- $source2->subdir ($subdir);
- $target->subdir ($subdir);
- # "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(Fcm::Cm::Exception->WC_INVALID_BRANCH, $wct);
- }
- if ($source->branch() eq $target->branch()) {
- return _cm_err(Fcm::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(
- Fcm::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 ($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(Fcm::Cm::Abort->NULL);
- }
- # If more than one merge available, prompt user to enter a revision number
- # to merge from, default to $revs [0]
- # --------------------------------------------------------------------------
- if ($non_interactive || @revs == 1) {
- $source->pegrev($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 = (Fcm::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(Fcm::Cm::Exception->MERGE_REV_INVALID, $reply)
- }
- $source->pegrev($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(Fcm::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 $s = Fcm::CmBranch->new (URL => $source->url_peg);
- my $b = Fcm::CmBranch->new (URL => $base->url_peg);
- $s->subdir ($subdir) if $subdir;
- $b->subdir ($subdir) if $subdir;
- # Diagnostic
- $CLI_MESSAGE->('MERGE_CF', $s->path_peg(), $b->path_peg());
- # Delta of the "svn merge" command
- @delta = ($b->url_peg, $s->url_peg);
- # Template message
- $mesg = 'Merged into ' . $target->path . ': ' . $source->path_peg .
- ' cf. ' . $base->path_peg . "\n";
- }
- # Run "svn merge" in "--dry-run" mode to see the result
- # ----------------------------------------------------------------------------
- my @out = &run_command (
- [qw/svn merge --dry-run/, @delta],
- METHOD => 'qx', PRINT => ($dry_run and $verbose),
- );
- # Abort merge if it will result in no change
- if (not @out) {
- return _cm_abort(Fcm::Cm::Abort->NULL);
- }
- # Report result of "svn merge --dry-run"
- if ($dry_run || !$non_interactive) {
- $CLI_MESSAGE->('MERGE_DRY');
- $CLI_MESSAGE->('SEPARATOR');
- $CLI_MESSAGE->(q{}, join(q{}, @out));
- $CLI_MESSAGE->('SEPARATOR');
- }
- return if $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 (!$non_interactive && $CLI_PROMPT->('merge', 'MERGE') ne 'y') {
- return _cm_abort();
- }
- $CLI_MESSAGE->('MERGE');
- run_command([qw/svn merge/, @delta], PRINT => $verbose);
- # Prepare the commit log
- # ----------------------------------------------------------------------------
- # Read in any existing message
- my $ci_mesg = Fcm::CmCommitMessage->new;
- $ci_mesg->read_file;
- $ci_mesg->auto_mesg ([$mesg, @{ $ci_mesg->auto_mesg }]);
- $ci_mesg->write_file;
- if ($verbose) {
- $CLI_MESSAGE->('SEPARATOR');
- $CLI_MESSAGE->('MERGE_CI', $mesg);
- }
- return;
- }
- # ------------------------------------------------------------------------------
- # SYNOPSIS
- # &Fcm::Cm::cm_mkpatch ();
- #
- # DESCRIPTION
- # This is a FCM command to create a patching script from particular revisions
- # of a URL.
- # ------------------------------------------------------------------------------
- sub cm_mkpatch {
- # Process command line options and arguments
- # ----------------------------------------------------------------------------
- my (@exclude, $organisation, $revision);
- my $rc = GetOptions(
- 'exclude=s' => \@exclude,
- 'organisation=s' => \$organisation,
- 'r|revision=s' => \$revision,
- );
- if (!$rc) {
- _cli_err();
- }
- # 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 = $organisation ? $organisation : 'original';
- # Make sure revision option is set correctly
- my @revs = $revision ? split (/:/, $revision) : ();
- @revs = @revs [0, 1] if @revs > 2;
- # Arguments
- my ($u, $outdir) = @ARGV;
- if (!$u) {
- _cli_err();
- }
- my $url = Fcm::CmUrl->new (URL => $u);
- if (!$url->is_url()) {
- return _cm_err(Fcm::Cm::Exception->INVALID_URL, $u);
- }
- if (!$url->url_exists()) {
- return _cm_err(Fcm::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') ne 'y') {
- return _cm_abort();
- }
- rmtree($outdir) || return _cm_err(Fcm::Cm::Exception->RMTREE, $outdir);
- }
- # (Re-)create output directory
- mkpath($outdir) || return _cm_err(Fcm::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 @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';
- push @paths, $path;
- }
- # If a patch is being used, make sure it isn't just property changes
- if ($use_patch) {
- my @changedpaths;
- for my $path (@paths) {
- (my $file = $path) =~ s#^$url_path/*##;
- if ($log{$rev}{paths}{$path}{action} eq 'M') {
- my ($diff) = &run_command (
- [qw/svn diff --no-diff-deleted --summarize -c/,
- $rev, $url->url . '/' . $file. '@' . $rev],
- METHOD => 'qx');
- next unless $diff =~ /^[A-Z]/;
- }
- push @changedpaths, $path;
- }
- @paths = @changedpaths;
- }
- next unless @paths;
- # Create the patch using "svn diff"
- my @patch = ();
- if ($use_patch) {
- @patch = &run_command ([qw/svn diff --no-diff-deleted -c/, $rev,
- $url->url], METHOD => 'qx');
- if (@patch) {
- # Don't use the patch if it may contain subversion keywords
- for (@patch) {
- $use_patch = 0 if /\$[a-zA-Z:]+ *\$/;
- }
- } 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(Fcm::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(?:/*|$)#;
- }
- if ($log{$rev}{paths}{$path}{action} eq 'D') {
- # Script to delete file
- push @after_script, 'svn delete ' . $file;
- } else {
- my $export_required = 0;
- my $recursive_add = 0;
- my $is_newfile = 0;
- # Skip property changes
- if ($log{$rev}{paths}{$path}{action} eq 'M') {
- my ($diff) = &run_command (
- [qw/svn diff --no-diff-deleted --summarize -c/,
- $rev, $url->url . '/' . $file. '@' . $rev],
- METHOD => 'qx');
- next CHANGED unless $diff =~ /^[A-Z]/;
- }
- # Determine if the file is a directory
- my $is_dir = 0;
- if ($log{$rev}{paths}{$path}{action} ne 'M') {
- my @info = &run_command ([qw/svn info/, $url_file], METHOD => 'qx');
- for (@info) {
- if (/^Node Kind: (\w+)/) {
- $is_dir = 1 if $1 eq 'directory';
- last;
- }
- }
- }
- # Decide how to treat added files
- if ($log{$rev}{paths}{$path}{action} eq 'A') {
- # Determine if the file is copied
- if (exists $log{$rev}{paths}{$path}{'copyfrom-path'}) {
- if ($is_dir) {
- # A copied directory needs to be treated as a new file, exported
- # and added recursively
- $is_newfile = 1;
- $export_required = 1;
- $recursive_add = 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 = Fcm::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 = Fcm::CmUrl->new (
- URL => $url->root . $attrib{'copyfrom-path'} . '@' .
- $attrib{'copyfrom-rev'},
- );
- $cp_cp_url->branch ($url->branch);
- 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 path
- if (not $is_newfile) {
- for my $exclude (@exclude) {
- if ($copyfrom_path =~ m#^$exclude(?:/*|$)#) {
- $is_newfile = 1;
- last;
- }
- }
- }
- # Script to copy 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
- $is_newfile = 1;
- # If it's a directory then create it (in case patch doesn't)
- push @before_script, 'mkdir ' . $file if $is_dir;
- }
- }
- if ($log{$rev}{paths}{$path}{action} eq 'R') {
- # Script to delete file
- push @before_script, 'svn delete ' . $file;
- # Now treat as new file
- $is_newfile = 1;
- }
- # Script to add the file, if required
- if ($is_newfile) {
- if ($recursive_add) {
- push @after_script, 'svn add ' . $file;
- } else {
- push @after_script, 'svn add --non-recursive ' . $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 @mime_type = &run_command
- ([qw/svn propget svn:mime-type/, $url_file], METHOD => 'qx');
- for (@mime_type) {
- $export_required = 1 if not /^text\//;
- }
- # 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);
- &run_command ([qw/svn 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
- my $shell = Fcm::Config->instance()->setting(qw/TOOL SHELL/);
- print FILE <<EOF;
- #!$shell
- # ------------------------------------------------------------------------------
- # 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
- EOF
- # Script content
- print FILE @before_script if @before_script;
- print FILE "patch -p0 <\${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';
- 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
- my $shell = Fcm::Config->instance()->setting(qw/TOOL SHELL/);
- print FILE <<EOF;
- #!$shell
- # ------------------------------------------------------------------------------
- # 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
- 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
- export LANG=en_GB
- # Commands to apply patches
- EOF
- # Script content
- print FILE @script;
- # Script footer
- print FILE <<EOF;
- # 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(Fcm::Cm::Abort->NULL);
- }
- return 1;
- }
- # ------------------------------------------------------------------------------
- # CLI: fcm add.
- sub _cli_command_add {
- my @args = map {($_ eq '--check' || $_ eq '-c' ? () : $_)} @_;
- my %option = (st_check_handler => $CLI_HANDLER_OF{'WC_STATUS_PATH'});
- return (
- @args == @_ ? _svn("add", @args) : cm_check_unknown(\%option, @args)
- );
- }
- # ------------------------------------------------------------------------------
- # CLI: fcm checkout.
- sub _cli_command_checkout {
- if (@ARGV) {
- my $target = is_url($ARGV[-1]) ? cwd() : $ARGV[-1];
- if (-d $target && is_wc($target)) {
- return _cm_err(Fcm::Cm::Exception->WC_EXIST, $target);
- }
- }
- return _svn('checkout', @ARGV);
- }
- # ------------------------------------------------------------------------------
- # CLI: fcm delete.
- sub _cli_command_delete {
- my @args = map {($_ eq '--check' || $_ eq '-c' ? () : $_)} @_;
- my %option = (st_check_handler => $CLI_HANDLER_OF{'WC_STATUS_PATH'});
- return (
- @args == @_ ? _svn("delete", @args) : cm_check_missing(\%option, @args)
- );
- }
- # ------------------------------------------------------------------------------
- # CLI: fcm switch.
- sub _cli_command_switch {
- local(@ARGV) = @_;
- if (grep {$_ eq '--relocate'} @ARGV) {
- return _svn('switch', @ARGV);
- }
- my %option;
- if (!GetOptions(\%option, 'non-interactive', 'revision|r=s', 'quiet|q')) {
- _cli_err();
- }
- if (!$option{'non-interactive'}) {
- $option{st_check_handler} = $CLI_HANDLER_OF{WC_STATUS};
- }
- if (!@ARGV) {
- _cli_err();
- }
- $CLI_MESSAGE->(q{}, join(q{}, cm_switch(\%option, @ARGV)));
- }
- # ------------------------------------------------------------------------------
- # CLI: fcm update.
- sub _cli_command_update {
- local(@ARGV) = @_;
- my %option;
- if (!GetOptions(\%option, 'non-interactive', 'revision|r=s', 'quiet|q')) {
- _cli_err();
- }
- if (!$option{'non-interactive'}) {
- $option{st_check_handler} = $CLI_HANDLER_OF{WC_STATUS};
- }
- $CLI_MESSAGE->(q{}, join(q{}, cm_update(\%option, @ARGV)));
- }
- # ------------------------------------------------------------------------------
- # CLI error.
- sub _cli_err {
- my ($key, @args) = @_;
- $key ||= 'CLI_USAGE';
- my $message = sprintf($CLI_MESSAGE_FOR_ERROR{$key}, @args);
- die(Fcm::CLI::Exception->new({message => $message}));
- }
- # ------------------------------------------------------------------------------
- # Handles abort exception.
- sub _cli_e_handler_of_cm_abort {
- my ($function, $e) = @_;
- if ($e->get_code() eq $e->FAIL) {
- die(sprintf($CLI_MESSAGE_FOR_ABORT{FAIL}, $function));
- }
- else {
- $CLI_MESSAGE->($e->get_code(), $function);
- }
- }
- # ------------------------------------------------------------------------------
- # Handles CM exception.
- sub _cli_e_handler_of_cm_exception {
- my ($function, $e) = @_;
- die(sprintf($CLI_MESSAGE_FOR_ERROR{$e->get_code()}, $e->get_targets()));
- }
- # ------------------------------------------------------------------------------
- # Handles CLI exception.
- sub _cli_e_handler_of_cli_exception {
- my ($function, $e) = @_;
- $CLI_MESSAGE->('CLI', $e);
- $CLI_MESSAGE->('CLI_HELP', $function);
- }
- # ------------------------------------------------------------------------------
- # The default handler of the "WC_STATUS" event.
- sub _cli_handler_of_wc_status {
- my ($name, $target_list_ref, $status_list_ref) = @_;
- if (@{$status_list_ref}) {
- $CLI_MESSAGE->('STATUS', join(q{}, @{$status_list_ref}));
- if ($CLI_PROMPT->($name, 'CONTINUE') 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) = @_;
- $CLI_MESSAGE->(q{}, join(q{}, @{$status_list_ref}));
- 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;
- }
- # ------------------------------------------------------------------------------
- # Prints help for a given $subcommand.
- sub _cli_help {
- my ($key, $exit_val) = @_;
- my $pod
- = File::Spec->catfile(dirname($INC{'Fcm/Cm.pm'}), 'CLI', "fcm-$key.pod");
- my $has_pod = -f $pod;
- if ($has_pod) {
- pod2usage({
- '-exitval' => defined($exit_val) ? $exit_val : 2,
- '-input' => $pod,
- '-verbose' => 1,
- });
- }
- if (!$has_pod || exists($CLI_MORE_HELP_FOR{$key})) {
- local(@ARGV) = ($key);
- return _svn('help', $key);
- }
- }
- # ------------------------------------------------------------------------------
- # 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 = Fcm::Util::tidy_url(Fcm::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 = (Fcm::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 Fcm::Interactive::get_input.
- sub _cli_prompt {
- my %option
- = (type => 'yn', default => 'n', (ref($_[0]) ? %{shift(@_)} : ()));
- my ($name, $key, @args) = @_;
- return Fcm::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) {
- run_command([qw{svn 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) {
- run_command([qw{svn add}, @paths]);
- }
- }
- # ------------------------------------------------------------------------------
- # FCM wrapper to SVN switch.
- sub cm_switch {
- my %option = %{shift()};
- my ($target, $path) = @_;
- $path ||= cwd();
- if (!-e $path) {
- return _cm_err(Fcm::Cm::Exception->NOT_EXIST, $path);
- }
- if (!is_wc($path)) {
- return _cm_err(Fcm::Cm::Exception->INVALID_WC, $path);
- }
- # Check for merge template in the commit log file in the working copy
- my $path_of_wc = get_wct($path);
- my $ci_mesg = Fcm::CmCommitMessage->new();
- $ci_mesg->dir($path_of_wc);
- $ci_mesg->read_file();
- if (@{$ci_mesg->auto_mesg()}) {
- return _cm_err(
- Fcm::Cm::Exception->SWITCH_UNSAFE,
- $path eq $path_of_wc ? $ci_mesg->base() : $ci_mesg->file(),
- );
- }
- # Check for any local modifications
- if (defined($option{st_check_handler})) {
- my $handler = $CLI_HANDLER_OF{WC_STATUS};
- _svn_status_checker('switch', 'MODIFIED', $handler)->([$path_of_wc]);
- }
- # Invokes "svn switch"
- _svn(
- {METHOD => 'qx', PRINT => !$option{quiet}},
- 'switch',
- ($option{'non-interactive'} ? '--non-interactive' : ()),
- ($option{revision} ? ('-r', $option{revision}) : ()),
- ($option{quiet} ? '--quiet' : ()),
- _cm_get_source(
- $target,
- Fcm::CmBranch->new(URL => get_url_of_wc($path_of_wc)),
- )->url_peg(),
- ($path_of_wc eq cwd() ? () : $path_of_wc),
- );
- }
- # ------------------------------------------------------------------------------
- # 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(Fcm::Cm::Exception->NOT_EXIST, $target);
- }
- if (!is_wc($target)) {
- return _cm_err(Fcm::Cm::Exception->INVALID_WC, $target);
- }
- $target = get_wct($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', 'OUT_OF_DATE'], 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} = (
- Fcm::Keyword::expand(get_url_of_wc($targets[0]), $option{revision})
- )[1];
- }
- return _svn_update(\@targets, \%option);
- }
- # ------------------------------------------------------------------------------
- # Raises an abort exception.
- sub _cm_abort {
- my ($code) = @_;
- $code ||= Fcm::Cm::Abort->USER;
- die(bless({code => $code, message => 'abort'}, 'Fcm::Cm::Abort'));
- }
- # ------------------------------------------------------------------------------
- # Raises a failure.
- sub _cm_err {
- my ($code, @targets) = @_;
- die(bless(
- {code => $code, message => "ERROR: $code", targets => \@targets},
- 'Fcm::Cm::Exception',
- ));
- }
- # ------------------------------------------------------------------------------
- # Returns the corresponding Fcm::CmBranch instance for $src_url w.r.t. $target.
- sub _cm_get_source {
- my ($src_url, $target) = @_;
- my $source = Fcm::CmBranch->new(URL => $src_url);
- if (!$source->is_url()) {
- # Not a full URL, construct full URL based on current URL
- $source->url_peg($target->url_peg());
- my $project = $target->project();
- my ($path) = $src_url =~ qr{\A/*(.*)\z}xms;
- if (index($path, $project) == 0) {
- # Argument contains the full path under the repository root
- $path = substr($path, length($project));
- }
- if ($path =~ $PATTERN_OF{FCM_BRANCH_PATH}) {
- # Argument contains the full branch name
- $path = join(q{/}, $target->project_path(), $path);
- }
- else {
- # Argument contains the shorter branch name
- $path = join(q{/}, $target->project_path(), 'branches', $path);
- }
- $source->path_peg($path);
- }
- # Replace source sub-directory with the target sub-directory
- $source->subdir($target->subdir());
- # Ensure that the branch name exists
- if (!$source->url_exists()) {
- return _cm_err(Fcm::Cm::Exception->INVALID_URL, $src_url);
- }
- # Ensure that the branch name is valid
- if (!$source->branch()) {
- return _cm_err(Fcm::Cm::Exception->INVALID_BRANCH, $src_url);
- }
- # Ensure that the source and target URLs are in the same project
- if ($source->project_url() ne $target->project_url()) {
- return _cm_err(
- Fcm::Cm::Exception->DIFF_PROJECTS,
- $target->url_peg(),
- $source->url_peg(),
- );
- }
- return $source;
- }
- # ------------------------------------------------------------------------------
- # Runs "svn".
- sub _svn {
- my @args = @_;
- my %option;
- if (@args && ref($args[0])) {
- %option = %{shift(@args)};
- }
- return run_command(
- ['svn', @args],
- PRINT => ($args[0] ne 'cat' && !grep {$_ eq '--xml'} @args),
- %option,
- );
- }
- # ------------------------------------------------------------------------------
- # Returns the results of "svn status".
- sub _svn_status_get {
- my ($target_list_ref, $show_updates) = @_;
- my @targets = (defined($target_list_ref) ? @{$target_list_ref} : ());
- for my $target (@targets) {
- if ($target eq cwd()) {
- $target = q{.};
- }
- }
- my @options = ($show_updates ? qw{--show-updates} : ());
- return _svn({METHOD => 'qx', PRINT => 0}, '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};
- }
- return sub {
- my ($target_list_ref) = @_;
- my @status = _svn_status_get($target_list_ref, $show_updates);
- if ($show_updates) {
- @status = map {$_ =~ $PATTERN_OF{ST_AGAINST_REV} ? () : $_} @status;
- }
- my @status_of_interest = grep {$matcher->($_)} @status;
- if (defined($handler)) {
- return $handler->($name, $target_list_ref, \@status_of_interest);
- }
- return @status_of_interest;
- }
- }
- # ------------------------------------------------------------------------------
- # Runs "svn update".
- sub _svn_update {
- my ($target_list_ref, $option_hash_ref) = @_;
- my %option = (defined($option_hash_ref) ? %{$option_hash_ref} : ());
- _svn(
- {METHOD => 'qx', PRINT => !$option{quiet}},
- 'update',
- ($option{'non-interactive'} ? '--non-interactive' : ()),
- ($option{revision} ? ('-r', $option{revision}) : ()),
- ($option{quiet} ? '--quiet' : ()),
- (defined($target_list_ref) ? @{$target_list_ref} : ()),
- );
- }
- # ------------------------------------------------------------------------------
- # Abort exception.
- package Fcm::Cm::Abort;
- use base qw{Fcm::Exception};
- use constant {FAIL => 'FAIL', NULL => 'NULL', USER => 'USER'};
- sub get_code {
- return $_[0]->{code};
- }
- # ------------------------------------------------------------------------------
- # Resource exception.
- package Fcm::Cm::Exception;
- our @ISA = qw{Fcm::Cm::Abort};
- use constant {
- CHDIR => 'CHDIR',
- 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_EXIST => 'WC_EXIST',
- 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
- Fcm::Cm
- =head1 SYNOPSIS
- use Fcm::Cm qw{cli};
- # Use as a wrapper to Subversion, and other FCM code management commands
- cli('info', '--revision', 'HEAD', $url);
- use Fcm::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, $target_list_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, $target_list_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, $target_list_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 cli($function,@args)
- Implements the FCM code management CLI. If --help or -h is specified in @args,
- it displays help and returns. Otherwise, it attempts to expand any FCM location
- and revision keywords in @args. Calls the relevant FCM code management function
- according to $function, or a SVN command if $function is not modified by FCM.
- =item cm_check_missing(\%option,@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, $target_list_ref, $status_list_ref) where
- $name is "delete", $target_list_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,@targets)
- Similar to cm_check_missing(\%option,@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,@targets)
- Invokes "svn update" at the root of each working copy specified by @targets. If
- @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, $target_list_ref, $status_list_ref), where $name is
- 'update', $target_list_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 Fcm::Cm::Abort
- This exception @ISA L<Fcm::Exception|Fcm::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 Fcm::Cm::Exception
- This exception @ISA L<Fcm::Abort|Fcm::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_EXIST
- The target working copy 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<Fcm::CmUrl|Fcm::CmUrl> and L<Fcm::CmBranch|Fcm::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
|