forked from jpetso/versioncontrol
/
VersioncontrolOperation.php
1187 lines (1089 loc) · 51.8 KB
/
VersioncontrolOperation.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?php
// $Id$
/**
* @file
* Operation class
*/
require_once 'VersioncontrolItem.php';
require_once 'VersioncontrolBranch.php';
require_once 'VersioncontrolTag.php';
/**
* @name VCS operations
* a.k.a. stuff that is recorded for display purposes.
*/
//@{
define('VERSIONCONTROL_OPERATION_COMMIT', 1);
define('VERSIONCONTROL_OPERATION_BRANCH', 2);
define('VERSIONCONTROL_OPERATION_TAG', 3);
//@}
/**
* Stuff that happened in a repository at a specific time
*
*/
abstract class VersioncontrolOperation implements ArrayAccess {
// Attributes
/**
* db identifier (before vc_op_id)
*
* The Drupal-specific operation identifier (a simple integer)
* which is unique among all operations (commits, branch ops, tag ops)
* in all repositories.
*
* @var int
*/
public $vc_op_id;
/**
* who actually perform the change
*
* @var string
*/
public $committer;
/**
* The time when the operation was performed, given as
* Unix timestamp. (For commits, this is the time when the revision
* was committed, whereas for branch/tag operations it is the time
* when the files were branched or tagged.)
*
* @var timestamp
*/
public $date;
/**
* The VCS specific repository-wide revision identifier,
* like '' in CVS, '27491' in Subversion or some SHA-1 key in various
* distributed version control systems. If there is no such revision
* (which may be the case for version control systems that don't support
* atomic commits) then the 'revision' element is an empty string.
* For branch and tag operations, this element indicates the
* (repository-wide) revision of the files that were branched or tagged.
*
* @var string
*/
public $revision;
/**
* The log message for the commit, tag or branch operation.
* If a version control system doesn't support messages for the current
* operation type, this element should be empty.
*
* @var string
*/
public $message;
/**
* The system specific VCS username of the user who executed
* this operation(aka who write the change)
*
* @var string
*/
public $author;
/**
* The repository where this operation occurs,
* given as a structured array, like the return value
* of Versioncontrolrepository::getRepository().
*
* @var VersioncontrolRepository
*/
public $repository;
/**
* The type of the operation - one of the
* VERSIONCONTROL_OPERATION_{COMMIT,BRANCH,TAG} constants.
*
* @var string
*/
public $type;
/**
* An array of branches or tags that were affected by this
* operation. Branch and tag operations are known to only affect one
* branch or tag, so for these there will be only one element (with 0
* as key) in 'labels'. Commits might affect any number of branches,
* including none. Commits that emulate branches and/or tags (like
* in Subversion, where they're not a native concept) can also include
* add/delete/move operations for labels, as detailed below.
* Mind that the main development branch - e.g. 'HEAD', 'trunk'
* or 'master' - is also considered a branch. Each element in 'labels'
* is a VersioncontrolLabel(VersioncontrolBranch VersioncontrolTag)
*
* @var array
*/
public $labels;
/**
* All possible operation constraints.
* Each constraint is identified by its key which denotes the array key within
* the $constraints parameter that is given to self::getOperations().
* The array value of each element is a description array containing the
* elements 'callback' and 'cardinality'.
*
*/
private static $constraint_info = array();
/**
* FIXME: ?
*/
private static $error_messages = array();
/**
* Constructor
*/
public function __construct($type, $committer, $date, $revision, $message, $author = NULL, $repository = NULL, $vc_op_id = NULL) {
$this->type = $type;
$this->committer = $committer;
$this->date = $date;
$this->revision = $revision;
$this->message = $message;
$this->author = (is_null($author))? $committer: $author;
$this->repository = $repository;
$this->vc_op_id = $vc_op_id;
}
// Associations
// Operations
/**
* Retrieve a set of commit, branch or tag operations that match
* the given constraints.
*
* @static
* @param $constraints
* An optional array of constraints. Possible array elements are:
*
* - 'vcs': An array of strings, like array('cvs', 'svn', 'git').
* If given, only operations for these backends will be returned.
* - 'repo_ids': An array of repository ids. If given, only operations
* for the corresponding repositories will be returned.
* - 'types': An array containing any combination of the three
* VERSIONCONTROL_OPERATION_{COMMIT,BRANCH,TAG} constants, like
* array(VERSIONCONTROL_OPERATION_COMMIT, VERSIONCONTROL_OPERATION_TAG).
* If given, only operations of this type will be returned.
* - 'branches': An array of strings, like array('HEAD', 'DRUPAL-5').
* If given, only commits or branch operations on one of these branches
* will be returned.
* - 'tags': An array of strings, like array('DRUPAL-6-1', 'DRUPAL-6--1-0').
* If given, only tag operations with one of these tag names will be
* returned.
* - 'revisions': An array of strings, each containing a VCS-specific
* (global) revision, like '27491' for Subversion or some SHA-1 key in
* various distributed version control systems. If given, only
* operations with that revision identifier will be returned. Note that
* this constraint only works for version control systems that support
* global revision identifiers, so this will filter out all
* CVS operations.
* - 'labels': A combination of the 'branches' and 'tags' constraints.
* - 'paths': An array of strings (item locations), like
* array(
* '/trunk/contributions/modules/versioncontrol',
* '/trunk/contributions/themes/b2',
* ).
* If given, only operations affecting one of these items
* (or its children, in case the item is a directory) will be returned.
* - 'message': A string, or an array of strings (which will be combined with
* an "OR" operator). If given, only operations containing the string(s)
* in their log message will be returned.
* - 'item_revision_ids': An array of item revision ids. If given, only
* operations affecting one of the items with that id will be returned.
* - 'item_revisions': An array of strings, each containing a VCS-specific
* file-level revision, like '1.15.2.3' for CVS, '27491' for Subversion,
* or some SHA-1 key in various distributed version control systems.
* If given, only operations affecting one of the items with that
* item revision will be returned.
* - 'vc_op_ids': An array of operation ids. If given, only operations
* matching those ids will be returned.
* - 'date_lower': A Unix timestamp. If given, no operations will be
* retrieved that were performed earlier than this lower bound.
* - 'date_lower': A Unix timestamp. If given, no operations will be
* retrieved that were performed later than this upper bound.
* - 'uids': An array of Drupal user ids. If given, the result set will only
* contain operations that were performed by any of the specified users.
* - 'usernames': An array of system-specific usernames (the ones that the
* version control systems themselves get to see), like
* array('dww', 'jpetso'). If given, the result set will only contain
* operations that were performed by any of the specified users.
* - 'user_relation': If set to VERSIONCONTROL_USER_ASSOCIATED, only
* operations whose authors can be associated to Drupal users will be
* returned. If set to VERSIONCONTROL_USER_ASSOCIATED_ACTIVE, only users
* will be considered that are not blocked.
*
* @param $options
* An optional array of additional options for retrieving the operations.
* The following array keys are supported:
*
* - 'query_type': If unset, the standard db_query() function is used to
* retrieve all operations that match the given constraints.
* Can be set to 'range' or 'pager' to use the db_query_range()
* or pager_query() functions instead. Additional options are required
* in this case.
* - 'count': Required if 'query_type' is either 'range' or 'pager'.
* Specifies the number of operations to be returned by this function.
* - 'from': Required if 'query_type' is 'range'. Specifies the first
* result row to return. (Usually you want to pass 0 for this one.)
* - 'pager_element': Optional for 'pager' as 'query_type'. An optional
* integer to distinguish between multiple pagers on one page.
*
* @return
* An array of operations, reversely sorted by the time of the operation.
* Each element contains an "operation array" with the 'vc_op_id' identifier
* as key (which doesn't influence the sorting) and the following keys:
*
* - 'vc_op_id': The Drupal-specific operation identifier (a simple integer)
* which is unique among all operations (commits, branch ops, tag ops)
* in all repositories.
* - 'type': The type of the operation - one of the
* VERSIONCONTROL_OPERATION_{COMMIT,BRANCH,TAG} constants.
* Note that if you pass branch or tag constraints, this function might
* nevertheless return commit operations too - that happens for version
* control systems without native branches or tags (like Subversion)
* when a branch or tag is affected by the commit.
* - 'repository': The repository where this operation occurred.
* This is a structured "repository array", like is returned
* by versioncontrol_get_repository().
* - 'date': The time when the operation was performed, given as
* Unix timestamp. (For commits, this is the time when the revision
* was committed, whereas for branch/tag operations it is the time
* when the files were branched or tagged.)
* - 'uid': The Drupal user id of the operation author, or 0 if no
* Drupal user could be associated to the author.
* - 'username': The system specific VCS username of the author.
* - 'message': The log message for the commit, tag or branch operation.
* If a version control system doesn't support messages for any of them,
* this element contains an empty string.
* - 'revision': The VCS specific repository-wide revision identifier,
* like '' in CVS, '27491' in Subversion or some SHA-1 key in various
* distributed version control systems. If there is no such revision
* (which may be the case for version control systems that don't support
* atomic commits) then the 'revision' element is an empty string.
* For branch and tag operations, this element indicates the
* (repository-wide) revision of the files that were branched or tagged.
*
* - 'labels': An array of branches or tags that were affected by this
* operation. Branch and tag operations are known to only affect one
* branch or tag, so for these there will be only one element (with 0
* as key) in 'labels'. Commits might affect any number of branches,
* including none. Commits that emulate branches and/or tags (like
* in Subversion, where they're not a native concept) can also include
* add or delete operations for labels, as detailed below.
* Mind that the main development branch - e.g. 'HEAD', 'trunk'
* or 'master' - is also considered a branch. Each element in 'labels'
* is a VersioncontrolLabel(VersioncontrolBranch VersioncontrolTag)
*
* If not a single operation matches these constraints,
* an empty array is returned.
*/
public static function getOperations($constraints = array(), $options = array()) {
$tables = array(
'versioncontrol_operations' => array('alias' => 'op'),
'versioncontrol_repositories' => array(
'alias' => 'r',
'join_on' => 'op.repo_id = r.repo_id',
),
);
// Construct the actual query, and let other modules provide "native"
// custom constraints as well.
$query_info = self::_constructQuery(
$constraints, $tables
);
if (empty($query_info)) {
return array();
}
$query = 'SELECT DISTINCT(op.vc_op_id), op.type, op.date, op.uid,
op.author, op.committer, op.message, op.revision, r.repo_id, r.vcs
FROM '. $query_info['from'] .
(empty($query_info['where']) ? '' : ' WHERE '. $query_info['where']) .'
ORDER BY op.date DESC, op.vc_op_id DESC';
$result = _versioncontrol_query($query, $query_info['params'], $options);
$operations = array();
$op_id_placeholders = array();
$op_ids = array();
$repo_ids = array();
while ($row = db_fetch_object($result)) {
// Remember which repositories and backends are being used for the
// results of this query.
if (!in_array($row->repo_id, $repo_ids)) {
$repo_ids[] = $row->repo_id;
}
// Construct an operation array - nearly done already.
// 'repo_id' is replaced by 'repository' further down
$operations[$row->vc_op_id] = $row;
$op_ids[] = $row->vc_op_id;
$op_id_placeholders[] = '%d';
}
if (empty($operations)) {
return array();
}
// Add the corresponding repository array to each operation.
$repositories = VersioncontrolRepositoryCache::getInstance()->getRepositories(array('repo_ids' => $repo_ids));
foreach ($operations as $vc_op_id => $operation) {
$repo = $repositories[$operation->repo_id];
$operationObj = new $repo->backend->classes['operation']($operation->type,
$operation->committer, $operation->date, $operation->revision, $operation->message,
$operation->author, $repo, $operation->vc_op_id);
$operationObj->labels = array();
$operationObj->uid = $operation->uid;
$operations[$operation->vc_op_id] = $operationObj;
}
// Add the corresponding labels to each operation.
$result = db_query('SELECT op.vc_op_id, oplabel.action,
label.label_id, label.name, label.type
FROM {versioncontrol_operations} op
INNER JOIN {versioncontrol_operation_labels} oplabel
ON op.vc_op_id = oplabel.vc_op_id
INNER JOIN {versioncontrol_labels} label
ON oplabel.label_id = label.label_id
WHERE op.vc_op_id IN
('. implode(',', $op_id_placeholders) .')', $op_ids);
while ($row = db_fetch_object($result)) {
switch ($row->type) {
case VERSIONCONTROL_LABEL_TAG:
$operations[$row->vc_op_id]->labels[] = new VersioncontrolTag(
$row->name, $row->action, $row->label_id,
$operations[$row->vc_op_id]->repository
);
break;
case VERSIONCONTROL_LABEL_BRANCH:
$operations[$row->vc_op_id]->labels[] = new VersioncontrolBranch(
$row->name, $row->action, $row->label_id,
$operations[$row->vc_op_id]->repository
);
break;
}
}
return $operations;
}
/**
* Convenience function, calling versioncontrol_get_operations() with a preset
* of array(VERSIONCONTROL_OPERATION_COMMIT) for the 'types' constraint
* (so only commits are returned). Parameters and result array are the same
* as those from versioncontrol_get_operations().
*
* @static
*/
public static function getCommits($constraints = array(), $options = array()) {
if (isset($constraints['types']) && !in_array(VERSIONCONTROL_OPERATION_COMMIT, $constraints['types'])) {
return array(); // no commits in the original constraints, intersects to empty
}
$constraints['types'] = array(VERSIONCONTROL_OPERATION_COMMIT);
return VersioncontrolOperation::getOperations($constraints, $options);
}
/**
* Convenience function, calling versioncontrol_get_operations() with a preset
* of array(VERSIONCONTROL_OPERATION_TAG) for the 'types' constraint
* (so only tag operations or commits affecting emulated tags are returned).
* Parameters and result array are the same as those
* from versioncontrol_get_operations().
*
* @static
*/
public static function getTags($constraints = array(), $options = array()) {
if (isset($constraints['types']) && !in_array(VERSIONCONTROL_OPERATION_TAG, $constraints['types'])) {
return array(); // no tags in the original constraints, intersects to empty
}
$constraints['types'] = array(VERSIONCONTROL_OPERATION_TAG);
return VersioncontrolOperation::getOperations($constraints, $options);
}
/**
* Convenience function, calling versioncontrol_get_operations() with a preset
* of array(VERSIONCONTROL_OPERATION_BRANCH) for the 'types' constraint
* (so only branch operations or commits affecting emulated branches
* are returned). Parameters and result array are the same as those
* from versioncontrol_get_operations().
*
* @static
*/
public static function getBranches($constraints = array(), $options = array()) {
if (isset($constraints['types']) && !in_array(VERSIONCONTROL_OPERATION_BRANCH, $constraints['types'])) {
return array(); // no branches in the original constraints, intersects to empty
}
$constraints['types'] = array(VERSIONCONTROL_OPERATION_BRANCH);
return VersioncontrolOperation::getOperations($constraints, $options);
}
/**
* Retrieve all items that were affected by an operation.
*
* @param $fetch_source_items
* If TRUE, source and replaced items will be retrieved as well,
* and stored as additional properties inside each item array.
* If FALSE, only current/new items will be retrieved.
* If NULL (default), source and replaced items will be retrieved for commits
* but not for branch or tag operations.
*
* @return
* A structured array containing all items that were affected by the given
* operation. Array keys are the current/new paths, even if the item doesn't
* exist anymore (as is the case with delete actions in commits).
* The associated array elements are structured item arrays and consist of
* the following elements:
*
* - 'type': Specifies the item type, which is either
* VERSIONCONTROL_ITEM_FILE or VERSIONCONTROL_ITEM_DIRECTORY for items
* that still exist, or VERSIONCONTROL_ITEM_FILE_DELETED respectively
* VERSIONCONTROL_ITEM_DIRECTORY_DELETED for items that have been
* removed (by a commit's delete action).
* - 'path': The path of the item at the specific revision.
* - 'revision': The (file-level) revision when the item was changed.
* If there is no such revision (which may be the case for
* directory items) then the 'revision' element is an empty string.
* - 'item_revision_id': Identifier of this item revision in the database.
* Note that you can only rely on this element to exist for
* operation items - functions that interface directly with the VCS
* (such as versioncontrol_get_directory_contents() or
* versioncontrol_get_parallel_items()) might not include
* this identifier, for obvious reasons.
*
* If the @p $fetch_source_items parameter is TRUE,
* versioncontrol_fetch_source_items() will be called on the list of items
* in order to retrieve additional information about their origin.
* The following elements will be set for each item in addition
* to the ones listed above:
*
* - 'action': Specifies how the item was changed.
* One of the predefined VERSIONCONTROL_ACTION_* values.
* - 'source_items': An array with the previous revision(s) of the affected
* item. Empty if 'action' is VERSIONCONTROL_ACTION_ADDED. The key for
* all items in this array is the respective item path.
* - 'replaced_item': The previous but technically unrelated item at the
* same location as the current item. Only exists if this previous item
* was deleted and replaced by a different one that was just moved
* or copied to this location.
* - 'line_changes': Only exists if line changes have been recorded for this
* action - if so, this is an array containing the number of added lines
* in an element with key 'added', and the number of removed lines in
* the 'removed' key.
* FIXME refactor me to oo
*/
public function getItems($fetch_source_items = NULL) {
$items = array();
$result = db_query(
'SELECT ir.item_revision_id, ir.path, ir.revision, ir.type
FROM {versioncontrol_operation_items} opitem
INNER JOIN {versioncontrol_item_revisions} ir
ON opitem.item_revision_id = ir.item_revision_id
WHERE opitem.vc_op_id = %d AND opitem.type = %d',
$this->vc_op_id, VERSIONCONTROL_OPERATION_MEMBER_ITEM);
while ($item_revision = db_fetch_object($result)) {
$items[$item_revision->path] = new VersioncontrolItem($item_revision->type, $item_revision->path, $item_revision->revision, NULL, $this->repository, NULL, $item_revision->item_revision_id);
$items[$item_revision->path]->selected_label = new stdClass();
$items[$item_revision->path]->selected_label->get_from = 'operation';
$items[$item_revision->path]->selected_label->operation = &$this;
//TODO inherit from operation class insteadof types?
if ($this->type == VERSIONCONTROL_OPERATION_COMMIT) {
$items[$item_revision->path]->commit_operation = $this;
}
}
if (!isset($fetch_source_items)) {
// By default, fetch source items for commits but not for branch or tag ops.
$fetch_source_items = ($this->type == VERSIONCONTROL_OPERATION_COMMIT);
}
if ($fetch_source_items) {
versioncontrol_fetch_source_items($this->repository, $items);
}
ksort($items); // similar paths should be next to each other
return $items;
}
/**
* Replace the set of affected labels of the actual object with the one in
* @p $labels. If any of the given labels does not yet exist in the
* database, a database entry (including new 'label_id' array element) will
* be written as well.
*/
public function updateLabels($labels) {
module_invoke_all('versioncontrol_operation_labels',
'update', $this, $labels
);
$this->_setLabels($labels);
}
/**
* Insert a commit, branch or tag operation into the database, and call the
* necessary module hooks. Only call this function after the operation has been
* successfully executed.
*
* @param $operation_items
* A structured array containing the exact details of happened to each
* item in this operation. The structure of this array is the same as
* the return value of versioncontrol_get_operation_items() - that is,
* elements for 'type', 'path' and 'revision' - but doesn't include the
* 'item_revision_id' element, that one will be filled in by this function.
*
* For commit operations, you also have to fill in the 'action' and
* 'source_items' elements (and optionally 'replaced_item') that are also
* described in the versioncontrol_get_operation_items() API documentation.
* The 'line_changes' element, as in versioncontrol_get_operation_items(),
* is optional to provide.
*
* This parameter is passed by reference as the insert operation will
* check the validity of a few item properties and will also assign an
* 'item_revision_id' property to each of the given items. So when this
* function returns with a result other than NULL, the @p $operation_items
* array will also be up to snuff for further processing.
*
* @return
* The finalized operation array, with all of the 'vc_op_id', 'repository'
* and 'uid' properties filled in, and 'repo_id' removed if it existed before.
* Labels are now equipped with an additional 'label_id' property.
* (For more info on these labels, see the API documentation for
* versioncontrol_get_operations() and versioncontrol_get_operation_items().)
* In case of an error, NULL is returned instead of the operation array.
*/
public final function insert(&$operation_items) {
$this->_fill(TRUE);
if (!isset($this->repository)) {
return NULL;
}
// Ok, everything's there, insert the operation into the database.
$this->repo_id = $this->repository->repo_id; // for drupal_write_record()
//FIXME $this->uid = 0;
drupal_write_record('versioncontrol_operations', $this);
unset($this->repo_id);
// drupal_write_record() has now added the 'vc_op_id' to the $operation array.
// Insert labels that are attached to the operation.
$this->_setLabels($this->labels);
$vcs = $this->repository->vcs;
// So much for the operation itself, now the more verbose part: items.
ksort($operation_items); // similar paths should be next to each other
foreach ($operation_items as $path => $item) {
$item->sanitize();
$item->ensure();
$this->_insert_operation_item($item,
VERSIONCONTROL_OPERATION_MEMBER_ITEM);
$item['selected_label'] = new stdClass();
$item['selected_label']->get_from = 'operation';
$item['selected_label']->successor_item = &$this;
// If we've got source items (which is the case for commit operations),
// add them to the item revisions and source revisions tables as well.
foreach ($item->source_items as $key => $source_item) {
$source_item->ensure();
$item->insertSourceRevision($source_item, $item->action);
// Cache other important items in the operations table for 'path' search
// queries, because joining the source revisions table is too expensive.
switch ($item['action']) {
case VERSIONCONTROL_ACTION_MOVED:
case VERSIONCONTROL_ACTION_COPIED:
case VERSIONCONTROL_ACTION_MERGED:
if ($item->path != $source_item->path) {
$this->_insert_operation_item($source_item,
VERSIONCONTROL_OPERATION_CACHED_AFFECTED_ITEM);
}
break;
default: // No additional caching for added, modified or deleted items.
break;
}
$source_item->selected_label = new stdClass();
$source_item->selected_label->get_from = 'other_item';
$source_item->selected_label->other_item = &$item;
$source_item->selected_label->other_item_tags = array('successor_item');
$item->source_items[$key] = $source_item;
}
// Plus a special case for the "added" action, as it needs an entry in the
// source items table but contains no items in the 'source_items' property.
if ($item->action == VERSIONCONTROL_ACTION_ADDED) {
$item->insertSourceRevision(0, $item['action']);
}
// If we've got a replaced item (might happen for copy/move commits),
// add it to the item revisions and source revisions table as well.
if (isset($item->replaced_item)) {
$item->replaced_item->ensure();
$item->insertSourceRevision($item->replaced_item,
VERSIONCONTROL_ACTION_REPLACED);
$item->replaced_item->selected_label = new stdClass();
$item->replaced_item->selected_label->get_from = 'other_item';
$item->replaced_item->selected_label->other_item = &$item;
$item->replaced_item->selected_label->other_item_tags = array('successor_item');
}
$operation_items[$path] = $item;
}
// Notify the backend first.
$this->_insert($operation_items);
// Everything's done, let the world know about it!
module_invoke_all('versioncontrol_operation',
'insert', $this, $operation_items
);
// This one too, as there is also an update function & hook for it.
// Pretend that the labels didn't exist beforehand.
$labels = $this->labels;
$this->labels = array();
module_invoke_all('versioncontrol_operation_labels',
'insert', $this, $labels
);
$this->labels = $labels;
// Rules integration, because we like to enable people to be flexible.
// FIXME change callback
if (module_exists('rules')) {
rules_invoke_event('versioncontrol_operation_insert', array(
'operation' => $this,
'items' => $operation_items,
));
}
//FIXME avoid return, it's on the object
return $this;
}
/**
* Let child backend repo classes add information that _is not_ in
* VersioncontrolRepository::data without modifying general flow if
* necessary.
*/
protected function _insert($operation_items) {
}
/**
* Delete a commit, a branch operation or a tag operation from the database,
* and call the necessary hooks.
*
* @param $operation
* The commit, branch operation or tag operation array containing
* the operation that should be deleted.
*/
public final function delete() {
$operation_items = $this->getItems();
// As versioncontrol_update_operation_labels() provides an update hook for
// operation labels, we should also have a delete hook for completeness.
module_invoke_all('versioncontrol_operation_labels',
'delete', $this, array());
// Announce deletion of the operation before anything has happened.
// Calls hook_versioncontrol_commit(), hook_versioncontrol_branch_operation()
// or hook_versioncontrol_tag_operation().
module_invoke_all('versioncontrol_operation',
'delete', $this, $operation_items);
// Provide an opportunity for the backend to delete its own stuff.
$this->_delete($operation_items);
db_query('DELETE FROM {versioncontrol_operation_labels}
WHERE vc_op_id = %d', $this->vc_op_id);
db_query('DELETE FROM {versioncontrol_operation_items}
WHERE vc_op_id = %d', $this->vc_op_id);
db_query('DELETE FROM {versioncontrol_operations}
WHERE vc_op_id = %d', $this->vc_op_id);
}
/**
* Let child backend repo classes add information that _is not_ in
* VersioncontrolRepository::data without modifying general flow if
* necessary.
*/
protected function _delete($operation_items) {
}
/**
* Assemble a list of query constraints from the given @p $constraints and
* @p $tables arrays. Both of these are likely to be altered to match the
* actual query, although in practice you probably won't need them anymore.
*
* @static
* @return
* A query information array with keys 'from', 'where' and 'params', or an
* empty array if the constraints were invalid or will return an empty result
* set anyways. The 'from' and 'where' elements are strings to be used inside
* an SQL query (but don't include the actual FROM and WHERE keywords),
* and the 'params' element is an array with query parameter values for the
* returned WHERE clause.
*/
private static function _constructQuery(&$constraints, &$tables) {
// Let modules alter the query by transforming custom constraints into
// stuff that Version Control API can understand.
drupal_alter('versioncontrol_operation_constraints', $constraints);
$and_constraints = array();
$params = array();
$constraint_info = self::_constraintInfo();
$join_callbacks = array();
foreach ($constraints as $key => $constraint_value) {
if (!isset($constraint_info[$key])) {
return array(); // No such constraint -> empty result.
}
// Standardization: put everything into an array if it isn't already.
if ($constraint_info[$key]['cardinality'] == VERSIONCONTROL_CONSTRAINT_SINGLE) {
$constraints[$key] = array($constraints[$key]);
}
elseif ($constraint_info[$key]['cardinality'] == VERSIONCONTROL_CONSTRAINT_SINGLE_OR_MULTIPLE && !is_array($constraint_value)) {
$constraints[$key] = array($constraints[$key]);
}
if (empty($constraints[$key])) {
return array(); // Empty set of constraint options -> empty result.
}
// Single-value constraints get the originally provided constraint value.
// All others get the multiple-value constraint array.
if ($constraint_info[$key]['cardinality'] == VERSIONCONTROL_CONSTRAINT_SINGLE) {
$constraints[$key] = reset($constraints[$key]);
}
// If the constraint unconditionally requires extra tables, add them to
// the $tables array by calling the join callback.
if (!empty($constraint_info[$key]['join callback'])) {
$function = $constraint_info[$key]['join callback'];
if (!isset($join_callbacks[$function])) { // no need to call it twice
$join_callbacks[$function] = TRUE;
$function($tables);
}
}
$function = $constraint_info[$key]['callback'];
$function($constraints[$key], $tables, $and_constraints, $params);
}
// Now that we have all the information, let's construct some usable query parts.
$from = array();
foreach ($tables as $table_name => $table_info) {
if (!empty($table_info['real_table'])) {
$table_name = $table_info['real_table'];
}
$table_string = '{'. $table_name .'} '. $table_info['alias'];
if (isset($table_info['join_on'])) {
$table_string .= ' ON '. $table_info['join_on'] .' ';
}
$from[] = $table_string;
}
return array(
'from' => implode(' INNER JOIN ', $from),
'where' => '('. implode(' AND ', $and_constraints) .')',
'params' => $params,
);
}
/**
* Gather a list of all possible operation constraints.
* Each constraint is identified by its key which denotes the array key within
* the $constraints parameter that is given to versioncontrol_get_operations().
* The array value of each element is a description array containing the
* elements 'callback' and 'cardinality'.
*
* @static
*/
private static function _constraintInfo() {
if (empty(self::$constraint_info)) {
foreach (module_implements('versioncontrol_operation_constraint_info') as $module) {
$function = $module .'_versioncontrol_operation_constraint_info';
$constraints = $function();
foreach ($constraints as $key => $info) {
self::$constraint_info[$key] = $info;
if (!isset($info['callback'])) {
self::$constraint_info[$key]['callback'] = $module .'_operation_constraint_'. $key;
}
if (!isset($info['cardinality'])) {
self::$constraint_info[$key]['cardinality'] = VERSIONCONTROL_CONSTRAINT_MULTIPLE;
}
}
}
}
return self::$constraint_info;
}
/**
* Fill in various operation members into the object(commit, branch op or tag
* op), in case those values are not given.
*
* @param $operation
* The plain operation array that might lack have some properties yet.
* @param $include_unauthorized
* If FALSE, the 'uid' property will receive a value of 0 for known
* but unauthorized users. If TRUE, all known users are mapped to their uid.
*/
private function _fill($include_unauthorized = FALSE) {
// If not already there, retrieve the full repository object.
// FIXME: take one always set member, not sure if root is one | set other condition here
if (!isset($this->repository->root) && isset($this->repository->repo_id)) {
$this->repository = VersioncontrolRepository::getRepository($this->repository->repo_id);
unset($this->repository->repo_id);
}
// If not already there, retrieve the Drupal user id of the committer.
if (!isset($this->author)) {
$uid = versioncontrol_get_account_uid_for_username(
$this->repository->repo_id, $this->author, $include_unauthorized
);
// If no uid could be retrieved, blame the commit on user 0 (anonymous).
$this->author = isset($this->author) ? $this->author : 0;
}
// For insertions (which have 'date' set, as opposed to write access checks),
// fill in the log message if it's unset. We don't want to do this for
// write access checks because empty messages are denied access,
// which requires distinguishing between unset and empty.
if (isset($this->date) && !isset($this->message)) {
$this->message = '';
}
}
/**
* Retrieve or set the list of access errors.
*/
private function _accessErrors($new_messages = NULL) {
if (isset($new_messages)) {
self::$error_messages = $new_messages;
}
return self::$error_messages;
}
/**
* Write @p $labels to the database as set of affected labels of the
* actual operation object. Label ids are not required to exist yet.
* After this the set of labels, all of them with 'label_id' filled in.
*
* @return
*/
private function _setLabels($labels) {
db_query("DELETE FROM {versioncontrol_operation_labels}
WHERE vc_op_id = %d", $this->vc_op_id);
foreach ($labels as &$label) {
$label->ensure();
db_query("INSERT INTO {versioncontrol_operation_labels}
(vc_op_id, label_id, action) VALUES (%d, %d, %d)",
$this->vc_op_id, $label->label_id, $label->action);
}
$this->labels = $labels;
}
/**
* Insert an operation item entry into the {versioncontrol_operation_items} table.
* The item is expected to have an 'item_revision_id' property already.
*/
private function _insert_operation_item($item, $type) {
// Before inserting that item entry, make sure it doesn't exist already.
db_query("DELETE FROM {versioncontrol_operation_items}
WHERE vc_op_id = %d AND item_revision_id = %d",
$this->vc_op_id, $item->item_revision_id);
db_query("INSERT INTO {versioncontrol_operation_items}
(vc_op_id, item_revision_id, type) VALUES (%d, %d, %d)",
$this->vc_op_id, $item->item_revision_id, $type);
}
/**
* If versioncontrol_has_commit_access(), versioncontrol_has_branch_access()
* or versioncontrol_has_tag_access() returned FALSE, you can use this function
* to retrieve the list of error messages from the various access checks.
* The error messages do not include trailing linebreaks, it is expected that
* those are inserted by the caller.
*/
protected function getAccessErrors() {
return $this->_accessErrors();
}
/**
* Determine if a commit, branch or tag operation may be executed or not.
* Call this function inside a pre-commit hook.
*
* @param $operation
* A single operation array like the ones returned by
* versioncontrol_get_operations(), but leaving out on a few details that
* will instead be determined by this function. This array describes
* the operation that is about to happen. Here's the allowed elements:
*
* - 'type': The type of the operation - one of the
* VERSIONCONTROL_OPERATION_{COMMIT,BRANCH,TAG} constants.
* - 'repository': The repository where this operation occurs,
* given as a structured array, like the return value
* of versioncontrol_get_repository().
* You can either pass this or 'repo_id'.
* - 'repo_id': The repository where this operation occurs, given as a simple
* integer id. You can either pass this or 'repository'.
* - 'uid': The Drupal user id of the committer. Passing this is optional -
* if it isn't set, this function will determine the uid.
* - 'username': The system specific VCS username of the committer.
* - 'message': The log message for the commit, tag or branch operation.
* If a version control system doesn't support messages for the current
* operation type, this element must not be set. Operations with
* log messages that are set but empty will be denied access.
*
* - 'labels': An array of branches or tags that will be affected by this
* operation. Branch and tag operations are known to only affect one
* branch or tag, so for these there will be only one element (with 0
* as key) in 'labels'. Commits might affect any number of branches,
* including none. Commits that emulate branches and/or tags (like
* in Subversion, where they're not a native concept) can also include
* add/delete/move operations for labels, as detailed below.
* Mind that the main development branch - e.g. 'HEAD', 'trunk'
* or 'master' - is also considered a branch. Each element in 'labels'
* is a VersioncontrolLabel(VersioncontrolBranch VersioncontrolTag)
*
* @param $operation_items
* A structured array containing the exact details of what is about to happen
* to each item in this commit. The structure of this array is the same as
* the return value of versioncontrol_get_operation_items() - that is,
* elements for 'type', 'path', 'revision', 'action', 'source_items' and
* 'replaced_item' - but doesn't include the 'item_revision_id' element as
* there's no relation to the database yet.
*
* The 'action', 'source_items', 'replaced_item' and 'revision' elements
* of each item are optional and may be left unset.
*
* @return
* TRUE if the operation may happen, or FALSE if not.
* If FALSE is returned, you can retrieve the concerning error messages
* by calling versioncontrol_get_access_errors().
*/
protected function hasWriteAccess($operation, $operation_items) {
$operation = _versioncontrol_fill_operation($operation);
// If we can't determine this operation's repository,
// we can't really allow the operation in the first place.
if (!isset($operation['repository'])) {
switch ($operation['type']) {
case VERSIONCONTROL_OPERATION_COMMIT:
$type = t('commit');
break;
case VERSIONCONTROL_OPERATION_BRANCH:
$type = t('branch');
break;
case VERSIONCONTROL_OPERATION_TAG:
$type = t('tag');
break;
}
$this->_accessErrors(array(t(
'** ERROR: Version Control API cannot determine a repository
** for the !commit-branch-or-tag information given by the VCS backend.',
array('!commit-branch-or-tag' => $type)
)));
return FALSE;
}
// If the user doesn't have commit access at all, we can't allow this as well.
$repo_data = $operation['repository']['data']['versioncontrol'];
if (!$repo_data['allow_unauthorized_access']) {
if (!versioncontrol_is_account_authorized($operation['repository'], $operation['uid'])) {
$this->_accessErrors(array(t(
'** ERROR: !user does not have commit access to this repository.',
array('!user' => $operation['username'])
)));
return FALSE;
}
}