model.php 115 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183
  1. <?php
  2. /**
  3. * The model file of repo module of ZenTaoPMS.
  4. *
  5. * @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.cnezsoft.com)
  6. * @license ZPL(http://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
  7. * @author Yanyi Cao <caoyanyi@cnezsoft.com>
  8. * @package repo
  9. * @property repoTao $repoTao
  10. * @link https://www.zentao.net
  11. */
  12. class repoModel extends model
  13. {
  14. /**
  15. * 检查代码库的权限。
  16. * Check repo priv.
  17. *
  18. * @param object $repo
  19. * @access public
  20. * @return bool
  21. */
  22. public function checkPriv($repo)
  23. {
  24. $account = $this->app->user->account;
  25. $acl = !empty($repo->acl->acl) ? $repo->acl->acl : 'custom';
  26. if(empty($repo->acl)) $repo->acl = new stdclass();
  27. if(empty($repo->acl->users)) $repo->acl->users = array();
  28. if(empty($repo->acl->groups)) $repo->acl->groups = array();
  29. if(strpos(",{$this->app->company->admins},", ",$account,") !== false || $acl == 'open') return true;
  30. if($acl == 'custom' && empty(array_filter($repo->acl->groups)) && empty(array_filter($repo->acl->users))) return true;
  31. if($acl == 'private')
  32. {
  33. $userProducts = explode(',', $this->app->user->view->products);
  34. $repoProducts = explode(',', $repo->product);
  35. if(array_intersect($userProducts, $repoProducts)) return true;
  36. }
  37. if(!empty($repo->acl->groups))
  38. {
  39. foreach($this->app->user->groups as $group)
  40. {
  41. if(in_array($group, $repo->acl->groups)) return true;
  42. }
  43. }
  44. if(!empty($repo->acl->users) and in_array($account, $repo->acl->users)) return true;
  45. return false;
  46. }
  47. /**
  48. * 设置菜单链接信息。
  49. * Set menu.
  50. *
  51. * @param array $repos
  52. * @param int $repoID
  53. * @access public
  54. * @return void
  55. */
  56. public function setMenu($repos, $repoID = 0)
  57. {
  58. if(empty($repoID)) $repoID = $this->session->repoID ? $this->session->repoID : key($repos);
  59. if(!isset($repos[$repoID])) $repoID = key($repos);
  60. $repoID = (int)$repoID;
  61. /* Check the privilege. */
  62. if($repoID)
  63. {
  64. $repo = $this->getByID($repoID);
  65. if(!$repo || !$this->checkPriv($repo)) $repoID = 0;
  66. if(!$repo || !in_array(strtolower($repo->SCM), $this->config->repo->gitServiceList)) unset($this->lang->devops->menu->mr);
  67. if(!$repo || !in_array($repo->SCM, $this->config->repo->notSyncSCM))
  68. {
  69. unset($this->lang->devops->menu->tag);
  70. unset($this->lang->devops->menu->branch);
  71. }
  72. }
  73. if(!in_array($this->app->methodName, $this->config->repo->notSetMenuVars)) common::setMenuVars('devops', $repoID);
  74. if(!session_id()) session_start();
  75. $this->session->set('repoID', $repoID);
  76. session_write_close();
  77. }
  78. /**
  79. * 获取代码库列表。
  80. * Get repo list.
  81. *
  82. * @param int $projectID
  83. * @param string $SCM Subversion|Git|Gitlab
  84. * @param string $orderBy
  85. * @param object $pager
  86. * @param bool $getCodePath
  87. * @access public
  88. * @return array
  89. * @param bool $lastSubmitTime
  90. * @param string $type
  91. * @param int $param
  92. */
  93. public function getList($projectID = 0, $SCM = '', $orderBy = 'id_desc', $pager = null, $getCodePath = false, $lastSubmitTime = false, $type = '', $param = 0)
  94. {
  95. $repoQuery = $type == 'bySearch' ? $this->repoTao->processSearchQuery($param) : '';
  96. $repos = $this->getListByCondition($repoQuery, $SCM, $orderBy, $pager);
  97. /* Get products. */
  98. $productIdList = $this->loadModel('product')->getProductIDByProject($projectID, false);
  99. foreach($repos as $i => $repo)
  100. {
  101. $repo->acl = json_decode($repo->acl);
  102. $repo->codePath = $repo->path;
  103. if(!$this->checkPriv($repo))
  104. {
  105. unset($repos[$i]);
  106. }
  107. elseif($projectID)
  108. {
  109. $hasPriv = false;
  110. foreach(explode(',', $repo->product) as $productID)
  111. {
  112. if(isset($productIdList[$productID])) $hasPriv = true;
  113. }
  114. if(!$hasPriv) unset($repos[$i]);
  115. }
  116. if($lastSubmitTime) $repo->lastSubmitTime = $repo->lastCommit ? $repo->lastCommit : $this->repoTao->getLastRevision($repo->id);
  117. if(in_array(strtolower($repo->SCM), $this->config->repo->gitServiceList)) $repo = $this->processGitService($repo, $getCodePath);
  118. }
  119. return $repos;
  120. }
  121. /**
  122. * 根据SCM和权限获取代码库列表。
  123. * Get list by SCM.
  124. *
  125. * @param string $scm
  126. * @param string $type all|haspriv
  127. * @access public
  128. * @return array
  129. */
  130. public function getListBySCM($scm, $type = 'all')
  131. {
  132. $repos = $this->dao->select('*,acl')->from(TABLE_REPO)->where('deleted')->eq('0')
  133. ->andWhere('SCM')->in($scm)
  134. ->andWhere('synced')->eq(1)
  135. ->fetchAll('id', false);
  136. foreach($repos as $i => $repo)
  137. {
  138. if($repo->encrypt == 'base64') $repo->password = base64_decode($repo->password);
  139. $repo->acl = json_decode($repo->acl);
  140. $repo->codePath = $repo->path;
  141. if($type == 'haspriv' and !$this->checkPriv($repo)) unset($repos[$i]);
  142. if(in_array(strtolower($repo->SCM), $this->config->repo->gitServiceList)) $repo = $this->processGitService($repo);
  143. }
  144. return $repos;
  145. }
  146. /**
  147. * 创建版本库。
  148. * Create a repo.
  149. *
  150. * @param object $repo
  151. * @param bool $isPipelineServer
  152. * @access public
  153. * @return int|false
  154. */
  155. public function create($repo, $isPipelineServer)
  156. {
  157. $this->dao->insert(TABLE_REPO)->data($repo, 'serviceToken')
  158. ->batchCheck($this->config->repo->create->requiredFields, 'notempty')
  159. ->batchCheckIF(!in_array($repo->SCM, $this->config->repo->notSyncSCM), 'path,client', 'notempty')
  160. ->batchCheckIF($isPipelineServer, 'serviceHost,serviceProject', 'notempty')
  161. ->batchCheckIF($repo->SCM == 'Subversion', $this->config->repo->svn->requiredFields, 'notempty')
  162. ->check('name', 'unique', "`SCM` = " . $this->dao->sqlobj->quote($repo->SCM))
  163. ->checkIF(!$isPipelineServer, 'path', 'unique', "`SCM` = " . $this->dao->sqlobj->quote($repo->SCM) . " and `serviceHost` = " . $this->dao->sqlobj->quote($repo->serviceHost))
  164. ->autoCheck()
  165. ->exec();
  166. if(dao::isError()) return false;
  167. $repoID = $this->dao->lastInsertID();
  168. $repo = $this->getByID($repoID);
  169. if(in_array($repo->SCM, $this->config->repo->notSyncSCM))
  170. {
  171. $token = uniqid();
  172. $res = $this->loadModel($repo->SCM)->addPushWebhook($repo, $token);
  173. if($res !== true)
  174. {
  175. $this->dao->delete()->from(TABLE_REPO)->where('id')->eq($repoID)->exec();
  176. dao::$errors['webhook'][] = isset($res['message']) ? $res['message'] : $this->lang->gitlab->failCreateWebhook;
  177. return false;
  178. }
  179. else
  180. {
  181. $this->dao->update(TABLE_REPO)->set('password')->eq($token)->where('id')->eq($repoID)->exec();
  182. }
  183. }
  184. $this->rmClientVersionFile();
  185. return $repoID;
  186. }
  187. /**
  188. * 创建远程版本库。
  189. * Create a repo.
  190. *
  191. * @param object $repo
  192. * @access public
  193. * @return int|false
  194. */
  195. public function createRepo($repo)
  196. {
  197. $check = $this->checkName($repo->name);
  198. if(!$check)
  199. {
  200. dao::$errors['name'] = $this->lang->repo->error->repoNameInvalid;
  201. return false;
  202. }
  203. $response = $this->createGitlabRepo($repo, $repo->namespace);
  204. $this->loadModel('gitlab');
  205. if(!empty($response->id))
  206. {
  207. $repo->path = $response->path;
  208. $repo->serviceProject = $response->serviceProject;
  209. $repo->extra = $response->extra;
  210. $repo->SCM = 'Gitlab';
  211. unset($repo->namespace);
  212. $repoID = $this->create($repo, false);
  213. if(dao::isError())
  214. {
  215. $this->gitlab->apiDeleteProject($repo->serviceHost, $response->id);
  216. return false;
  217. }
  218. return $repoID;
  219. }
  220. return $this->gitlab->apiErrorHandling($response);
  221. }
  222. /**
  223. * 创建gitlab远程版本库。
  224. * Create gitlab repo.
  225. *
  226. * @param object $repo
  227. * @param string $namespace
  228. * @access public
  229. * @return object|false
  230. */
  231. public function createGitlabRepo($repo, $namespace)
  232. {
  233. $project = new stdclass();
  234. $project->name = $repo->name;
  235. $project->path = $repo->name;
  236. $project->description = $repo->desc;
  237. $project->namespace_id = (int)$namespace;
  238. $project->initialize_with_readme = true;
  239. $response = $this->loadModel('gitlab')->apiCreateProject($repo->serviceHost, $project);
  240. if(empty($response->id)) return $response;
  241. $result = new stdclass();
  242. $result->id = $response->id;
  243. $result->path = $response->web_url;
  244. $result->serviceProject = $response->id;
  245. $result->extra = $response->id;
  246. return $result;
  247. }
  248. /**
  249. * 批量创建版本库。
  250. * Batch create repos.
  251. *
  252. * @param array $repos
  253. * @param int $serviceHost
  254. * @param string $scm
  255. * @access public
  256. * @return bool
  257. */
  258. public function batchCreate($repos, $serviceHost, $scm)
  259. {
  260. $this->loadModel('instance');
  261. foreach($repos as $index => $repo)
  262. {
  263. if(empty($repo->product)) continue;
  264. if(empty($repo->name))
  265. {
  266. dao::$errors["name[$index]"] = sprintf($this->lang->error->notempty, $this->lang->repo->name);
  267. return false;
  268. }
  269. $repo->serviceHost = $serviceHost;
  270. $repo->SCM = $scm;
  271. $this->dao->insert(TABLE_REPO)->data($repo)
  272. ->batchCheck($this->config->repo->create->requiredFields, 'notempty')
  273. ->check('serviceHost,serviceProject', 'notempty')
  274. ->check('name', 'unique', "`SCM` = " . $this->dao->sqlobj->quote($repo->SCM))
  275. ->check('serviceProject', 'unique', "`SCM` = " . $this->dao->sqlobj->quote($repo->SCM) . " and `serviceHost` = " . $this->dao->sqlobj->quote($repo->serviceHost))
  276. ->autoCheck()
  277. ->exec();
  278. if(dao::isError()) return false;
  279. $repoID = $this->dao->lastInsertID();
  280. if(in_array($repo->SCM, $this->config->repo->notSyncSCM))
  281. {
  282. /* Add webhook. */
  283. $repo = $this->getByID($repoID);
  284. $this->loadModel($repo->SCM)->addPushWebhook($repo);
  285. $this->{$repo->SCM}->updateCodePath($repo->serviceHost, (int)$repo->serviceProject, (int)$repo->id);
  286. }
  287. $this->loadModel('action')->create('repo', $repoID, 'created');
  288. if(method_exists($this->instance, 'saveWaitSyncData')) $this->instance->saveWaitSyncData('repo', $repoID, 'add', false);
  289. }
  290. return true;
  291. }
  292. /**
  293. * 更新版本库。
  294. * Update a repo.
  295. *
  296. * @param object $data
  297. * @param object $repo
  298. * @param bool $isPipelineServer
  299. * @access public
  300. * @return bool
  301. */
  302. public function update($data, $repo, $isPipelineServer)
  303. {
  304. if(($repo->serviceHost != $data->serviceHost || $repo->serviceProject != $data->serviceProject) && $data->SCM == 'Gitlab')
  305. {
  306. $repo->gitService = $data->serviceHost;
  307. $repo->project = $data->serviceProject;
  308. $token = uniqid();
  309. $res = $this->loadModel('gitlab')->addPushWebhook($repo, $token);
  310. if($res !== true)
  311. {
  312. dao::$errors['webhook'][] = isset($res['message']) ? $res['message'] : $this->lang->gitlab->failCreateWebhook;
  313. return false;
  314. }
  315. else
  316. {
  317. $data->password = $token;
  318. }
  319. }
  320. if($data->SCM == 'Subversion' && $data->path != $repo->path)
  321. {
  322. $data->synced = 0;
  323. $data->lastSync = null;
  324. $data->lastCommit = null;
  325. }
  326. if($data->encrypt == 'base64') $data->password = base64_encode((string)$data->password);
  327. $this->dao->update(TABLE_REPO)->data($data, 'serviceToken')
  328. ->batchCheck($this->config->repo->edit->requiredFields, 'notempty')
  329. ->batchCheckIF($data->SCM != 'Gitlab', 'path,client', 'notempty')
  330. ->batchCheckIF($isPipelineServer, 'serviceHost,serviceProject', 'notempty')
  331. ->batchCheckIF($data->SCM == 'Subversion', $this->config->repo->svn->requiredFields, 'notempty')
  332. ->check('name', 'unique', "`SCM` = " . $this->dao->sqlobj->quote($data->SCM) . " and `id` != $repo->id")
  333. ->checkIF(!$isPipelineServer, 'path', 'unique', "`SCM` = " . $this->dao->sqlobj->quote($data->SCM) . " and `serviceHost` = " . $this->dao->sqlobj->quote($data->serviceHost) . " and `id` != $repo->id")
  334. ->autoCheck()
  335. ->where('id')->eq($repo->id)->exec();
  336. $this->rmClientVersionFile();
  337. if(in_array($data->SCM, $this->config->repo->notSyncSCM))
  338. {
  339. $this->loadModel($data->SCM)->updateCodePath($data->serviceHost, (int)$data->serviceProject, $repo->id);
  340. $data->path = $this->getByID($repo->id)->path;
  341. $this->updateCommitDate($repo->id);
  342. }
  343. if(($repo->serviceHost != $data->serviceHost || $repo->serviceProject != $data->serviceProject || $repo->SCM == 'Subversion') && $repo->path != $data->path)
  344. {
  345. $this->repoTao->deleteInfoByID($repo->id);
  346. return false;
  347. }
  348. return true;
  349. }
  350. /**
  351. * 代码提交关联任务、需求、Bug。
  352. * Link commit to story, bug, task.
  353. *
  354. * @param int $repoID
  355. * @param string $revision
  356. * @param string $type
  357. * @param string $from repo|commit
  358. * @access public
  359. * @return void
  360. */
  361. public function link($repoID, $revision, $type = 'story', $from = 'repo')
  362. {
  363. $this->loadModel('action');
  364. if($type == 'story') $links = $objects['stories'] = $this->post->stories;
  365. if($type == 'bug') $links = $objects['bugs'] = $this->post->bugs;
  366. if($type == 'task') $links = $objects['task'] = $this->post->tasks;
  367. $revisionInfo = $this->dao->select('*')->from(TABLE_REPOHISTORY)->where('repo')->eq($repoID)->andWhere('revision')->eq($revision)->fetch();
  368. if(empty($revisionInfo))
  369. {
  370. $repo = $this->getByID($repoID);
  371. if(in_array($repo->SCM, $this->config->repo->notSyncSCM))
  372. {
  373. $scm = $this->app->loadClass('scm');
  374. $scm->setEngine($repo);
  375. $logs = $scm->getCommits($revision, 1);
  376. $this->saveCommit($repoID, $logs, 0);
  377. }
  378. else
  379. {
  380. $this->updateCommit($repoID);
  381. }
  382. }
  383. $revisionInfo = $this->dao->select('*')->from(TABLE_REPOHISTORY)->where('repo')->eq($repoID)->andWhere('revision')->eq($revision)->fetch();
  384. if(empty($revisionInfo))
  385. {
  386. dao::$errors = $this->lang->fail;
  387. return false;
  388. }
  389. $revisionID = $revisionInfo->id;
  390. $committer = $this->dao->select('account')->from(TABLE_USER)->where('commiter')->eq($revisionInfo->committer)->fetch('account');
  391. if(empty($committer)) $committer = $revisionInfo->committer;
  392. if($from == 'repo') $committer = $this->app->user->account;
  393. foreach($links as $linkID)
  394. {
  395. $relation = new stdclass;
  396. $relation->AType = 'revision';
  397. $relation->AID = $revisionID;
  398. $relation->relation = 'commit';
  399. $relation->BType = $type;
  400. $relation->BID = $linkID;
  401. $relation->product = 0;
  402. /* record module related information. */
  403. $this->loadModel($type)->updateLinkedCommits((int)$linkID, $repoID, [$revisionID]);
  404. $this->dao->replace(TABLE_RELATION)->data($relation)->exec();
  405. $this->action->create($type, (int)$linkID, 'linked2revision', '', substr($revisionInfo->revision, 0, 10), $committer);
  406. }
  407. return !dao::isError();
  408. }
  409. /**
  410. * 删除一个版本库。
  411. * Delete a repo.
  412. *
  413. * @param int $repoID
  414. * @access public
  415. * @return bool
  416. */
  417. public function deleteRepo($repoID)
  418. {
  419. $this->repoTao->deleteInfoByID($repoID);
  420. $this->dao->delete()->from(TABLE_REPO)->where('id')->eq($repoID)->exec();
  421. if(dao::isError()) return false;
  422. $this->loadModel('action')->create('repo', $repoID, 'deleted', '');
  423. return true;
  424. }
  425. /**
  426. * 取消代码提交关联的任务、需求、Bug。
  427. * Unlink object and commit revision.
  428. *
  429. * @param int $repoID
  430. * @param string $revision
  431. * @param string $objectType story|bug|task
  432. * @param int $objectID
  433. * @access public
  434. * @return bool
  435. */
  436. public function unlink($repoID, $revision, $objectType, $objectID)
  437. {
  438. $revisionID = $this->dao->select('id')->from(TABLE_REPOHISTORY)->where('repo')->eq($repoID)->andWhere('revision')->eq($revision)->fetch('id');
  439. $this->dao->delete()->from(TABLE_RELATION)
  440. ->where('AID')->eq($revisionID)
  441. ->andWhere('AType')->eq('revision')
  442. ->andWhere('relation')->eq('commit')
  443. ->andWhere('BType')->eq($objectType)
  444. ->andWhere('BID')->eq($objectID)->exec();
  445. $this->dao->delete()->from(TABLE_RELATION)
  446. ->where('AType')->eq($objectType)
  447. ->andWhere('AID')->eq($objectID)
  448. ->andWhere('BType')->eq('commit')
  449. ->andWhere('BID')->eq($revisionID)
  450. ->andWhere('relation')->eq('completedin')->exec();
  451. $this->dao->delete()->from(TABLE_RELATION)
  452. ->where('AType')->eq('commit')
  453. ->andWhere('AID')->eq($revisionID)
  454. ->andWhere('BType')->eq('story')
  455. ->andWhere('BID')->eq($objectID)
  456. ->andWhere('relation')->eq('completedfrom')->exec();
  457. if(!dao::isError()) $this->loadModel('action')->create($objectType, $objectID, 'unlinkedfromrevision', '', substr($revision, 0, 10));
  458. return !dao::isError();
  459. }
  460. /**
  461. * 设置代码库id。
  462. * Save repo state.
  463. *
  464. * @param int $repoID
  465. * @param int $objectID
  466. * @access public
  467. * @return int
  468. */
  469. public function saveState($repoID = 0, $objectID = 0)
  470. {
  471. if(session_id()) session_write_close();
  472. if(!defined('RUN_MODE') || RUN_MODE != 'test') session_start();
  473. if($repoID > 0) $this->session->set('repoID', (int)$repoID);
  474. $repos = $this->getRepoPairs($this->app->tab, $objectID);
  475. if($repoID == 0 && $this->session->repoID == '') $this->session->set('repoID', key($repos));
  476. if(!isset($repos[$this->session->repoID])) $this->session->set('repoID', key($repos));
  477. $repoID = (int)$this->session->repoID;
  478. session_write_close();
  479. return $repoID;
  480. }
  481. /**
  482. * 获取代码库列表键值对。
  483. * Get repo pairs.
  484. *
  485. * @param string $type project|execution|repo
  486. * @param int $projectID
  487. * @param bool $showScm
  488. * @access public
  489. * @return array
  490. */
  491. public function getRepoPairs($type, $projectID = 0, $showScm = true)
  492. {
  493. if(common::isTutorialMode()) return $this->loadModel('tutorial')->getRepoPairs();
  494. $repos = $this->dao->select('*,acl')->from(TABLE_REPO)
  495. ->where('deleted')->eq(0)
  496. ->fetchAll('id', false);
  497. /* Get products. */
  498. $productIdList = ($type == 'project' or $type == 'execution') ? $this->loadModel('product')->getProductIDByProject($projectID, false) : array();
  499. $repoPairs = array();
  500. foreach($repos as $repo)
  501. {
  502. $repo->acl = json_decode($repo->acl);
  503. $scm = '';
  504. if($showScm) $scm = $repo->SCM == 'Subversion' ? '[svn] ' : '[' . strtolower($repo->SCM) . '] ';
  505. if($this->checkPriv($repo))
  506. {
  507. if(($type == 'project' or $type == 'execution') and $projectID)
  508. {
  509. foreach($productIdList as $productID)
  510. {
  511. if(strpos(",$repo->product,", ",$productID,") !== false) $repoPairs[$repo->id] = $scm . $repo->name;
  512. }
  513. }
  514. else
  515. {
  516. $repoPairs[$repo->id] = $scm . $repo->name;
  517. }
  518. }
  519. }
  520. return $repoPairs;
  521. }
  522. /**
  523. * 根据应用获取代码库分组。
  524. * Get repos group by repo type.
  525. *
  526. * @param string $type
  527. * @param int $projectID
  528. * @param array $scmList
  529. * @access public
  530. * @return array
  531. */
  532. public function getRepoGroup($type, $projectID = 0, $scmList = array())
  533. {
  534. $repos = $this->getList(0, implode(',', $scmList));
  535. $productIds = $productItems = array();
  536. if($projectID)
  537. {
  538. $productIds = $this->loadModel('product')->getProductIDByProject($projectID, false);
  539. }
  540. else
  541. {
  542. foreach($repos as $repo) $productIds = array_merge($productIds, explode(',', $repo->product));
  543. }
  544. $products = $this->loadModel('product')->getByIdList(array_unique($productIds));
  545. foreach($products as $productID => $product)
  546. {
  547. $productItem = array();
  548. $productItem['pid'] = $productID;
  549. $productItem['type'] = $product->shadow ? $this->lang->project->common : 'product';
  550. $productItem['text'] = $product->name;
  551. $productItem['items'] = array();
  552. $productItems[$productID] = $productItem;
  553. }
  554. /* Get project products. */
  555. $projectProductIds = in_array($type, array('project', 'execution')) ? $this->loadModel('product')->getProductIDByProject($projectID, false) : array();
  556. /* Get repo data for dropmenu. */
  557. $repoPairs = array();
  558. foreach($repos as $repo)
  559. {
  560. $repo->acl = is_string($repo->acl) ? json_decode($repo->acl) : $repo->acl;
  561. if($this->checkPriv($repo))
  562. {
  563. $repoItem = array();
  564. $repoItem['id'] = $repo->id;
  565. $repoItem['text'] = $repo->name;
  566. $repoItem['keys'] = zget(common::convert2Pinyin(array($repo->name)), $repo->name, '');
  567. $repoItem['data-app'] = $this->app->tab;
  568. $repoProducts = explode(',', $repo->product);
  569. foreach($repoProducts as $productID)
  570. {
  571. if(in_array($type, array('project', 'execution')) && $projectID && !in_array($productID, $projectProductIds)) continue;
  572. if(strpos(",$repo->product,", ",$productID,") !== false)
  573. {
  574. if(!isset($repoPairs[$productID])) $repoPairs[$productID] = $productItems[$productID];
  575. $repoPairs[$productID]['items'][] = $repoItem;
  576. }
  577. }
  578. }
  579. }
  580. return $repoPairs;
  581. }
  582. /**
  583. * 根据ID获取代码库。
  584. * Get repo by id.
  585. *
  586. * @param int $repoID
  587. * @access public
  588. * @return object|false
  589. */
  590. public function getByID($repoID)
  591. {
  592. if(common::isTutorialMode()) return $this->loadModel('tutorial')->getRepo();
  593. if(empty($repoID)) return false;
  594. $repo = $this->dao->select('*')->from(TABLE_REPO)->where('id')->eq($repoID)->fetch();
  595. if(!$repo) return false;
  596. /* Update repo table for old version. */
  597. if(empty($repo->serviceHost) && in_array($repo->SCM, $this->config->repo->gitServiceTypeList))
  598. {
  599. $repo->serviceHost = $repo->client;
  600. $repo->serviceProject = $repo->extra;
  601. $this->dao->update(TABLE_REPO)->data(array('serviceHost' => $repo->serviceHost, 'serviceProject' => $repo->serviceProject))->where('id')->eq($repoID)->exec();
  602. /* Add webhook. */
  603. if($repo->SCM == 'Gitlab') $this->loadModel('gitlab')->updateCodePath((int)$repo->serviceHost, (int)$repo->serviceProject, $repo->id);
  604. }
  605. if($repo->encrypt == 'base64') $repo->password = base64_decode($repo->password);
  606. $repo->codePath = $repo->path;
  607. if(in_array(strtolower($repo->SCM), $this->config->repo->gitServiceList)) $repo = $this->processGitService($repo);
  608. $repo->acl = json_decode($repo->acl);
  609. if(empty($repo->acl)) $repo->acl = new stdclass();
  610. if(empty($repo->acl->acl)) $repo->acl->acl = 'custom';
  611. $repo->serviceHost = (int)$repo->serviceHost;
  612. $repo->gitService = $repo->serviceHost;
  613. $repo->serviceProject = $repo->SCM == 'Gitlab' ? (int)$repo->serviceProject : $repo->serviceProject;
  614. return $repo;
  615. }
  616. /**
  617. * 根据URL获取代码库。
  618. * Get repo by url.
  619. *
  620. * @param string $url
  621. * @access public
  622. * @return array
  623. */
  624. public function getRepoByUrl($url)
  625. {
  626. if(empty($url)) return array('result' => 'fail', 'message' => 'Url is empty.');
  627. $matches = $this->repoTao->getMatchedReposByUrl($url);
  628. if(empty($matches)) return array('result' => 'fail', 'message' => 'No matched gitlab.');
  629. $conditions = array();
  630. foreach($matches as $matched) $conditions[] = "(`serviceHost`='{$matched['gitlab']}' and `serviceProject`='{$matched['project']}')";
  631. $matchedRepos = $this->getListByCondition('(' . implode(' OR ', $conditions). ')', 'Gitlab');
  632. if(empty($matchedRepos)) return array('result' => 'fail', 'message' => 'No matched gitlab.');
  633. $matchedRepo = new stdclass();
  634. foreach($matchedRepos as $repo)
  635. {
  636. if(!empty($repo->preMerge))
  637. {
  638. $matchedRepo = $repo;
  639. break;
  640. }
  641. }
  642. if(empty($matchedRepo)) return array('result' => 'fail', 'message' => 'Matched gitlab is not open pre merge.');
  643. if(empty($matchedRepo->job)) return array('result' => 'fail', 'message' => 'No linked job.');
  644. $job = $this->dao->select('*')->from(TABLE_JOB)->where('id')->eq($matchedRepo->job)->andWhere('deleted')->eq(0)->fetch();
  645. if(empty($job)) return array('result' => 'fail', 'message' => 'Linked job is not exists.');
  646. $matchedRepo->job = $job;
  647. return array('result' => 'success', 'data' => $matchedRepo);
  648. }
  649. /**
  650. * 根据URL获取代码库列表。
  651. * Get repo list by url.
  652. *
  653. * @param string $url
  654. * @access public
  655. * @return array
  656. */
  657. public function getRepoListByUrl($url = '')
  658. {
  659. if(empty($url)) return array('status' => 'fail', 'message' => 'Url is empty.');
  660. $matches = $this->repoTao->getMatchedReposByUrl($url);
  661. if(empty($matches)) return array('status' => 'fail', 'message' => 'No matched gitlab.');
  662. $conditions = array();
  663. foreach($matches as $matched) $conditions[] = "(`serviceHost`='{$matched['gitlab']}' and `serviceProject`='{$matched['project']}')";
  664. $matchedRepos = $this->getListByCondition('(' . implode(' OR ', $conditions). ')', 'Gitlab');
  665. foreach($matchedRepos as $key => $repo)
  666. {
  667. if(!$this->checkPriv($repo)) unset($matchedRepos[$key]);
  668. }
  669. if(empty($matchedRepos)) return array('status' => 'fail', 'message' => 'No matched gitlab.');
  670. return array('status' => 'success', 'repos' => $matchedRepos);
  671. }
  672. /**
  673. * 根据ID列表获取代码库列表。
  674. * Get by id list.
  675. *
  676. * @param array $idList
  677. * @access public
  678. * @return array
  679. */
  680. public function getByIdList($idList)
  681. {
  682. $repos = $this->dao->select('*')->from(TABLE_REPO)->where('deleted')->eq(0)->andWhere('id')->in($idList)->fetchAll('id', false);
  683. foreach($repos as $repo)
  684. {
  685. if($repo->encrypt == 'base64') $repo->password = base64_decode($repo->password);
  686. $repo->acl = json_decode($repo->acl);
  687. }
  688. return $repos;
  689. }
  690. /**
  691. * 获取代码库的分支列表。
  692. * Get git branches.
  693. *
  694. * @param object $repo
  695. * @param bool $printLabel
  696. * @param string $source select current repo's branches from scm or database.
  697. * @access public
  698. * @return array
  699. */
  700. public function getBranches($repo, $printLabel = false, $source = 'scm')
  701. {
  702. if($source == 'database')
  703. {
  704. $branches = $this->dao->select('branch')->from(TABLE_REPOBRANCH)
  705. ->where('repo')->eq($repo->id)
  706. ->fetchPairs();
  707. }
  708. else
  709. {
  710. $this->scm = $this->app->loadClass('scm');
  711. $this->scm->setEngine($repo);
  712. $branches = $this->scm->branch();
  713. }
  714. if($printLabel)
  715. {
  716. foreach($branches as &$branch) $branch = 'Branch::' . $branch;
  717. }
  718. return $branches;
  719. }
  720. /**
  721. * 根据提交ID获取提交信息。
  722. * Get commit by id.
  723. *
  724. * @param array $revisions
  725. * @access public
  726. * @return void
  727. */
  728. public function getCommitsByRevisions($revisions)
  729. {
  730. return $this->dao->select('id')->from(TABLE_REPOHISTORY)->where('revision')->in($revisions)->fetchPairs('id');
  731. }
  732. /**
  733. * 获取代码库的提交列表。
  734. * Get commits.
  735. *
  736. * @param object $repo
  737. * @param string $entry
  738. * @param string $revision
  739. * @param string $type
  740. * @param object $pager
  741. * @param string $begin
  742. * @param string $end
  743. * @param mixed $query
  744. * @access public
  745. * @return array
  746. */
  747. public function getCommits($repo, $entry, $revision = 'HEAD', $type = 'dir', $pager = null, $begin = '', $end = '', $query = null)
  748. {
  749. if(common::isTutorialMode()) return $this->loadModel('tutorial')->getCommits();
  750. if(!isset($repo->id)) return array();
  751. if(in_array($repo->SCM, $this->config->repo->notSyncSCM)) return $this->loadModel('gitlab')->getCommits($repo, $entry, $pager, $begin, $end, $query);
  752. $entry = ltrim($entry, '/');
  753. $entry = empty($entry) ? '' : '/' . $entry;
  754. $revisionTime = $this->repoTao->getLatestCommitTime($repo->id, $revision, $repo->SCM == 'Subversion' ? '' : (string)$this->cookie->repoBranch);
  755. $hasBranch = $repo->SCM != 'Subversion' && $this->cookie->repoBranch;
  756. $historyIdList = array();
  757. if($entry != '/' && !empty($entry))
  758. {
  759. $historyIdList = $this->dao->select('DISTINCT t2.id,t2.`time`')->from(TABLE_REPOFILES)->alias('t1')
  760. ->leftJoin(TABLE_REPOHISTORY)->alias('t2')->on('t1.revision=t2.id')
  761. ->beginIF($hasBranch)->leftJoin(TABLE_REPOBRANCH)->alias('t3')->on('t2.id=t3.revision')->fi()
  762. ->where('t1.repo')->eq($repo->id)
  763. ->beginIF($begin)->andWhere('t2.`time`')->ge($begin)->fi()
  764. ->beginIF($end)->andWhere('t2.`time`')->le($end)->fi()
  765. ->beginIF($revisionTime)->andWhere('t2.`time`')->le($revisionTime)->fi()
  766. ->andWhere('left(t2.`comment`, 12)')->ne('Merge branch')
  767. ->beginIF($hasBranch)->andWhere('t3.branch')->eq($this->cookie->repoBranch)->fi()
  768. ->beginIF($type == 'dir')
  769. ->andWhere('t1.parent', true)->like(rtrim($entry, '/') . "/%")
  770. ->orWhere('t1.parent')->eq(rtrim($entry, '/'))
  771. ->markRight(1)
  772. ->fi()
  773. ->beginIF($type == 'file')->andWhere('t1.path')->eq("$entry")->fi()
  774. ->orderBy('t2.`time` desc')
  775. ->page($pager, 't2.id')
  776. ->fetchPairs('id', 'id');
  777. }
  778. $comments = $this->dao->select('DISTINCT t1.*')->from(TABLE_REPOHISTORY)->alias('t1')
  779. ->beginIF($hasBranch)->leftJoin(TABLE_REPOBRANCH)->alias('t2')->on('t1.id=t2.revision')->fi()
  780. ->where('t1.repo')->eq($repo->id)
  781. ->andWhere('left(t1.`comment`, 12)')->ne('Merge branch')
  782. ->beginIF($revisionTime)->andWhere('t1.`time`')->le($revisionTime)->fi()
  783. ->beginIF($begin)->andWhere('t1.`time`')->ge($begin)->fi()
  784. ->beginIF($end)->andWhere('t1.`time`')->le($end)->fi()
  785. ->beginIF($query)->andWhere($query)->fi()
  786. ->beginIF($hasBranch)->andWhere('t2.branch')->eq($this->cookie->repoBranch)->fi()
  787. ->beginIF($entry != '/' && !empty($entry))->andWhere('t1.id')->in($historyIdList)->fi()
  788. ->beginIF($begin)->andWhere('t1.time')->ge($begin)->fi()
  789. ->beginIF($end)->andWhere('t1.time')->le($end)->fi()
  790. ->orderBy('time desc');
  791. if($entry == '/' || empty($entry)) $comments->page($pager, 't1.id');
  792. $comments = $comments->fetchAll('revision');
  793. foreach($comments as $repoComment)
  794. {
  795. $repoComment->originalComment = $repoComment->comment;
  796. $repoComment->comment = $this->replaceCommentLink($repoComment->comment);
  797. }
  798. return $comments;
  799. }
  800. /**
  801. * 获取最后一次提交的信息。
  802. * Get latest commit.
  803. *
  804. * @param int $repoID
  805. * @param bool $checkCount
  806. * @access public
  807. * @return object|false
  808. */
  809. public function getLatestCommit($repoID, $checkCount = true)
  810. {
  811. $repo = $this->fetchByID($repoID);
  812. $branchID = (string)$this->cookie->repoBranch;
  813. $lastComment = $this->dao->select('t1.*')->from(TABLE_REPOHISTORY)->alias('t1')
  814. ->leftJoin(TABLE_REPOBRANCH)->alias('t2')->on('t1.id=t2.revision')
  815. ->where('t1.repo')->eq($repoID)
  816. ->beginIF($repo->SCM != 'Subversion' && $branchID)->andWhere('t2.branch')->eq($branchID)->fi()
  817. ->beginIF($repo->SCM == 'Subversion')->andWhere('t1.time')->ne('1970-01-01 08:00:00')->fi()
  818. ->orderBy('t1.`time` desc')
  819. ->fetch();
  820. if(empty($lastComment)) return false;
  821. $lastComment->svnRevision = intval($lastComment->revision);
  822. if(!$checkCount) return $lastComment;
  823. $count = $this->dao->select('count(DISTINCT t1.id) as count')->from(TABLE_REPOHISTORY)->alias('t1')
  824. ->leftJoin(TABLE_REPOBRANCH)->alias('t2')->on('t1.id=t2.revision')
  825. ->where('t1.repo')->eq($repoID)
  826. ->beginIF($repo->SCM != 'Subversion' && $branchID)->andWhere('t2.branch')->eq($branchID)->fi()
  827. ->fetch('count');
  828. if($repo->SCM == 'Git' && $lastComment->commit != $count)
  829. {
  830. $this->fixCommit($repo->id);
  831. $lastComment->commit = $count;
  832. }
  833. return $lastComment;
  834. }
  835. /**
  836. * 从数据库中获取提交记录。
  837. * Get revisions from db.
  838. *
  839. * @param int $repoID
  840. * @param int $limit
  841. * @param string $maxRevision
  842. * @param string $minRevision
  843. * @access public
  844. * @return array
  845. */
  846. public function getRevisionsFromDB($repoID, $limit = 0, $maxRevision = '', $minRevision = '')
  847. {
  848. $revisions = $this->dao->select('DISTINCT t1.*')->from(TABLE_REPOHISTORY)->alias('t1')
  849. ->beginIF($this->cookie->repoBranch)->leftJoin(TABLE_REPOBRANCH)->alias('t2')->on('t1.id=t2.revision')->fi()
  850. ->where('t1.repo')->eq($repoID)
  851. ->beginIF(!empty($maxRevision))->andWhere('t1.revision')->le($maxRevision)->fi()
  852. ->beginIF(!empty($minRevision))->andWhere('t1.revision')->ge($minRevision)->fi()
  853. ->beginIF($this->cookie->repoBranch)->andWhere('t2.branch')->eq($this->cookie->repoBranch)->fi()
  854. ->orderBy('t1.revision desc')
  855. ->beginIF(!empty($limit))->limit($limit)->fi()
  856. ->fetchAll('revision');
  857. $commiters = $this->loadModel('user')->getCommiters();
  858. foreach($revisions as $revision)
  859. {
  860. $revision->comment = $this->replaceCommentLink($revision->comment);
  861. $revision->committer = isset($commiters[$revision->committer]) ? $commiters[$revision->committer] : $revision->committer;
  862. }
  863. return $revisions;
  864. }
  865. /**
  866. * 获取代码提交记录。
  867. * Get history.
  868. *
  869. * @param int $repoID
  870. * @param array $revisions
  871. * @access public
  872. * @return array
  873. */
  874. public function getHistory($repoID, $revisions)
  875. {
  876. return $this->dao->select('DISTINCT t1.*')->from(TABLE_REPOHISTORY)->alias('t1')
  877. ->beginIF($this->cookie->repoBranch)->leftJoin(TABLE_REPOBRANCH)->alias('t2')->on('t1.id=t2.revision')->fi()
  878. ->where('t1.repo')->eq($repoID)
  879. ->andWhere('t1.revision')->in($revisions)
  880. ->beginIF($this->cookie->repoBranch)->andWhere('t2.branch')->eq($this->cookie->repoBranch)->fi()
  881. ->fetchAll('revision');
  882. }
  883. /**
  884. * 查询提交记录的名称。
  885. * Get git revisionName.
  886. *
  887. * @param string $revision
  888. * @param int $commit
  889. * @access public
  890. * @return string
  891. */
  892. public function getGitRevisionName($revision, $commit)
  893. {
  894. if(empty($commit)) return substr($revision, 0, 10);
  895. return substr($revision, 0, 10) . '<span title="' . sprintf($this->lang->repo->commitTitle, $commit) . '"> (' . $commit . ') </span>';
  896. }
  897. /**
  898. * 获取缓存文件位置。
  899. * Get cache file.
  900. *
  901. * @param int $repoID
  902. * @param string $path
  903. * @param string $revision
  904. * @access public
  905. * @return string
  906. */
  907. public function getCacheFile($repoID, $path, $revision)
  908. {
  909. $cachePath = $this->app->getCacheRoot() . '/repo/' . $repoID;
  910. if(!is_dir($cachePath)) mkdir($cachePath, 0777, true);
  911. if(!is_writable($cachePath)) return false;
  912. return $cachePath . '/' . md5("{$this->cookie->repoBranch}-$path-$revision");
  913. }
  914. /**
  915. * 查询代码库关联的产品列表。
  916. * Get products by repoID.
  917. *
  918. * @param int $repoID
  919. * @access public
  920. * @return array
  921. */
  922. public function getProductsByRepo($repoID)
  923. {
  924. $repo = $this->getByID($repoID);
  925. if(empty($repo->id)) return array();
  926. return $this->dao->select('id,name')->from(TABLE_PRODUCT)
  927. ->where('id')->in($repo->product)
  928. ->andWhere('deleted')->eq(0)
  929. ->fetchPairs();
  930. }
  931. /**
  932. * 保存代码提交信息并返回保存数量。
  933. * Save commit.
  934. *
  935. * @param int $repoID
  936. * @param array $logs
  937. * @param int $version
  938. * @param string $branch
  939. * @access public
  940. * @return int
  941. */
  942. public function saveCommit($repoID, $logs, $version, $branch = '')
  943. {
  944. $count = 0;
  945. if(empty($logs)) return $count;
  946. foreach($logs['commits'] as $i => $commit)
  947. {
  948. $existsRevision = $this->dao->select('id,revision')->from(TABLE_REPOHISTORY)->where('repo')->eq($repoID)->andWhere('revision')->eq($commit->revision)->fetch();
  949. if($existsRevision)
  950. {
  951. if($branch) $this->dao->replace(TABLE_REPOBRANCH)->set('repo')->eq($repoID)->set('revision')->eq($existsRevision->id)->set('branch')->eq($branch)->exec();
  952. continue;
  953. }
  954. $commit->repo = $repoID;
  955. $commit->commit = $version;
  956. $commit->comment = htmlSpecialString($commit->comment);
  957. $this->dao->insert(TABLE_REPOHISTORY)->data($commit)->exec();
  958. if(!dao::isError())
  959. {
  960. $commitID = $this->dao->lastInsertID();
  961. if($branch) $this->dao->replace(TABLE_REPOBRANCH)->set('repo')->eq($repoID)->set('revision')->eq($commitID)->set('branch')->eq($branch)->exec();
  962. if(!empty($logs['files']))
  963. {
  964. foreach($logs['files'][$i] as $file)
  965. {
  966. $parentPath = dirname($file->path);
  967. $copyfromPath = !empty($file->copyfromPath) ? $file->copyfromPath : '';
  968. $copyfromRev = !empty($file->copyfromRev) ? $file->copyfromRev : '';
  969. unset($file->copyfromPath);
  970. unset($file->copyfromRev);
  971. $file->parent = $parentPath == '\\' ? '/' : $parentPath;
  972. $file->revision = $commitID;
  973. $file->repo = $repoID;
  974. $this->dao->insert(TABLE_REPOFILES)->data($file)->exec();
  975. if($file->action == 'R' && !empty($file->oldPath))
  976. {
  977. $file->path = $file->oldPath;
  978. $file->parent = dirname($file->path);
  979. $file->oldPath = '';
  980. $file->action = 'D';
  981. $this->dao->insert(TABLE_REPOFILES)->data($file)->exec();
  982. }
  983. if(!empty($copyfromPath) && !empty($copyfromRev)) $this->repoTao->copySvnDir($repoID, $copyfromPath, $copyfromRev, $file->path);
  984. }
  985. }
  986. $revisionPairs[$commit->revision] = $commit->revision;
  987. $version ++;
  988. $count ++;
  989. }
  990. dao::$errors = array();
  991. }
  992. return $count;
  993. }
  994. /**
  995. * 保存单个提交信息。
  996. * Save One Commit.
  997. *
  998. * @param int $repoID
  999. * @param object $commit
  1000. * @param int $version
  1001. * @param string $branch
  1002. * @access public
  1003. * @return int
  1004. */
  1005. public function saveOneCommit($repoID, $commit, $version, $branch = '')
  1006. {
  1007. $existsRevision = $this->dao->select('id,revision')->from(TABLE_REPOHISTORY)->where('repo')->eq($repoID)->andWhere('revision')->eq($commit->revision)->fetch();
  1008. if($existsRevision)
  1009. {
  1010. if($branch) $this->dao->replace(TABLE_REPOBRANCH)->set('repo')->eq($repoID)->set('revision')->eq($existsRevision->id)->set('branch')->eq($branch)->exec();
  1011. return $version;
  1012. }
  1013. $history = new stdclass();
  1014. $history->repo = $repoID;
  1015. $history->commit = $version;
  1016. $history->revision = $commit->revision;
  1017. $history->comment = htmlSpecialString($commit->comment);
  1018. $history->committer = $commit->committer;
  1019. $history->time = $commit->time;
  1020. $this->dao->insert(TABLE_REPOHISTORY)->data($history)->exec();
  1021. if(!dao::isError())
  1022. {
  1023. $commitID = $this->dao->lastInsertID();
  1024. if($branch) $this->dao->replace(TABLE_REPOBRANCH)->set('repo')->eq($repoID)->set('revision')->eq($commitID)->set('branch')->eq($branch)->exec();
  1025. foreach($commit->change as $file => $info)
  1026. {
  1027. $parentPath = dirname($file);
  1028. $copyfromPath = !empty($info['copyfrom-path']) ? $info['copyfrom-path'] : '';
  1029. $copyfromRev = !empty($info['copyfrom-rev']) ? $info['copyfrom-rev']: '';
  1030. $repoFile = new stdclass();
  1031. $repoFile->repo = $repoID;
  1032. $repoFile->revision = $commitID;
  1033. $repoFile->path = $file;
  1034. $repoFile->parent = $parentPath == '\\' ? '/' : $parentPath;
  1035. $repoFile->type = $info['kind'];
  1036. $repoFile->action = $info['action'];
  1037. $repoFile->oldPath = empty($info['oldPath']) ? '' : $info['oldPath'];
  1038. $this->dao->insert(TABLE_REPOFILES)->data($repoFile)->exec();
  1039. if($repoFile->oldPath and $repoFile->action == 'R')
  1040. {
  1041. $parentPath = dirname($repoFile->oldPath);
  1042. $repoFile->path = $repoFile->oldPath;
  1043. $repoFile->parent = $parentPath == '\\' ? '/' : $parentPath;
  1044. $repoFile->type = $info['kind'];
  1045. $repoFile->action = 'D';
  1046. $repoFile->oldPath = '';
  1047. $this->dao->insert(TABLE_REPOFILES)->data($repoFile)->exec();
  1048. }
  1049. if(!empty($copyfromPath) && !empty($copyfromRev)) $this->repoTao->copySvnDir($repoID, $copyfromPath, $copyfromRev, $repoFile->path);
  1050. }
  1051. $version ++;
  1052. }
  1053. dao::$errors = array();
  1054. return $version;
  1055. }
  1056. /**
  1057. * 保存已存在的分支日志。
  1058. * Save exists log branch.
  1059. *
  1060. * @param int $repoID
  1061. * @param string $branch
  1062. * @access public
  1063. * @return bool
  1064. */
  1065. public function saveExistCommits4Branch($repoID, $branch)
  1066. {
  1067. $lastBranchLog = $this->dao->select('t1.time')->from(TABLE_REPOHISTORY)->alias('t1')
  1068. ->leftJoin(TABLE_REPOBRANCH)->alias('t2')->on('t1.id=t2.revision')
  1069. ->where('t1.repo')->eq($repoID)
  1070. ->andWhere('t2.branch')->eq($branch)
  1071. ->orderBy('time')
  1072. ->limit(1)
  1073. ->fetch();
  1074. if(empty($lastBranchLog)) return false;
  1075. $stmt = $this->dao->select('*')->from(TABLE_REPOHISTORY)->where('repo')->eq($repoID)->andWhere('time')->lt($lastBranchLog->time)->query();
  1076. while($log = $stmt->fetch())
  1077. {
  1078. $this->dao->REPLACE(TABLE_REPOBRANCH)->set('repo')->eq($repoID)->set('revision')->eq($log->id)->set('branch')->eq($branch)->exec();
  1079. }
  1080. return !dao::isError();
  1081. }
  1082. /**
  1083. * 更新代码库的提交次数。
  1084. * Update commit count.
  1085. *
  1086. * @param int $repoID
  1087. * @param int $count
  1088. * @access public
  1089. * @return bool
  1090. */
  1091. public function updateCommitCount($repoID, $count)
  1092. {
  1093. $this->dao->update(TABLE_REPO)->set('commits')->eq($count)->where('id')->eq($repoID)->exec();
  1094. return !dao::isError();
  1095. }
  1096. /**
  1097. * 获取未同步的提交。
  1098. * Get unsync commits.
  1099. *
  1100. * @param object $repo
  1101. * @access public
  1102. * @return array
  1103. */
  1104. public function getUnsyncedCommits($repo)
  1105. {
  1106. $repoID = $repo->id;
  1107. $lastInDB = $this->getLatestCommit($repoID);
  1108. $scm = $this->app->loadClass('scm');
  1109. $scm->setEngine($repo);
  1110. $logs = $scm->log('', $lastInDB ? $lastInDB->revision : 0);
  1111. if(empty($logs)) return array();
  1112. /* Process logs. */
  1113. $logs = array_reverse($logs, true);
  1114. foreach($logs as $i => $log)
  1115. {
  1116. if(isset($lastInDB->revision) and $lastInDB->revision == $log->revision)
  1117. {
  1118. unset($logs[$i]);
  1119. continue;
  1120. }
  1121. $log->author = $log->committer;
  1122. $log->msg = $log->comment;
  1123. $log->date = $log->time;
  1124. /* Process files. */
  1125. $log->files = array();
  1126. foreach($log->change as $file => $info) $log->files[$info['action']][] = $file;
  1127. }
  1128. return $logs;
  1129. }
  1130. /**
  1131. * 生成链接。
  1132. * Create link for repo.
  1133. *
  1134. * @param string $method
  1135. * @param string $params
  1136. * @param string $viewType
  1137. * @access public
  1138. * @return string
  1139. */
  1140. public function createLink($method, $params = '', $viewType = '')
  1141. {
  1142. if($this->config->requestType == 'GET') return helper::createLink('repo', $method, $params, $viewType);
  1143. $parsedParams = array();
  1144. parse_str($params, $parsedParams);
  1145. $pathParams = '';
  1146. $pathKey = 'path';
  1147. if(isset($parsedParams['entry'])) $pathKey = 'entry';
  1148. if(isset($parsedParams['file'])) $pathKey = 'file';
  1149. if(isset($parsedParams['root'])) $pathKey = 'root';
  1150. if(isset($parsedParams[$pathKey]))
  1151. {
  1152. $pathParams = 'repoPath=' . $parsedParams[$pathKey];
  1153. $parsedParams[$pathKey] = '';
  1154. }
  1155. $params = http_build_query($parsedParams);
  1156. $link = helper::createLink('repo', $method, $params, $viewType);
  1157. if(empty($pathParams)) return $link;
  1158. $link .= strpos($link, '?') === false ? '?' : '&';
  1159. $link .= $pathParams;
  1160. return $link;
  1161. }
  1162. /**
  1163. * 更新代码库的同步状态。
  1164. * Mark synced status.
  1165. *
  1166. * @param int $repoID
  1167. * @access public
  1168. * @return bool
  1169. */
  1170. public function markSynced($repoID)
  1171. {
  1172. $this->fixCommit($repoID);
  1173. $this->dao->update(TABLE_REPO)->set('synced')->eq(1)->where('id')->eq($repoID)->exec();
  1174. return !dao::isError();
  1175. }
  1176. /**
  1177. * 更新提交记录的排序。
  1178. * Fix commit.
  1179. *
  1180. * @param int $repoID
  1181. * @access public
  1182. * @return bool
  1183. */
  1184. public function fixCommit($repoID)
  1185. {
  1186. $historyList = $this->dao->select('DISTINCT t1.id,t1.`time`')->from(TABLE_REPOHISTORY)->alias('t1')
  1187. ->leftJoin(TABLE_REPOBRANCH)->alias('t2')->on('t1.id=t2.revision')
  1188. ->where('t1.repo')->eq($repoID)
  1189. ->beginIF($this->cookie->repoBranch)->andWhere('t2.branch')->eq($this->cookie->repoBranch)->fi()
  1190. ->orderBy('time')
  1191. ->query();
  1192. foreach($historyList as $i => $repoHistory)
  1193. {
  1194. $i++;
  1195. $this->dao->update(TABLE_REPOHISTORY)->set('`commit`')->eq($i)->where('id')->eq($repoHistory->id)->exec();
  1196. }
  1197. return !dao::isError();
  1198. }
  1199. /**
  1200. * 转义代码库文件路径。
  1201. * Encode repo path.
  1202. *
  1203. * @param string $path
  1204. * @access public
  1205. * @return string
  1206. */
  1207. public function encodePath($path = '')
  1208. {
  1209. if(empty($path)) return $path;
  1210. return helper::safe64Encode(urlencode($path));
  1211. }
  1212. /**
  1213. * 解析代码库文件路径。
  1214. * Decode repo path.
  1215. *
  1216. * @param string $path
  1217. * @access public
  1218. * @return string
  1219. */
  1220. public function decodePath($path = '')
  1221. {
  1222. if(empty($path)) return $path;
  1223. return trim(urldecode(helper::safe64Decode($path)), '/');
  1224. }
  1225. /**
  1226. * 删除客户端代码工具生成的版本文件。
  1227. * remove client version file.
  1228. *
  1229. * @access public
  1230. * @return void
  1231. */
  1232. public function rmClientVersionFile()
  1233. {
  1234. $clientVersionFile = $this->session->clientVersionFile;
  1235. if($clientVersionFile)
  1236. {
  1237. if(!session_id()) session_start();
  1238. $this->session->set('clientVersionFile', '');
  1239. session_write_close();
  1240. if(file_exists($clientVersionFile)) @unlink($clientVersionFile);
  1241. }
  1242. }
  1243. /**
  1244. * 替换提交记录中的链接。
  1245. * Replace comment link.
  1246. *
  1247. * @param string $comment
  1248. * @access public
  1249. * @return string
  1250. */
  1251. public function replaceCommentLink($comment)
  1252. {
  1253. $rules = $this->processRules();
  1254. $storyReg = '/' . $rules['storyReg'] . '/i';
  1255. $taskReg = '/' . $rules['taskReg'] . '/i';
  1256. $bugReg = '/' . $rules['bugReg'] . '/i';
  1257. if(preg_match_all($taskReg, $comment, $result))
  1258. {
  1259. $taskLinks = $this->addLink($result, 'task');
  1260. foreach($taskLinks as $search => $replace) $comment = str_replace($search, $replace, $comment);
  1261. }
  1262. if(preg_match_all($bugReg, $comment, $result))
  1263. {
  1264. $bugLinks = $this->addLink($result, 'bug');
  1265. foreach($bugLinks as $search => $replace) $comment = str_replace($search, $replace, $comment);
  1266. }
  1267. if(preg_match_all($storyReg, $comment, $result))
  1268. {
  1269. $storyLinks = $this->addLink($result, 'story');
  1270. foreach($storyLinks as $search => $replace) $comment = str_replace($search, $replace, $comment);
  1271. }
  1272. return $comment;
  1273. }
  1274. /**
  1275. * 解析提交记录中的链接。
  1276. * Add link.
  1277. *
  1278. * @param array $matches
  1279. * @param string $method
  1280. * @access public
  1281. * @return array
  1282. */
  1283. public function addLink($matches, $method)
  1284. {
  1285. if(empty($matches)) return array();
  1286. $replaceLines = array();
  1287. foreach($matches[3] as $i => $idList)
  1288. {
  1289. $links = $matches[2][$i] . ' ' . $matches[4][$i];
  1290. preg_match_all('/\d+/', $idList, $idMatches);
  1291. foreach($idMatches[0] as $id)
  1292. {
  1293. $links .= html::a(helper::createLink($method, 'view', "id=$id"), $id) . $matches[6][$i];
  1294. }
  1295. $replaceLines[$matches[0][$i]] = rtrim($links, $matches[6][$i]);
  1296. }
  1297. return $replaceLines;
  1298. }
  1299. /**
  1300. * 解析git和svn的注释,从中提取对象id列表。
  1301. * Parse the comment of git and svn, extract object id list from it.
  1302. *
  1303. * @param string $comment
  1304. * @access public
  1305. * @return array
  1306. */
  1307. public function parseComment($comment)
  1308. {
  1309. $rules = $this->processRules();
  1310. $stories = array();
  1311. $actions = array();
  1312. $designs = array();
  1313. $tasks = $this->repoTao->parseTaskComment($comment, $rules, $actions);
  1314. $bugs = $this->repoTao->parseBugComment($comment, $rules, $actions);
  1315. preg_match_all("/{$rules['taskReg']}/i", $comment, $matches);
  1316. if($matches[0])
  1317. {
  1318. foreach($matches[3] as $idList)
  1319. {
  1320. preg_match_all('/\d+/', $idList, $idMatches);
  1321. foreach($idMatches[0] as $id) $tasks[$id] = $id;
  1322. }
  1323. }
  1324. preg_match_all("/{$rules['bugReg']}/i", $comment, $matches);
  1325. if($matches[0])
  1326. {
  1327. foreach($matches[3] as $idList)
  1328. {
  1329. preg_match_all('/\d+/', $idList, $idMatches);
  1330. foreach($idMatches[0] as $id) $bugs[$id] = $id;
  1331. }
  1332. }
  1333. preg_match_all("/{$rules['storyReg']}/i", $comment, $matches);
  1334. if($matches[0])
  1335. {
  1336. foreach($matches[3] as $idList)
  1337. {
  1338. preg_match_all('/\d+/', $idList, $idMatches);
  1339. foreach($idMatches[0] as $id) $stories[$id] = $id;
  1340. }
  1341. }
  1342. preg_match_all("/{$rules['designReg']}/i", $comment, $matches);
  1343. if($matches[0])
  1344. {
  1345. $designs = implode(' ', $matches[1]);
  1346. if($designs) $designs = array_unique(explode(' ', str_replace(',', ' ', $designs)));
  1347. }
  1348. return array('stories' => $stories, 'tasks' => $tasks, 'bugs' => $bugs, 'actions' => $actions, 'designs' => $designs);
  1349. }
  1350. /**
  1351. * 转码提交注释信息。
  1352. * Convert encoding of comment.
  1353. *
  1354. * @param string $comment
  1355. * @param string $encodings
  1356. * @access public
  1357. * @return string
  1358. */
  1359. public function iconvComment($comment, $encodings)
  1360. {
  1361. /* Get encodings. */
  1362. if($encodings == '') return $comment;
  1363. /* Try convert. */
  1364. $encodings = explode(',', $encodings);
  1365. foreach($encodings as $encoding)
  1366. {
  1367. if($encoding == 'utf-8') continue;
  1368. $result = helper::convertEncoding($comment, $encoding);
  1369. if($result) return $result;
  1370. }
  1371. return $comment;
  1372. }
  1373. /**
  1374. * 解析提交指令规则。
  1375. * Process rules to REG.
  1376. *
  1377. * @access public
  1378. * @return array
  1379. */
  1380. public function processRules()
  1381. {
  1382. if(is_string($this->config->repo->rules)) $this->config->repo->rules = json_decode($this->config->repo->rules, true);
  1383. $rules = $this->config->repo->rules;
  1384. $idMarks = str_replace(';', '|', preg_replace('/([^;])/', '\\\\\1', trim($rules['id']['mark'], ';')));
  1385. $idSplits = str_replace(';', '|', preg_replace('/([^;])/', '\\\\\1', trim($rules['id']['split'], ';')));
  1386. $costs = str_replace(';', '|', trim($rules['task']['consumed'], ';'));
  1387. $costMarks = str_replace(';', '|', preg_replace('/([^;])/', '\\\\\1', trim($rules['mark']['consumed'], ';')));
  1388. $lefts = str_replace(';', '|', trim($rules['task']['left'], ';'));
  1389. $leftMarks = str_replace(';', '|', preg_replace('/([^;])/', '\\\\\1', trim($rules['mark']['left'], ';')));
  1390. $storyModule = str_replace(';', '|', trim($rules['module']['story'], ';'));
  1391. $taskModule = str_replace(';', '|', trim($rules['module']['task'], ';'));
  1392. $bugModule = str_replace(';', '|', trim($rules['module']['bug'], ';'));
  1393. $costUnit = str_replace(';', '|', trim($rules['unit']['consumed'], ';'));
  1394. $leftUnit = str_replace(';', '|', trim($rules['unit']['left'], ';'));
  1395. $startAction = str_replace(';', '|', trim($rules['task']['start'], ';'));
  1396. $finishAction = str_replace(';', '|', trim($rules['task']['finish'], ';'));
  1397. $effortAction = str_replace(';', '|', trim($rules['task']['logEfforts'], ';'));
  1398. $resolveAction = str_replace(';', '|', trim($rules['bug']['resolve'], ';'));
  1399. $storyReg = "(($storyModule) *(({$idMarks})[0-9]+(({$idSplits})[0-9]+)*))";
  1400. $taskReg = "(($taskModule) *(({$idMarks})[0-9]+(({$idSplits})[0-9]+)*))";
  1401. $bugReg = "(($bugModule) *(({$idMarks})[0-9]+(({$idSplits})[0-9]+)*))";
  1402. $costReg = "($costs) *(($costMarks)([0-9]+(\.?[0-9]+)?)($costUnit))";
  1403. $leftReg = "($lefts) *(($leftMarks)([0-9]+(\.?[0-9]+)?)($leftUnit))";
  1404. $startTaskReg = "({$startAction}) *{$taskReg}.*$costReg.*$leftReg";
  1405. $effortTaskReg = "({$effortAction}) *{$taskReg}.*$costReg.*$leftReg";
  1406. $finishTaskReg = "({$finishAction}) *{$taskReg}.*$costReg";
  1407. $resolveBugReg = "({$resolveAction}) *{$bugReg}";
  1408. $reg = array();
  1409. $reg['storyReg'] = $storyReg;
  1410. $reg['taskReg'] = $taskReg;
  1411. $reg['bugReg'] = $bugReg;
  1412. $reg['costReg'] = $costReg;
  1413. $reg['leftReg'] = $leftReg;
  1414. $reg['startTaskReg'] = $startTaskReg;
  1415. $reg['effortTaskReg'] = $effortTaskReg;
  1416. $reg['finishTaskReg'] = $finishTaskReg;
  1417. $reg['resolveBugReg'] = $resolveBugReg;
  1418. $reg['designReg'] = 'design(?:\s){0,}(?:#|:|:){0,}([0-9, ]{1,})';
  1419. return $reg;
  1420. }
  1421. /**
  1422. * 保存提交信息到系统。
  1423. * Save action to pms.
  1424. *
  1425. * @param array $objects
  1426. * @param object $log
  1427. * @param string $repoRoot
  1428. * @param string $encodings
  1429. * @param string $scm
  1430. * @param array $gitlabAccountPairs
  1431. * @access public
  1432. * @return bool
  1433. */
  1434. public function saveAction2PMS($objects, $log, $repoRoot = '', $encodings = 'utf-8', $scm = 'svn', $gitlabAccountPairs = array())
  1435. {
  1436. $committers = $this->loadModel('user')->getCommiters('account');
  1437. $log->author = zget($gitlabAccountPairs, $log->author, zget($committers, $log->author));
  1438. if(isset($this->app->user))
  1439. {
  1440. $account = $this->app->user->account;
  1441. $this->app->user->account = $log->author;
  1442. }
  1443. $action = new stdclass();
  1444. $action->actor = $log->author;
  1445. $action->date = $log->date;
  1446. $action->extra = $scm == 'svn' ? $log->revision : substr($log->revision, 0, 10);
  1447. $action->comment = $this->lang->repo->revisionA . ': #' . $action->extra . "<br />" . htmlSpecialString($this->iconvComment($log->msg, $encodings));
  1448. $this->loadModel('action');
  1449. $actions = $objects['actions'];
  1450. $changes = $this->createActionChanges($log, $repoRoot, $scm);
  1451. if(isset($actions['task']))
  1452. {
  1453. $this->loadModel('task');
  1454. $productsAndExecutions = $this->getTaskProductsAndExecutions($objects['tasks']);
  1455. foreach($actions['task'] as $taskID => $taskActions)
  1456. {
  1457. $task = $this->task->getById($taskID);
  1458. if(empty($task)) continue;
  1459. $action->objectType = 'task';
  1460. $action->objectID = $taskID;
  1461. $action->product = $productsAndExecutions[$taskID]['product'];
  1462. $action->execution = $productsAndExecutions[$taskID]['execution'];
  1463. $this->setTaskByCommit($task, $taskActions, $action, $changes, $scm);
  1464. unset($objects['tasks'][$taskID]);
  1465. dao::$errors = array();
  1466. }
  1467. }
  1468. if(isset($actions['bug'])) $objects['bugs'] = $this->setBugStatusByCommit($objects['bugs'], $actions, $action, $changes);
  1469. $action->action = $scm == 'svn' ? 'svncommited' : 'gitcommited';
  1470. $this->saveObjectToPms($objects, $action, $changes);
  1471. if(isset($this->app->user)) $this->app->user->account = $account;
  1472. return !dao::isError();
  1473. }
  1474. /**
  1475. * 保存commit触发的操作日志信息。
  1476. * Save an action to pms.
  1477. *
  1478. * @param object $action
  1479. * @param array $changes
  1480. * @access public
  1481. * @return bool
  1482. */
  1483. public function saveRecord($action, $changes)
  1484. {
  1485. /* Remove sql error. */
  1486. dao::getError();
  1487. $record = $this->dao->select('*')->from(TABLE_ACTION)
  1488. ->where('objectType')->eq($action->objectType)
  1489. ->andWhere('objectID')->eq($action->objectID)
  1490. ->andWhere('extra')->eq($action->extra)
  1491. ->andWhere('action')->eq($action->action)
  1492. ->beginIf(!empty($action->comment))->andWhere('comment')->eq(zget($action, 'comment', ''))->fi()
  1493. ->fetch();
  1494. if($record)
  1495. {
  1496. $this->dao->update(TABLE_ACTION)->data($action)->where('id')->eq($record->id)->exec();
  1497. if($changes)
  1498. {
  1499. $historyIdList = $this->dao->findByAction($record->id)->from(TABLE_HISTORY)->fetchPairs('id', 'id');
  1500. if($historyIdList) $this->dao->delete()->from(TABLE_HISTORY)->where('id')->in($historyIdList)->exec();
  1501. $this->loadModel('action')->logHistory($record->id, $changes);
  1502. }
  1503. }
  1504. else
  1505. {
  1506. $this->dao->insert(TABLE_ACTION)->data($action)->autoCheck()->exec();
  1507. if($changes)
  1508. {
  1509. $actionID = $this->dao->lastInsertID();
  1510. $this->loadModel('action')->logHistory($actionID, $changes);
  1511. }
  1512. }
  1513. return !dao::isError();
  1514. }
  1515. /**
  1516. * 从日志中为设置变更信息。
  1517. * Create changes for action from a log.
  1518. *
  1519. * @param object $log
  1520. * @param string $repoRoot
  1521. * @access public
  1522. * @return array
  1523. * @param string $scm
  1524. */
  1525. public function createActionChanges($log, $repoRoot, $scm = 'svn')
  1526. {
  1527. if(empty($log->files)) return array();
  1528. $oldSelf = $this->server->PHP_SELF;
  1529. $this->server->set('PHP_SELF', $this->config->webRoot, '', false, true);
  1530. $diff = '';
  1531. foreach($log->files as $action => $actionFiles)
  1532. {
  1533. foreach($actionFiles as $file)
  1534. {
  1535. $catLink = trim(html::a($this->buildURL('cat', $repoRoot . $file, (string) $log->revision, $scm), 'view', '', "data-toggle='modal' data-size='{\"width\": 800, \"height\": 500}'"));
  1536. $diffLink = trim(html::a($this->buildURL('diff', $repoRoot . $file, (string) $log->revision, $scm), 'diff', '', "data-toggle='modal' data-size='{\"width\": 800, \"height\": 500}'"));
  1537. $catLink = str_replace('+', '%2B', $catLink);
  1538. $diffLink = str_replace('+', '%2B', $diffLink);
  1539. $diff .= $action . " " . $file . " $catLink ";
  1540. $diff .= $action == 'M' ? "$diffLink\n" : "\n" ;
  1541. }
  1542. }
  1543. $this->server->set('PHP_SELF', $oldSelf);
  1544. $change = new stdclass();
  1545. $change->field = $scm == 'svn' ? 'subversion' : 'git';
  1546. $change->old = '';
  1547. $change->new = '';
  1548. $change->diff = trim($diff);
  1549. return array($change);
  1550. }
  1551. /**
  1552. * 根据任务列表获取产品和执行。
  1553. * Get products and executions of tasks.
  1554. *
  1555. * @param array $tasks
  1556. * @access public
  1557. * @return array
  1558. */
  1559. public function getTaskProductsAndExecutions($tasks)
  1560. {
  1561. $records = array();
  1562. $products = $this->dao->select('t1.id,t1.execution,t2.product')->from(TABLE_TASK)->alias('t1')
  1563. ->leftJoin(TABLE_PROJECTPRODUCT)->alias('t2')->on('t1.execution = t2.project')
  1564. ->where('t1.id')->in($tasks)
  1565. ->fetchGroup('id','product');
  1566. $executions = $this->dao->select('id, execution')->from(TABLE_TASK)->where('id')->in($tasks)->fetchPairs();
  1567. foreach($executions as $taskID => $executionID)
  1568. {
  1569. $record = array();
  1570. $record['execution'] = $executionID;
  1571. $record['product'] = isset($products[$taskID]) ? "," . implode(',', array_keys($products[$taskID])) . "," : ",0,";
  1572. $records[$taskID] = $record;
  1573. }
  1574. return $records;
  1575. }
  1576. /**
  1577. * 根据bug列表获取产品和执行。
  1578. * Get products and executions of bugs.
  1579. *
  1580. * @param array $bugs
  1581. * @access public
  1582. * @return array
  1583. */
  1584. public function getBugProductsAndExecutions($bugs)
  1585. {
  1586. $records = $this->dao->select('id, execution, product')->from(TABLE_BUG)->where('id')->in($bugs)->fetchAll('id');
  1587. foreach($records as $record) $record->product = ",{$record->product},";
  1588. return $records;
  1589. }
  1590. /**
  1591. * 构造git和svn的展示链接。
  1592. * Build url for git and svn.
  1593. *
  1594. * @param string $methodName
  1595. * @param string $url
  1596. * @param string $revision
  1597. * @access public
  1598. * @return string
  1599. * @param string $scm
  1600. */
  1601. public function buildURL($methodName, $url, $revision, $scm = 'svn')
  1602. {
  1603. $buildedURL = helper::createLink($scm, $methodName, "url=&revision=$revision", 'html');
  1604. $buildedURL .= strpos($buildedURL, '?') === false ? '?' : '&';
  1605. $buildedURL .= 'repoUrl=' . helper::safe64Encode($url);
  1606. return $buildedURL;
  1607. }
  1608. /**
  1609. * 处理代码库信息,增加代码路径和api路径。
  1610. * Process git service, add code path and api path.
  1611. *
  1612. * @param object $repo
  1613. * @param bool $getCodePath
  1614. * @access public
  1615. * @return object
  1616. */
  1617. public function processGitService($repo, $getCodePath = false)
  1618. {
  1619. $service = $this->loadModel('pipeline')->getByID((int)$repo->serviceHost);
  1620. if(!$service) return $repo;
  1621. if(in_array($repo->SCM, $this->config->repo->notSyncSCM))
  1622. {
  1623. if($getCodePath)
  1624. {
  1625. if($repo->SCM == 'Gitlab') $repo->serviceProject = (int)$repo->serviceProject;
  1626. $project = $this->loadModel($repo->SCM)->apiGetSingleProject((int)$repo->serviceHost, $repo->serviceProject);
  1627. if(isset($project->web_url) && $repo->path != $project->web_url)
  1628. {
  1629. $repo->path = $project->web_url;
  1630. $this->dao->update(TABLE_REPO)->set('path')->eq($repo->path)->where('id')->eq($repo->id)->exec();
  1631. }
  1632. }
  1633. $repo->path = (!$repo->path && $service) ? sprintf($this->config->repo->{$service->type}->apiPath, $service->url, $repo->serviceProject) : $repo->path;
  1634. $repo->apiPath = sprintf($this->config->repo->{$service->type}->apiPath, $service->url, $repo->serviceProject);
  1635. $repo->client = $service ? $service->url : '';
  1636. $repo->password = $service ? $service->token : '';
  1637. $repo->codePath = isset($project->web_url) ? $project->web_url : $repo->path;
  1638. }
  1639. else
  1640. {
  1641. if(!is_dir($repo->path) && !is_writable(dirname($repo->path)))
  1642. {
  1643. $path = $this->app->getAppRoot() . "www/data/repo/{$repo->name}_{$repo->SCM}";
  1644. $repo->path = $path;
  1645. $this->dao->update(TABLE_REPO)->set('path')->eq($repo->path)->where('id')->eq($repo->id)->exec();
  1646. }
  1647. $repo->codePath = $service ? "{$service->url}/{$repo->serviceProject}" : $repo->path;
  1648. }
  1649. $repo->gitService = (int)$repo->serviceHost;
  1650. return $repo;
  1651. }
  1652. /**
  1653. * 处理webhook请求。
  1654. * Handle received GitLab webhook.
  1655. *
  1656. * @param string $event
  1657. * @param object $data
  1658. * @param object $repotime
  1659. * @access public
  1660. * @return bool
  1661. * @param object $repo
  1662. */
  1663. public function handleWebhook($event, $data, $repo)
  1664. {
  1665. if(!in_array($event, array('Push Hook', 'Merge Request Hook', 'branch_updated'))) return false;
  1666. if(empty($data->commits)) return false;
  1667. /* Update code commit history. */
  1668. $commentGroup = $this->loadModel('job')->getTriggerGroup('commit', array($repo->id));
  1669. if(!in_array($repo->SCM, $this->config->repo->notSyncSCM)) return $this->loadModel('git')->updateCommit($repo, $commentGroup, false);
  1670. $scm = $this->app->loadClass('scm');
  1671. $scm->setEngine($repo);
  1672. $jobs = zget($commentGroup, $repo->id, array());
  1673. $accountPairs = array();
  1674. $userList = $this->loadModel($repo->SCM)->apiGetUsers($repo->gitService);
  1675. $accountIDPairs = $this->loadModel('pipeline')->getUserBindedPairs($repo->gitService, strtolower($repo->SCM), 'openID,account');
  1676. foreach($userList as $gitlabUser) $accountPairs[$gitlabUser->realname] = zget($accountIDPairs, $gitlabUser->id, '');
  1677. foreach($data->commits as $commit)
  1678. {
  1679. $time = zget($commit, 'timestamp', '');
  1680. if(isset($commit->author->when)) $time = $commit->author->when;
  1681. $log = new stdclass();
  1682. $log->revision = isset($commit->id) ? $commit->id : $commit->sha;
  1683. $log->msg = $commit->message;
  1684. $log->author = isset($commit->author->identity->name) ? $commit->author->identity->name : $commit->author->name;
  1685. $log->date = date("Y-m-d H:i:s", strtotime($time));
  1686. $log->files = array();
  1687. $log->repo = $repo;
  1688. if(!isset($commit->added))
  1689. {
  1690. $diffs = $scm->engine->getFilesByCommit($log->revision);
  1691. if(!empty($diffs))
  1692. {
  1693. foreach($diffs as $diff) $log->files[$diff->action][] = $diff->path;
  1694. }
  1695. }
  1696. else
  1697. {
  1698. foreach($commit->added as $file) $log->files['A'][] = $file;
  1699. foreach($commit->removed as $file) $log->files['D'][] = $file;
  1700. foreach($commit->modified as $file) $log->files['M'][] = $file;
  1701. }
  1702. $objects = $this->parseComment($log->msg);
  1703. $this->saveAction2PMS($objects, $log, $repo->path, $repo->encoding, 'git', $accountPairs);
  1704. foreach($jobs as $job)
  1705. {
  1706. foreach(explode(',', $job->comment) as $comment)
  1707. {
  1708. if(strpos($log->msg, $comment) !== false)
  1709. {
  1710. $this->loadModel('job')->exec($job->id, array(), 'commit');
  1711. continue 2;
  1712. }
  1713. }
  1714. }
  1715. if(!empty($objects['stories']) || !empty($objects['tasks']) || !empty($objects['bugs']))
  1716. {
  1717. $historyLog = new stdclass();
  1718. $historyLog->committer = $log->author;
  1719. $historyLog->revision = $log->revision;
  1720. $historyLog->comment = $commit->message;
  1721. $historyLog->time = date("Y-m-d H:i:s", strtotime($time));
  1722. $this->saveCommit($repo->id, array('commits' => [$historyLog]), 0);
  1723. $revisions = $this->dao->select('id')->from(TABLE_REPOHISTORY)
  1724. ->where('revision')->in($log->revision)
  1725. ->andWhere('repo')->eq($repo->id)
  1726. ->fetchPairs('id');
  1727. foreach (array('stories' => 'story', 'tasks' => 'task', 'bugs' => 'bug') as $objectType=>$modelType)
  1728. {
  1729. if(!empty($objects[$objectType]))
  1730. {
  1731. foreach($objects[$objectType] as $modelID)
  1732. {
  1733. $this->loadModel($modelType)->updateLinkedCommits((int)$modelID, $repo->id, $revisions);
  1734. }
  1735. }
  1736. }
  1737. }
  1738. }
  1739. return !dao::isError();
  1740. }
  1741. /**
  1742. * Get execution pairs.
  1743. *
  1744. * @param int $product
  1745. * @param int $branch
  1746. * @access public
  1747. * @return array
  1748. */
  1749. public function getExecutionPairs($product, $branch = 0)
  1750. {
  1751. $pairs = array();
  1752. $executions = $this->loadModel('execution')->getList(0, 'all', 'undone', 0, $product, $branch);
  1753. $parents = $this->dao->select('distinct parent,parent')->from(TABLE_EXECUTION)->where('type')->eq('stage')->andWhere('grade')->gt(1)->andWhere('deleted')->eq(0)->fetchPairs();
  1754. foreach($executions as $execution)
  1755. {
  1756. if(!empty($parents[$execution->id]) or ($execution->type == 'stage' and in_array($execution->attribute, array('request', 'design', 'review')))) continue;
  1757. if($execution->type == 'stage' and $execution->grade > 1)
  1758. {
  1759. $parentExecutions = $this->dao->select('id,name')->from(TABLE_EXECUTION)->where('id')->in(trim($execution->path, ','))->andWhere('type')->in('stage,kanban,sprint')->orderBy('grade')->fetchPairs();
  1760. $execution->name = implode('/', $parentExecutions);
  1761. }
  1762. $pairs[$execution->id] = $execution->name;
  1763. }
  1764. return $pairs;
  1765. }
  1766. /**
  1767. * 获取代码库的clone地址。
  1768. * Get clone url.
  1769. *
  1770. * @param object $repo
  1771. * @access public
  1772. * @return object
  1773. */
  1774. public function getCloneUrl($repo)
  1775. {
  1776. if(empty($repo->id)) return new stdclass();
  1777. $url = new stdClass();
  1778. if($repo->SCM == 'Subversion')
  1779. {
  1780. $url->svn = $repo->path;
  1781. }
  1782. elseif($repo->SCM == 'Gitlab')
  1783. {
  1784. $project = $this->loadModel('gitlab')->apiGetSingleProject($repo->gitService, (int)$repo->serviceProject);
  1785. if(isset($project->id))
  1786. {
  1787. $url->http = $project->http_url_to_repo ?? '';
  1788. $url->ssh = $project->ssh_url_to_repo ?? '';
  1789. }
  1790. }
  1791. elseif($repo->SCM == 'Gitea')
  1792. {
  1793. $project = $this->loadModel('gitea')->apiGetSingleProject($repo->gitService, (string)$repo->serviceProject);
  1794. if(isset($project->id))
  1795. {
  1796. $url->http = $project->clone_url;
  1797. $url->ssh = $project->ssh_url;
  1798. }
  1799. }
  1800. elseif($repo->SCM == 'Gogs')
  1801. {
  1802. $project = $this->loadModel('gogs')->apiGetSingleProject($repo->gitService, (string)$repo->serviceProject);
  1803. if(isset($project->id))
  1804. {
  1805. $url->http = $project->clone_url;
  1806. $url->ssh = $project->ssh_url;
  1807. }
  1808. }
  1809. else
  1810. {
  1811. $this->scm = $this->app->loadClass('scm');
  1812. $this->scm->setEngine($repo);
  1813. $url = $this->scm->getCloneUrl();
  1814. }
  1815. return $url;
  1816. }
  1817. /**
  1818. * 获取代码文件的提交信息。
  1819. * Get file commits.
  1820. *
  1821. * @param object $repo
  1822. * @param string $branch
  1823. * @param string $parent
  1824. * @access public
  1825. * @return array
  1826. */
  1827. public function getFileCommits($repo, $branch, $parent = '')
  1828. {
  1829. /* Get file commits by repo. */
  1830. if($repo->SCM != 'Subversion' && empty($branch)) $branch = $this->cookie->repoBranch;
  1831. $parent = '/' . ltrim($parent, '/');
  1832. if($repo->prefix) $parent = rtrim($repo->prefix . $parent, '/');
  1833. $fileCommits = $this->dao->select('t1.id,t1.path,t1.type,t1.action,t1.oldPath,t1.parent,t2.revision,t2.comment,t2.committer,t2.time')->from(TABLE_REPOFILES)->alias('t1')
  1834. ->leftJoin(TABLE_REPOHISTORY)->alias('t2')->on('t1.revision=t2.id')
  1835. ->beginIF($repo->SCM != 'Subversion' && $branch)->leftJoin(TABLE_REPOBRANCH)->alias('t3')->on('t2.id=t3.revision')->fi()
  1836. ->where('t1.repo')->eq($repo->id)
  1837. ->andWhere('left(t2.`comment`, 12)')->ne('Merge branch')
  1838. ->beginIF($repo->SCM != 'Subversion' && $branch)->andWhere('t3.branch')->eq($branch)->fi()
  1839. ->beginIF($repo->SCM == 'Subversion')->andWhere('t1.parent')->eq("$parent")->fi()
  1840. ->beginIF($repo->SCM != 'Subversion')->andWhere('t1.parent')->like("$parent%")->fi()
  1841. ->orderBy('t2.`time` asc')
  1842. ->fetchAll('path');
  1843. $files = $folders = $fileSort = $dirSort = array();
  1844. $existsFiles = array();
  1845. foreach($fileCommits as $fileCommit)
  1846. {
  1847. if($fileCommit->action != 'D' && strpos($fileCommit->path, $parent) === 0) $existsFiles[$fileCommit->path] = true;
  1848. if($fileCommit->action == 'R' && isset($existsFiles[$fileCommit->oldPath])) unset($existsFiles[$fileCommit->oldPath]);
  1849. }
  1850. foreach($fileCommits as $fileCommit)
  1851. {
  1852. /* Filter by parent. */
  1853. if(!isset($existsFiles[$fileCommit->path])) continue;
  1854. $pathList = explode('/', ltrim($fileCommit->path, '/'));
  1855. $file = new stdclass();
  1856. $file->revision = $fileCommit->revision;
  1857. $file->comment = $fileCommit->comment;
  1858. $file->account = $fileCommit->committer;
  1859. $file->date = $fileCommit->time;
  1860. $file->kind = 'file';
  1861. $file->name = end($pathList);
  1862. if($fileCommit->parent == $parent && $fileCommit->type == 'file')
  1863. {
  1864. $files[] = $file;
  1865. $fileSort[] = $file->name;
  1866. }
  1867. else
  1868. {
  1869. $childPath = explode('/', ltrim(substr($fileCommit->path, strlen($parent)), '/'));
  1870. $fileName = $fileCommit->type == 'dir' ? end($pathList) : $childPath[0];
  1871. if(in_array($fileName, $dirSort)) continue;
  1872. $file->name = $fileName;
  1873. $file->kind = 'dir';
  1874. $folders[] = $file;
  1875. $dirSort[] = $fileName;
  1876. }
  1877. }
  1878. array_multisort($fileSort, SORT_ASC, $files);
  1879. array_multisort($dirSort, SORT_ASC, $folders);
  1880. return array_merge($folders, $files);
  1881. }
  1882. /**
  1883. * 获取目录树。
  1884. * Get html for file tree.
  1885. *
  1886. * @param object $repo
  1887. * @param string $branch
  1888. * @param array $diffs
  1889. * @access public
  1890. * @return array
  1891. */
  1892. public function getFileTree($repo, $branch = '', $diffs = null)
  1893. {
  1894. set_time_limit(0);
  1895. $allFiles = array();
  1896. if(is_null($diffs))
  1897. {
  1898. if($repo->SCM != 'Subversion' && empty($branch)) $branch = $this->cookie->repoBranch;
  1899. $files = $this->dao->select('t1.path,t2.time,t1.action')->from(TABLE_REPOFILES)->alias('t1')
  1900. ->leftJoin(TABLE_REPOHISTORY)->alias('t2')->on('t1.revision=t2.id')
  1901. ->leftJoin(TABLE_REPOBRANCH)->alias('t3')->on('t2.id=t3.revision')
  1902. ->where('t1.repo')->eq($repo->id)
  1903. ->andWhere('t1.type')->eq('file')
  1904. ->andWhere('left(t2.`comment`, 12)')->ne('Merge branch')
  1905. ->beginIF($repo->SCM != 'Subversion' && $branch)->andWhere('t3.branch')->eq($branch)->fi()
  1906. ->orderBy('t2.`time` asc')
  1907. ->fetchAll('path');
  1908. $removeDirs = array();
  1909. if($repo->SCM == 'Subversion')
  1910. {
  1911. $removeDirs = $this->dao->select('t2.time,t1.path')->from(TABLE_REPOFILES)->alias('t1')
  1912. ->leftJoin(TABLE_REPOHISTORY)->alias('t2')->on('t1.revision=t2.id')
  1913. ->where('t1.repo')->eq($repo->id)
  1914. ->andWhere('t1.type')->eq('dir')
  1915. ->andWhere('t1.action')->eq('D')
  1916. ->fetchPairs();
  1917. }
  1918. foreach($files as $file)
  1919. {
  1920. foreach($removeDirs as $removeTime => $dir)
  1921. {
  1922. if(strpos($file->path, $dir . '/') === 0 and $file->time <= $removeTime)
  1923. {
  1924. $file->action = 'D';
  1925. break;
  1926. }
  1927. }
  1928. if($file->action != 'D') $allFiles[] = $file->path;
  1929. }
  1930. }
  1931. else
  1932. {
  1933. foreach($diffs as $diff) $allFiles[] = $diff->fileName;
  1934. }
  1935. return $this->repoTao->buildFileTree($allFiles);
  1936. }
  1937. /**
  1938. * 根据提交获取关联信息。
  1939. * Get relation by commit.
  1940. *
  1941. * @param int $repoID
  1942. * @param string $commit
  1943. * @param string $type story|bug|task
  1944. * @access public
  1945. * @return array
  1946. */
  1947. public function getRelationByCommit($repoID, $commit, $type = '')
  1948. {
  1949. $relationList = $this->dao->select('t1.BID as id, t1.BType as type')->from(TABLE_RELATION)->alias('t1')
  1950. ->leftJoin(TABLE_REPOHISTORY)->alias('t2')->on('t1.AID = t2.id')
  1951. ->where('t2.revision')->eq($commit)
  1952. ->andWhere('t2.repo')->eq($repoID)
  1953. ->andWhere('t1.AType')->eq('revision')
  1954. ->beginIF($type)->andWhere('t1.BType')->eq($type)->fi()
  1955. ->fetchGroup('type', 'id');
  1956. $stories = empty($relationList['story']) ? array() : $this->loadModel('story')->getByList(array_keys($relationList['story']));
  1957. $bugs = empty($relationList['bug']) ? array() : $this->loadModel('bug')->getByIdList(array_keys($relationList['bug']));
  1958. $tasks = empty($relationList['task']) ? array() : $this->loadModel('task')->getByIdList(array_keys($relationList['task']));
  1959. $index = 0;
  1960. $titleList = array();
  1961. foreach($relationList as $objectType => $objects)
  1962. {
  1963. foreach($objects as $object)
  1964. {
  1965. $titleList[$index] = array(
  1966. 'id' => $object->id,
  1967. 'type' => $objectType,
  1968. 'title' => "#$object->id "
  1969. );
  1970. if($objectType == 'story')
  1971. {
  1972. $story = zget($stories, $object->id, array());
  1973. $titleList[$index]['title'] .= zget($story, 'title', '');
  1974. }
  1975. elseif($objectType == 'bug')
  1976. {
  1977. $bug = zget($bugs, $object->id, array());
  1978. $titleList[$index]['title'] .= zget($bug, 'title', '');
  1979. }
  1980. elseif($objectType == 'task')
  1981. {
  1982. $task = zget($tasks, $object->id, array());
  1983. $titleList[$index]['title'] .= zget($task, 'name', '');
  1984. }
  1985. $index ++;
  1986. }
  1987. }
  1988. return $type ? $titleList : array_values($titleList);
  1989. }
  1990. /**
  1991. * 根据关联对象获取提交。
  1992. * Get relation commit.
  1993. *
  1994. * @param int $objectID
  1995. * @param string $objectType story|bug|task
  1996. * @access public
  1997. * @return array
  1998. */
  1999. public function getCommitsByObject($objectID, $objectType)
  2000. {
  2001. return $this->dao->select('t2.*')->from(TABLE_RELATION)->alias('t1')
  2002. ->leftJoin(TABLE_REPOHISTORY)->alias('t2')->on('t1.AID = t2.id')
  2003. ->where('t1.BID')->eq($objectID)
  2004. ->andWhere('t1.BType')->eq($objectType)
  2005. ->andWhere('t1.AType')->eq('revision')
  2006. ->andWhere('t1.relation')->eq('commit')
  2007. ->fetchAll('', false);
  2008. }
  2009. /*
  2010. * 移除没有权限的项目。
  2011. * Remove projects without privileges.
  2012. *
  2013. * @param array $productIDList
  2014. * @param array $projectIDList
  2015. * @access public
  2016. * @return array
  2017. */
  2018. /**
  2019. * @param mixed[] $productIDList
  2020. * @param mixed[] $projectIDList
  2021. */
  2022. public function filterProject($productIDList, $projectIDList = array())
  2023. {
  2024. /* Get all projects that can be accessed. */
  2025. $accessProjects = array();
  2026. foreach($productIDList as $productID)
  2027. {
  2028. $projects = $this->loadModel('product')->getProjectPairsByProduct((int)$productID);
  2029. $accessProjects = $accessProjects + $projects;
  2030. }
  2031. /* Get linked projects. */
  2032. $linkedProjects = $this->dao->select('id,name')->from(TABLE_PROJECT)->where('id')->in($projectIDList)->fetchPairs('id', 'name');
  2033. return $accessProjects + $linkedProjects; // Merge projects can be accessed and exists.
  2034. }
  2035. /**
  2036. * 更新代码提交历史。
  2037. * Update commit history.
  2038. *
  2039. * @param int $repoID
  2040. * @param int $objectID
  2041. * @param string $branchID
  2042. * @access public
  2043. * @return bool
  2044. */
  2045. public function updateCommit($repoID, $objectID = 0, $branchID = '')
  2046. {
  2047. $repo = $this->getByID($repoID);
  2048. if($repo->SCM == 'Gitlab') return true;
  2049. /* Update code commit history. */
  2050. $commentGroup = $this->loadModel('job')->getTriggerGroup('commit', array($repoID));
  2051. if(in_array($repo->SCM, $this->config->repo->gitTypeList))
  2052. {
  2053. $branch = $this->cookie->repoBranch;
  2054. if($branchID)
  2055. {
  2056. $currentBranches = $this->getBranches($repo, false, 'database');
  2057. if(!in_array($branch, $currentBranches))
  2058. {
  2059. $link = $this->createLink('showSyncCommit', "repoID=$repoID&objectID=$objectID&branch=$branchID", '', false);
  2060. return $this->app->control->locate($link);
  2061. }
  2062. }
  2063. $this->loadModel('git')->updateCommit($repo, $commentGroup, false);
  2064. $_COOKIE['repoBranch'] = $branch;
  2065. }
  2066. if($repo->SCM == 'Subversion') $this->loadModel('svn')->updateCommit($repo, $commentGroup, false);
  2067. return !dao::isError();
  2068. }
  2069. /**
  2070. * 移除已经删除的分支。
  2071. * Delete the deleted branch.
  2072. *
  2073. * @param int $repoID
  2074. * @param array $latestBranches
  2075. * @access public
  2076. * @return bool
  2077. */
  2078. public function checkDeletedBranches($repoID, $latestBranches)
  2079. {
  2080. if(empty($latestBranches)) return false;
  2081. $currentBranches = $this->dao->select('branch')->from(TABLE_REPOBRANCH)->where('repo')->eq($repoID)->groupBy('branch')->fetchPairs('branch');
  2082. $deletedBranches = array_diff($currentBranches, $latestBranches);
  2083. foreach($deletedBranches as $deletedBranch)
  2084. {
  2085. if($deletedBranch == 'master') continue;
  2086. $revisionIds = $this->dao->select('revision')->from(TABLE_REPOBRANCH)->where('repo')->eq($repoID)->andWhere('branch')->eq($deletedBranch)->fetchPairs('revision');
  2087. $branchRevisionIds = $this->dao->select('revision,count(branch) as count')->from(TABLE_REPOBRANCH)->where('revision')->in($revisionIds)->groupBy('revision')->having('count')->eq(1)->fetchPairs('revision', 'revision');
  2088. $fileIds = $this->dao->select('id')->from(TABLE_REPOFILES)->where('revision')->in($branchRevisionIds)->fetchPairs('id');
  2089. $this->dao->delete()->from(TABLE_REPOHISTORY)->where('id')->in($branchRevisionIds)->exec();
  2090. $this->dao->delete()->from(TABLE_REPOFILES)->where('id')->in($fileIds)->exec();
  2091. $this->dao->delete()->from(TABLE_REPOBRANCH)->where('repo')->eq($repoID)->andWhere('branch')->eq($deletedBranch)->exec();
  2092. }
  2093. return true;
  2094. }
  2095. /**
  2096. * 判断按钮是否可点击。
  2097. * Judge an action is clickable or not.
  2098. *
  2099. * @param object $repo
  2100. * @param string $action
  2101. * @access public
  2102. * @return bool
  2103. */
  2104. public static function isClickable($repo, $action)
  2105. {
  2106. $action = strtolower($action);
  2107. if($action == 'execjob') return common::hasPriv('sonarqube', $action) && !$repo->exec;
  2108. if($action == 'reportview') return common::hasPriv('sonarqube', $action) && !$repo->report;
  2109. if(!commonModel::hasPriv('repo', $action)) return false;
  2110. return true;
  2111. }
  2112. /**
  2113. * 获取gitlab项目列表。
  2114. * Get gitlab projects.
  2115. *
  2116. * @param int $gitlabID
  2117. * @param string $projectFilter
  2118. * @access public
  2119. * @return array
  2120. */
  2121. public function getGitlabProjects($gitlabID, $projectFilter = '')
  2122. {
  2123. if($this->app->user->admin || ($projectFilter == 'ALL' && common::hasPriv('repo', 'create')))
  2124. {
  2125. $projects = $this->loadModel('gitlab')->apiGetProjects($gitlabID, 'true', 0, 0, false);
  2126. }
  2127. else
  2128. {
  2129. $gitlabUser = $this->loadModel('pipeline')->getOpenIdByAccount($gitlabID, 'gitlab', $this->app->user->account);
  2130. if(!$gitlabUser) return array();
  2131. $projects = $this->loadModel('gitlab')->apiGetProjects($gitlabID, $projectFilter ? 'false' : 'true');
  2132. $groupIDList = array(0 => 0);
  2133. $groups = $this->gitlab->apiGetGroups($gitlabID, 'name_asc', 'developer');
  2134. foreach($groups as $group) $groupIDList[] = $group->id;
  2135. if($projectFilter == 'IS_DEVELOPER')
  2136. {
  2137. foreach($projects as $key => $project)
  2138. {
  2139. if(!$this->gitlab->checkUserAccess($gitlabID, 0, $project, $groupIDList, 'developer')) unset($projects[$key]);
  2140. }
  2141. }
  2142. }
  2143. $importedProjects = $this->getImportedProjects($gitlabID);
  2144. $projects = array_filter($projects, function($project) use ($importedProjects) { return !in_array($project->id, $importedProjects); });
  2145. return $projects;
  2146. }
  2147. /**
  2148. * Get repo groups.
  2149. *
  2150. * @param int $serverID
  2151. * @param int $groupID
  2152. * @access public
  2153. * @return string|array|false
  2154. */
  2155. public function getGroups($serverID, $groupID = 0)
  2156. {
  2157. $server = $this->loadModel('pipeline')->getByID($serverID);
  2158. if(empty($server->type)) return false;
  2159. $getGroupFunc = 'get' . $server->type . 'Groups';
  2160. $groups = $this->$getGroupFunc($serverID);
  2161. if($groupID !== 0)
  2162. {
  2163. foreach($groups as $group)
  2164. {
  2165. if($group['value'] == $groupID) return $group['text'];
  2166. }
  2167. return false;
  2168. }
  2169. return $groups;
  2170. }
  2171. /**
  2172. * Get gitlab groups.
  2173. *
  2174. * @param int $gitlabID
  2175. * @access public
  2176. * @return void
  2177. */
  2178. public function getGitlabGroups($gitlabID)
  2179. {
  2180. $groups = $this->loadModel('gitlab')->apiGetGroups($gitlabID, 'name_asc');
  2181. $options = array();
  2182. foreach($groups as $group)
  2183. {
  2184. $options[] = array('text' => $group->name, 'value' => $group->id);
  2185. }
  2186. return $options;
  2187. }
  2188. /**
  2189. * Get gitea groups.
  2190. *
  2191. * @param int $giteaID
  2192. * @access public
  2193. * @return array
  2194. */
  2195. public function getGiteaGroups($giteaID)
  2196. {
  2197. $groups = $this->loadModel('gitea')->apiGetGroups($giteaID);
  2198. $options = array();
  2199. foreach($groups as $group)
  2200. {
  2201. $options[] = array('text' => $group->username, 'value' => $group->id);
  2202. }
  2203. return $options;
  2204. }
  2205. /**
  2206. * 更新版本库最后提交时间。
  2207. * Update repo last commited date.
  2208. *
  2209. * @param int $repoID
  2210. * @access public
  2211. * @return void
  2212. */
  2213. public function updateCommitDate($repoID)
  2214. {
  2215. $repo = $this->getByID($repoID);
  2216. if(empty($repo->id)) return;
  2217. if(in_array($repo->SCM, $this->config->repo->notSyncSCM))
  2218. {
  2219. $scm = $this->app->loadClass('scm');
  2220. $scm->setEngine($repo);
  2221. $commits = $scm->engine->getCommitsByPath('', '', 'HEAD', 1, 1);
  2222. if(empty($commits)) return;
  2223. $commitDate = $repo->SCM == 'Gitlab' ? $commits[0]->committed_date : $commits[0]->author->when;
  2224. if(!empty($commitDate))
  2225. {
  2226. $lastCommitDate = date('Y-m-d H:i:s', strtotime($commitDate));
  2227. $this->dao->update(TABLE_REPO)->set('lastCommit')->eq($lastCommitDate)->where('id')->eq($repoID)->exec();
  2228. }
  2229. }
  2230. }
  2231. /**
  2232. * 检查gitea连接。
  2233. * Check gitea connection.
  2234. *
  2235. * @param string $scm
  2236. * @param string $name
  2237. * @param int|string $serviceHost
  2238. * @param int|string $serviceProject
  2239. * @access public
  2240. * @return string|false
  2241. */
  2242. public function checkGiteaConnection($scm, $name, $serviceHost, $serviceProject)
  2243. {
  2244. if($name != '' and $serviceProject != '')
  2245. {
  2246. $module = strtolower($scm);
  2247. $project = $this->loadModel($module)->apiGetSingleProject($serviceHost, $serviceProject);
  2248. if(isset($project->tokenCloneUrl))
  2249. {
  2250. $path = $this->app->getAppRoot() . 'www/data/repo/' . $name . '_' . $module;
  2251. if(!realpath($path))
  2252. {
  2253. $cmd = 'git clone --progress -v "' . $project->tokenCloneUrl . '" "' . $path . '" > "' . $this->app->getTmpRoot() . "log/clone.progress.$module.{$name}.log\" 2>&1 &";
  2254. if(PHP_OS == 'WINNT') $cmd = "start /b $cmd";
  2255. exec($cmd);
  2256. }
  2257. return $path;
  2258. }
  2259. else
  2260. {
  2261. dao::$errors['serviceProject'] = $this->lang->repo->error->noCloneAddr;
  2262. return false;
  2263. }
  2264. }
  2265. return false;
  2266. }
  2267. /**
  2268. * 根据路径获取gitlab文件列表。
  2269. * Get gitlab files by path.
  2270. *
  2271. * @param object $repo
  2272. * @param string $path
  2273. * @param string $branch
  2274. * @access public
  2275. * @return array
  2276. */
  2277. public function getGitlabFilesByPath($repo, $path = '', $branch = '')
  2278. {
  2279. $fileList = $this->getTreeByGraphql($repo, $path, $branch, 'blobs');
  2280. $folderList = $this->getTreeByGraphql($repo, $path, $branch, 'trees');
  2281. if(empty($fileList) && empty($folderList)) return array();
  2282. $files = array();
  2283. $folders = array();
  2284. $fileSort = $dirSort = array(); // Use it to sort array.
  2285. foreach($fileList as $file)
  2286. {
  2287. if(in_array($file->name, $fileSort)) continue;
  2288. $base64Name = $this->encodePath($file->path);
  2289. $fileInfo = new stdclass();
  2290. $fileInfo->id = $base64Name;
  2291. $fileInfo->name = $file->name;
  2292. $fileInfo->text = $file->name;
  2293. $fileInfo->path = $file->path;
  2294. $fileInfo->key = $base64Name;
  2295. $fileInfo->kind = 'file';
  2296. $files[] = $fileInfo;
  2297. $fileSort[] = $file->name;
  2298. }
  2299. foreach($folderList as $dir)
  2300. {
  2301. if(in_array($dir->name, $dirSort)) continue;
  2302. $base64Name = $this->encodePath($dir->path);
  2303. $folder = new stdclass();
  2304. $folder->id = $base64Name;
  2305. $folder->name = $dir->name;
  2306. $folder->text = $dir->name;
  2307. $folder->path = $dir->path;
  2308. $folder->key = $base64Name;
  2309. $folder->kind = 'dir';
  2310. $folder->items = array('url' => helper::createLink('repo', 'ajaxGetFiles', "repoID={$repo->id}&branch={$branch}&path=" . helper::safe64Encode($dir->path)));
  2311. $folders[] = $folder;
  2312. $dirSort[] = $dir->name;
  2313. }
  2314. array_multisort($fileSort, SORT_ASC, $files);
  2315. array_multisort($dirSort, SORT_ASC, $folders);
  2316. return array_merge($folders, $files);
  2317. }
  2318. /**
  2319. * 通过Graphql获取GitLab文件列表。
  2320. * Get GitLab files by Graphql.
  2321. *
  2322. * @param object $repo
  2323. * @param string $path
  2324. * @param string $branch
  2325. * @param string $type
  2326. * @access public
  2327. * @return array
  2328. */
  2329. public function getTreeByGraphql($repo, $path = '', $branch = '', $type = 'blobs')
  2330. {
  2331. if(!$branch) $branch = $this->cookie->branch;
  2332. $this->loadModel('gitlab');
  2333. $fileList = array();
  2334. $endCursor = '';
  2335. $hasNextPage = true;
  2336. while($hasNextPage)
  2337. {
  2338. $query = 'query { project(fullPath: "%s") {repository {tree(path: "' . trim($path, '/') . '", ref: "' . $branch . '") {' . $type . '(after: "' . $endCursor . '") {pageInfo {endCursor hasNextPage} nodes {sha name path}}}}}}';
  2339. $response = $this->gitlab->apiGetByGraphql($repo, $query);
  2340. if(!$endCursor && !isset($response->data->project->repository)) return array();
  2341. if(empty($response->data->project->repository->tree)) break;
  2342. $fileList = array_merge($fileList, $response->data->project->repository->tree->{$type}->nodes);
  2343. $hasNextPage = $response->data->project->repository->tree->{$type}->pageInfo->hasNextPage;
  2344. $endCursor = $response->data->project->repository->tree->{$type}->pageInfo->endCursor;
  2345. }
  2346. return $fileList;
  2347. }
  2348. /**
  2349. * 查询提交记录的版本号。
  2350. * Get history revision.
  2351. *
  2352. * @param int $repoID
  2353. * @param string $revision
  2354. * @param bool $withCommit
  2355. * @param string $condition
  2356. * @access public
  2357. * @return string|object|false
  2358. */
  2359. public function getHistoryRevision($repoID, $revision, $withCommit = false, $condition = 'eq')
  2360. {
  2361. $field = $withCommit ? 'revision, commit' : 'revision';
  2362. return $this->dao->select($field)->from(TABLE_REPOHISTORY)
  2363. ->where('repo')->eq($repoID)
  2364. ->beginIF($condition != 'lt')->andWhere('revision')->eq($revision)->fi()
  2365. ->beginIF($condition == 'lt')->andWhere('revision')->lt($revision)->fi()
  2366. ->orderBy('id desc')
  2367. ->fetch($withCommit ? '' : 'revision');
  2368. }
  2369. /**
  2370. * 通过指令开始任务。
  2371. * Start task by commit.
  2372. *
  2373. * @param object $task
  2374. * @param array $params
  2375. * @param object $action
  2376. * @param array $changes
  2377. * @access private
  2378. * @return bool
  2379. */
  2380. private function startTask($task, $params, $action, $changes)
  2381. {
  2382. $now = helper::now();
  2383. $newTask = new stdclass();
  2384. $newTask->id = $task->id;
  2385. $newTask->status = 'doing';
  2386. $newTask->left = $params['left'];
  2387. $newTask->consumed = $params['consumed'] + $task->consumed;
  2388. $newTask->realStarted = $now;
  2389. $newTask->lastEditedBy = $this->app->user->account;
  2390. $newTask->lastEditedDate = $now;
  2391. if($newTask->left == 0 && empty($task->team))
  2392. {
  2393. $newTask->status = 'done';
  2394. $newTask->finishedBy = $this->app->user->account;
  2395. $newTask->finishedDate = $now;
  2396. $newTask->assignedTo = $task->openedBy;
  2397. }
  2398. $this->loadModel('task');
  2399. $currentTeam = !empty($task->team) ? $this->task->getTeamByAccount($task->team) : array();
  2400. $effort = new stdclass();
  2401. $effort->date = helper::today();
  2402. $effort->task = $newTask->id;
  2403. $effort->consumed = zget($newTask, 'consumed', 0);
  2404. $effort->left = zget($newTask, 'left', 0);
  2405. $effort->account = $this->app->user->account;
  2406. $effort->work = $this->lang->action->label->started . $this->lang->task->task . " : " . $task->name;
  2407. $effort->consumed = !empty($task->team) && $currentTeam ? $effort->consumed - $currentTeam->consumed : $effort->consumed - $task->consumed;
  2408. if($effort->consumed > 0) $effortID = $this->task->addTaskEffort($effort);
  2409. if($task->mode == 'linear' && !empty($effortID)) $this->task->updateEffortOrder($effortID, $currentTeam->order);
  2410. $taskChanges = $this->task->start($task, $newTask);
  2411. if($taskChanges)
  2412. {
  2413. $taskChanges = array_merge($taskChanges, $changes);
  2414. $action->action = $newTask->left == 0 ? 'finished' : 'started';
  2415. $this->saveRecord($action, $taskChanges);
  2416. $this->task->afterStart($task, array(), (float)$newTask->left, array());
  2417. }
  2418. return !dao::isError();
  2419. }
  2420. /**
  2421. * 根据指令完成任务。
  2422. * Finish task by commit.
  2423. *
  2424. * @param object $task
  2425. * @param array $params
  2426. * @param object $action
  2427. * @param array $changes
  2428. * @access private
  2429. * @return bool
  2430. */
  2431. private function finishTask($task, $params, $action, $changes)
  2432. {
  2433. $now = helper::now();
  2434. $newTask = new stdclass();
  2435. $newTask->status = 'done';
  2436. $newTask->left = zget($params, 'left', 0);
  2437. $newTask->consumed = $params['consumed'] + $task->consumed;
  2438. $newTask->assignedTo = $task->openedBy;
  2439. $newTask->realStarted = $task->realStarted ? $task->realStarted : $now;
  2440. $newTask->finishedDate = $now;
  2441. $newTask->lastEditedDate = $now;
  2442. $newTask->assignedDate = $now;
  2443. $newTask->finishedBy = $this->app->user->account;
  2444. $newTask->lastEditedBy = $this->app->user->account;
  2445. $this->loadModel('task');
  2446. if(empty($task->team))
  2447. {
  2448. $consumed = $params['consumed'];
  2449. }
  2450. else
  2451. {
  2452. $currentTeam = $this->task->getTeamByAccount($task->team);
  2453. $consumed = $currentTeam ? $task->consumed - $currentTeam->consumed : $newTask->consumed;
  2454. }
  2455. $effort = new stdclass();
  2456. $effort->date = helper::today();
  2457. $effort->task = $task->id;
  2458. $effort->left = 0;
  2459. $effort->account = $this->app->user->account;
  2460. $effort->consumed = $consumed > 0 ? $consumed : 0;
  2461. $effort->work = $this->lang->action->label->finished . $this->lang->task->task . " : " . $task->name;
  2462. if($effort->consumed > 0) $effortID = $this->task->addTaskEffort($effort);
  2463. if($task->mode == 'linear' && !empty($effortID)) $this->task->updateEffortOrder($effortID, $currentTeam->order);
  2464. $taskChanges = $this->task->finish($task, $newTask);
  2465. if($taskChanges)
  2466. {
  2467. $taskChanges = array_merge($taskChanges, $changes);
  2468. $action->action = 'finished';
  2469. $this->saveRecord($action, $taskChanges);
  2470. $this->task->afterStart($task, array(), 0, array());
  2471. }
  2472. return !dao::isError();
  2473. }
  2474. /**
  2475. * 根据提交信息设置任务信息。
  2476. * Set task by commit.
  2477. *
  2478. * @param object $task
  2479. * @param array $taskActions
  2480. * @param object $action
  2481. * @param array $changes
  2482. * @param string $scm
  2483. * @access public
  2484. * @return bool
  2485. */
  2486. public function setTaskByCommit($task, $taskActions, $action, $changes, $scm)
  2487. {
  2488. foreach($taskActions as $taskAction => $params)
  2489. {
  2490. if($taskAction == 'start' && $task->status == 'wait')
  2491. {
  2492. $this->startTask($task, $params, $action, $changes);
  2493. dao::$errors = array();
  2494. }
  2495. elseif($taskAction == 'effort' && in_array($task->status, array('wait', 'pause', 'doing')))
  2496. {
  2497. $action->action = $scm == 'svn' ? 'svncommited' : 'gitcommited';
  2498. $this->saveEffortForCommit($task->id, $params, $action, $changes);
  2499. }
  2500. elseif($taskAction == 'finish' and in_array($task->status, array('wait', 'pause', 'doing')))
  2501. {
  2502. $this->finishTask($task, $params, $action, $changes);
  2503. }
  2504. }
  2505. return !dao::isError();
  2506. }
  2507. /**
  2508. * 根据提交信息设置工时。
  2509. * Set effort by commit message.
  2510. *
  2511. * @param int $taskID
  2512. * @param array $params
  2513. * @param object $action
  2514. * @param array $changes
  2515. * @access public
  2516. * @return bool
  2517. */
  2518. public function saveEffortForCommit($taskID, $params, $action, $changes)
  2519. {
  2520. unset($_POST['consumed']);
  2521. unset($_POST['left']);
  2522. $_POST['date'][1] = date('Y-m-d');
  2523. $_POST['consumed'][1] = $params['consumed'];
  2524. $_POST['left'][1] = $params['left'];
  2525. $_POST['work'][1] = str_replace('<br />', "\n", $action->comment);
  2526. $this->loadModel('task');
  2527. $workhour = form::batchData($this->config->task->form->recordWorkhour)->get();
  2528. $this->task->recordWorkhour($taskID, $workhour);
  2529. $this->saveRecord($action, $changes);
  2530. return !dao::isError();
  2531. }
  2532. /**
  2533. * 根据提交信息设置Bug状态。
  2534. * Set bug status by commit.
  2535. *
  2536. * @param array $bugs
  2537. * @param array $actions
  2538. * @param object $action
  2539. * @param array $changes
  2540. * @access public
  2541. * @return array
  2542. */
  2543. public function setBugStatusByCommit($bugs, $actions, $action, $changes)
  2544. {
  2545. global $app;
  2546. $productsAndExecutions = $this->loadModel('bug')->getByIdList($bugs);
  2547. foreach($actions['bug'] as $bugID => $bugActions)
  2548. {
  2549. $app->rawModule = 'bug';
  2550. $bug = $this->bug->getByID($bugID);
  2551. if(empty($bug)) continue;
  2552. $action->objectType = 'bug';
  2553. $action->objectID = $bugID;
  2554. $action->product = ",{$productsAndExecutions[$bugID]->product},";
  2555. $action->execution = $productsAndExecutions[$bugID]->execution;
  2556. foreach($bugActions as $bugAction => $params)
  2557. {
  2558. $_POST = array();
  2559. if($bugAction == 'resolve' && $bug->status == 'active')
  2560. {
  2561. $app->rawMethod = 'resolve';
  2562. $this->post->set('resolvedBuild', 'trunk');
  2563. $this->post->set('resolution', 'fixed');
  2564. $newBug = form::data($this->config->bug->form->resolve)
  2565. ->setDefault('assignedTo', $bug->openedBy)
  2566. ->add('id', $bug->id)
  2567. ->add('execution', $bug->execution)
  2568. ->get();
  2569. $changes = array();
  2570. $result = $this->bug->resolve($newBug);
  2571. if($result)
  2572. {
  2573. $newBug = $this->bug->getByID($bugID);
  2574. $changes = common::createChanges($bug, $newBug);
  2575. }
  2576. foreach($changes as $change) $changes[] = $change;
  2577. if($changes)
  2578. {
  2579. $action->action = 'resolved';
  2580. $action->extra = 'fixed';
  2581. $this->saveRecord($action, $changes);
  2582. }
  2583. }
  2584. }
  2585. dao::$errors = array();
  2586. unset($bugs[$bugID]);
  2587. }
  2588. return $bugs;
  2589. }
  2590. /**
  2591. * 保存提交信息关联的日志。
  2592. * Save commit linkage log.
  2593. *
  2594. * @param array $objects
  2595. * @param object $action
  2596. * @param array $changes
  2597. * @access public
  2598. * @return bool
  2599. */
  2600. public function saveObjectToPms($objects, $action, $changes)
  2601. {
  2602. $singular = array('stories' => 'story', 'tasks' => 'task', 'bugs' => 'bug', 'designs' => 'design');
  2603. foreach(array_keys($objects) as $objectType)
  2604. {
  2605. if($objectType == 'actions') continue;
  2606. if($objects[$objectType])
  2607. {
  2608. $objectList = array();
  2609. if($objectType == 'stories')
  2610. {
  2611. $objectList = $this->loadModel('story')->getByList($objects[$objectType]);
  2612. }
  2613. elseif($objectType == 'bugs')
  2614. {
  2615. $objectList = $this->getBugProductsAndExecutions($objects[$objectType]);
  2616. }
  2617. elseif($objectType != 'designs')
  2618. {
  2619. $objectList = $this->getTaskProductsAndExecutions($objects[$objectType]);
  2620. }
  2621. foreach($objects[$objectType] as $objectID)
  2622. {
  2623. $objectID = (int)$objectID;
  2624. if(!isset($objectList[$objectID])) continue;
  2625. $action->objectType = $singular[$objectType];
  2626. $action->objectID = $objectID;
  2627. if($objectType != 'designs')
  2628. {
  2629. $action->product = in_array($objectType, array('stories', 'bugs')) ? $objectList[$objectID]->product : $objectList[$objectID]['product'];
  2630. $action->execution = in_array($objectType, array('stories', 'bugs')) ? 0 : $objectList[$objectID]['execution'];
  2631. }
  2632. $this->saveRecord($action, $changes);
  2633. }
  2634. }
  2635. }
  2636. return !dao::isError();
  2637. }
  2638. /**
  2639. * 获取并列展示的对比信息。
  2640. * Get appose diff.
  2641. *
  2642. * @param array $diffs
  2643. * @access public
  2644. * @return array
  2645. */
  2646. public function getApposeDiff($diffs)
  2647. {
  2648. foreach($diffs as $diffFile)
  2649. {
  2650. if(empty($diffFile->contents)) continue;
  2651. foreach($diffFile->contents as $content)
  2652. {
  2653. $old = array();
  2654. $new = array();
  2655. foreach($content->lines as $line)
  2656. {
  2657. if($line->type != 'new') $old[$line->oldlc] = $line->line;
  2658. if($line->type != 'old') $new[$line->newlc] = $line->line;
  2659. }
  2660. $content->old = $old;
  2661. $content->new = $new;
  2662. }
  2663. }
  2664. return $diffs;
  2665. }
  2666. /**
  2667. * 根据条件获取版本库列表。
  2668. * Get repo list by condition.
  2669. *
  2670. * @param string $repoQuery
  2671. * @param string $SCM
  2672. * @param string $orderBy
  2673. * @param object $pager
  2674. * @access public
  2675. * @return array
  2676. */
  2677. public function getListByCondition($repoQuery, $SCM, $orderBy = 'id_desc', $pager = null)
  2678. {
  2679. return $this->dao->select('*')->from(TABLE_REPO)
  2680. ->where('deleted')->eq('0')
  2681. ->beginIF(!empty($repoQuery))->andWhere($repoQuery)->fi()
  2682. ->beginIF($SCM)->andWhere('SCM')->in($SCM)->fi()
  2683. ->orderBy($orderBy)
  2684. ->page($pager)
  2685. ->fetchAll('id', false);
  2686. }
  2687. /*
  2688. * 保存对象和分支的关联关系。
  2689. * Save object and branch relation.
  2690. *
  2691. * @param int $repoID
  2692. * @param string $branch
  2693. * @param int $objectID
  2694. * @param string $objectType
  2695. * @param string $relation
  2696. * @access public
  2697. * @return bool
  2698. */
  2699. /**
  2700. * @param int $repoID
  2701. * @param string $branch
  2702. * @param int $objectID
  2703. * @param string $objectType
  2704. * @param string $relation
  2705. */
  2706. public function saveRelation($repoID, $branch, $objectID, $objectType, $relation = 'linkrepobranch')
  2707. {
  2708. $relate = new stdclass();
  2709. $relate->product = 0;
  2710. $relate->AType = $objectType;
  2711. $relate->AID = $objectID;
  2712. $relate->BType = $branch;
  2713. $relate->BID = $repoID;
  2714. $relate->relation = $relation;
  2715. $this->dao->replace(TABLE_RELATION)->data($relate)->exec();
  2716. return !dao::isError();
  2717. }
  2718. /**
  2719. * 获取对象关联的代码分支。
  2720. * Get linked branch of object.
  2721. *
  2722. * @param int $objectID
  2723. * @param string $objectType
  2724. * @access public
  2725. * @return array
  2726. * @param int $repoID
  2727. */
  2728. public function getLinkedBranch($objectID = 0, $objectType = '', $repoID = 0)
  2729. {
  2730. return $this->dao->select('BID,BType,AType')->from(TABLE_RELATION)
  2731. ->where('relation')->eq('linkrepobranch')
  2732. ->beginIF($objectType)->andWhere('AType')->eq($objectType)->fi()
  2733. ->beginIF($repoID)->andWhere('BID')->eq($repoID)->fi()
  2734. ->beginIF($objectID)->andWhere('AID')->eq($objectID)->fi()
  2735. ->fetchAll();
  2736. }
  2737. /**
  2738. * 移除对象关联的代码分支。
  2739. * Get linked branch of object.
  2740. *
  2741. * @param int $objectID
  2742. * @param string $objectType
  2743. * @param int $repoID
  2744. * @param string $branch
  2745. * @access public
  2746. * @return array
  2747. */
  2748. public function unlinkObjectBranch($objectID, $objectType, $repoID, $branch)
  2749. {
  2750. $this->dao->delete()->from(TABLE_RELATION)
  2751. ->where('AType')->eq($objectType)
  2752. ->andWhere('relation')->eq('linkrepobranch')
  2753. ->andWhere('AID')->eq($objectID)
  2754. ->andWhere('BID')->eq($repoID)
  2755. ->andWhere('BType')->eq($branch)
  2756. ->exec();
  2757. return !dao::isError();
  2758. }
  2759. /**
  2760. * 通过产品ID和代码库类型获取代码库列表。
  2761. * Get repo list by product id.
  2762. *
  2763. * @param int $productID
  2764. * @param string $scm
  2765. * @param int $limit
  2766. * @access public
  2767. * @return array
  2768. */
  2769. public function getListByProduct($productID, $scm = '', $limit = 0)
  2770. {
  2771. return $this->dao->select('*')->from(TABLE_REPO)
  2772. ->where('deleted')->eq('0')
  2773. ->andWhere("FIND_IN_SET({$productID}, `product`)")
  2774. ->beginIF($scm)->andWhere('SCM')->in($scm)->fi()
  2775. ->beginIF($limit)->limit($limit)->fi()
  2776. ->fetchAll('id');
  2777. }
  2778. /**
  2779. * 获取代码库服务器已经导入的项目/代码库。
  2780. * Get the imported projects/repositories by service host id.
  2781. *
  2782. * @param int $hostID
  2783. * @return array
  2784. */
  2785. public function getImportedProjects($hostID)
  2786. {
  2787. $importedProjects = $this->dao->select('serviceProject')->from(TABLE_REPO)
  2788. ->where('serviceHost')->eq($hostID)
  2789. ->andWhere('deleted')->eq('0')
  2790. ->fetchAll('serviceProject');
  2791. if(dao::isError()) return array();
  2792. return array_keys($importedProjects);
  2793. }
  2794. /**
  2795. * 隐藏DevOps菜单,执行和项目模块使用。
  2796. * Hide DevOps menu.
  2797. *
  2798. * @param int $objectID
  2799. * @access public
  2800. * @return int
  2801. */
  2802. public function setHideMenu($objectID)
  2803. {
  2804. $menuGroup = $this->app->tab == 'project' ? array('project', 'waterfall') : array('execution');
  2805. $repoPairs = $this->loadModel('repo')->getRepoPairs($this->app->tab, $objectID);
  2806. $showMR = false;
  2807. $showTag = false;
  2808. $showBranch = false;
  2809. $showCommit = false;
  2810. $hasTagSCM = array_map('strtolower', $this->config->repo->notSyncSCM);
  2811. foreach($repoPairs as $repoID => $repoName)
  2812. {
  2813. preg_match('/^\[(\w+)\]/', $repoName, $matches);
  2814. $result = isset($matches[1]) ? $matches[1] : '';
  2815. if($repoID == $this->session->repoID && in_array($result, $hasTagSCM))
  2816. {
  2817. $showTag = true;
  2818. $showBranch = true;
  2819. }
  2820. if(in_array($result, $this->config->repo->gitServiceList)) $showMR = true;
  2821. }
  2822. $showMR = $showMR && common::hasPriv('mr', 'browse');
  2823. $showTag = $showTag && common::hasPriv('repo', 'browsetag');
  2824. $showBranch = $showBranch && common::hasPriv('repo', 'browsebranch');
  2825. $showReview = $repoPairs && common::hasPriv('repo', 'review');
  2826. $showCommit = $repoPairs && common::hasPriv('repo', 'log');
  2827. foreach($menuGroup as $module)
  2828. {
  2829. if(!isset($this->lang->{$module}->menu->devops['subMenu'])) continue;
  2830. if(!$showMR) unset($this->lang->{$module}->menu->devops['subMenu']->mr);
  2831. if(!$showTag) unset($this->lang->{$module}->menu->devops['subMenu']->tag);
  2832. if(!$showBranch) unset($this->lang->{$module}->menu->devops['subMenu']->branch);
  2833. if(!$showReview) unset($this->lang->{$module}->menu->devops['subMenu']->review);
  2834. if(!$showCommit) unset($this->lang->{$module}->menu->devops['subMenu']->commit);
  2835. if(count((array)$this->lang->{$module}->menu->devops['subMenu']) < 2) unset($this->lang->{$module}->menu->devops['subMenu']);
  2836. }
  2837. return $objectID;
  2838. }
  2839. /**
  2840. * Check repo name.
  2841. *
  2842. * @param string $name
  2843. * @access public
  2844. * @return bool
  2845. */
  2846. public function checkName($name)
  2847. {
  2848. $pattern = "/^[a-z_]{1}[a-z0-9_\-\.]*$/i";
  2849. return preg_match($pattern, $name);
  2850. }
  2851. }