Cm.pm 85 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721
  1. # ------------------------------------------------------------------------------
  2. # NAME
  3. # Fcm::Cm
  4. #
  5. # DESCRIPTION
  6. # This module contains the FCM code management functionalities and wrappers
  7. # to Subversion commands.
  8. #
  9. # COPYRIGHT
  10. # (C) Crown copyright Met Office. All rights reserved.
  11. # For further details please refer to the file COPYRIGHT.txt
  12. # which you should have received as part of this distribution.
  13. # ------------------------------------------------------------------------------
  14. use strict;
  15. use warnings;
  16. package Fcm::Cm;
  17. use base qw{Exporter};
  18. our @EXPORT_OK = qw(cli cm_check_missing cm_check_unknown cm_switch cm_update);
  19. use Cwd qw{cwd};
  20. use Getopt::Long qw{GetOptions :config bundling};
  21. use Fcm::CLI::Exception;
  22. use Fcm::Config;
  23. use Fcm::CmBranch;
  24. use Fcm::CmUrl;
  25. use Fcm::Keyword;
  26. use Fcm::Util qw{
  27. get_url_of_wc
  28. get_url_peg_of_wc
  29. get_wct
  30. is_url
  31. is_wc
  32. run_command
  33. tidy_url
  34. };
  35. use File::Basename qw{basename dirname};
  36. use File::Path qw{mkpath rmtree};
  37. use File::Spec;
  38. use File::Temp qw{tempfile};
  39. use Pod::Usage qw{pod2usage};
  40. # ------------------------------------------------------------------------------
  41. # CLI message handler
  42. our $CLI_MESSAGE = \&_cli_message;
  43. # List of CLI messages
  44. our %CLI_MESSAGE_FOR = (
  45. q{} => "%s",
  46. BRANCH_LIST => "%s at %s: %d branch(es) found for %s.\n",
  47. CHDIR_WCT => "%s: working directory changed to top of working copy.\n",
  48. CF => "Conflicts in: %s\n",
  49. MERGE => "Performing merge ...\n",
  50. MERGE_CF => "About to merge in changes from %s compared with %s\n",
  51. MERGE_CI => "The following is added to the commit message file:\n%s",
  52. MERGE_DRY => "This merge will result in the following change:\n",
  53. MERGE_REVS => "Merge(s) available from %s: %s\n",
  54. OUT_DIR => "Output directory: %s\n",
  55. PATCH_DONE => "%s: patch generated.\n",
  56. PATCH_REV => "Patch created for changeset %s\n",
  57. SEPARATOR => q{-} x 80 . "\n",
  58. STATUS => "Status of the target working copy(ies):\n%s",
  59. );
  60. # CLI abort and error messages
  61. our %CLI_MESSAGE_FOR_ABORT = (
  62. FAIL => "%s: command failed.\n",
  63. NULL => "%s: command will result in no change.\n",
  64. USER => "%s: abort by user.\n",
  65. );
  66. # CLI abort and error messages
  67. our %CLI_MESSAGE_FOR_ERROR = (
  68. CHDIR => "%s: cannot change to directory.\n",
  69. CLI => "%s",
  70. CLI_HELP => "Type 'fcm help %s' for usage.\n",
  71. CLI_MERGE_ARG1 => "Arg 1 must be the source in auto/custom mode.\n",
  72. CLI_MERGE_ARG2 => "Arg 2 must be the source in custom mode"
  73. . " if --revision not set.\n",
  74. CLI_OPT_ARG => "--%s: invalid argument [%s].\n",
  75. CLI_OPT_WITH_OPT => "--%s: must be specified with --%s.\n",
  76. CLI_USAGE => "incorrect usage",
  77. DIFF_PROJECTS => "%s (target) and %s (source) are not related.\n",
  78. INVALID_BRANCH => "%s: not a valid URL of a standard FCM branch.\n",
  79. INVALID_PROJECT => "%s: not a valid URL of a standard FCM project.\n",
  80. INVALID_TARGET => "%s: not a valid working copy or URL.\n",
  81. INVALID_URL => "%s: not a valid URL.\n",
  82. INVALID_WC => "%s: not a valid working copy.\n",
  83. MERGE_REV_INVALID => "%s: not a revision in the available merge list.\n",
  84. MERGE_SELF => "%s: cannot be merged to its own working copy: %s.\n",
  85. MERGE_UNRELATED => "%s: target and %s: source not directly related.\n",
  86. MERGE_UNSAFE => "%s: source contains changes outside the target"
  87. . " sub-directory. Please merge with a full tree.\n",
  88. MKPATH => "%s: cannot create directory.\n",
  89. NOT_EXIST => "%s: does not exist.\n",
  90. PARENT_NOT_EXIST => "%s: parent %s no longer exists.\n",
  91. RMTREE => "%s: cannot remove.\n",
  92. ST_CONFLICT => "File(s) in conflicts:\n%s",
  93. ST_MISSING => "File(s) missing:\n%s",
  94. ST_OUT_OF_DATE => "File(s) out of date:\n%s",
  95. SWITCH_UNSAFE => "%s: merge template exists."
  96. . " Please remove before retrying.\n",
  97. WC_EXIST => "%s: working copy already exists.\n",
  98. WC_INVALID_BRANCH => "%s: not a working copy of a standard FCM branch.\n",
  99. WC_URL_NOT_EXIST => "%s: working copy URL does not exists at HEAD.\n",
  100. );
  101. # List of CLI prompt messages
  102. our %CLI_MESSAGE_FOR_PROMPT = (
  103. CF_OVERWRITE => qq{%s: existing changes will be overwritten.\n}
  104. . qq{ Do you wish to continue?},
  105. CI => qq{Would you like to commit this change?},
  106. CI_BRANCH_SHARED => qq{\n}
  107. . qq{*** WARNING: YOU ARE COMMITTING TO A %s BRANCH.\n}
  108. . qq{*** Please ensure that you have the}
  109. . qq{ owner's permission.\n\n}
  110. . qq{Would you like to commit this change?},
  111. CI_BRANCH_USER => qq{\n}
  112. . qq{*** WARNING: YOU ARE COMMITTING TO A BRANCH}
  113. . qq{ NOT OWNED BY YOU.\n}
  114. . qq{*** Please ensure that you have the}
  115. . qq{ owner's permission.\n\n}
  116. . qq{Would you like to commit this change?},
  117. CI_TRUNK => qq{\n}
  118. . qq{*** WARNING: YOU ARE COMMITTING TO THE TRUNK.\n}
  119. . qq{*** Please ensure that your change conforms to}
  120. . qq{ your project's working practices.\n\n}
  121. . qq{Would you like to commit this change?},
  122. CONTINUE => qq{Are you sure you want to continue?},
  123. MERGE => qq{Would you like to go ahead with the merge?},
  124. MERGE_REV => qq{Please enter the revision you wish to merge from},
  125. MKPATCH_OVERWRITE => qq{%s: output location exists. OK to overwrite?},
  126. RUN_SVN_COMMAND => qq{Would you like to run "svn %s"?},
  127. );
  128. # List of CLI warning messages
  129. our %CLI_MESSAGE_FOR_WARNING = (
  130. BRANCH_SUBDIR => "%s: is a sub-directory of a branch in a FCM project.\n",
  131. CF_BINARY => "%s: ignoring binary file, please resolve manually.\n",
  132. INVALID_BRANCH => $CLI_MESSAGE_FOR_ERROR{INVALID_BRANCH},
  133. ST_IN_TRAC_DIFF => "%s: local changes cannot be displayed in Trac.\n"
  134. );
  135. # CLI prompt handler and title prefix
  136. our $CLI_PROMPT = \&_cli_prompt;
  137. our $CLI_PROMPT_PREFIX = q{fcm };
  138. # List of exception handlers [$class, CODE->($function, $e)]
  139. our @CLI_EXCEPTION_HANDLERS = (
  140. ['Fcm::CLI::Exception', \&_cli_e_handler_of_cli_exception],
  141. ['Fcm::Cm::Exception' , \&_cli_e_handler_of_cm_exception],
  142. ['Fcm::Cm::Abort' , \&_cli_e_handler_of_cm_abort],
  143. );
  144. # Event handlers
  145. our %CLI_HANDLER_OF = (
  146. 'WC_STATUS' => \&_cli_handler_of_wc_status,
  147. 'WC_STATUS_PATH' => \&_cli_handler_of_wc_status_path,
  148. );
  149. # Handlers of sub-commands
  150. our %CLI_IMPL_OF = (
  151. 'add' => \&_cli_command_add,
  152. 'branch' => \&cm_branch,
  153. 'commit' => \&cm_commit,
  154. 'conflicts' => \&cm_conflicts,
  155. 'checkout' => \&_cli_command_checkout,
  156. 'delete' => \&_cli_command_delete,
  157. 'diff' => \&cm_diff,
  158. 'merge' => \&cm_merge,
  159. 'mkpatch' => \&cm_mkpatch,
  160. 'switch' => \&_cli_command_switch,
  161. 'update' => \&_cli_command_update,
  162. );
  163. # List of overridden subcommands that need to display "svn help"
  164. our %CLI_MORE_HELP_FOR = map {($_, 1)} qw{add diff delete switch update};
  165. # The preferred name of subcommand aliases
  166. our %CLI_PREFERRED_NAME_OF = (
  167. 'ann' => 'blame',
  168. 'annotate' => 'blame',
  169. 'br' => 'branch',
  170. 'ci' => 'commit',
  171. 'cf' => 'conflicts',
  172. 'co' => 'checkout',
  173. 'cp' => 'copy',
  174. 'del' => 'delete',
  175. 'di' => 'diff',
  176. 'ls' => 'list',
  177. 'mv' => 'move',
  178. 'pd' => 'propdel',
  179. 'pdel' => 'propdel',
  180. 'pe' => 'propedit',
  181. 'pedit' => 'propedit',
  182. 'pg' => 'propget',
  183. 'pget' => 'propget',
  184. 'pl' => 'proplist',
  185. 'plist' => 'proplist',
  186. 'praise' => 'blame',
  187. 'ps' => 'propset',
  188. 'pset' => 'propset',
  189. 'remove' => 'delete',
  190. 'ren' => 'move',
  191. 'rename' => 'move',
  192. 'rm' => 'delete',
  193. 'sw' => 'switch',
  194. 'up' => 'update',
  195. );
  196. # List of subcommands that accept URL inputs
  197. our %CLI_SUBCOMMAND_URL = map {($_, 1)} qw{
  198. blame
  199. branch
  200. cat
  201. checkout
  202. copy
  203. delete
  204. diff
  205. export
  206. import
  207. info
  208. list
  209. lock
  210. log
  211. merge
  212. mkdir
  213. mkpatch
  214. move
  215. propdel
  216. propedit
  217. propget
  218. proplist
  219. propset
  220. switch
  221. unlock
  222. };
  223. # List of subcommands that accept revision inputs
  224. our %CLI_SUBCOMMAND_REV = map {($_, 1)} qw{
  225. blame
  226. branch
  227. cat
  228. checkout
  229. copy
  230. diff
  231. export
  232. info
  233. list
  234. log
  235. merge
  236. mkpatch
  237. move
  238. propdel
  239. propedit
  240. propget
  241. proplist
  242. propset
  243. switch
  244. };
  245. # Common patterns
  246. our %PATTERN_OF = (
  247. # A CLI option
  248. CLI_OPT => qr{
  249. \A (?# beginning)
  250. (--\w[\w-]*=) (?# capture 1, a long option label)
  251. (.*) (?# capture 2, the value of the option)
  252. \z (?# end)
  253. }xms,
  254. # A CLI revision option
  255. CLI_OPT_REV => qr{
  256. \A (?# beginning)
  257. (--revision(?:=|\z)|-r) (?# capture 1, --revision, --revision= or -r)
  258. (.*) (?# capture 2, trailing value)
  259. \z (?# end)
  260. }xms,
  261. # A CLI revision option range
  262. CLI_OPT_REV_RANGE => qr{
  263. \A (?# beginning)
  264. ( (?# capture 1, begin)
  265. (?:\{[^\}]+\}+) (?# a date in curly braces)
  266. | (?# or)
  267. [^:]+ (?# anything but a colon)
  268. ) (?# capture 1, end)
  269. (?::(.*))? (?# colon, and capture 2 til the end)
  270. \z (?# end)
  271. }xms,
  272. # A FCM branch path look-alike, should be configurable in the future
  273. FCM_BRANCH_PATH => qr{
  274. \A (?# beginning)
  275. /* (?# some slashes)
  276. (?: (?# group 1, begin)
  277. (?:trunk/*(?:@\d+)?\z) (?# trunk at a revision)
  278. | (?# or)
  279. (?:trunk|branches|tags)/+ (?# trunk, branch or tags)
  280. ) (?# group 1, end)
  281. }xms,
  282. # Last line of output from "svn status -u"
  283. ST_AGAINST_REV => qr{
  284. \A (?# beginning)
  285. Status\sagainst\srevision:.* (?# output of svn status -u)
  286. \z (?# end)
  287. }xms,
  288. # Extract path from "svn status"
  289. ST_PATH => qr{
  290. \A (?# beginning)
  291. .{6} (?# 6 columns)
  292. \s+ (?# spaces)
  293. (.+) (?# capture 1, target path)
  294. \z (?# end)
  295. }xms,
  296. # A legitimate "svn" revision
  297. SVN_REV => qr{
  298. \A (?# beginning)
  299. (?:\d+|HEAD|BASE|COMMITTED|PREV|\{.+\}) (?# digit, reserved words, date)
  300. \z (?# end)
  301. }ixms,
  302. );
  303. # Status matchers
  304. our %ST_MATCHER_FOR = (
  305. MISSING => sub {substr($_[0], 0, 1) eq '!'},
  306. MODIFIED => sub {substr($_[0], 0, 6) =~ qr{\S}xms},
  307. OUT_OF_DATE => sub {substr($_[0], 7, 1) eq '*'},
  308. UNKNOWN => sub {substr($_[0], 0, 1) eq '?'},
  309. );
  310. # ------------------------------------------------------------------------------
  311. # Entry function for the FCM code management CLI. Calls the relevant FCM code
  312. # management function or SVN command based on $function.
  313. sub cli {
  314. my ($function, @args) = @_;
  315. if (exists($CLI_PREFERRED_NAME_OF{$function})) {
  316. $function = $CLI_PREFERRED_NAME_OF{$function};
  317. }
  318. if (grep {$_ eq '-h' || $_ eq '--help'} @args) {
  319. return _cli_help($function, 'NOEXIT');
  320. }
  321. if (exists($CLI_SUBCOMMAND_URL{$function})) {
  322. _cli_keyword_expand_url(\@args);
  323. }
  324. if (exists($CLI_SUBCOMMAND_REV{$function})) {
  325. _cli_keyword_expand_rev(\@args);
  326. }
  327. if (exists($CLI_IMPL_OF{$function})) {
  328. eval {
  329. local(@ARGV) = @args;
  330. return $CLI_IMPL_OF{$function}->(@args);
  331. };
  332. if ($@) {
  333. my $e = $@;
  334. for (@CLI_EXCEPTION_HANDLERS) {
  335. my ($class, $handler) = @{$_};
  336. if ($class->caught($e)) {
  337. return $handler->($function, $e);
  338. }
  339. }
  340. die($e);
  341. }
  342. }
  343. else {
  344. return _svn($function, @args);
  345. }
  346. }
  347. # ------------------------------------------------------------------------------
  348. # SYNOPSIS
  349. # &Fcm::Cm::cm_branch ();
  350. #
  351. # DESCRIPTION
  352. # This is a FCM command to check information, create or delete a branch in
  353. # a Subversion repository.
  354. # ------------------------------------------------------------------------------
  355. sub cm_branch {
  356. # Process command line options
  357. # ----------------------------------------------------------------------------
  358. my (
  359. $info,
  360. $delete,
  361. $create,
  362. $list,
  363. $branch_of_branch,
  364. $name,
  365. $non_interactive,
  366. $password,
  367. $rev,
  368. $rev_flag,
  369. $show_all,
  370. $show_children,
  371. $show_other,
  372. $show_siblings,
  373. $svn_non_interactive,
  374. @tickets,
  375. $type,
  376. @userlist,
  377. $verbose,
  378. );
  379. my $rc = GetOptions(
  380. 'info|i' => \$info,
  381. 'delete|d' => \$delete,
  382. 'create|c' => \$create,
  383. 'list|l' => \$list,
  384. 'branch-of-branch' => \$branch_of_branch,
  385. 'name|n=s' => \$name,
  386. 'non-interactive' => \$non_interactive,
  387. 'password=s' => \$password,
  388. 'revision|r=s' => \$rev,
  389. 'rev-flag=s' => \$rev_flag,
  390. 'show-all|a' => \$show_all,
  391. 'show-children' => \$show_children,
  392. 'show-other' => \$show_other,
  393. 'show-siblings' => \$show_siblings,
  394. 'svn-non-interactive' => \$svn_non_interactive,
  395. 'ticket|k=s' => \@tickets,
  396. 'type|t=s' => \$type,
  397. 'user|u=s' => \@userlist,
  398. 'verbose|v' => \$verbose,
  399. );
  400. if (!$rc) {
  401. _cli_err();
  402. }
  403. my $num_options = 0;
  404. $num_options++ if defined $info;
  405. $num_options++ if defined $delete;
  406. $num_options++ if defined $create;
  407. $num_options++ if defined $list;
  408. if ($num_options > 1) {
  409. _cli_err();
  410. }
  411. # Get URL of repository or branch
  412. # ----------------------------------------------------------------------------
  413. my $url;
  414. if ($ARGV[0]) {
  415. $url = Fcm::CmUrl->new (URL => $ARGV[0]);
  416. if (not $url->is_url) {
  417. # An argument is specified and is not a URL
  418. # Assume that it is a path with a working copy
  419. if (&is_wc ($ARGV[0])) {
  420. $url = Fcm::CmUrl->new (URL => &get_url_of_wc ($ARGV[0]));
  421. } else {
  422. return _cm_err(Fcm::Cm::Exception->INVALID_WC, $ARGV[0]);
  423. }
  424. }
  425. } else {
  426. # An argument is not specified
  427. # Assume that the current directory is a working copy
  428. if (&is_wc ()) {
  429. $url = Fcm::CmUrl->new (URL => &get_url_of_wc ());
  430. } else {
  431. return _cm_err(Fcm::Cm::Exception->INVALID_TARGET, '.');
  432. }
  433. }
  434. # Ensure $url->url_peg is a URL of a standard FCM project
  435. if (!$url->project_url()) {
  436. return _cm_err(Fcm::Cm::Exception->INVALID_PROJECT, $url->url_peg());
  437. }
  438. if ($create) {
  439. # The --create option is specified, create a branch
  440. # --------------------------------------------------------------------------
  441. # Check branch type flags
  442. if ($type) {
  443. $type = uc ($type);
  444. if ($type =~ /^(USER|SHARE)$/) {
  445. $type = 'DEV' . $Fcm::Config::DELIMITER . $1;
  446. } elsif ($type =~ /^(CONFIG|REL)$/) {
  447. $type = 'PKG' . $Fcm::Config::DELIMITER . $1;
  448. } elsif ($type =~ /^(DEV|TEST|PKG)$/) {
  449. $type = $1 . $Fcm::Config::DELIMITER . 'USER';
  450. } elsif ($type !~ /^(?:DEV|TEST|PKG)$Fcm::Config::DELIMITER(?:USER|SHARE)$/
  451. and $type !~ /^PKG$Fcm::Config::DELIMITER(?:CONFIG|REL)/) {
  452. _cli_err('CLI_OPT_ARG', 'type', $type);
  453. }
  454. } else {
  455. $type = 'DEV' . $Fcm::Config::DELIMITER . 'USER';
  456. }
  457. # Check branch name
  458. if (!$name) {
  459. _cli_err('CLI_OPT_WITH_OPT', 'name', 'create');
  460. }
  461. if ($name !~ qr{\A[\w.-]+\z}xms) {
  462. _cli_err('CLI_OPT_ARG', 'name', $name);
  463. }
  464. # Check revision flag is valid
  465. if ($rev_flag) {
  466. $rev_flag = uc ($rev_flag);
  467. if ($rev_flag !~ qr{\A (?:NORMAL|NUMBER|NONE) \z}xms) {
  468. _cli_err('CLI_OPT_ARG', 'rev-flag', $rev_flag);
  469. }
  470. } else {
  471. $rev_flag = 'NORMAL';
  472. }
  473. # Handle multiple tickets
  474. @tickets = split (
  475. /$Fcm::Config::DELIMITER_LIST/,
  476. join ($Fcm::Config::DELIMITER_LIST, @tickets)
  477. );
  478. s/^#// for (@tickets);
  479. @tickets = sort {$a <=> $b} @tickets;
  480. # Determine whether to create a branch of a branch
  481. $url->branch ('trunk') unless $branch_of_branch;
  482. # Create the branch
  483. my $branch = Fcm::CmBranch->new;
  484. $branch->create (
  485. SRC => $url,
  486. TYPE => $type,
  487. NAME => $name,
  488. PASSWORD => $password,
  489. REV_FLAG => $rev_flag,
  490. TICKET => \@tickets,
  491. REV => $rev,
  492. NON_INTERACTIVE => $non_interactive,
  493. SVN_NON_INTERACTIVE => $svn_non_interactive,
  494. );
  495. } elsif ($list) {
  496. # The option --list is specified
  497. # List branches owned by current or specified users
  498. # --------------------------------------------------------------------------
  499. # Get URL of the project "branches/" sub-directory
  500. $url->subdir ('');
  501. $url->branch ('');
  502. my @branches = $url->branch_list($rev);
  503. if (!$show_all) {
  504. @userlist = split(qr{:}xms, join(q{:}, @userlist));
  505. if (!@userlist) {
  506. @userlist = (Fcm::Config->instance()->user_id());
  507. }
  508. my %filter = map {($_, 1)} @userlist;
  509. @branches = grep {
  510. $filter{Fcm::CmBranch->new(URL => $_)->branch_owner()}
  511. } @branches
  512. }
  513. # Output, number of branches found
  514. $CLI_MESSAGE->(
  515. 'BRANCH_LIST',
  516. $url->project_url_peg(),
  517. $rev ? "r$rev" : 'HEAD',
  518. scalar(@branches),
  519. ($show_all ? '[--show-all]' : join(q{, }, sort(@userlist))),
  520. );
  521. if (@branches) {
  522. # Output the URL of each branch
  523. if (not $verbose) {
  524. my $project = $url->project_url;
  525. @branches = map {Fcm::Keyword::unexpand($_)} @branches;
  526. }
  527. @branches = map {$_ . "\n"} sort @branches;
  528. $CLI_MESSAGE->(q{}, join(q{}, @branches));
  529. } else {
  530. # No branch found, exit with an error code
  531. return;
  532. }
  533. } else {
  534. # The option --info or --delete is specified
  535. # Report branch information (and/or delete a branch)
  536. # --------------------------------------------------------------------------
  537. # Set verbose level
  538. Fcm::Config->instance()->verbose ($verbose ? 1 : 0);
  539. # Set up the branch, report any error
  540. my $branch = Fcm::CmBranch->new (URL => $url->url_peg);
  541. if (!$branch->branch()) {
  542. return _cm_err(Fcm::Cm::Exception->INVALID_BRANCH, $branch->url_peg());
  543. }
  544. if (!$branch->url_exists()) {
  545. return _cm_err(Fcm::Cm::Exception->NOT_EXIST, $branch->url_peg());
  546. }
  547. # Remove the sub-directory part of the URL
  548. $branch->subdir ('');
  549. # Report branch info
  550. $branch->display_info (
  551. SHOW_CHILDREN => ($show_all || $show_children),
  552. SHOW_OTHER => ($show_all || $show_other ),
  553. SHOW_SIBLINGS => ($show_all || $show_siblings),
  554. );
  555. # Delete branch if --delete is specified
  556. $branch->del (
  557. PASSWORD => $password,
  558. NON_INTERACTIVE => $non_interactive,
  559. SVN_NON_INTERACTIVE => $svn_non_interactive,
  560. ) if $delete;
  561. }
  562. }
  563. # ------------------------------------------------------------------------------
  564. # SYNOPSIS
  565. # &Fcm::Cm::cm_commit ();
  566. #
  567. # DESCRIPTION
  568. # This is a FCM wrapper to the "svn commit" command.
  569. # ------------------------------------------------------------------------------
  570. sub cm_commit {
  571. my ($dry_run, $svn_non_interactive, $password);
  572. my $rc = GetOptions(
  573. 'dry-run' => \$dry_run,
  574. 'svn-non-interactive' => \$svn_non_interactive,
  575. 'password=s' => \$password,
  576. );
  577. if (!$rc) {
  578. _cli_err();
  579. }
  580. # The remaining argument is the path to a working copy
  581. my ($path) = @ARGV;
  582. if ($path) {
  583. if (!-e $path) {
  584. return _cm_err(Fcm::Cm::Exception->NOT_EXIST, $path);
  585. }
  586. } else {
  587. # No argument specified, use current working directory
  588. $path = cwd ();
  589. }
  590. # Make sure we are in a working copy
  591. if (!is_wc($path)) {
  592. return _cm_err(Fcm::Cm::Exception->INVALID_WC, $path);
  593. }
  594. # Make sure we are at the top level of the working copy
  595. # (otherwise we might miss any template commit message)
  596. my $dir = &get_wct ($path);
  597. if ($dir ne cwd ()) {
  598. chdir($dir) || return _cm_err(Fcm::Cm::Exception->CHDIR, $dir);
  599. $CLI_MESSAGE->('CHDIR_WCT', $dir);
  600. }
  601. # Get update status of working copy
  602. # Check working copy files are not in conflict, missing, or out of date
  603. my @status = _svn_status_get([], 1);
  604. unless (defined $dry_run) {
  605. my (@conflict, @missing, @outdate);
  606. for (@status) {
  607. if (/^C/) {
  608. push @conflict, $_;
  609. next;
  610. }
  611. if (/^!/) {
  612. push @missing, $_;
  613. next;
  614. }
  615. if (/^.{7}\*/) {
  616. push @outdate, $_;
  617. next;
  618. }
  619. # Check that all files which have been added have the svn:executable
  620. # property set correctly (in case the developer adds a script before they
  621. # remember to set the execute bit)
  622. next unless /^A.{7} *\d+ +(.*)/;
  623. my $file = $1;
  624. next unless -f $file;
  625. my ($command, @arguments)
  626. = (-x $file && !-l $file) ? ('propset', '*') : ('propdel');
  627. run_command(['svn', $command, qw{-q svn:executable}, @arguments, $file]);
  628. }
  629. # Abort commit if files are in conflict, missing, or out of date
  630. if (@conflict or @missing or @outdate) {
  631. for (
  632. ['ST_CONFLICT' , \@conflict],
  633. ['ST_MISSING' , \@missing ],
  634. ['ST_OUT_OF_DATE', \@outdate ],
  635. ) {
  636. my ($key, $array_ref) = @{$_};
  637. if (@{$array_ref}) {
  638. $CLI_MESSAGE->($key, join(q{}, @{$array_ref}));
  639. }
  640. }
  641. return _cm_abort(Fcm::Cm::Abort->FAIL);
  642. }
  643. }
  644. # Read in any existing message
  645. my $ci_mesg = Fcm::CmCommitMessage->new;
  646. $ci_mesg->read_file;
  647. # Execute "svn status" for a list of changed items
  648. @status = grep !/^\?/, _svn_status_get();
  649. # Abort if there is no change in the working copy
  650. if (!@status) {
  651. return _cm_abort(Fcm::Cm::Abort->NULL);
  652. }
  653. # Get associated URL of current working copy
  654. my $url = Fcm::CmUrl->new (URL => &get_url_of_wc ());
  655. # Include URL, or project, branch and sub-directory info in @status
  656. unshift @status, "\n";
  657. if ($url->project and $url->branch) {
  658. unshift @status, (
  659. '[Project: ' . $url->project . ']' . "\n",
  660. '[Branch : ' . $url->branch . ']' . "\n",
  661. '[Sub-dir: ' . ($url->subdir ? $url->subdir : '<top>') . ']' . "\n",
  662. );
  663. } else {
  664. unshift @status, '[URL: ' . $url->url . ']' . "\n";
  665. }
  666. # Use a temporary file to store the final commit log message
  667. $ci_mesg->ignore_mesg (\@status);
  668. my $logfile = $ci_mesg->edit_file (TEMP => 1);
  669. # Check with the user to see if he/she wants to go ahead
  670. my $reply = 'n';
  671. if (!defined($dry_run)) {
  672. # Add extra warning for trunk commit
  673. my @prompt_args;
  674. my $user = Fcm::Config->instance()->user_id();
  675. if ($url->is_trunk()) {
  676. @prompt_args = ('CI_TRUNK');
  677. }
  678. elsif ($user && $url->is_branch() && $url->branch_owner() ne $user) {
  679. if (exists $Fcm::CmUrl::owner_keywords{$url->branch_owner}) {
  680. @prompt_args = (
  681. 'CI_BRANCH_SHARED',
  682. uc($Fcm::CmUrl::owner_keywords{$url->branch_owner()}),
  683. );
  684. }
  685. else {
  686. @prompt_args = ('CI_BRANCH_USER');
  687. }
  688. }
  689. else {
  690. @prompt_args = ('CI');
  691. }
  692. $reply = $CLI_PROMPT->('commit', @prompt_args);
  693. }
  694. if ($reply eq 'y') {
  695. # Commit the change if user replies "y" for "yes"
  696. my @command = (
  697. qw/svn commit -F/, $logfile,
  698. ($svn_non_interactive ? '--non-interactive' : ()),
  699. (defined $password ? ('--password', $password) : ()),
  700. );
  701. my $rc;
  702. &run_command (\@command, RC => \$rc, ERROR => 'warn');
  703. if ($rc) {
  704. # Commit failed
  705. # Write temporary commit log content to commit log message file
  706. $ci_mesg->write_file;
  707. # Fail the command
  708. return _cm_abort(Fcm::Cm::Abort->FAIL);
  709. }
  710. # Remove commit message file
  711. unlink $ci_mesg->file;
  712. # Update the working copy
  713. $CLI_MESSAGE->(q{}, join(q{}, _svn_update()));
  714. } else {
  715. $ci_mesg->write_file;
  716. if (!$dry_run) {
  717. return _cm_abort();
  718. }
  719. }
  720. return;
  721. }
  722. # ------------------------------------------------------------------------------
  723. # SYNOPSIS
  724. # &Fcm::Cm::cm_conflicts ();
  725. #
  726. # DESCRIPTION
  727. # This is a FCM command for resolving conflicts within working copy using a
  728. # graphical merge tool.
  729. # ------------------------------------------------------------------------------
  730. sub cm_conflicts {
  731. # Path to the working copy
  732. my $path = $ARGV[0];
  733. $path = cwd () if not $path;
  734. # Check for any files with conflicts
  735. my @status = grep /^C.{4} *(.*)/, &run_command (
  736. [qw/svn st/, ($path eq cwd () ? () : $path)], METHOD => 'qx',
  737. );
  738. my @files = map {m/^C.{4} *(.*)/; $1} @status;
  739. # Save current working directory
  740. my $topdir = cwd ();
  741. # Set up environment for graphical merge
  742. # Use environment variable if set, otherwise use default setting
  743. local(%ENV) = %ENV;
  744. $ENV{FCM_GRAPHIC_MERGE}
  745. ||= Fcm::Config->instance()->setting (qw/TOOL GRAPHIC_MERGE/);
  746. FILE:
  747. for my $file (@files) {
  748. # Print name of file in conflicts
  749. $CLI_MESSAGE->('CF', $file);
  750. # Determine directory and base name of file in conflicts
  751. my $base = basename $file;
  752. my $dir = dirname $file;
  753. # Change to container directory of file in conflicts
  754. chdir(File::Spec->catfile($topdir, $dir))
  755. || return _cm_err(Fcm::Cm::Exception->CHDIR, $dir);
  756. # Use "svn info" to determine conflict marker files
  757. my @info = &run_command ([qw/svn info/, $base], METHOD => 'qx');
  758. # Ignore if $base is a binary file
  759. if (-B $base) {
  760. $CLI_MESSAGE->('CF_BINARY', $base);
  761. next FILE;
  762. }
  763. # Get conflicts markers files
  764. my ($older, $mine, $yours);
  765. for (@info) {
  766. $older = $1 if (/^Conflict Previous Base File: (.*)/);
  767. $mine = $1 if (/^Conflict Previous Working File: (.*)/);
  768. $yours = $1 if (/^Conflict Current Base File: (.*)/);
  769. }
  770. if (-f $base and (stat $base)[9] > (stat $mine)[9] + 1) {
  771. # If $base is newer (by more than a second), it may contain saved changes
  772. if ($CLI_PROMPT->('conflicts', 'CF_OVERWRITE', $base) ne 'y') {
  773. next FILE;
  774. }
  775. }
  776. # Launch graphic merge tool
  777. my $rc;
  778. my $command = [qw/fcm_graphic_merge/, $base, $mine, $older, $yours];
  779. # $rc == 0: all conflicts resovled
  780. # $rc == 1: some conflicts not resolved
  781. # $rc == 2: trouble
  782. eval {
  783. run_command($command, RC => \$rc);
  784. };
  785. if ($@) {
  786. if (!defined($rc) || $rc > 1) {
  787. die($@);
  788. }
  789. }
  790. next FILE if $rc;
  791. # Prompt user to run "svn resolved" on the file
  792. if ($CLI_PROMPT->('conflicts', 'RUN_SVN_COMMAND', 'resolved') eq 'y') {
  793. run_command([qw{svn resolved}, $base]);
  794. }
  795. }
  796. }
  797. # ------------------------------------------------------------------------------
  798. # SYNOPSIS
  799. # &Fcm::Cm::cm_diff ();
  800. #
  801. # DESCRIPTION
  802. # This is a wrapper to "svn diff". It adds two extra functionalities. The
  803. # first one allows the command to show differences relative to the base of
  804. # the branch. The second one allows differences to be displayed via a
  805. # graphical tool.
  806. # ------------------------------------------------------------------------------
  807. sub cm_diff {
  808. # Set up environment for graphical diff
  809. # Use environment variable if set, otherwise use default setting
  810. local(%ENV) = %ENV;
  811. $ENV{FCM_GRAPHIC_DIFF}
  812. ||= Fcm::Config->instance()->setting(qw/TOOL GRAPHIC_DIFF/);
  813. # Check for the --branch options
  814. # ----------------------------------------------------------------------------
  815. my $branch = grep {$_ eq '-b' or $_ eq '--branch'} @ARGV;
  816. if (not $branch) {
  817. # The --branch option not specified, just call "svn diff"
  818. # Convert the --graphical to qw/--diff-cmd fcm_graphical_diff/
  819. # Convert the --summarise to --summarize
  820. @ARGV = map {
  821. my @return;
  822. if ($_ eq '-g' or $_ eq '--graphical') {
  823. @return = (qw/--diff-cmd fcm_graphic_diff/)
  824. } elsif ($_ eq '--summarise') {
  825. @return = ('--summarize');
  826. } else {
  827. @return = ($_);
  828. }
  829. @return;
  830. } @ARGV;
  831. # Execute the command
  832. return _svn('diff', @ARGV);
  833. }
  834. # The --branch option is specified
  835. # ----------------------------------------------------------------------------
  836. # Determine whether the --graphical option is specified,
  837. # if so set the appropriate command
  838. # ----------------------------------------------------------------------------
  839. my ($diff_cmd, $extensions, $graphical, $summarise, $trac, $wiki);
  840. my $rc = GetOptions (
  841. 'b|branch' => \$branch,
  842. 'diff-cmd=s' => \$diff_cmd,
  843. 'x|extensions=s' => \$extensions,
  844. 'g|graphical' => \$graphical,
  845. 'summarise|summarize' => \$summarise,
  846. 't|trac' => \$trac,
  847. 'wiki' => \$wiki,
  848. );
  849. if (!$rc) {
  850. _cli_err();
  851. }
  852. my @diff_cmd = ();
  853. if ($graphical) {
  854. @diff_cmd = (qw/--diff-cmd fcm_graphic_diff/);
  855. } elsif ($diff_cmd) {
  856. @diff_cmd = ('--diff-cmd', $diff_cmd);
  857. push @diff_cmd, '--extensions', split (/\s+/, $extensions) if $extensions;
  858. }
  859. # The remaining argument should either be a URL or a PATH
  860. my ($url_arg, $path_arg);
  861. if (@ARGV) {
  862. my $arg = Fcm::CmUrl->new (URL => $ARGV[0]);
  863. if ($arg->is_url) {
  864. $url_arg = $ARGV[0];
  865. } else {
  866. $path_arg = $ARGV[0];
  867. }
  868. }
  869. # Get repository and branch information
  870. # ----------------------------------------------------------------------------
  871. my ($url, $path);
  872. if (defined $url_arg) {
  873. # If a URL is specified, get repository and branch information from it
  874. $url = Fcm::CmBranch->new (URL => $url_arg);
  875. } else {
  876. # Get repository and branch information from the specified path or the
  877. # current directory if it is a working copy
  878. $path = $path_arg ? $path_arg : cwd ();
  879. if (!is_wc($path)) {
  880. return _cm_err(Fcm::Cm::Exception->INVALID_WC, $path);
  881. }
  882. $url = Fcm::CmBranch->new (URL => &get_url_peg_of_wc ($path));
  883. }
  884. # Check that URL is a standard FCM branch
  885. if (!$url->is_branch()) {
  886. return _cm_err(Fcm::Cm::Exception->INVALID_BRANCH, $url->url_peg());
  887. }
  888. # Save and remove sub-directory part of the URL
  889. my $subdir = $url->subdir ();
  890. $url->subdir ('');
  891. # Check that $url exists
  892. if (!$url->url_exists()) {
  893. return _cm_err(Fcm::Cm::Exception->INVALID_URL, $url->url_peg());
  894. }
  895. # Compare current branch with its parent
  896. # ----------------------------------------------------------------------------
  897. my $parent = Fcm::CmBranch->new (URL => $url->parent->url);
  898. $parent->pegrev ($url->pegrev) if $url->pegrev;
  899. if (!$parent->url_exists()) {
  900. return _cm_err(
  901. Fcm::Cm::Exception->PARENT_NOT_EXIST, $url->url_peg(), $parent->url(),
  902. );
  903. }
  904. my $base = $parent->base_of_merge_from ($url);
  905. # Ensure the correct diff (syntax) is displayed
  906. # ----------------------------------------------------------------------------
  907. # Reinstate the sub-tree part into the URL
  908. $url->subdir ($subdir);
  909. $base->subdir ($subdir);
  910. # Ensure the branch URL has a peg revision
  911. $url->pegrev ($url->svninfo (FLAG => 'Last Changed Rev')) if not $url->pegrev;
  912. if ($trac or $wiki) {
  913. # Trac/wiki
  914. # --------------------------------------------------------------------------
  915. if (!$url_arg && _svn_status_get([$path_arg ? $path_arg : q{.}])) {
  916. $CLI_MESSAGE->('ST_IN_TRAC_DIFF', ($path_arg ? $path_arg : q{.}));
  917. }
  918. # Trac wiki syntax
  919. my $wiki_syntax = 'diff:' . $base->path_peg . '//' . $url->path_peg;
  920. if ($wiki) {
  921. # Print Trac wiki syntax only
  922. $CLI_MESSAGE->(q{}, "$wiki_syntax\n");
  923. } else { # if $trac
  924. # Use Trac to view "diff"
  925. my $browser = Fcm::Config->instance()->setting(qw/WEB_BROWSER/);
  926. $browser ||= 'firefox';
  927. my $trac_url = Fcm::Keyword::get_browser_url($url->project_url());
  928. $trac_url =~ s{/intertrac/.*$}{/intertrac/$wiki_syntax}xms;
  929. &run_command ([$browser, $trac_url], METHOD => 'exec', PRINT => 1);
  930. }
  931. } else {
  932. # Execute the "diff" command
  933. # --------------------------------------------------------------------------
  934. my @command = (
  935. qw/svn diff/, @diff_cmd,
  936. ($summarise ? ('--summarize') : ()),
  937. '--old', $base->url_peg,
  938. '--new', ($url_arg ? $url->url_peg : ($path_arg ? $path_arg : '.')),
  939. );
  940. &run_command (\@command, PRINT => 1);
  941. }
  942. }
  943. # ------------------------------------------------------------------------------
  944. # SYNOPSIS
  945. # &Fcm::Cm::cm_merge ();
  946. #
  947. # DESCRIPTION
  948. # This is a wrapper to "svn merge".
  949. # ------------------------------------------------------------------------------
  950. sub cm_merge {
  951. # Options
  952. # ----------------------------------------------------------------------------
  953. my ($custom, $dry_run, $non_interactive, $reverse, $rev, $verbose);
  954. my $rc = GetOptions(
  955. 'custom' => \$custom,
  956. 'dry-run' => \$dry_run,
  957. 'non-interactive' => \$non_interactive,
  958. 'reverse' => \$reverse,
  959. 'revision|r=s' => \$rev,
  960. 'verbose|v' => \$verbose,
  961. );
  962. if (!$rc) {
  963. _cli_err();
  964. }
  965. # Find out the URL of the working copy
  966. # ----------------------------------------------------------------------------
  967. my ($target, $wct);
  968. if (&is_wc ()) {
  969. $wct = &get_wct ();
  970. if ($wct ne cwd ()) {
  971. chdir($wct) || return _cm_err(Fcm::Cm::Exception->CHDIR, $wct);
  972. $CLI_MESSAGE->('CHDIR_WCT', $wct);
  973. }
  974. $target = Fcm::CmBranch->new (URL => &get_url_of_wc ($wct));
  975. } else {
  976. return _cm_err(Fcm::Cm::Exception->INVALID_WC, '.');
  977. }
  978. if (!$target->url_exists()) {
  979. return _cm_err(Fcm::Cm::Exception->WC_URL_NOT_EXIST, '.');
  980. }
  981. # The target must be at the top of a branch
  982. # $subdir will be used later to determine whether the merge is allowed or not
  983. my $subdir = $target->subdir;
  984. $target->subdir ('') if $subdir;
  985. # Check for any local modifications
  986. # ----------------------------------------------------------------------------
  987. if (!$dry_run && !$non_interactive) {
  988. _svn_status_checker('merge', 'MODIFIED', $CLI_HANDLER_OF{WC_STATUS})->();
  989. }
  990. # Determine the SOURCE URL
  991. # ----------------------------------------------------------------------------
  992. my $source;
  993. if ($reverse) {
  994. # Reverse merge, the SOURCE is the the working copy URL
  995. $source = Fcm::CmBranch->new (URL => $target->url);
  996. } else {
  997. # Automatic/custom merge, argument 1 is the SOURCE of the merge
  998. my $source_url = shift (@ARGV);
  999. if (!$source_url) {
  1000. _cli_err('CLI_MERGE_ARG1');
  1001. }
  1002. $source = _cm_get_source($source_url, $target);
  1003. }
  1004. # Parse the revision option
  1005. # ----------------------------------------------------------------------------
  1006. if ($reverse && !$rev) {
  1007. _cli_err('CLI_OPT_WITH_OPT', 'revision', 'reverse');
  1008. }
  1009. my @revs = (($reverse || $custom) && $rev ? split(qr{:}xms, $rev) : ());
  1010. # Determine the merge delta and the commit log message
  1011. # ----------------------------------------------------------------------------
  1012. my (@delta, $mesg);
  1013. my $separator = '-' x 80 . "\n";
  1014. if ($reverse) {
  1015. # Reverse merge
  1016. # --------------------------------------------------------------------------
  1017. if (@revs == 1) {
  1018. $revs[1] = ($revs[0] - 1);
  1019. } else {
  1020. @revs = sort {$b <=> $a} @revs;
  1021. }
  1022. $source->pegrev ($source->svninfo (FLAG => 'Last Changed Rev'))
  1023. unless $source->pegrev;
  1024. $source->subdir ($subdir);
  1025. # "Delta" of the "svn merge" command
  1026. @delta = ('-r' . $revs[0] . ':' . $revs[1], $source->url_peg);
  1027. # Template message
  1028. $mesg = 'Reversed r' . $revs[0] .
  1029. (($revs[1] < $revs[0] - 1) ? ':' . $revs[1] : '') . ' of ' .
  1030. $source->path . "\n";
  1031. } elsif ($custom) {
  1032. # Custom merge
  1033. # --------------------------------------------------------------------------
  1034. if (@revs) {
  1035. # Revision specified
  1036. # ------------------------------------------------------------------------
  1037. # Only one revision N specified, use (N - 1):N as the delta
  1038. unshift @revs, ($revs[0] - 1) if @revs == 1;
  1039. $source->pegrev ($source->svninfo (FLAG => 'Last Changed Rev'))
  1040. unless $source->pegrev;
  1041. $source->subdir ($subdir);
  1042. $target->subdir ($subdir);
  1043. # "Delta" of the "svn merge" command
  1044. @delta = ('-r' . $revs[0] . ':' . $revs[1], $source->url_peg);
  1045. # Template message
  1046. $mesg = 'Custom merge into ' . $target->path . ': r' . $revs[1] .
  1047. ' cf. r' . $revs[0] . ' of ' . $source->path_peg . "\n";
  1048. } else {
  1049. # Revision not specified
  1050. # ------------------------------------------------------------------------
  1051. # Get second source URL
  1052. my $source2_url = shift (@ARGV);
  1053. if (!$source2_url) {
  1054. _cli_err('CLI_MERGE_ARG2');
  1055. }
  1056. my $source2 = _cm_get_source($source2_url, $target);
  1057. $source->pegrev ($source->svninfo (FLAG => 'Last Changed Rev'))
  1058. unless $source->pegrev;
  1059. $source2->pegrev ($source2->svninfo (FLAG => 'Last Changed Rev'))
  1060. unless $source2->pegrev;
  1061. $source->subdir ($subdir);
  1062. $source2->subdir ($subdir);
  1063. $target->subdir ($subdir);
  1064. # "Delta" of the "svn merge" command
  1065. @delta = ($source->url_peg, $source2->url_peg);
  1066. # Template message
  1067. $mesg = 'Custom merge into ' . $target->path . ': ' . $source->path_peg .
  1068. ' cf. ' . $source2->path_peg . "\n";
  1069. }
  1070. } else {
  1071. # Automatic merge
  1072. # --------------------------------------------------------------------------
  1073. # Check to ensure source branch is not the same as the target branch
  1074. if (!$target->branch()) {
  1075. return _cm_err(Fcm::Cm::Exception->WC_INVALID_BRANCH, $wct);
  1076. }
  1077. if ($source->branch() eq $target->branch()) {
  1078. return _cm_err(Fcm::Cm::Exception->MERGE_SELF, $target->url_peg(), $wct);
  1079. }
  1080. # Only allow the merge if the source and target are "directly related"
  1081. # --------------------------------------------------------------------------
  1082. my $anc = $target->ancestor ($source);
  1083. return _cm_err(
  1084. Fcm::Cm::Exception->MERGE_UNRELATED, $target->url_peg(), $source->url_peg
  1085. ) unless
  1086. ($anc->url eq $target->url and $anc->url_peg eq $source->parent->url_peg)
  1087. or
  1088. ($anc->url eq $source->url and $anc->url_peg eq $target->parent->url_peg)
  1089. or
  1090. ($anc->url eq $source->parent->url and $anc->url eq $target->parent->url);
  1091. # Check for available merges from the source
  1092. # --------------------------------------------------------------------------
  1093. my @revs = $target->avail_merge_from ($source, 1);
  1094. if (@revs) {
  1095. if ($verbose) {
  1096. # Verbose mode, print log messages of available merges
  1097. $CLI_MESSAGE->('MERGE_REVS', $source->path_peg(), q{});
  1098. for (@revs) {
  1099. $CLI_MESSAGE->('SEPARATOR');
  1100. $CLI_MESSAGE->(q{}, $source->display_svnlog($_));
  1101. }
  1102. $CLI_MESSAGE->('SEPARATOR');
  1103. }
  1104. else {
  1105. # Normal mode, list revisions of available merges
  1106. $CLI_MESSAGE->('MERGE_REVS', $source->path_peg(), join(q{ }, @revs));
  1107. }
  1108. } else {
  1109. return _cm_abort(Fcm::Cm::Abort->NULL);
  1110. }
  1111. # If more than one merge available, prompt user to enter a revision number
  1112. # to merge from, default to $revs [0]
  1113. # --------------------------------------------------------------------------
  1114. if ($non_interactive || @revs == 1) {
  1115. $source->pegrev($revs[0]);
  1116. }
  1117. else {
  1118. my $reply = $CLI_PROMPT->(
  1119. {type => q{}, default => $revs[0]}, 'merge', 'MERGE_REV',
  1120. );
  1121. if (!defined($reply)) {
  1122. return _cm_abort();
  1123. }
  1124. # Expand revision keyword if necessary
  1125. if ($reply) {
  1126. $reply = (Fcm::Keyword::expand($target->project_url(), $reply))[1];
  1127. }
  1128. # Check that the reply is a number in the available merges list
  1129. if (!grep {$_ eq $reply} @revs) {
  1130. return _cm_err(Fcm::Cm::Exception->MERGE_REV_INVALID, $reply)
  1131. }
  1132. $source->pegrev($reply);
  1133. }
  1134. # If the working copy top is pointing to a sub-directory of a branch,
  1135. # we need to check whether the merge will result in losing changes made in
  1136. # other sub-directories of the source.
  1137. if ($subdir and not $target->allow_subdir_merge_from ($source, $subdir)) {
  1138. return _cm_err(Fcm::Cm::Exception->MERGE_UNSAFE, $source->url_peg());
  1139. }
  1140. # Calculate the base of the merge
  1141. my $base = $target->base_of_merge_from ($source);
  1142. # $source and $base must take into account the sub-directory
  1143. my $s = Fcm::CmBranch->new (URL => $source->url_peg);
  1144. my $b = Fcm::CmBranch->new (URL => $base->url_peg);
  1145. $s->subdir ($subdir) if $subdir;
  1146. $b->subdir ($subdir) if $subdir;
  1147. # Diagnostic
  1148. $CLI_MESSAGE->('MERGE_CF', $s->path_peg(), $b->path_peg());
  1149. # Delta of the "svn merge" command
  1150. @delta = ($b->url_peg, $s->url_peg);
  1151. # Template message
  1152. $mesg = 'Merged into ' . $target->path . ': ' . $source->path_peg .
  1153. ' cf. ' . $base->path_peg . "\n";
  1154. }
  1155. # Run "svn merge" in "--dry-run" mode to see the result
  1156. # ----------------------------------------------------------------------------
  1157. my @out = &run_command (
  1158. [qw/svn merge --dry-run/, @delta],
  1159. METHOD => 'qx', PRINT => ($dry_run and $verbose),
  1160. );
  1161. # Abort merge if it will result in no change
  1162. if (not @out) {
  1163. return _cm_abort(Fcm::Cm::Abort->NULL);
  1164. }
  1165. # Report result of "svn merge --dry-run"
  1166. if ($dry_run || !$non_interactive) {
  1167. $CLI_MESSAGE->('MERGE_DRY');
  1168. $CLI_MESSAGE->('SEPARATOR');
  1169. $CLI_MESSAGE->(q{}, join(q{}, @out));
  1170. $CLI_MESSAGE->('SEPARATOR');
  1171. }
  1172. return if $dry_run;
  1173. # Prompt the user to see if (s)he would like to go ahead
  1174. # ----------------------------------------------------------------------------
  1175. # Go ahead with merge only if user replies "y"
  1176. if (!$non_interactive && $CLI_PROMPT->('merge', 'MERGE') ne 'y') {
  1177. return _cm_abort();
  1178. }
  1179. $CLI_MESSAGE->('MERGE');
  1180. run_command([qw/svn merge/, @delta], PRINT => $verbose);
  1181. # Prepare the commit log
  1182. # ----------------------------------------------------------------------------
  1183. # Read in any existing message
  1184. my $ci_mesg = Fcm::CmCommitMessage->new;
  1185. $ci_mesg->read_file;
  1186. $ci_mesg->auto_mesg ([$mesg, @{ $ci_mesg->auto_mesg }]);
  1187. $ci_mesg->write_file;
  1188. if ($verbose) {
  1189. $CLI_MESSAGE->('SEPARATOR');
  1190. $CLI_MESSAGE->('MERGE_CI', $mesg);
  1191. }
  1192. return;
  1193. }
  1194. # ------------------------------------------------------------------------------
  1195. # SYNOPSIS
  1196. # &Fcm::Cm::cm_mkpatch ();
  1197. #
  1198. # DESCRIPTION
  1199. # This is a FCM command to create a patching script from particular revisions
  1200. # of a URL.
  1201. # ------------------------------------------------------------------------------
  1202. sub cm_mkpatch {
  1203. # Process command line options and arguments
  1204. # ----------------------------------------------------------------------------
  1205. my (@exclude, $organisation, $revision);
  1206. my $rc = GetOptions(
  1207. 'exclude=s' => \@exclude,
  1208. 'organisation=s' => \$organisation,
  1209. 'r|revision=s' => \$revision,
  1210. );
  1211. if (!$rc) {
  1212. _cli_err();
  1213. }
  1214. # Excluded paths, convert glob into regular patterns
  1215. @exclude = split (/:/, join (':', @exclude));
  1216. for (@exclude) {
  1217. s#\*#[^/]*#; # match any number of non-slash character
  1218. s#\?#[^/]#; # match a non-slash character
  1219. s#/*$##; # remove trailing slash
  1220. }
  1221. # Organisation prefix
  1222. $organisation = $organisation ? $organisation : 'original';
  1223. # Make sure revision option is set correctly
  1224. my @revs = $revision ? split (/:/, $revision) : ();
  1225. @revs = @revs [0, 1] if @revs > 2;
  1226. # Arguments
  1227. my ($u, $outdir) = @ARGV;
  1228. if (!$u) {
  1229. _cli_err();
  1230. }
  1231. my $url = Fcm::CmUrl->new (URL => $u);
  1232. if (!$url->is_url()) {
  1233. return _cm_err(Fcm::Cm::Exception->INVALID_URL, $u);
  1234. }
  1235. if (!$url->url_exists()) {
  1236. return _cm_err(Fcm::Cm::Exception->NOT_EXIST, $u);
  1237. }
  1238. if (!$url->branch()) {
  1239. $CLI_MESSAGE->('INVALID_BRANCH', $u);
  1240. }
  1241. elsif ($url->subdir()) {
  1242. $CLI_MESSAGE->('BRANCH_SUBDIR', $u);
  1243. }
  1244. if (@revs) {
  1245. # If HEAD revision is given, convert it into a number
  1246. # --------------------------------------------------------------------------
  1247. for my $rev (@revs) {
  1248. $rev = $url->svninfo (FLAG => 'Revision') if uc ($rev) eq 'HEAD';
  1249. }
  1250. } else {
  1251. # If no revision is given, use the HEAD
  1252. # --------------------------------------------------------------------------
  1253. $revs[0] = $url->svninfo (FLAG => 'Revision');
  1254. }
  1255. $revs[1] = $revs[0] if @revs == 1;
  1256. # Check that output directory is set
  1257. # ----------------------------------------------------------------------------
  1258. $outdir = File::Spec->catfile (cwd (), 'fcm-mkpatch-out') if not $outdir;
  1259. if (-e $outdir) {
  1260. # Ask user to confirm removal of old output directory if it exists
  1261. if ($CLI_PROMPT->('mkpatch', 'MKPATCH_OVERWRITE') ne 'y') {
  1262. return _cm_abort();
  1263. }
  1264. rmtree($outdir) || return _cm_err(Fcm::Cm::Exception->RMTREE, $outdir);
  1265. }
  1266. # (Re-)create output directory
  1267. mkpath($outdir) || return _cm_err(Fcm::Cm::Exception->MKPATH, $outdir);
  1268. $CLI_MESSAGE->('OUT_DIR', $outdir);
  1269. # Get and process log of URL
  1270. # ----------------------------------------------------------------------------
  1271. my @script = (); # main output script
  1272. my %log = $url->svnlog (REV => \@revs);
  1273. my $url_path = $url->path;
  1274. for my $rev (sort {$a <=> $b} keys %log) {
  1275. # Look at the changed paths for each revision
  1276. my $use_patch = 1; # OK to use a patch file?
  1277. my @paths;
  1278. PATH: for my $path (sort keys %{ $log{$rev}{paths} }) {
  1279. my $file = $path;
  1280. # Skip paths outside of the branch
  1281. next PATH unless $file =~ s#^$url_path/*##;
  1282. # Skip excluded paths
  1283. for my $exclude (@exclude) {
  1284. if ($file =~ m#^$exclude(?:/*|$)#) {
  1285. # Can't use a patch file if any files have been excluded
  1286. $use_patch = 0;
  1287. next PATH;
  1288. }
  1289. }
  1290. # Can't use a patch file if any files have been added or replaced
  1291. $use_patch = 0 if $log{$rev}{paths}{$path}{action} eq 'A' or
  1292. $log{$rev}{paths}{$path}{action} eq 'R';
  1293. push @paths, $path;
  1294. }
  1295. # If a patch is being used, make sure it isn't just property changes
  1296. if ($use_patch) {
  1297. my @changedpaths;
  1298. for my $path (@paths) {
  1299. (my $file = $path) =~ s#^$url_path/*##;
  1300. if ($log{$rev}{paths}{$path}{action} eq 'M') {
  1301. my ($diff) = &run_command (
  1302. [qw/svn diff --no-diff-deleted --summarize -c/,
  1303. $rev, $url->url . '/' . $file. '@' . $rev],
  1304. METHOD => 'qx');
  1305. next unless $diff =~ /^[A-Z]/;
  1306. }
  1307. push @changedpaths, $path;
  1308. }
  1309. @paths = @changedpaths;
  1310. }
  1311. next unless @paths;
  1312. # Create the patch using "svn diff"
  1313. my @patch = ();
  1314. if ($use_patch) {
  1315. @patch = &run_command ([qw/svn diff --no-diff-deleted -c/, $rev,
  1316. $url->url], METHOD => 'qx');
  1317. if (@patch) {
  1318. # Don't use the patch if it may contain subversion keywords
  1319. for (@patch) {
  1320. $use_patch = 0 if /\$[a-zA-Z:]+ *\$/;
  1321. }
  1322. } else {
  1323. $use_patch = 0;
  1324. }
  1325. }
  1326. # Create a directory for this revision in the output directory
  1327. my $outdir_rev = File::Spec->catfile ($outdir, $rev);
  1328. mkpath($outdir_rev)
  1329. || return _cm_err(Fcm::Cm::Exception->MKPATH, $outdir_rev);
  1330. # Parse commit log message
  1331. my @msg = split /\n/, $log{$rev}{msg};
  1332. for (@msg) {
  1333. # Re-instate line break
  1334. $_ .= "\n";
  1335. # Remove line if it matches a merge template
  1336. $_ = '' if /^Reversed r\d+(?::\d+)? of \S+$/;
  1337. $_ = '' if /^Custom merge into \S+:.+$/;
  1338. $_ = '' if /^Merged into \S+: \S+ cf\. \S+$/;
  1339. # Modify Trac ticket link
  1340. s/(?:#|ticket:)(\d+)/${organisation}_ticket:$1/g;
  1341. # Modify Trac changeset link
  1342. s/(?:r|changeset:)(\d+)/${organisation}_changeset:$1/g;
  1343. s/\[(\d+)\]/${organisation}_changeset:$1/g;
  1344. }
  1345. push @msg, '(' . $organisation . '_changeset:' . $rev . ')' . "\n";
  1346. # Write commit log message in a file
  1347. my $f_revlog = File::Spec->catfile ($outdir_rev, 'log-message');
  1348. open FILE, '>', $f_revlog or die $f_revlog, ': cannot open (', $!, ')';
  1349. print FILE @msg;
  1350. close FILE or die $f_revlog, ': cannot close (', $!, ')';
  1351. # Handle each changed path
  1352. my $export_file = 1; # name for next exported file (gets incremented)
  1353. my $patch_needed = 0; # is a patch file required?
  1354. my @before_script = (); # patch script to run before patch applied
  1355. my @after_script = (); # patch script to run after patch applied
  1356. my @copied_dirs = (); # copied directories
  1357. CHANGED: for my $path (@paths) {
  1358. (my $file = $path) =~ s#^$url_path/*##;
  1359. my $url_file = $url->url . '/' . $file . '@' . $rev;
  1360. # Skip paths within copied directories
  1361. for my $copied_dir (@copied_dirs) {
  1362. next CHANGED if $file =~ m#^$copied_dir(?:/*|$)#;
  1363. }
  1364. if ($log{$rev}{paths}{$path}{action} eq 'D') {
  1365. # Script to delete file
  1366. push @after_script, 'svn delete ' . $file;
  1367. } else {
  1368. my $export_required = 0;
  1369. my $recursive_add = 0;
  1370. my $is_newfile = 0;
  1371. # Skip property changes
  1372. if ($log{$rev}{paths}{$path}{action} eq 'M') {
  1373. my ($diff) = &run_command (
  1374. [qw/svn diff --no-diff-deleted --summarize -c/,
  1375. $rev, $url->url . '/' . $file. '@' . $rev],
  1376. METHOD => 'qx');
  1377. next CHANGED unless $diff =~ /^[A-Z]/;
  1378. }
  1379. # Determine if the file is a directory
  1380. my $is_dir = 0;
  1381. if ($log{$rev}{paths}{$path}{action} ne 'M') {
  1382. my @info = &run_command ([qw/svn info/, $url_file], METHOD => 'qx');
  1383. for (@info) {
  1384. if (/^Node Kind: (\w+)/) {
  1385. $is_dir = 1 if $1 eq 'directory';
  1386. last;
  1387. }
  1388. }
  1389. }
  1390. # Decide how to treat added files
  1391. if ($log{$rev}{paths}{$path}{action} eq 'A') {
  1392. # Determine if the file is copied
  1393. if (exists $log{$rev}{paths}{$path}{'copyfrom-path'}) {
  1394. if ($is_dir) {
  1395. # A copied directory needs to be treated as a new file, exported
  1396. # and added recursively
  1397. $is_newfile = 1;
  1398. $export_required = 1;
  1399. $recursive_add = 1;
  1400. push @copied_dirs, $file;
  1401. } else {
  1402. # History exists for this file
  1403. my $copyfrom_path = $log{$rev}{paths}{$path}{'copyfrom-path'};
  1404. my $copyfrom_rev = $log{$rev}{paths}{$path}{'copyfrom-rev'};
  1405. my $cp_url = Fcm::CmUrl->new (
  1406. URL => $url->root . $copyfrom_path . '@' . $copyfrom_rev,
  1407. );
  1408. if ($copyfrom_path =~ s#^$url_path/*##) {
  1409. # File is copied from a file under the specified URL
  1410. # Check source exists
  1411. $is_newfile = 1 unless $cp_url->url_exists ($rev - 1);
  1412. } else {
  1413. # File copied from outside of the specified URL
  1414. $is_newfile = 1;
  1415. # Check branches can be determined
  1416. if ($url->branch and $cp_url->branch) {
  1417. # Follow its history, stop on copy
  1418. my %cp_log = $cp_url->svnlog (STOP_ON_COPY => 1);
  1419. # "First" revision of the copied file
  1420. my $cp_rev = (sort {$a <=> $b} keys %cp_log) [0];
  1421. my %attrib = %{ $cp_log{$cp_rev}{paths}{$cp_url->path} }
  1422. if $cp_log{$cp_rev}{paths}{$cp_url->path};
  1423. # Check whether the "first" revision is copied from elsewhere.
  1424. if (exists $attrib{'copyfrom-path'}) {
  1425. # If source exists in the specified URL, set up the copy
  1426. my $cp_cp_url = Fcm::CmUrl->new (
  1427. URL => $url->root . $attrib{'copyfrom-path'} . '@' .
  1428. $attrib{'copyfrom-rev'},
  1429. );
  1430. $cp_cp_url->branch ($url->branch);
  1431. if ($cp_cp_url->url_exists ($rev - 1)) {
  1432. ($copyfrom_path = $cp_cp_url->path) =~ s#^$url_path/*##;
  1433. # Check path is defined - if not it probably means the
  1434. # branch doesn't follow the FCM naming convention
  1435. $is_newfile = 0 if $copyfrom_path;
  1436. }
  1437. }
  1438. # Note: The logic above does not cover all cases. However, it
  1439. # should do the right thing for the most common case. Even
  1440. # where it gets it wrong the file contents should always be
  1441. # correct even if the file history is not.
  1442. }
  1443. }
  1444. # Check whether file is copied from an excluded path
  1445. if (not $is_newfile) {
  1446. for my $exclude (@exclude) {
  1447. if ($copyfrom_path =~ m#^$exclude(?:/*|$)#) {
  1448. $is_newfile = 1;
  1449. last;
  1450. }
  1451. }
  1452. }
  1453. # Script to copy file, if required
  1454. push @before_script, 'svn copy ' . $copyfrom_path . ' ' . $file
  1455. if not $is_newfile;
  1456. }
  1457. } else {
  1458. # History does not exist, must be a new file
  1459. $is_newfile = 1;
  1460. # If it's a directory then create it (in case patch doesn't)
  1461. push @before_script, 'mkdir ' . $file if $is_dir;
  1462. }
  1463. }
  1464. if ($log{$rev}{paths}{$path}{action} eq 'R') {
  1465. # Script to delete file
  1466. push @before_script, 'svn delete ' . $file;
  1467. # Now treat as new file
  1468. $is_newfile = 1;
  1469. }
  1470. # Script to add the file, if required
  1471. if ($is_newfile) {
  1472. if ($recursive_add) {
  1473. push @after_script, 'svn add ' . $file;
  1474. } else {
  1475. push @after_script, 'svn add --non-recursive ' . $file;
  1476. }
  1477. }
  1478. # Decide whether the file needs to be exported
  1479. if (not $is_dir) {
  1480. if (not $use_patch) {
  1481. $export_required = 1;
  1482. } else {
  1483. # Export the file if it is binary
  1484. my @mime_type = &run_command
  1485. ([qw/svn propget svn:mime-type/, $url_file], METHOD => 'qx');
  1486. for (@mime_type) {
  1487. $export_required = 1 if not /^text\//;
  1488. }
  1489. # Only create a patch file if necessary
  1490. $patch_needed = 1 if not $export_required;
  1491. }
  1492. }
  1493. if ($export_required) {
  1494. # Download the file using "svn export"
  1495. my $export = File::Spec->catfile ($outdir_rev, $export_file);
  1496. &run_command ([qw/svn export -q -r/, $rev, $url_file, $export]);
  1497. # Copy the exported file into the file
  1498. push @before_script,
  1499. 'cp -r ${fcm_patch_dir}/' . $export_file . ' ' . $file;
  1500. $export_file++;
  1501. }
  1502. }
  1503. }
  1504. # Write the patch file
  1505. if ($patch_needed) {
  1506. my $patchfile = File::Spec->catfile ($outdir_rev, 'patchfile');
  1507. open FILE, '>', $patchfile
  1508. or die $patchfile, ': cannot open (', $!, ')';
  1509. print FILE @patch;
  1510. close FILE or die $patchfile, ': cannot close (', $!, ')';
  1511. }
  1512. # Add line break to each line in @before_script and @after_script
  1513. @before_script = map {($_ ? $_ . ' || exit 1' . "\n" : "\n")}
  1514. @before_script if (@before_script);
  1515. @after_script = map {($_ ? $_ . ' || exit 1' . "\n" : "\n")}
  1516. @after_script if (@after_script);
  1517. # Write patch script to output
  1518. my $out = File::Spec->catfile ($outdir_rev, 'apply-patch');
  1519. open FILE, '>', $out or die $out, ': cannot open (', $!, ')';
  1520. # Script header
  1521. my $shell = Fcm::Config->instance()->setting(qw/TOOL SHELL/);
  1522. print FILE <<EOF;
  1523. #!$shell
  1524. # ------------------------------------------------------------------------------
  1525. # NAME
  1526. # apply-patch
  1527. #
  1528. # DESCRIPTION
  1529. # This script is generated automatically by the "fcm mkpatch" command. It
  1530. # applies the patch to the current working directory which must be a working
  1531. # copy of a valid project tree that can accept the import of the patches.
  1532. #
  1533. # Patch created from $organisation URL: $u
  1534. # Changeset: $rev
  1535. # ------------------------------------------------------------------------------
  1536. this=`basename \$0`
  1537. echo "\$this: Applying patch for changeset $rev."
  1538. # Location of the patch, base on the location of this script
  1539. cd `dirname \$0` || exit 1
  1540. fcm_patch_dir=\$PWD
  1541. # Change directory back to the working copy
  1542. cd \$OLDPWD || exit 1
  1543. # Check working copy does not have local changes
  1544. status=`svn status`
  1545. if [[ -n \$status ]]; then
  1546. echo "\$this: working copy contains changes, abort." >&2
  1547. exit 1
  1548. fi
  1549. if [[ -a "#commit_message#" ]]; then
  1550. echo "\$this: existing commit message in "#commit_message#", abort." >&2
  1551. exit 1
  1552. fi
  1553. # Apply the changes
  1554. EOF
  1555. # Script content
  1556. print FILE @before_script if @before_script;
  1557. print FILE "patch -p0 <\${fcm_patch_dir}/patchfile || exit 1\n"
  1558. if $patch_needed;
  1559. print FILE @after_script if @after_script;
  1560. # Script footer
  1561. print FILE <<EOF;
  1562. # Copy in the commit message
  1563. cp \${fcm_patch_dir}/log-message "#commit_message#"
  1564. echo "\$this: finished normally."
  1565. #EOF
  1566. EOF
  1567. close FILE or die $out, ': cannot close (', $!, ')';
  1568. # Add executable permission
  1569. chmod 0755, $out;
  1570. # Script to commit the change
  1571. push @script, '${fcm_patches_dir}/' . $rev . '/apply-patch';
  1572. push @script, 'svn commit -F "#commit_message#"';
  1573. push @script, 'rm -f "#commit_message#"';
  1574. push @script, 'svn update';
  1575. push @script, '';
  1576. $CLI_MESSAGE->('PATCH_REV', $rev);
  1577. }
  1578. # Write the main output script if necessary. Otherwise remove output directory
  1579. # ----------------------------------------------------------------------------
  1580. if (@script) {
  1581. # Add line break to each line in @script
  1582. @script = map {($_ ? $_ . ' || exit 1' . "\n" : "\n")} @script;
  1583. # Write script to output
  1584. my $out = File::Spec->catfile ($outdir, 'fcm-import-patch');
  1585. open FILE, '>', $out or die $out, ': cannot open (', $!, ')';
  1586. # Script header
  1587. my $shell = Fcm::Config->instance()->setting(qw/TOOL SHELL/);
  1588. print FILE <<EOF;
  1589. #!$shell
  1590. # ------------------------------------------------------------------------------
  1591. # NAME
  1592. # fcm-import-patch
  1593. #
  1594. # SYNOPSIS
  1595. # fcm-import-patch TARGET
  1596. #
  1597. # DESCRIPTION
  1598. # This script is generated automatically by the "fcm mkpatch" command, as are
  1599. # the revision "patches" created in the same directory. The script imports the
  1600. # patches into TARGET, which must either be a URL or a working copy of a valid
  1601. # project tree that can accept the import of the patches.
  1602. #
  1603. # Patch created from $organisation URL: $u
  1604. # ------------------------------------------------------------------------------
  1605. this=`basename \$0`
  1606. # Check argument
  1607. target=\$1
  1608. # First argument must be a URL or working copy
  1609. if [[ -z \$target ]]; then
  1610. echo "\$this: the first argument must be a URL or a working copy, abort." >&2
  1611. exit 1
  1612. fi
  1613. if [[ \$target == svn://* || \$target == svn+ssh://* || \\
  1614. \$target == http://* || \$target == https://* || \\
  1615. \$target == file://* ]]; then
  1616. # A URL, checkout a working copy in a temporary location
  1617. fcm_tmp_dir=`mktemp -d \${TMPDIR:=/tmp}/\$this.XXXXXX`
  1618. fcm_working_copy=\$fcm_tmp_dir
  1619. svn checkout -q \$target \$fcm_working_copy || exit 1
  1620. else
  1621. fcm_working_copy=\$target
  1622. fi
  1623. # Location of the patches, base on the location of this script
  1624. cd `dirname \$0` || exit 1
  1625. fcm_patches_dir=\$PWD
  1626. # Change directory to the working copy
  1627. cd \$fcm_working_copy || exit 1
  1628. # Set the language to avoid encoding problems
  1629. export LANG=en_GB
  1630. # Commands to apply patches
  1631. EOF
  1632. # Script content
  1633. print FILE @script;
  1634. # Script footer
  1635. print FILE <<EOF;
  1636. # Remove temporary working copy, if necessary
  1637. if [[ -d \$fcm_tmp_dir && -w \$fcm_tmp_dir ]]; then
  1638. rm -rf \$fcm_tmp_dir
  1639. fi
  1640. echo "\$this: finished normally."
  1641. #EOF
  1642. EOF
  1643. close FILE or die $out, ': cannot close (', $!, ')';
  1644. # Add executable permission
  1645. chmod 0755, $out;
  1646. # Diagnostic
  1647. $CLI_MESSAGE->('PATCH_DONE', $outdir);
  1648. } else {
  1649. # Remove output directory
  1650. rmtree $outdir or die $outdir, ': cannot remove';
  1651. # Diagnostic
  1652. return _cm_abort(Fcm::Cm::Abort->NULL);
  1653. }
  1654. return 1;
  1655. }
  1656. # ------------------------------------------------------------------------------
  1657. # CLI: fcm add.
  1658. sub _cli_command_add {
  1659. my @args = map {($_ eq '--check' || $_ eq '-c' ? () : $_)} @_;
  1660. my %option = (st_check_handler => $CLI_HANDLER_OF{'WC_STATUS_PATH'});
  1661. return (
  1662. @args == @_ ? _svn("add", @args) : cm_check_unknown(\%option, @args)
  1663. );
  1664. }
  1665. # ------------------------------------------------------------------------------
  1666. # CLI: fcm checkout.
  1667. sub _cli_command_checkout {
  1668. if (@ARGV) {
  1669. my $target = is_url($ARGV[-1]) ? cwd() : $ARGV[-1];
  1670. if (-d $target && is_wc($target)) {
  1671. return _cm_err(Fcm::Cm::Exception->WC_EXIST, $target);
  1672. }
  1673. }
  1674. return _svn('checkout', @ARGV);
  1675. }
  1676. # ------------------------------------------------------------------------------
  1677. # CLI: fcm delete.
  1678. sub _cli_command_delete {
  1679. my @args = map {($_ eq '--check' || $_ eq '-c' ? () : $_)} @_;
  1680. my %option = (st_check_handler => $CLI_HANDLER_OF{'WC_STATUS_PATH'});
  1681. return (
  1682. @args == @_ ? _svn("delete", @args) : cm_check_missing(\%option, @args)
  1683. );
  1684. }
  1685. # ------------------------------------------------------------------------------
  1686. # CLI: fcm switch.
  1687. sub _cli_command_switch {
  1688. local(@ARGV) = @_;
  1689. if (grep {$_ eq '--relocate'} @ARGV) {
  1690. return _svn('switch', @ARGV);
  1691. }
  1692. my %option;
  1693. if (!GetOptions(\%option, 'non-interactive', 'revision|r=s', 'quiet|q')) {
  1694. _cli_err();
  1695. }
  1696. if (!$option{'non-interactive'}) {
  1697. $option{st_check_handler} = $CLI_HANDLER_OF{WC_STATUS};
  1698. }
  1699. if (!@ARGV) {
  1700. _cli_err();
  1701. }
  1702. $CLI_MESSAGE->(q{}, join(q{}, cm_switch(\%option, @ARGV)));
  1703. }
  1704. # ------------------------------------------------------------------------------
  1705. # CLI: fcm update.
  1706. sub _cli_command_update {
  1707. local(@ARGV) = @_;
  1708. my %option;
  1709. if (!GetOptions(\%option, 'non-interactive', 'revision|r=s', 'quiet|q')) {
  1710. _cli_err();
  1711. }
  1712. if (!$option{'non-interactive'}) {
  1713. $option{st_check_handler} = $CLI_HANDLER_OF{WC_STATUS};
  1714. }
  1715. $CLI_MESSAGE->(q{}, join(q{}, cm_update(\%option, @ARGV)));
  1716. }
  1717. # ------------------------------------------------------------------------------
  1718. # CLI error.
  1719. sub _cli_err {
  1720. my ($key, @args) = @_;
  1721. $key ||= 'CLI_USAGE';
  1722. my $message = sprintf($CLI_MESSAGE_FOR_ERROR{$key}, @args);
  1723. die(Fcm::CLI::Exception->new({message => $message}));
  1724. }
  1725. # ------------------------------------------------------------------------------
  1726. # Handles abort exception.
  1727. sub _cli_e_handler_of_cm_abort {
  1728. my ($function, $e) = @_;
  1729. if ($e->get_code() eq $e->FAIL) {
  1730. die(sprintf($CLI_MESSAGE_FOR_ABORT{FAIL}, $function));
  1731. }
  1732. else {
  1733. $CLI_MESSAGE->($e->get_code(), $function);
  1734. }
  1735. }
  1736. # ------------------------------------------------------------------------------
  1737. # Handles CM exception.
  1738. sub _cli_e_handler_of_cm_exception {
  1739. my ($function, $e) = @_;
  1740. die(sprintf($CLI_MESSAGE_FOR_ERROR{$e->get_code()}, $e->get_targets()));
  1741. }
  1742. # ------------------------------------------------------------------------------
  1743. # Handles CLI exception.
  1744. sub _cli_e_handler_of_cli_exception {
  1745. my ($function, $e) = @_;
  1746. $CLI_MESSAGE->('CLI', $e);
  1747. $CLI_MESSAGE->('CLI_HELP', $function);
  1748. }
  1749. # ------------------------------------------------------------------------------
  1750. # The default handler of the "WC_STATUS" event.
  1751. sub _cli_handler_of_wc_status {
  1752. my ($name, $target_list_ref, $status_list_ref) = @_;
  1753. if (@{$status_list_ref}) {
  1754. $CLI_MESSAGE->('STATUS', join(q{}, @{$status_list_ref}));
  1755. if ($CLI_PROMPT->($name, 'CONTINUE') ne 'y') {
  1756. return _cm_abort();
  1757. }
  1758. }
  1759. return @{$status_list_ref};
  1760. }
  1761. # ------------------------------------------------------------------------------
  1762. # The default handler of the "WC_STATUS_PATH" event.
  1763. sub _cli_handler_of_wc_status_path {
  1764. my ($name, $target_list_ref, $status_list_ref) = @_;
  1765. $CLI_MESSAGE->(q{}, join(q{}, @{$status_list_ref}));
  1766. my @paths = map {chomp(); ($_ =~ $PATTERN_OF{ST_PATH})} @{$status_list_ref};
  1767. my @paths_of_interest;
  1768. while (my $path = shift(@paths)) {
  1769. my %handler_of = (
  1770. a => sub {push(@paths_of_interest, $path, @paths); @paths = ()},
  1771. n => sub {},
  1772. y => sub {push(@paths_of_interest, $path)},
  1773. );
  1774. my $reply = $CLI_PROMPT->(
  1775. {type => 'yna'}, $name, 'RUN_SVN_COMMAND', "$name $path",
  1776. );
  1777. $handler_of{$reply}->();
  1778. }
  1779. return @paths_of_interest;
  1780. }
  1781. # ------------------------------------------------------------------------------
  1782. # Prints help for a given $subcommand.
  1783. sub _cli_help {
  1784. my ($key, $exit_val) = @_;
  1785. my $pod
  1786. = File::Spec->catfile(dirname($INC{'Fcm/Cm.pm'}), 'CLI', "fcm-$key.pod");
  1787. my $has_pod = -f $pod;
  1788. if ($has_pod) {
  1789. pod2usage({
  1790. '-exitval' => defined($exit_val) ? $exit_val : 2,
  1791. '-input' => $pod,
  1792. '-verbose' => 1,
  1793. });
  1794. }
  1795. if (!$has_pod || exists($CLI_MORE_HELP_FOR{$key})) {
  1796. local(@ARGV) = ($key);
  1797. return _svn('help', $key);
  1798. }
  1799. }
  1800. # ------------------------------------------------------------------------------
  1801. # Expands location keywords in a list.
  1802. sub _cli_keyword_expand_url {
  1803. my ($arg_list_ref) = @_;
  1804. ARG:
  1805. for my $arg (@{$arg_list_ref}) {
  1806. my ($label, $value) = ($arg =~ $PATTERN_OF{CLI_OPT});
  1807. if (!$label) {
  1808. ($label, $value) = (q{}, $arg);
  1809. }
  1810. if (!$value) {
  1811. next ARG;
  1812. }
  1813. eval {
  1814. $value = Fcm::Util::tidy_url(Fcm::Keyword::expand($value));
  1815. };
  1816. if ($@) {
  1817. if ($value ne 'fcm:revision') {
  1818. die($@);
  1819. }
  1820. }
  1821. $arg = $label . $value;
  1822. }
  1823. }
  1824. # ------------------------------------------------------------------------------
  1825. # Expands revision keywords in -r and --revision options in a list.
  1826. sub _cli_keyword_expand_rev {
  1827. my ($arg_list_ref) = @_;
  1828. my @targets;
  1829. for my $arg (@{$arg_list_ref}) {
  1830. if (-e $arg && is_wc($arg) || is_url($arg)) {
  1831. push(@targets, $arg);
  1832. }
  1833. }
  1834. if (!@targets) {
  1835. push(@targets, get_url_of_wc());
  1836. }
  1837. if (!@targets) {
  1838. return;
  1839. }
  1840. my @old_arg_list = @{$arg_list_ref};
  1841. my @new_arg_list = ();
  1842. ARG:
  1843. while (defined(my $arg = shift(@old_arg_list))) {
  1844. my ($key, $value) = $arg =~ $PATTERN_OF{CLI_OPT_REV};
  1845. if (!$key) {
  1846. push(@new_arg_list, $arg);
  1847. next ARG;
  1848. }
  1849. push(@new_arg_list, '--revision');
  1850. if (!$value) {
  1851. $value = shift(@old_arg_list);
  1852. }
  1853. my @revs = grep {defined()} ($value =~ $PATTERN_OF{CLI_OPT_REV_RANGE});
  1854. my ($url, @url_list) = @targets;
  1855. for my $rev (@revs) {
  1856. if ($rev !~ $PATTERN_OF{SVN_REV}) {
  1857. $rev = (Fcm::Keyword::expand($url, $rev))[1];
  1858. }
  1859. if (@url_list) {
  1860. $url = shift(@url_list);
  1861. }
  1862. }
  1863. push(@new_arg_list, join(q{:}, @revs));
  1864. }
  1865. @{$arg_list_ref} = @new_arg_list;
  1866. }
  1867. # ------------------------------------------------------------------------------
  1868. # Prints a message.
  1869. sub _cli_message {
  1870. my ($key, @args) = @_;
  1871. for (
  1872. [\*STDOUT, \%CLI_MESSAGE_FOR , q{} ],
  1873. [\*STDERR, \%CLI_MESSAGE_FOR_WARNING, q{[WARNING] }],
  1874. [\*STDERR, \%CLI_MESSAGE_FOR_ABORT , q{[ABORT] } ],
  1875. [\*STDERR, \%CLI_MESSAGE_FOR_ERROR , q{[ERROR] } ],
  1876. ) {
  1877. my ($handle, $hash_ref, $prefix) = @{$_};
  1878. if (exists($hash_ref->{$key})) {
  1879. return printf({$handle} $prefix . $hash_ref->{$key}, @args);
  1880. }
  1881. }
  1882. }
  1883. # ------------------------------------------------------------------------------
  1884. # Wrapper for Fcm::Interactive::get_input.
  1885. sub _cli_prompt {
  1886. my %option
  1887. = (type => 'yn', default => 'n', (ref($_[0]) ? %{shift(@_)} : ()));
  1888. my ($name, $key, @args) = @_;
  1889. return Fcm::Interactive::get_input(
  1890. title => $CLI_PROMPT_PREFIX . $name,
  1891. message => sprintf($CLI_MESSAGE_FOR_PROMPT{$key}, @args),
  1892. %option,
  1893. );
  1894. }
  1895. # ------------------------------------------------------------------------------
  1896. # Check missing status and delete.
  1897. sub cm_check_missing {
  1898. my %option = %{shift()};
  1899. my $checker
  1900. = _svn_status_checker('delete', 'MISSING', $option{st_check_handler});
  1901. my @paths = $checker->(\@_);
  1902. if (@paths) {
  1903. run_command([qw{svn delete}, @paths]);
  1904. }
  1905. }
  1906. # ------------------------------------------------------------------------------
  1907. # Check unknown status and add.
  1908. sub cm_check_unknown {
  1909. my %option = %{shift()};
  1910. my $checker
  1911. = _svn_status_checker('add', 'UNKNOWN', $option{st_check_handler});
  1912. my @paths = $checker->(\@_);
  1913. if (@paths) {
  1914. run_command([qw{svn add}, @paths]);
  1915. }
  1916. }
  1917. # ------------------------------------------------------------------------------
  1918. # FCM wrapper to SVN switch.
  1919. sub cm_switch {
  1920. my %option = %{shift()};
  1921. my ($target, $path) = @_;
  1922. $path ||= cwd();
  1923. if (!-e $path) {
  1924. return _cm_err(Fcm::Cm::Exception->NOT_EXIST, $path);
  1925. }
  1926. if (!is_wc($path)) {
  1927. return _cm_err(Fcm::Cm::Exception->INVALID_WC, $path);
  1928. }
  1929. # Check for merge template in the commit log file in the working copy
  1930. my $path_of_wc = get_wct($path);
  1931. my $ci_mesg = Fcm::CmCommitMessage->new();
  1932. $ci_mesg->dir($path_of_wc);
  1933. $ci_mesg->read_file();
  1934. if (@{$ci_mesg->auto_mesg()}) {
  1935. return _cm_err(
  1936. Fcm::Cm::Exception->SWITCH_UNSAFE,
  1937. $path eq $path_of_wc ? $ci_mesg->base() : $ci_mesg->file(),
  1938. );
  1939. }
  1940. # Check for any local modifications
  1941. if (defined($option{st_check_handler})) {
  1942. my $handler = $CLI_HANDLER_OF{WC_STATUS};
  1943. _svn_status_checker('switch', 'MODIFIED', $handler)->([$path_of_wc]);
  1944. }
  1945. # Invokes "svn switch"
  1946. _svn(
  1947. {METHOD => 'qx', PRINT => !$option{quiet}},
  1948. 'switch',
  1949. ($option{'non-interactive'} ? '--non-interactive' : ()),
  1950. ($option{revision} ? ('-r', $option{revision}) : ()),
  1951. ($option{quiet} ? '--quiet' : ()),
  1952. _cm_get_source(
  1953. $target,
  1954. Fcm::CmBranch->new(URL => get_url_of_wc($path_of_wc)),
  1955. )->url_peg(),
  1956. ($path_of_wc eq cwd() ? () : $path_of_wc),
  1957. );
  1958. }
  1959. # ------------------------------------------------------------------------------
  1960. # FCM wrapper to SVN update.
  1961. sub cm_update {
  1962. my %option = %{shift()};
  1963. my @targets = @_;
  1964. if (!@targets) {
  1965. @targets = (cwd());
  1966. }
  1967. for my $target (@targets) {
  1968. if (!-e $target) {
  1969. return _cm_err(Fcm::Cm::Exception->NOT_EXIST, $target);
  1970. }
  1971. if (!is_wc($target)) {
  1972. return _cm_err(Fcm::Cm::Exception->INVALID_WC, $target);
  1973. }
  1974. $target = get_wct($target);
  1975. if ($target eq cwd()) {
  1976. $target = q{.};
  1977. }
  1978. }
  1979. if (defined($option{st_check_handler})) {
  1980. my ($matcher_keys_ref, $show_updates)
  1981. = defined($option{revision}) ? (['MODIFIED' ], undef)
  1982. : (['MODIFIED', 'OUT_OF_DATE'], 1 )
  1983. ;
  1984. my $matcher = sub {
  1985. for my $key (@{$matcher_keys_ref}) {
  1986. $ST_MATCHER_FOR{$key}->(@_) && return 1;
  1987. }
  1988. };
  1989. _svn_status_checker(
  1990. 'update', $matcher, $option{st_check_handler}, $show_updates,
  1991. )->(\@targets);
  1992. }
  1993. if ($option{revision} && $option{revision} !~ $PATTERN_OF{SVN_REV}) {
  1994. $option{revision} = (
  1995. Fcm::Keyword::expand(get_url_of_wc($targets[0]), $option{revision})
  1996. )[1];
  1997. }
  1998. return _svn_update(\@targets, \%option);
  1999. }
  2000. # ------------------------------------------------------------------------------
  2001. # Raises an abort exception.
  2002. sub _cm_abort {
  2003. my ($code) = @_;
  2004. $code ||= Fcm::Cm::Abort->USER;
  2005. die(bless({code => $code, message => 'abort'}, 'Fcm::Cm::Abort'));
  2006. }
  2007. # ------------------------------------------------------------------------------
  2008. # Raises a failure.
  2009. sub _cm_err {
  2010. my ($code, @targets) = @_;
  2011. die(bless(
  2012. {code => $code, message => "ERROR: $code", targets => \@targets},
  2013. 'Fcm::Cm::Exception',
  2014. ));
  2015. }
  2016. # ------------------------------------------------------------------------------
  2017. # Returns the corresponding Fcm::CmBranch instance for $src_url w.r.t. $target.
  2018. sub _cm_get_source {
  2019. my ($src_url, $target) = @_;
  2020. my $source = Fcm::CmBranch->new(URL => $src_url);
  2021. if (!$source->is_url()) {
  2022. # Not a full URL, construct full URL based on current URL
  2023. $source->url_peg($target->url_peg());
  2024. my $project = $target->project();
  2025. my ($path) = $src_url =~ qr{\A/*(.*)\z}xms;
  2026. if (index($path, $project) == 0) {
  2027. # Argument contains the full path under the repository root
  2028. $path = substr($path, length($project));
  2029. }
  2030. if ($path =~ $PATTERN_OF{FCM_BRANCH_PATH}) {
  2031. # Argument contains the full branch name
  2032. $path = join(q{/}, $target->project_path(), $path);
  2033. }
  2034. else {
  2035. # Argument contains the shorter branch name
  2036. $path = join(q{/}, $target->project_path(), 'branches', $path);
  2037. }
  2038. $source->path_peg($path);
  2039. }
  2040. # Replace source sub-directory with the target sub-directory
  2041. $source->subdir($target->subdir());
  2042. # Ensure that the branch name exists
  2043. if (!$source->url_exists()) {
  2044. return _cm_err(Fcm::Cm::Exception->INVALID_URL, $src_url);
  2045. }
  2046. # Ensure that the branch name is valid
  2047. if (!$source->branch()) {
  2048. return _cm_err(Fcm::Cm::Exception->INVALID_BRANCH, $src_url);
  2049. }
  2050. # Ensure that the source and target URLs are in the same project
  2051. if ($source->project_url() ne $target->project_url()) {
  2052. return _cm_err(
  2053. Fcm::Cm::Exception->DIFF_PROJECTS,
  2054. $target->url_peg(),
  2055. $source->url_peg(),
  2056. );
  2057. }
  2058. return $source;
  2059. }
  2060. # ------------------------------------------------------------------------------
  2061. # Runs "svn".
  2062. sub _svn {
  2063. my @args = @_;
  2064. my %option;
  2065. if (@args && ref($args[0])) {
  2066. %option = %{shift(@args)};
  2067. }
  2068. return run_command(
  2069. ['svn', @args],
  2070. PRINT => ($args[0] ne 'cat' && !grep {$_ eq '--xml'} @args),
  2071. %option,
  2072. );
  2073. }
  2074. # ------------------------------------------------------------------------------
  2075. # Returns the results of "svn status".
  2076. sub _svn_status_get {
  2077. my ($target_list_ref, $show_updates) = @_;
  2078. my @targets = (defined($target_list_ref) ? @{$target_list_ref} : ());
  2079. for my $target (@targets) {
  2080. if ($target eq cwd()) {
  2081. $target = q{.};
  2082. }
  2083. }
  2084. my @options = ($show_updates ? qw{--show-updates} : ());
  2085. return _svn({METHOD => 'qx', PRINT => 0}, 'status', @options, @targets);
  2086. }
  2087. # ------------------------------------------------------------------------------
  2088. # Returns a "svn status" checker.
  2089. sub _svn_status_checker {
  2090. my ($name, $matcher, $handler, $show_updates) = @_;
  2091. if (!ref($matcher)) {
  2092. $matcher = $ST_MATCHER_FOR{$matcher};
  2093. }
  2094. return sub {
  2095. my ($target_list_ref) = @_;
  2096. my @status = _svn_status_get($target_list_ref, $show_updates);
  2097. if ($show_updates) {
  2098. @status = map {$_ =~ $PATTERN_OF{ST_AGAINST_REV} ? () : $_} @status;
  2099. }
  2100. my @status_of_interest = grep {$matcher->($_)} @status;
  2101. if (defined($handler)) {
  2102. return $handler->($name, $target_list_ref, \@status_of_interest);
  2103. }
  2104. return @status_of_interest;
  2105. }
  2106. }
  2107. # ------------------------------------------------------------------------------
  2108. # Runs "svn update".
  2109. sub _svn_update {
  2110. my ($target_list_ref, $option_hash_ref) = @_;
  2111. my %option = (defined($option_hash_ref) ? %{$option_hash_ref} : ());
  2112. _svn(
  2113. {METHOD => 'qx', PRINT => !$option{quiet}},
  2114. 'update',
  2115. ($option{'non-interactive'} ? '--non-interactive' : ()),
  2116. ($option{revision} ? ('-r', $option{revision}) : ()),
  2117. ($option{quiet} ? '--quiet' : ()),
  2118. (defined($target_list_ref) ? @{$target_list_ref} : ()),
  2119. );
  2120. }
  2121. # ------------------------------------------------------------------------------
  2122. # Abort exception.
  2123. package Fcm::Cm::Abort;
  2124. use base qw{Fcm::Exception};
  2125. use constant {FAIL => 'FAIL', NULL => 'NULL', USER => 'USER'};
  2126. sub get_code {
  2127. return $_[0]->{code};
  2128. }
  2129. # ------------------------------------------------------------------------------
  2130. # Resource exception.
  2131. package Fcm::Cm::Exception;
  2132. our @ISA = qw{Fcm::Cm::Abort};
  2133. use constant {
  2134. CHDIR => 'CHDIR',
  2135. INVALID_BRANCH => 'INVALID_BRANCH',
  2136. INVALID_PROJECT => 'INVALID_PROJECT',
  2137. INVALID_TARGET => 'INVALID_TARGET',
  2138. INVALID_URL => 'INVALID_URL',
  2139. INVALID_WC => 'INVALID_WC',
  2140. MERGE_REV_INVALID => 'MERGE_REV_INVALID',
  2141. MERGE_SELF => 'MERGE_SELF',
  2142. MERGE_UNRELATED => 'MERGE_UNRELATED',
  2143. MERGE_UNSAFE => 'MERGE_UNSAFE',
  2144. MKPATH => 'MKPATH',
  2145. NOT_EXIST => 'NOT_EXIST',
  2146. PARENT_NOT_EXIST => 'PARENT_NOT_EXIST',
  2147. RMTREE => 'RMTREE',
  2148. SWITCH_UNSAFE => 'SWITCH_UNSAFE',
  2149. WC_EXIST => 'WC_EXIST',
  2150. WC_INVALID_BRANCH => 'WC_INVALID_BRANCH',
  2151. WC_URL_NOT_EXIST => 'WC_URL_NOT_EXIST',
  2152. };
  2153. sub get_targets {
  2154. return @{$_[0]->{targets}};
  2155. }
  2156. 1;
  2157. __END__
  2158. =pod
  2159. =head1 NAME
  2160. Fcm::Cm
  2161. =head1 SYNOPSIS
  2162. use Fcm::Cm qw{cli};
  2163. # Use as a wrapper to Subversion, and other FCM code management commands
  2164. cli('info', '--revision', 'HEAD', $url);
  2165. use Fcm::Cm qw{cm_check_missing cm_check_unknown cm_switch cm_update};
  2166. # Checks status for "missing" items and "svn delete" them
  2167. $missing_st_handler = sub {
  2168. my ($name, $target_list_ref, $status_list_ref) = @_;
  2169. # ...
  2170. return @paths_of_interest;
  2171. };
  2172. cm_check_missing({st_check_handler => $missing_st_handler}, @targets);
  2173. # Checks status for "unknown" items and "svn add" them
  2174. $unknown_st_handler = sub {
  2175. my ($name, $target_list_ref, $status_list_ref) = @_;
  2176. # ...
  2177. return @paths_of_interest;
  2178. };
  2179. cm_check_unknown({st_check_handler => $unknown_st_handler}, @targets);
  2180. # Sets up a status checker
  2181. $st_check_handler = sub {
  2182. my ($name, $target_list_ref, $status_list_ref) = @_;
  2183. # ...
  2184. };
  2185. # Switches a "working copy" at the "root" level to a new URL target
  2186. cm_switch(
  2187. {
  2188. 'non-interactive' => $non_interactive_flag,
  2189. 'quiet' => $quiet_flag,
  2190. 'revision' => $revision,
  2191. 'st_check_handler' => $st_check_handler,
  2192. },
  2193. $target, $path_of_wc,
  2194. );
  2195. # Runs "svn update" on each working copy from their "root" level
  2196. cm_update(
  2197. {
  2198. 'non-interactive' => $non_interactive_flag,
  2199. 'quiet' => $quiet_flag,
  2200. 'revision' => $revision,
  2201. 'st_check_handler' => $st_check_handler,
  2202. },
  2203. @targets,
  2204. );
  2205. =head1 DESCRIPTION
  2206. Wraps the Subversion client and implements other FCM code management
  2207. functionalities.
  2208. =head1 FUNCTIONS
  2209. =over 4
  2210. =item cli($function,@args)
  2211. Implements the FCM code management CLI. If --help or -h is specified in @args,
  2212. it displays help and returns. Otherwise, it attempts to expand any FCM location
  2213. and revision keywords in @args. Calls the relevant FCM code management function
  2214. according to $function, or a SVN command if $function is not modified by FCM.
  2215. =item cm_check_missing(\%option,@targets)
  2216. Use "svn status" to check for missing items in @targets. If @targets is an empty
  2217. list, the function adds the current working directory to it. Expects
  2218. $option{st_check_handler} to be a CODE reference. Calls
  2219. $option{st_check_handler} with ($name, $target_list_ref, $status_list_ref) where
  2220. $name is "delete", $target_list_ref is \@targets, and $status_list_ref is an
  2221. ARRAY reference to a list of "svn status" output with the "missing" status.
  2222. $option{st_check_handler} should return a list of interesting paths, which will
  2223. be scheduled for removal using "svn delete".
  2224. =item cm_check_unknown(\%option,@targets)
  2225. Similar to cm_check_missing(\%option,@targets) but checks for "unknown" items,
  2226. which will be scheduled for addition using "svn add".
  2227. =item cm_switch(\%option,$target,$path_of_wc)
  2228. Invokes "svn switch" at the root of a working copy specified by $path_of_wc (or
  2229. the current working directory if $path_of_wc is not specified).
  2230. $option{'non-interactive'}, $option{quiet}, $option{revision} determines the
  2231. options (of the same name) that are passed to "svn switch". If
  2232. $option{st_check_handler} is set, it should be a CODE reference, and will be
  2233. called with ('switch', [$path_of_wc], $status_list_ref), where $status_list_ref
  2234. is an ARRAY reference to the output returned by "svn status" on $path_of_wc.
  2235. This can be used for the application to display the working copy status to the
  2236. user before prompting him/her to continue. The return value of
  2237. $option{st_check_handler} is ignored.
  2238. =item cm_update(\%option,@targets)
  2239. Invokes "svn update" at the root of each working copy specified by @targets. If
  2240. @targets is an empty list, the function adds the current working directory to
  2241. it. $option{'non-interactive'}, $option{quiet}, $option{revision} determines the
  2242. options (of the same name) that are passed to "svn update". If
  2243. $option{st_check_handler} is set, it should be a CODE reference, and will be
  2244. called with ($name, $target_list_ref, $status_list_ref), where $name is
  2245. 'update', $target_list_ref is \@targets and $status_list_ref is an ARRAY
  2246. reference to the output returned by "svn status -u" on the @targets. This can be
  2247. used for the application to display the working copy update status to the user
  2248. before prompting him/her to continue. The return value of
  2249. $option{st_check_handler} is ignored.
  2250. =back
  2251. =head1 DIAGNOSTICS
  2252. The following exceptions can be raised:
  2253. =over 4
  2254. =item Fcm::Cm::Abort
  2255. This exception @ISA L<Fcm::Exception|Fcm::Exception>. It is raised if a command
  2256. is aborted for some reason. The $e->get_code() method can be used to retrieve an
  2257. error code, which can be one of the following:
  2258. =over 4
  2259. =item $e->FAIL
  2260. The command aborts because of a failure.
  2261. =item $e->NULL
  2262. The command aborts because it will result in no change.
  2263. =item $e->USER
  2264. The command aborts because of an action by the user.
  2265. =back
  2266. =item Fcm::Cm::Exception
  2267. This exception @ISA L<Fcm::Abort|Fcm::Abort>. It is raised if a command fails
  2268. with a known reason. The $e->get_targets() method can be used to retrieve a list
  2269. of targets/resources associated with this exception. The $e->get_code() method
  2270. can be used to retrieve an error code, which can be one of the following:
  2271. =over 4
  2272. =item $e->CHDIR
  2273. Fails to change directory to a target.
  2274. =item $e->INVALID_BRANCH
  2275. A target is not a valid branch URL in the standard FCM project layout.
  2276. =item $e->INVALID_PROJECT
  2277. A target is not a valid project URL in the standard FCM project layout.
  2278. =item $e->INVALID_TARGET
  2279. A target is not a valid Subversion URL or working copy.
  2280. =item $e->INVALID_URL
  2281. A target is not a valid Subversion URL.
  2282. =item $e->INVALID_WC
  2283. A target is not a valid Subversion working copy.
  2284. =item $e->MERGE_REV_INVALID
  2285. An invalid revision (target element 0) is specified for a merge.
  2286. =item $e->MERGE_SELF
  2287. Attempt to merge a URL (target element 0) to its own working copy (target
  2288. element 1).
  2289. =item $e->MERGE_UNRELATED
  2290. The merge target (target element 0) is not directly related to the merge source
  2291. (target element 1).
  2292. =item $e->MERGE_UNSAFE
  2293. A merge source (target element 0) contains changes outside the target
  2294. sub-directory.
  2295. =item $e->MKPATH
  2296. Fail to create a directory (target element 0) recursively.
  2297. =item $e->NOT_EXIST
  2298. A target does not exist.
  2299. =item $e->PARENT_NOT_EXIST
  2300. The parent of the target no longer exists.
  2301. =item $e->RMTREE
  2302. Fail to remove a directory (target element 0) recursively.
  2303. =item $e->SWITCH_UNSAFE
  2304. A merge template exists in the commit message file (target element 0) in a
  2305. working copy target.
  2306. =item $e->WC_EXIST
  2307. The target working copy already exists.
  2308. =item $e->WC_INVALID_BRANCH
  2309. The URL of the target working copy is not a valid branch URL in the standard FCM
  2310. project layout.
  2311. =item $e->WC_URL_NOT_EXIST
  2312. The URL of the target working copy no longer exists at the HEAD revision.
  2313. =back
  2314. =back
  2315. =head1 TO DO
  2316. Reintegrate with L<Fcm::CmUrl|Fcm::CmUrl> and L<Fcm::CmBranch|Fcm::CmBranch>,
  2317. but separate this module into the CLI part and the CM part. Expose the remaining
  2318. CM functions when this is done.
  2319. Use L<SVN::Client|SVN::Client> to interface with Subversion.
  2320. Move C<mkpatch> out of this module.
  2321. =head1 COPYRIGHT
  2322. E<169> Crown copyright Met Office. All rights reserved.
  2323. =cut