Completed
Pull Request — master (#658)
by Stig
10:45 queued 04:00
created

DNProject::getCommitMessage()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 7

Duplication

Lines 4
Ratio 44.44 %

Importance

Changes 0
Metric Value
dl 4
loc 9
rs 9.6666
c 0
b 0
f 0
cc 2
eloc 7
nc 2
nop 1
1
<?php
2
3
/**
4
 * DNProject represents a project that relates to a group of target
5
 * environments.
6
 *
7
 * @property string Name
8
 * @property string CVSPath
9
 * @property int DiskQuotaMB
10
 *
11
 * @method HasManyList Environments()
12
 * @method ManyManyList Viewers()
13
 * @method ManyManyList StarredBy()
14
 */
15
class DNProject extends DataObject {
0 ignored issues
show
Coding Style introduced by
The property $has_many is not named in camelCase.

This check marks property names that have not been written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection string becomes databaseConnectionString.

Loading history...
Coding Style introduced by
The property $many_many is not named in camelCase.

This check marks property names that have not been written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection string becomes databaseConnectionString.

Loading history...
Coding Style introduced by
The property $summary_fields is not named in camelCase.

This check marks property names that have not been written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection string becomes databaseConnectionString.

Loading history...
Coding Style introduced by
The property $searchable_fields is not named in camelCase.

This check marks property names that have not been written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection string becomes databaseConnectionString.

Loading history...
Coding Style introduced by
The property $singular_name is not named in camelCase.

This check marks property names that have not been written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection string becomes databaseConnectionString.

Loading history...
Coding Style introduced by
The property $plural_name is not named in camelCase.

This check marks property names that have not been written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection string becomes databaseConnectionString.

Loading history...
Coding Style introduced by
The property $default_sort is not named in camelCase.

This check marks property names that have not been written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection string becomes databaseConnectionString.

Loading history...
Coding Style introduced by
The property $show_repository_url is not named in camelCase.

This check marks property names that have not been written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection string becomes databaseConnectionString.

Loading history...
Coding Style introduced by
The property $relation_cache is not named in camelCase.

This check marks property names that have not been written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection string becomes databaseConnectionString.

Loading history...
Coding Style introduced by
The property $has_cloned_cache is not named in camelCase.

This check marks property names that have not been written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection string becomes databaseConnectionString.

Loading history...
Coding Style introduced by
The property $_current_member_cache is not named in camelCase.

This check marks property names that have not been written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection string becomes databaseConnectionString.

Loading history...
Coding Style introduced by
The property $repository_interfaces is not named in camelCase.

This check marks property names that have not been written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection string becomes databaseConnectionString.

Loading history...
16
17
	/**
18
	 * @var array
19
	 */
20
	public static $db = array(
21
		"Name" => "Varchar",
22
		"CVSPath" => "Varchar(255)",
23
		"DiskQuotaMB" => "Int",
24
		"AllowedEnvironmentType" => "Varchar(255)",
25
	);
26
27
	/**
28
	 * @var array
29
	 */
30
	public static $has_many = array(
31
		"Environments" => "DNEnvironment",
32
		"CreateEnvironments" => "DNCreateEnvironment"
33
	);
34
35
	/**
36
	 * @var array
37
	 */
38
	public static $many_many = array(
39
		"Viewers" => "Group",
40
		'StarredBy' => "Member"
41
	);
42
43
	/**
44
	 * @var array
45
	 */
46
	public static $summary_fields = array(
47
		"Name",
48
		"ViewersList",
49
	);
50
51
	/**
52
	 * @var array
53
	 */
54
	public static $searchable_fields = array(
55
		"Name",
56
	);
57
58
	/**
59
	 * @var string
60
	 */
61
	private static $singular_name = 'Project';
0 ignored issues
show
Unused Code introduced by
The property $singular_name is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
62
63
	/**
64
	 * @var string
65
	 */
66
	private static $plural_name = 'Projects';
67
68
	/**
69
	 * @var string
70
	 */
71
	private static $default_sort = 'Name';
72
73
	/**
74
	 * Display the repository URL on the project page.
75
	 *
76
	 * @var bool
77
	 */
78
	private static $show_repository_url = false;
0 ignored issues
show
Unused Code introduced by
The property $show_repository_url is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
79
80
	/**
81
	 * In-memory cache for currentBuilds per environment since fetching them from
82
	 * disk is pretty resource hungry.
83
	 *
84
	 * @var array
85
	 */
86
	protected static $relation_cache = array();
87
88
	/**
89
	 * In-memory cache to determine whether clone repo was called.
90
	 * @var array
91
	 */
92
	private static $has_cloned_cache = array();
93
94
	/**
95
	 * @var bool|Member
96
	 */
97
	protected static $_current_member_cache = null;
98
99
	/**
100
	 * Used by the sync task
101
	 *
102
	 * @param string $path
103
	 * @return \DNProject
104
	 */
105
	public static function create_from_path($path) {
106
		$project = DNProject::create();
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
107
		$project->Name = $path;
108
		$project->write();
109
110
		// add the administrators group as the viewers of the new project
111
		$adminGroup = Group::get()->filter('Code', 'administrators')->first();
112
		if($adminGroup && $adminGroup->exists()) {
113
			$project->Viewers()->add($adminGroup);
114
		}
115
		return $project;
116
	}
117
118
	/**
119
	 * This will clear the cache for the git getters and should be called when the local git repo is updated
120
	 */
121
	public function clearGitCache() {
122
		$cache = self::get_git_cache();
123
		// we only need to clear the tag cache since everything else is cached by SHA, that is for commit and
124
		// commit message.
125
		$cache->clean(Zend_Cache::CLEANING_MODE_MATCHING_TAG, ['gitonomy', 'tags', 'project_'.$this->ID]);
126
	}
127
128
	/**
129
	 * @return \Zend_Cache_Frontend_Output
130
	 */
131
	public static function get_git_cache() {
132
		return SS_Cache::factory('gitonomy', 'Output', [
133
			'automatic_serialization' => true,
134
			'lifetime' => 60*60*24*7 // seven days
135
		]);
136
	}
137
138
	/**
139
	 * Return the used quota in MB.
140
	 *
141
	 * @param int $round Number of decimal places to round to
142
	 * @return double The used quota size in MB
143
	 */
144
	public function getUsedQuotaMB($round = 2) {
145
		$size = 0;
146
147
		foreach($this->Environments() as $environment) {
148
			foreach($environment->DataArchives()->filter('IsBackup', 0) as $archive) {
149
				$size += $archive->ArchiveFile()->getAbsoluteSize();
150
			}
151
		}
152
153
		// convert bytes to megabytes and round
154
		return round(($size / 1024) / 1024, $round);
155
	}
156
157
	/**
158
	 * Getter for DiskQuotaMB field to provide a default for existing
159
	 * records that have no quota field set, as it will need to default
160
	 * to a globally set size.
161
	 *
162
	 * @return string|int The quota size in MB
163
	 */
164
	public function getDiskQuotaMB() {
165
		$size = $this->getField('DiskQuotaMB');
166
167
		if(empty($size)) {
168
			$defaults = $this->config()->get('defaults');
169
			$size = (isset($defaults['DiskQuotaMB'])) ? $defaults['DiskQuotaMB'] : 0;
170
		}
171
172
		return $size;
173
	}
174
175
	/**
176
	 * Has the disk quota been exceeded?
177
	 *
178
	 * @return boolean
179
	 */
180
	public function HasExceededDiskQuota() {
181
		return $this->getUsedQuotaMB(0) >= $this->getDiskQuotaMB();
182
	}
183
184
	/**
185
	 * Is there a disk quota set for this project?
186
	 *
187
	 * @return boolean
188
	 */
189
	public function HasDiskQuota() {
190
		return $this->getDiskQuotaMB() > 0;
191
	}
192
193
	/**
194
	 * Returns the current disk quota usage as a percentage
195
	 *
196
	 * @return int
197
	 */
198
	public function DiskQuotaUsagePercent() {
199
		$quota = $this->getDiskQuotaMB();
200
		if($quota > 0) {
201
			return $this->getUsedQuotaMB() * 100 / $quota;
202
		}
203
		return 100;
204
	}
205
206
	/**
207
	 * Get the menu to be shown on projects
208
	 *
209
	 * @return ArrayList
210
	 */
211
	public function Menu() {
212
		$list = new ArrayList();
213
214
		$controller = Controller::curr();
215
		$actionType = $controller->getField('CurrentActionType');
216
217
		if(DNRoot::FlagSnapshotsEnabled() && $this->isProjectReady()) {
218
			$list->push(new ArrayData(array(
219
				'Link' => sprintf('naut/project/%s/snapshots', $this->Name),
220
				'Title' => 'Snapshots',
221
				'IsCurrent' => $this->isSection() && $controller->getAction() == 'snapshots',
222
				'IsSection' => $this->isSection() && $actionType == DNRoot::ACTION_SNAPSHOT
223
			)));
224
		}
225
226
		$this->extend('updateMenu', $list);
227
228
		return $list;
229
	}
230
231
	/**
232
	 * Is this project currently at the root level of the controller that handles it?
233
	 *
234
	 * @return bool
235
	 */
236
	public function isCurrent() {
237
		return $this->isSection() && Controller::curr()->getAction() == 'project';
238
	}
239
240
	/**
241
	 * Return the current object from $this->Menu()
242
	 * Good for making titles and things
243
	 *
244
	 * @return DataObject
245
	 */
246
	public function CurrentMenu() {
247
		return $this->Menu()->filter('IsSection', true)->First();
248
	}
249
250
	/**
251
	 * Is this project currently in a controller that is handling it or performing a sub-task?
252
	 *
253
	 * @return bool
254
	 */
255
	public function isSection() {
256
		$controller = Controller::curr();
257
		$project = $controller->getField('CurrentProject');
258
		return $project && $this->ID == $project->ID;
259
	}
260
261
	/**
262
	 * Restrict access to viewing this project
263
	 *
264
	 * @param Member|null $member
265
	 * @return boolean
266
	 */
267
	public function canView($member = null) {
268
		if(!$member) {
269
			$member = Member::currentUser();
270
		}
271
272
		if(Permission::checkMember($member, 'ADMIN')) {
273
			return true;
274
		}
275
276
		return $member->inGroups($this->Viewers());
277
	}
278
279
	/**
280
	 * @param Member|null $member
281
	 *
282
	 * @return bool
283
	 */
284 View Code Duplication
	public function canRestore($member = null) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
285
		if ($this->allowedAny(
286
			array(
287
				DNRoot::ALLOW_PROD_SNAPSHOT,
288
				DNRoot::ALLOW_NON_PROD_SNAPSHOT
289
			),
290
			$member
291
		)) {
292
			return true;
293
		}
294
295
		return (bool)$this->Environments()->filterByCallback(function($env) use($member) {
296
			return $env->canRestore($member);
297
		})->Count();
298
	}
299
300
	/**
301
	 * @param Member|null $member
302
	 * @return bool
303
	 */
304 View Code Duplication
	public function canBackup($member = null) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
305
		if ($this->allowedAny(
306
			array(
307
				DNRoot::ALLOW_PROD_SNAPSHOT,
308
				DNRoot::ALLOW_NON_PROD_SNAPSHOT
309
			),
310
			$member
311
		)) {
312
			return true;
313
		}
314
315
		return (bool)$this->Environments()->filterByCallback(function($env) use($member) {
316
			return $env->canBackup($member);
317
		})->Count();
318
	}
319
320
	/**
321
	 * @param Member|null $member
322
	 * @return bool
323
	 */
324 View Code Duplication
	public function canUploadArchive($member = null) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
325
		if ($this->allowedAny(
326
			array(
327
				DNRoot::ALLOW_PROD_SNAPSHOT,
328
				DNRoot::ALLOW_NON_PROD_SNAPSHOT
329
			),
330
			$member
331
		)) {
332
			return true;
333
		}
334
335
		return (bool)$this->Environments()->filterByCallback(function($env) use($member) {
336
			return $env->canUploadArchive($member);
337
		})->Count();
338
	}
339
340
	/**
341
	 * @param Member|null $member
342
	 * @return bool
343
	 */
344 View Code Duplication
	public function canDownloadArchive($member = null) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
345
		if ($this->allowedAny(
346
			array(
347
				DNRoot::ALLOW_PROD_SNAPSHOT,
348
				DNRoot::ALLOW_NON_PROD_SNAPSHOT
349
			),
350
			$member
351
		)) {
352
			return true;
353
		}
354
355
		return (bool)$this->Environments()->filterByCallback(function($env) use($member) {
356
			return $env->canDownloadArchive($member);
357
		})->Count();
358
	}
359
360
	/**
361
	 * This is a permission check for the front-end only.
362
	 *
363
	 * Only admins can create environments for now. Also, we need to check the value
364
	 * of AllowedEnvironmentType which dictates which backend to use to render the form.
365
	 *
366
	 * @param Member|null $member
367
	 *
368
	 * @return bool
369
	 */
370
	public function canCreateEnvironments($member = null) {
371
		$envType = $this->AllowedEnvironmentType;
0 ignored issues
show
Documentation introduced by
The property AllowedEnvironmentType does not exist on object<DNProject>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
372
		if($envType) {
373
			$env = Injector::inst()->get($envType);
374
			if($env instanceof EnvironmentCreateBackend) {
375
				return $this->allowed(DNRoot::ALLOW_CREATE_ENVIRONMENT, $member);
376
			}
377
		}
378
		return false;
379
	}
380
381
	/**
382
	 * @return DataList
383
	 */
384
	public function DataArchives() {
385
		$envIds = $this->Environments()->column('ID');
386
		return DNDataArchive::get()->filter('EnvironmentID', $envIds);
387
	}
388
389
	/**
390
	 * Return all archives which are "manual upload requests",
391
	 * meaning they don't have a file attached to them (yet).
392
	 *
393
	 * @return DataList
394
	 */
395
	public function PendingManualUploadDataArchives() {
396
		return $this->DataArchives()->filter('ArchiveFileID', null);
397
	}
398
399
	/**
400
	 * Build an environment variable array to be used with this project.
401
	 *
402
	 * This is relevant if every project needs to use an individual SSH pubkey.
403
	 *
404
	 * Include this with all Gitonomy\Git\Repository, and
405
	 * \Symfony\Component\Process\Processes.
406
	 *
407
	 * @return array
408
	 */
409
	public function getProcessEnv() {
410
		if(file_exists($this->getPrivateKeyPath())) {
411
			// Key-pair is available, use it.
412
			$processEnv = array(
413
				'IDENT_KEY' => $this->getPrivateKeyPath(),
414
				'GIT_SSH' => BASE_PATH . "/deploynaut/git-deploy.sh"
415
			);
416
		} else {
417
			$processEnv = array();
418
		}
419
		$this->extend('updateProcessEnv', $processEnv);
420
421
		return $processEnv;
422
	}
423
424
	/**
425
	 * Get a string of people allowed to view this project
426
	 *
427
	 * @return string
428
	 */
429
	public function getViewersList() {
430
		return implode(", ", $this->Viewers()->column("Title"));
431
	}
432
433
	/**
434
	 * @return DNData
435
	 */
436
	public function DNData() {
437
		return DNData::inst();
438
	}
439
440
	/**
441
	 * Provides a DNBuildList of builds found in this project.
442
	 *
443
	 * @return DNReferenceList
444
	 */
445
	public function DNBuildList() {
446
		return DNReferenceList::create($this, $this->DNData());
447
	}
448
449
	/**
450
	 * Provides a list of the branches in this project.
451
	 *
452
	 * @return DNBranchList
453
	 */
454
	public function DNBranchList() {
455
		if($this->CVSPath && !$this->repoExists()) {
456
			$this->cloneRepo();
457
		}
458
		return DNBranchList::create($this, $this->DNData());
459
	}
460
461
	/**
462
	 * Provides a list of the tags in this project.
463
	 *
464
	 * @return DNReferenceList
465
	 */
466
	public function DNTagList() {
467
		if($this->CVSPath && !$this->repoExists()) {
468
			$this->cloneRepo();
469
		}
470
		return DNReferenceList::create($this, $this->DNData(), null, null, true);
471
	}
472
473
	/**
474
	 * @return false|Gitonomy\Git\Repository
475
	 */
476
	public function getRepository() {
477
		if(!$this->repoExists()) {
478
			return false;
479
		}
480
481
		return new Gitonomy\Git\Repository($this->getLocalCVSPath());
482
	}
483
484
	/**
485
	 * Provides a list of environments found in this project.
486
	 * CAUTION: filterByCallback will change this into an ArrayList!
487
	 *
488
	 * @return ArrayList
489
	 */
490
	public function DNEnvironmentList() {
491
492
		if(!self::$_current_member_cache) {
493
			self::$_current_member_cache = Member::currentUser();
0 ignored issues
show
Documentation Bug introduced by
It seems like \Member::currentUser() can also be of type object<DataObject>. However, the property $_current_member_cache is declared as type boolean|object<Member>. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
494
		}
495
496
		if(self::$_current_member_cache === false) {
497
			return new ArrayList();
498
		}
499
500
		$currentMember = self::$_current_member_cache;
501
		return $this->Environments()
502
			->filterByCallBack(function($item) use ($currentMember) {
503
				return $item->canView($currentMember);
504
			});
505
	}
506
507
	/**
508
	 * @param string $usage
509
	 * @return ArrayList
510
	 */
511
	public function EnvironmentsByUsage($usage) {
512
		return $this->DNEnvironmentList()->filter('Usage', $usage);
513
	}
514
515
	/**
516
	 * Returns a map of envrionment name to build name
517
	 *
518
	 * @return false|DNDeployment
519
	 */
520
	public function currentBuilds() {
521
		if(!isset(self::$relation_cache['currentBuilds.'.$this->ID])) {
522
			$currentBuilds = array();
523
			foreach($this->Environments() as $env) {
524
				$currentBuilds[$env->Name] = $env->CurrentBuild();
525
			}
526
			self::$relation_cache['currentBuilds.'.$this->ID] = $currentBuilds;
527
		}
528
		return self::$relation_cache['currentBuilds.'.$this->ID];
529
	}
530
531
	/**
532
	 * @param string
533
	 * @return string
534
	 */
535
	public function Link($action = '') {
536
		return Controller::join_links("naut", "project", $this->Name, $action);
537
	}
538
539
	/**
540
	 * @return string|null
541
	 */
542
	public function CreateEnvironmentLink() {
543
		if($this->canCreateEnvironments()) {
544
			return $this->Link('createenv');
545
		}
546
		return null;
547
	}
548
549
	/**
550
	 * @return string
551
	 */
552
	public function ToggleStarLink() {
553
		return $this->Link('/star');
554
	}
555
556
	/**
557
	 * @return bool
558
	 */
559
	public function IsStarred() {
560
		$member = Member::currentUser();
561
		if($member === null) {
562
			return false;
563
		}
564
		$favourited = $this->StarredBy()->filter('MemberID', $member->ID);
565
		if($favourited->count() == 0) {
566
			return false;
567
		}
568
		return true;
569
	}
570
571
	/**
572
	 * @param string $action
573
	 * @return string
574
	 */
575
	public function APILink($action) {
576
		return Controller::join_links("naut", "api", $this->Name, $action);
577
	}
578
579
	/**
580
	 * @return FieldList
581
	 */
582
	public function getCMSFields() {
583
		$fields = parent::getCMSFields();
584
585
		/** @var GridField $environments */
586
		$environments = $fields->dataFieldByName("Environments");
587
588
		$fields->fieldByName("Root")->removeByName("Viewers");
589
		$fields->fieldByName("Root")->removeByName("Environments");
590
		$fields->fieldByName("Root")->removeByName("LocalCVSPath");
591
592
		$diskQuotaDesc = 'This is the maximum amount of disk space (in megabytes) that all environments within this '
593
			. 'project can use for stored snapshots';
594
		$fields->dataFieldByName('DiskQuotaMB')->setDescription($diskQuotaDesc);
595
596
		$projectNameDesc = 'Changing the name will <strong>reset</strong> the deploy configuration and avoid using non'
597
			. 'alphanumeric characters';
598
		$fields->fieldByName('Root.Main.Name')
599
			->setTitle('Project name')
600
			->setDescription($projectNameDesc);
601
602
		$fields->fieldByName('Root.Main.CVSPath')
603
			->setTitle('Git repository')
604
			->setDescription('E.g. [email protected]:silverstripe/silverstripe-installer.git');
605
606
		$workspaceField = new ReadonlyField('LocalWorkspace', 'Git workspace', $this->getLocalCVSPath());
607
		$workspaceField->setDescription('This is where the GIT repository are located on this server');
608
		$fields->insertAfter($workspaceField, 'CVSPath');
0 ignored issues
show
Documentation introduced by
'CVSPath' is of type string, but the function expects a object<FormField>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
609
610
		$readAccessGroups = ListboxField::create('Viewers', 'Project viewers', Group::get()->map()->toArray())
611
			->setMultiple(true)
612
			->setDescription('These groups can view the project in the front-end.');
613
		$fields->addFieldToTab("Root.Main", $readAccessGroups);
614
615
		$this->setCreateProjectFolderField($fields);
616
		$this->setEnvironmentFields($fields, $environments);
617
618
		$environmentTypes = ClassInfo::implementorsOf('EnvironmentCreateBackend');
619
		$types = array();
620
		foreach($environmentTypes as $type) {
621
			$types[$type] = $type;
622
		}
623
624
		$fields->addFieldsToTab('Root.Main', array(
625
			DropdownField::create(
626
				'AllowedEnvironmentType',
627
				'Allowed Environment Type',
628
				$types
629
			)->setDescription('This defined which form to show on the front end for '
630
				. 'environment creation. This will not affect backend functionality.')
631
			->setEmptyString(' - None - '),
632
		));
633
634
		return $fields;
635
	}
636
637
	/**
638
	 * If there isn't a capistrano env project folder, show options to create one
639
	 *
640
	 * @param FieldList $fields
641
	 */
642
	public function setCreateProjectFolderField(&$fields) {
643
		// Check if the capistrano project folder exists
644
		if(!$this->Name) {
645
			return;
646
		}
647
648
		if($this->projectFolderExists()) {
649
			return;
650
		}
651
652
		$createFolderNotice = new LabelField('CreateEnvFolderNotice', 'Warning: No Capistrano project folder exists');
653
		$createFolderNotice->addExtraClass('message warning');
654
		$fields->insertBefore($createFolderNotice, 'Name');
0 ignored issues
show
Documentation introduced by
'Name' is of type string, but the function expects a object<FormField>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
655
		$createFolderField = new CheckboxField('CreateEnvFolder', 'Create folder');
656
		$createFolderField->setDescription('Would you like to create the capistrano project folder?');
657
		$fields->insertAfter($createFolderField, 'CreateEnvFolderNotice');
658
	}
659
660
	/**
661
	 * @return boolean
662
	 */
663
	public function projectFolderExists() {
664
		return file_exists($this->getProjectFolderPath());
665
	}
666
667
	/**
668
	 * @return bool
669
	 */
670
	public function repoExists() {
671
		return file_exists(sprintf('%s/HEAD', $this->getLocalCVSPath()));
672
	}
673
674
	/**
675
	 * Setup a job to clone a git repository.
676
	 * @return string resque token
677
	 */
678
	public function cloneRepo() {
679
		// Avoid this being called multiple times in the same request
680
		if(!isset(self::$has_cloned_cache[$this->ID])) {
681
			$fetch = DNGitFetch::create();
682
			$fetch->ProjectID = $this->ID;
683
			$fetch->write();
684
685
			// passing true here tells DNGitFetch to force a git clone, otherwise
686
			// it will just update the repo if it already exists. We want to ensure
687
			// we're always cloning a new repo in this case, as the git URL may have changed.
688
			$fetch->start(true);
689
690
			self::$has_cloned_cache[$this->ID] = true;
691
		}
692
	}
693
694
	/**
695
	 * @return string
696
	 */
697
	public function getLocalCVSPath() {
698
		return sprintf('%s/%s', DEPLOYNAUT_LOCAL_VCS_PATH, $this->Name);
699
	}
700
701
	public function onBeforeWrite() {
702
		parent::onBeforeWrite();
703
704
		if($this->CreateEnvFolder && !file_exists($this->getProjectFolderPath())) {
0 ignored issues
show
Documentation introduced by
The property CreateEnvFolder does not exist on object<DNProject>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
705
			mkdir($this->getProjectFolderPath());
706
		}
707
	}
708
709
	public function onAfterWrite() {
710
		parent::onAfterWrite();
711
712
		if(!$this->CVSPath) {
713
			return;
714
		}
715
716
		$changedFields = $this->getChangedFields(true, 2);
717
		if(isset($changedFields['CVSPath']) || isset($changedFields['Name'])) {
718
			$this->cloneRepo();
719
		}
720
	}
721
722
	/**
723
	 * Delete related environments and folders
724
	 */
725
	public function onAfterDelete() {
726
		parent::onAfterDelete();
727
728
		// Delete related environments
729
		foreach($this->Environments() as $env) {
730
			$env->delete();
731
		}
732
733
		// Delete local repository
734
		if(file_exists($this->getLocalCVSPath())) {
735
			Filesystem::removeFolder($this->getLocalCVSPath());
736
		}
737
738
		// Delete project template
739
		if(file_exists($this->getProjectFolderPath()) && Config::inst()->get('DNEnvironment', 'allow_web_editing')) {
740
			Filesystem::removeFolder($this->getProjectFolderPath());
741
		}
742
743
		// Delete the deploy key
744
		if(file_exists($this->getKeyDir())) {
745
			Filesystem::removeFolder($this->getKeyDir());
746
		}
747
	}
748
749
	/**
750
	 * Fetch the public key for this project.
751
	 *
752
	 * @return string|void
753
	 */
754
	public function getPublicKey() {
755
		$key = $this->getPublicKeyPath();
756
757
		if(file_exists($key)) {
758
			return trim(file_get_contents($key));
759
		}
760
	}
761
762
	/**
763
	 * This returns that path of the public key if a key directory is set. It doesn't check whether the file exists.
764
	 *
765
	 * @return string|null
766
	 */
767
	public function getPublicKeyPath() {
768
		if($privateKey = $this->getPrivateKeyPath()) {
769
			return $privateKey . '.pub';
770
		}
771
		return null;
772
	}
773
774
	/**
775
	 * This returns that path of the private key if a key directory is set. It doesn't check whether the file exists.
776
	 *
777
	 * @return string|null
778
	 */
779
	public function getPrivateKeyPath() {
780
		$keyDir = $this->getKeyDir();
781
		if(!empty($keyDir)) {
782
			$filter = FileNameFilter::create();
783
			$name = $filter->filter($this->Name);
784
			return $keyDir . '/' . $name;
785
		}
786
		return null;
787
	}
788
789
	/**
790
	 * Returns the location of the projects key dir if one exists.
791
	 *
792
	 * @return string|null
793
	 */
794
	public function getKeyDir() {
795
		$keyDir = $this->DNData()->getKeyDir();
796
		if(!$keyDir) {
797
			return null;
798
		}
799
800
		$filter = FileNameFilter::create();
801
		$name = $filter->filter($this->Name);
802
803
		return $this->DNData()->getKeyDir() . '/' . $name;
804
	}
805
806
	/**
807
	 * Setup a gridfield for the environment configs
808
	 *
809
	 * @param FieldList $fields
810
	 * @param GridField $environments
811
	 */
812
	protected function setEnvironmentFields(&$fields, $environments) {
813
		if(!$environments) {
814
			return;
815
		}
816
817
		$environments->getConfig()->addComponent(new GridFieldAddNewMultiClass());
818
		$environments->getConfig()->removeComponentsByType('GridFieldAddNewButton');
819
		$environments->getConfig()->removeComponentsByType('GridFieldAddExistingAutocompleter');
820
		$environments->getConfig()->removeComponentsByType('GridFieldDeleteAction');
821
		$environments->getConfig()->removeComponentsByType('GridFieldPageCount');
822
		if(Config::inst()->get('DNEnvironment', 'allow_web_editing')) {
823
			$addNewRelease = new GridFieldAddNewButton('toolbar-header-right');
824
			$addNewRelease->setButtonName('Add');
825
			$environments->getConfig()->addComponent($addNewRelease);
826
		}
827
828
		$fields->addFieldToTab("Root.Main", $environments);
829
	}
830
831
	/**
832
	 * Provide current repository URL to the users.
833
	 *
834
	 * @return void|string
835
	 */
836
	public function getRepositoryURL() {
837
		$showUrl = Config::inst()->get($this->class, 'show_repository_url');
838
		if($showUrl) {
839
			return $this->CVSPath;
840
		}
841
	}
842
843
	/**
844
	 * Whitelist configuration that describes how to convert a repository URL into a link
845
	 * to a web user interface for that URL
846
	 *
847
	 * Consists of a hash of "full.lower.case.domain" => {configuration} key/value pairs
848
	 *
849
	 * {configuration} can either be boolean true to auto-detect both the host and the
850
	 * name of the UI provider, or a nested array that overrides either one or both
851
	 * of the auto-detected valyes
852
	 *
853
	 * @var array
854
	 */
855
	static private $repository_interfaces = array(
0 ignored issues
show
Unused Code introduced by
The property $repository_interfaces is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
856
		'github.com' => array(
857
			'icon' => 'deploynaut/img/github.png',
858
			'name' => 'Github.com',
859
		),
860
		'bitbucket.org' => array(
861
			'commit' => 'commits',
862
			'name' => 'Bitbucket.org',
863
		),
864
		'repo.or.cz' => array(
865
			'scheme' => 'http',
866
			'name' => 'repo.or.cz',
867
			'regex' => array('^(.*)$' => '/w$1'),
868
		),
869
870
		/* Example for adding your own gitlab repository and override all auto-detected values (with their defaults)
0 ignored issues
show
Unused Code Comprehensibility introduced by
38% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
871
		'gitlab.mysite.com' => array(
872
			'icon' => 'deploynaut/img/git.png',
873
			'host' => 'gitlab.mysite.com',
874
			'name' => 'Gitlab',
875
			'regex' => array('.git$' => ''),
876
			'commit' => "commit"
877
		),
878
		*/
879
	);
880
881
	/**
882
	 * Get a ViewableData structure describing the UI tool that lets the user view the repository code
883
	 *
884
	 * @return ArrayData
885
	 */
886
	public function getRepositoryInterface() {
887
		$interfaces = $this->config()->repository_interfaces;
888
889
		/* Look for each whitelisted hostname */
890
		foreach($interfaces as $host => $interface) {
891
			/* See if the CVS Path is for this hostname, followed by some junk (maybe a port), then the path */
892
			if(preg_match('{^[^.]*' . $host . '(.*?)([/a-zA-Z].+)}', $this->CVSPath, $match)) {
893
894
				$path = $match[2];
895
896
				$scheme = isset($interface['scheme']) ? $interface['scheme'] : 'https';
897
				$host = isset($interface['host']) ? $interface['host'] : $host;
898
				$regex = isset($interface['regex']) ? $interface['regex'] : array('\.git$' => '');
899
900
				$components = explode('.', $host);
901
902
				foreach($regex as $pattern => $replacement) {
903
					$path = preg_replace('/' . $pattern . '/', $replacement, $path);
904
				}
905
906
				$uxurl = Controller::join_links($scheme . '://', $host, $path);
907
908
				if(array_key_exists('commit', $interface) && $interface['commit'] == false) {
909
					$commiturl = false;
910
				} else {
911
					$commiturl = Controller::join_links(
912
						$uxurl,
913
						isset($interface['commit']) ? $interface['commit'] : 'commit'
914
					);
915
				}
916
917
				return new ArrayData(array(
918
					'Name'      => isset($interface['name']) ? $interface['name'] : ucfirst($components[0]),
919
					'Icon'      => isset($interface['icon']) ? $interface['icon'] : 'deploynaut/img/git.png',
920
					'URL'       => $uxurl,
921
					'CommitURL' => $commiturl
922
				));
923
			}
924
		}
925
	}
926
927
	/**
928
	 * @return string
929
	 */
930
	protected function getProjectFolderPath() {
931
		return sprintf('%s/%s', $this->DNData()->getEnvironmentDir(), $this->Name);
932
	}
933
934
	/**
935
	 * Convenience wrapper for a single permission code.
936
	 *
937
	 * @param string $code
938
	 * @return SS_List
939
	 */
940
	public function whoIsAllowed($code) {
941
		return $this->whoIsAllowedAny(array($code));
942
	}
943
944
	/**
945
	 * List members who have $codes on this project.
946
	 * Does not support Permission::DENY_PERMISSION malarky, same as Permission::get_groups_by_permission anyway...
947
	 *
948
	 * @param array|string $codes
949
	 * @return SS_List
950
	 */
951
	public function whoIsAllowedAny($codes) {
952
		if(!is_array($codes)) $codes = array($codes);
953
954
		$SQLa_codes = Convert::raw2sql($codes);
955
		$SQL_codes = join("','", $SQLa_codes);
956
957
		return DataObject::get('Member')
958
			->where("\"PermissionRoleCode\".\"Code\" IN ('$SQL_codes') OR \"Permission\".\"Code\" IN ('$SQL_codes')")
959
			->filter("DNProject_Viewers.DNProjectID", $this->ID)
960
			->leftJoin('Group_Members', "\"Group_Members\".\"MemberID\" = \"Member\".\"ID\"")
961
			->leftJoin('Group', "\"Group_Members\".\"GroupID\" = \"Group\".\"ID\"")
962
			->leftJoin('DNProject_Viewers', "\"DNProject_Viewers\".\"GroupID\" = \"Group\".\"ID\"")
963
			->leftJoin('Permission', "\"Permission\".\"GroupID\" = \"Group\".\"ID\"")
964
			->leftJoin('Group_Roles', "\"Group_Roles\".\"GroupID\" = \"Group\".\"ID\"")
965
			->leftJoin('PermissionRole', "\"Group_Roles\".\"PermissionRoleID\" = \"PermissionRole\".\"ID\"")
966
			->leftJoin('PermissionRoleCode', "\"PermissionRoleCode\".\"RoleID\" = \"PermissionRole\".\"ID\"");
967
	}
968
969
	/**
970
	 * Convenience wrapper for a single permission code.
971
	 *
972
	 * @param string $code
973
	 * @param Member|null $member
974
	 *
975
	 * @return bool
976
	 */
977
	public function allowed($code, $member = null) {
978
		return $this->allowedAny(array($code), $member);
979
	}
980
981
	/**
982
	 * Checks if a group is allowed to the project and the permission code
983
	 *
984
	 * @param string $permissionCode
985
	 * @param Group $group
986
	 *
987
	 * @return bool
988
	 */
989
	public function groupAllowed($permissionCode, Group $group) {
990
		$viewers = $this->Viewers();
991
		if(!$viewers->find('ID', $group->ID)) {
992
			return false;
993
		}
994
		$groups = Permission::get_groups_by_permission($permissionCode);
995
		if(!$groups->find('ID', $group->ID)) {
996
			return false;
997
		}
998
		return true;
999
	}
1000
1001
	/**
1002
	 * Check if member has a permission code in this project.
1003
	 *
1004
	 * @param array|string $codes
1005
	 * @param Member|null $member
1006
	 *
1007
	 * @return bool
1008
	 */
1009
	public function allowedAny($codes, $member = null) {
1010
		if (!$member) {
1011
			$member = Member::currentUser();
1012
		}
1013
1014
		if(Permission::checkMember($member, 'ADMIN')) return true;
1015
1016
		$hits = $this->whoIsAllowedAny($codes)->filter('Member.ID', $member->ID)->count();
1017
		return ($hits>0 ? true : false);
1018
	}
1019
1020
	/**
1021
	 * Checks if the environment has been fully built.
1022
	 *
1023
	 * @return bool
1024
	 */
1025
	public function isProjectReady() {
1026
		if($this->getRunningInitialEnvironmentCreations()->count() > 0) {
1027
			// We're still creating the initial environments for this project so we're
1028
			// not quite done
1029
			return false;
1030
		}
1031
1032
		// Provide a hook for further checks. Logic stolen from
1033
		// {@see DataObject::extendedCan()}
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1034
		$isDone = $this->extend('isProjectReady');
1035
		if($isDone && is_array($isDone)) {
1036
			$isDone = array_filter($isDone, function($val) {
1037
				return !is_null($val);
1038
			});
1039
1040
			// If anything returns false then we're not ready.
1041
			if($isDone) return min($isDone);
1042
		}
1043
1044
		return true;
1045
	}
1046
1047
	/**
1048
	 * Returns a list of environments still being created.
1049
	 *
1050
	 * @return SS_List
1051
	 */
1052
	public function getRunningEnvironmentCreations() {
1053
		return $this->CreateEnvironments()
0 ignored issues
show
Bug introduced by
The method CreateEnvironments() does not exist on DNProject. Did you maybe mean canCreateEnvironments()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
1054
			->filter('Status', ['Queued', 'Started']);
1055
	}
1056
1057
	/**
1058
	 * Returns a list of initial environments created for this project.
1059
	 *
1060
	 * @return DataList
1061
	 */
1062
	public function getInitialEnvironmentCreations() {
1063
		return $this->CreateEnvironments()->filter('IsInitialEnvironment', true);
0 ignored issues
show
Bug introduced by
The method CreateEnvironments() does not exist on DNProject. Did you maybe mean canCreateEnvironments()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
1064
	}
1065
1066
	/**
1067
	 * Only returns initial environments that are being created.
1068
	 *
1069
	 * @return DataList
1070
	 */
1071
	public function getRunningInitialEnvironmentCreations() {
1072
		return $this->getInitialEnvironmentCreations()
1073
			->filter('Status', ['Queued', 'Started']);
1074
	}
1075
1076
	/**
1077
	 * Returns a list of completed initial environment creations. This includes failed tasks.
1078
	 *
1079
	 * @return DataList
1080
	 */
1081
	public function getCompleteInitialEnvironmentCreations() {
1082
		return $this->getInitialEnvironmentCreations()
1083
			->exclude('Status', ['Queued', 'Started']);
1084
	}
1085
1086
	/**
1087
	 * @return ValidationResult
1088
	 */
1089
	protected function validate() {
1090
		$validation = parent::validate();
1091
		if($validation->valid()) {
1092
			if(empty($this->Name)) {
1093
				return $validation->error('The stack must have a name.');
1094
			}
1095
1096
			// The name is used to build filepaths so should be restricted
1097
			if(!preg_match('/^[a-zA-Z0-9][a-zA-Z0-9\-\_]+$/', $this->Name)) {
1098
				return $validation->error('Project name can only contain alphanumeric, hyphens and underscores.');
1099
			}
1100
1101
			if(empty($this->CVSPath)) {
1102
				return $validation->error('You must provide a repository URL.');
1103
			}
1104
1105
			$existing = DNProject::get()->filter('Name', $this->Name);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1106
			if($this->ID) {
1107
				$existing = $existing->exclude('ID', $this->ID);
1108
			}
1109
			if($existing->count() > 0) {
1110
				return $validation->error('A stack already exists with that name.');
1111
			}
1112
		}
1113
		return $validation;
1114
	}
1115
1116
	/**
1117
	 * @param Member $member
1118
	 *
1119
	 * @return bool
1120
	 */
1121
	public function canCreate($member = null) {
1122
		if(!$member) $member = Member::currentUser();
1123
		if(!$member) return false;
1124
1125
		if(Permission::checkMember($member, 'ADMIN')) {
1126
			return true;
1127
		}
1128
1129
		// This calls canCreate on extensions.
1130
		return parent::canCreate($member);
1131
	}
1132
1133
	/**
1134
	 * This is a proxy call to gitonmy that caches the information per project and sha
1135
	 *
1136
	 * @param string $sha
1137
	 * @return false|\Gitonomy\Git\Commit
1138
	 */
1139
	public function getCommit($sha) {
1140
		$repo = $this->getRepository();
1141
		if(!$repo) {
1142
			return false;
1143
		}
1144
1145
		$cachekey = $this->ID.'_commit_'.$sha;
1146
		$cache = self::get_git_cache();
1147 View Code Duplication
		if (!($result = $cache->load($cachekey))) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1148
			$result = $repo->getCommit($sha);
1149
			$cache->save($result, $cachekey, ['gitonomy', 'commit', 'project_'.$this->ID]);
1150
		}
1151
		return $result;
1152
	}
1153
1154
	/**
1155
	 * @param \Gitonomy\Git\Commit $commit
1156
	 * @return string
1157
	 */
1158
	public function getCommitMessage(\Gitonomy\Git\Commit $commit) {
1159
		$cachekey = $this->ID.'_message_'.$commit->getRevision();
1160
		$cache = self::get_git_cache();
1161 View Code Duplication
		if (!($result = $cache->load($cachekey))) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1162
			$result = $commit->getMessage();
1163
			$cache->save($result, $cachekey, ['gitonomy', 'message', 'project_'.$this->ID]);
1164
		}
1165
		return $result;
1166
	}
1167
1168
	/**
1169
	 * @param \Gitonomy\Git\Commit $commit
1170
	 * @return mixed
1171
	 */
1172
	public function getCommitTags(\Gitonomy\Git\Commit $commit) {
1173
		$cachekey = $this->ID.'_tags_'.$commit->getRevision();
1174
		$cache = self::get_git_cache();
1175
		$result = $cache->load($cachekey);
1176
		// we check against false, because in many cases the tag list is an empty array
1177
		if ($result === false) {
1178
			$repo = $this->getRepository();
1179
			$result = $tags = $repo->getReferences()->resolveTags($commit->getRevision());
0 ignored issues
show
Unused Code introduced by
$tags is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1180
			$cache->save($result, $cachekey, ['gitonomy', 'tags', 'project_'.$this->ID]);
1181
		}
1182
		return $result;
1183
	}
1184
1185
}
1186
1187