Completed
Push — master ( 96acc8...3bb34a )
by Stig
01:14
created

DNProject::resolveRevision()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 13
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 13
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 9
nc 4
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
	private static $db = [
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
21
		"Name" => "Varchar",
22
		"IsNewDeployEnabled" => "Boolean",
23
		"CVSPath" => "Varchar(255)",
24
		"DiskQuotaMB" => "Int",
25
		"AllowedEnvironmentType" => "Varchar(255)"
26
	];
27
28
	/**
29
	 * @var array
30
	 */
31
	private static $has_many = [
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
32
		"Environments" => "DNEnvironment",
33
		"CreateEnvironments" => "DNCreateEnvironment",
34
		"Fetches" => "DNGitFetch"
35
	];
36
37
	/**
38
	 * @var array
39
	 */
40
	private static $many_many = [
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $many_many 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...
41
		"Viewers" => "Group",
42
		"StarredBy" => "Member"
43
	];
44
45
	/**
46
	 * @var array
47
	 */
48
	private static $summary_fields = [
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
49
		"Name",
50
		"ViewersList",
51
	];
52
53
	/**
54
	 * @var array
55
	 */
56
	private static $searchable_fields = [
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
57
		"Name",
58
	];
59
60
	/**
61
	 * @var string
62
	 */
63
	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...
64
65
	/**
66
	 * @var string
67
	 */
68
	private static $plural_name = 'Projects';
69
70
	/**
71
	 * @var string
72
	 */
73
	private static $default_sort = 'Name';
74
75
	/**
76
	 * In-memory cache for currentBuilds per environment since fetching them from
77
	 * disk is pretty resource hungry.
78
	 *
79
	 * @var array
80
	 */
81
	protected static $relation_cache = [];
82
83
	/**
84
	 * @var bool|Member
85
	 */
86
	protected static $_current_member_cache = null;
87
88
	/**
89
	 * Display the repository URL on the project page.
90
	 *
91
	 * @var bool
92
	 */
93
	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...
94
95
	/**
96
	 * In-memory cache to determine whether clone repo was called.
97
	 * @var array
98
	 */
99
	private static $has_cloned_cache = [];
100
101
	/**
102
	 * Whitelist configuration that describes how to convert a repository URL into a link
103
	 * to a web user interface for that URL
104
	 *
105
	 * Consists of a hash of "full.lower.case.domain" => {configuration} key/value pairs
106
	 *
107
	 * {configuration} can either be boolean true to auto-detect both the host and the
108
	 * name of the UI provider, or a nested array that overrides either one or both
109
	 * of the auto-detected values
110
	 *
111
	 * @var array
112
	 */
113
	private static $repository_interfaces = [
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...
114
		'github.com' => [
115
			'icon' => 'deploynaut/img/github.png',
116
			'name' => 'Github.com',
117
		],
118
		'bitbucket.org' => [
119
			'commit' => 'commits',
120
			'name' => 'Bitbucket.org',
121
		],
122
		'repo.or.cz' => [
123
			'scheme' => 'http',
124
			'name' => 'repo.or.cz',
125
			'regex' => ['^(.*)$' => '/w$1'],
126
		],
127
128
		/* 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...
129
		'gitlab.mysite.com' => array(
130
			'icon' => 'deploynaut/img/git.png',
131
			'host' => 'gitlab.mysite.com',
132
			'name' => 'Gitlab',
133
			'regex' => array('.git$' => ''),
134
			'commit' => "commit"
135
		),
136
		*/
137
	];
138
139
	/**
140
	 * Used by the sync task
141
	 *
142
	 * @param string $path
143
	 * @return \DNProject
144
	 */
145
	public static function create_from_path($path) {
146
		$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...
147
		$project->Name = $path;
148
		$project->write();
149
150
		// add the administrators group as the viewers of the new project
151
		$adminGroup = Group::get()->filter('Code', 'administrators')->first();
152
		if ($adminGroup && $adminGroup->exists()) {
153
			$project->Viewers()->add($adminGroup);
154
		}
155
		return $project;
156
	}
157
158
	/**
159
	 * This will clear the cache for the git getters and should be called when the local git repo is updated
160
	 */
161
	public function clearGitCache() {
162
		$cache = self::get_git_cache();
163
		// we only need to clear the tag cache since everything else is cached by SHA, that is for commit and
164
		// commit message.
165
		$cache->clean(Zend_Cache::CLEANING_MODE_MATCHING_TAG, ['gitonomy', 'tags', 'project_' . $this->ID]);
166
	}
167
168
	/**
169
	 * @return \Zend_Cache_Frontend_Output
170
	 */
171
	public static function get_git_cache() {
172
		return SS_Cache::factory('gitonomy', 'Output', [
173
			'automatic_serialization' => true,
174
			'lifetime' => 60 * 60 * 24 * 7 // seven days
175
		]);
176
	}
177
178
	/**
179
	 * Return the used quota in MB.
180
	 *
181
	 * @param int $round Number of decimal places to round to
182
	 * @return double The used quota size in MB
183
	 */
184
	public function getUsedQuotaMB($round = 2) {
185
		$size = 0;
186
187
		foreach ($this->Environments() as $environment) {
188
			foreach ($environment->DataArchives()->filter('IsBackup', 0) as $archive) {
189
				$size += $archive->ArchiveFile()->getAbsoluteSize();
190
			}
191
		}
192
193
		// convert bytes to megabytes and round
194
		return round(($size / 1024) / 1024, $round);
195
	}
196
197
	/**
198
	 * Getter for DiskQuotaMB field to provide a default for existing
199
	 * records that have no quota field set, as it will need to default
200
	 * to a globally set size.
201
	 *
202
	 * @return string|int The quota size in MB
203
	 */
204
	public function getDiskQuotaMB() {
205
		$size = $this->getField('DiskQuotaMB');
206
207
		if (empty($size)) {
208
			$defaults = $this->config()->get('defaults');
209
			$size = (isset($defaults['DiskQuotaMB'])) ? $defaults['DiskQuotaMB'] : 0;
210
		}
211
212
		return $size;
213
	}
214
215
	/**
216
	 * Has the disk quota been exceeded?
217
	 *
218
	 * @return boolean
219
	 */
220
	public function HasExceededDiskQuota() {
221
		return $this->getUsedQuotaMB(0) >= $this->getDiskQuotaMB();
222
	}
223
224
	/**
225
	 * Is there a disk quota set for this project?
226
	 *
227
	 * @return boolean
228
	 */
229
	public function HasDiskQuota() {
230
		return $this->getDiskQuotaMB() > 0;
231
	}
232
233
	/**
234
	 * Returns the current disk quota usage as a percentage
235
	 *
236
	 * @return int
237
	 */
238
	public function DiskQuotaUsagePercent() {
239
		$quota = $this->getDiskQuotaMB();
240
		if ($quota > 0) {
241
			return $this->getUsedQuotaMB() * 100 / $quota;
242
		}
243
		return 100;
244
	}
245
246
	/**
247
	 * Get the menu to be shown on projects
248
	 *
249
	 * @return ArrayList
250
	 */
251
	public function Menu() {
252
		$list = new ArrayList();
253
254
		$controller = Controller::curr();
255
		$actionType = $controller->getField('CurrentActionType');
256
257
		if ($this->isProjectReady()) {
258
			$list->push(new ArrayData([
259
				'Link' => $this->Link('snapshots'),
260
				'Title' => 'Snapshots',
261
				'IsCurrent' => $this->isSection() && $controller->getAction() == 'snapshots',
262
				'IsSection' => $this->isSection() && $actionType == DNRoot::ACTION_SNAPSHOT
263
			]));
264
		}
265
266
		$this->extend('updateMenu', $list);
267
268
		return $list;
269
	}
270
271
	/**
272
	 * Is this project currently at the root level of the controller that handles it?
273
	 *
274
	 * @return bool
275
	 */
276
	public function isCurrent() {
277
		return $this->isSection() && Controller::curr()->getAction() == 'project';
278
	}
279
280
	/**
281
	 * Return the current object from $this->Menu()
282
	 * Good for making titles and things
283
	 *
284
	 * @return DataObject
285
	 */
286
	public function CurrentMenu() {
287
		return $this->Menu()->filter('IsSection', true)->First();
288
	}
289
290
	/**
291
	 * Is this project currently in a controller that is handling it or performing a sub-task?
292
	 *
293
	 * @return bool
294
	 */
295
	public function isSection() {
296
		$controller = Controller::curr();
297
		$project = $controller->getField('CurrentProject');
298
		return $project && $this->ID == $project->ID;
299
	}
300
301
	/**
302
	 * Restrict access to viewing this project
303
	 *
304
	 * @param Member|null $member
305
	 * @return boolean
306
	 */
307
	public function canView($member = null) {
308
		if (!$member) {
309
			$member = Member::currentUser();
310
		}
311
312
		if (Permission::checkMember($member, 'ADMIN')) {
313
			return true;
314
		}
315
316
		return $member->inGroups($this->Viewers());
317
	}
318
319
	/**
320
	 * @param Member|null $member
321
	 *
322
	 * @return bool
323
	 */
324 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...
325
		if ($this->allowedAny(
326
			[
327
				DNRoot::ALLOW_PROD_SNAPSHOT,
328
				DNRoot::ALLOW_NON_PROD_SNAPSHOT
329
			],
330
			$member
331
		)
332
		) {
333
			return true;
334
		}
335
336
		return (bool) $this->Environments()->filterByCallback(function ($env) use ($member) {
337
			return $env->canRestore($member);
338
		})->Count();
339
	}
340
341
	/**
342
	 * @param Member|null $member
343
	 * @return bool
344
	 */
345 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...
346
		if ($this->allowedAny(
347
			[
348
				DNRoot::ALLOW_PROD_SNAPSHOT,
349
				DNRoot::ALLOW_NON_PROD_SNAPSHOT
350
			],
351
			$member
352
		)
353
		) {
354
			return true;
355
		}
356
357
		return (bool) $this->Environments()->filterByCallback(function ($env) use ($member) {
358
			return $env->canBackup($member);
359
		})->Count();
360
	}
361
362
	/**
363
	 * @param Member|null $member
364
	 * @return bool
365
	 */
366 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...
367
		if ($this->allowedAny(
368
			[
369
				DNRoot::ALLOW_PROD_SNAPSHOT,
370
				DNRoot::ALLOW_NON_PROD_SNAPSHOT
371
			],
372
			$member
373
		)
374
		) {
375
			return true;
376
		}
377
378
		return (bool) $this->Environments()->filterByCallback(function ($env) use ($member) {
379
			return $env->canUploadArchive($member);
380
		})->Count();
381
	}
382
383
	/**
384
	 * @param Member|null $member
385
	 * @return bool
386
	 */
387 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...
388
		if ($this->allowedAny(
389
			[
390
				DNRoot::ALLOW_PROD_SNAPSHOT,
391
				DNRoot::ALLOW_NON_PROD_SNAPSHOT
392
			],
393
			$member
394
		)
395
		) {
396
			return true;
397
		}
398
399
		return (bool) $this->Environments()->filterByCallback(function ($env) use ($member) {
400
			return $env->canDownloadArchive($member);
401
		})->Count();
402
	}
403
404
	/**
405
	 * This is a permission check for the front-end only.
406
	 *
407
	 * Only admins can create environments for now. Also, we need to check the value
408
	 * of AllowedEnvironmentType which dictates which backend to use to render the form.
409
	 *
410
	 * @param Member|null $member
411
	 *
412
	 * @return bool
413
	 */
414
	public function canCreateEnvironments($member = null) {
415
		$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...
416
		if ($envType) {
417
			$env = Injector::inst()->get($envType);
418
			if ($env instanceof EnvironmentCreateBackend) {
419
				return $this->allowed(DNRoot::ALLOW_CREATE_ENVIRONMENT, $member);
420
			}
421
		}
422
		return false;
423
	}
424
425
	/**
426
	 * @return DataList
427
	 */
428
	public function DataArchives() {
429
		$envIds = $this->Environments()->column('ID');
430
		return DNDataArchive::get()->filter('EnvironmentID', $envIds);
431
	}
432
433
	/**
434
	 * Return all archives which are "manual upload requests",
435
	 * meaning they don't have a file attached to them (yet).
436
	 *
437
	 * @return DataList
438
	 */
439
	public function PendingManualUploadDataArchives() {
440
		return $this->DataArchives()->filter('ArchiveFileID', null);
441
	}
442
443
	/**
444
	 * Build an environment variable array to be used with this project.
445
	 *
446
	 * This is relevant if every project needs to use an individual SSH pubkey.
447
	 *
448
	 * Include this with all Gitonomy\Git\Repository, and
449
	 * \Symfony\Component\Process\Processes.
450
	 *
451
	 * @return array
452
	 */
453
	public function getProcessEnv() {
454
		if (file_exists($this->getPrivateKeyPath())) {
455
			// Key-pair is available, use it.
456
			$processEnv = [
457
				'IDENT_KEY' => $this->getPrivateKeyPath(),
458
				'GIT_SSH' => BASE_PATH . "/deploynaut/git-deploy.sh"
459
			];
460
		} else {
461
			$processEnv = [];
462
		}
463
		$this->extend('updateProcessEnv', $processEnv);
464
465
		return $processEnv;
466
	}
467
468
	/**
469
	 * Get a string of people allowed to view this project
470
	 *
471
	 * @return string
472
	 */
473
	public function getViewersList() {
474
		return implode(", ", $this->Viewers()->column("Title"));
475
	}
476
477
	/**
478
	 * @return DNData
479
	 */
480
	public function DNData() {
481
		return DNData::inst();
482
	}
483
484
	/**
485
	 * Provides a DNBuildList of builds found in this project.
486
	 *
487
	 * @return DNReferenceList
488
	 */
489
	public function DNBuildList() {
490
		return DNReferenceList::create($this, $this->DNData());
491
	}
492
493
	/**
494
	 * Provides a list of the branches in this project.
495
	 *
496
	 * @return DNBranchList
497
	 */
498
	public function DNBranchList() {
499
		if ($this->CVSPath && !$this->repoExists()) {
500
			$this->cloneRepo();
501
		}
502
		return DNBranchList::create($this, $this->DNData());
503
	}
504
505
	/**
506
	 * Provides a list of the tags in this project.
507
	 *
508
	 * @return DNReferenceList
509
	 */
510
	public function DNTagList() {
511
		if ($this->CVSPath && !$this->repoExists()) {
512
			$this->cloneRepo();
513
		}
514
		return DNReferenceList::create($this, $this->DNData(), null, null, true);
515
	}
516
517
	/**
518
	 * @return false|Gitonomy\Git\Repository
519
	 */
520
	public function getRepository() {
521
		if (!$this->repoExists()) {
522
			return false;
523
		}
524
525
		return new \Gitonomy\Git\Repository($this->getLocalCVSPath());
526
	}
527
528
	/**
529
	 * Resolve a git reference like a branch or tag into a SHA.
530
	 * @return bool|string
531
	 */
532
	public function resolveRevision($value) {
533
		$repository = $this->getRepository();
534
		if (!$repository) {
535
			return false;
536
		}
537
538
		try {
539
			$revision = $this->repository->getRevision($value);
0 ignored issues
show
Bug introduced by
The property repository does not seem to exist. Did you mean show_repository_url?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
540
			return $revision->getCommit()->getHash();
541
		} catch (\Gitonomy\Git\Exception\ReferenceNotFoundException $e) {
542
			return false;
543
		}
544
	}
545
546
	/**
547
	 * Provides a list of environments found in this project.
548
	 * CAUTION: filterByCallback will change this into an ArrayList!
549
	 *
550
	 * @return ArrayList
551
	 */
552
	public function DNEnvironmentList() {
553
554
		if (!self::$_current_member_cache) {
555
			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...
556
		}
557
558
		if (self::$_current_member_cache === false) {
559
			return new ArrayList();
560
		}
561
562
		$currentMember = self::$_current_member_cache;
563
		return $this->Environments()
564
			->filterByCallBack(function ($item) use ($currentMember) {
565
				return $item->canView($currentMember);
566
			});
567
	}
568
569
	/**
570
	 * @param string $usage
571
	 * @return ArrayList
572
	 */
573
	public function EnvironmentsByUsage($usage) {
574
		return $this->DNEnvironmentList()->filter('Usage', $usage);
575
	}
576
577
	/**
578
	 * Returns a map of envrionment name to build name
579
	 *
580
	 * @return false|DNDeployment
581
	 */
582
	public function currentBuilds() {
583
		if (!isset(self::$relation_cache['currentBuilds.' . $this->ID])) {
584
			$currentBuilds = [];
585
			foreach ($this->Environments() as $env) {
586
				$currentBuilds[$env->Name] = $env->CurrentBuild();
587
			}
588
			self::$relation_cache['currentBuilds.' . $this->ID] = $currentBuilds;
589
		}
590
		return self::$relation_cache['currentBuilds.' . $this->ID];
591
	}
592
593
	/**
594
	 * @param string
595
	 * @return string
596
	 */
597
	public function Link($action = '') {
598
		return Controller::join_links("naut", "project", $this->Name, $action);
599
	}
600
601
	/**
602
	 * @return string|null
603
	 */
604
	public function CreateEnvironmentLink() {
605
		if ($this->canCreateEnvironments()) {
606
			return $this->Link('createenv');
607
		}
608
		return null;
609
	}
610
611
	/**
612
	 * @return string
613
	 */
614
	public function ToggleStarLink() {
615
		return $this->Link('/star');
616
	}
617
618
	/**
619
	 * @return bool
620
	 */
621
	public function IsStarred() {
622
		$member = Member::currentUser();
623
		if ($member === null) {
624
			return false;
625
		}
626
		$favourited = $this->StarredBy()->filter('MemberID', $member->ID);
627
		if ($favourited->count() == 0) {
628
			return false;
629
		}
630
		return true;
631
	}
632
633
	/**
634
	 * @param string $action
635
	 * @return string
636
	 */
637
	public function APILink($action) {
638
		return Controller::join_links("naut", "api", $this->Name, $action);
639
	}
640
641
	/**
642
	 * @return FieldList
643
	 */
644
	public function getCMSFields() {
645
		$fields = parent::getCMSFields();
646
647
		/** @var GridField $environments */
648
		$environments = $fields->dataFieldByName("Environments");
649
650
		$fields->fieldByName("Root")->removeByName("Viewers");
651
		$fields->fieldByName("Root")->removeByName("Environments");
652
		$fields->fieldByName("Root")->removeByName("LocalCVSPath");
653
654
		$diskQuotaDesc = 'This is the maximum amount of disk space (in megabytes) that all environments within this '
655
			. 'project can use for stored snapshots';
656
		$fields->dataFieldByName('DiskQuotaMB')->setDescription($diskQuotaDesc);
657
658
		$projectNameDesc = 'Changing the name will <strong>reset</strong> the deploy configuration and avoid using non'
659
			. 'alphanumeric characters';
660
		$fields->fieldByName('Root.Main.Name')
661
			->setTitle('Project name')
662
			->setDescription($projectNameDesc);
663
664
		$fields->fieldByName('Root.Main.IsNewDeployEnabled')
665
			->setTitle('New deploy form enabled for this project')
666
			->setDescription('Feature flag to change links to environment and deployments to the new deployment form for this project');
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 127 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
667
668
		$fields->fieldByName('Root.Main.CVSPath')
669
			->setTitle('Git repository')
670
			->setDescription('E.g. [email protected]:silverstripe/silverstripe-installer.git');
671
672
		$workspaceField = new ReadonlyField('LocalWorkspace', 'Git workspace', $this->getLocalCVSPath());
673
		$workspaceField->setDescription('This is where the GIT repository are located on this server');
674
		$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...
675
676
		$readAccessGroups = ListboxField::create('Viewers', 'Project viewers', Group::get()->map()->toArray())
677
			->setMultiple(true)
678
			->setDescription('These groups can view the project in the front-end.');
679
		$fields->addFieldToTab("Root.Main", $readAccessGroups);
680
681
		$this->setCreateProjectFolderField($fields);
682
		$this->setEnvironmentFields($fields, $environments);
683
684
		$environmentTypes = ClassInfo::implementorsOf('EnvironmentCreateBackend');
685
		$types = [];
686
		foreach ($environmentTypes as $type) {
687
			$types[$type] = $type;
688
		}
689
690
		$fields->addFieldsToTab('Root.Main', [
691
			DropdownField::create(
692
				'AllowedEnvironmentType',
693
				'Allowed Environment Type',
694
				$types
695
			)->setDescription('This defined which form to show on the front end for '
696
				. 'environment creation. This will not affect backend functionality.')
697
				->setEmptyString(' - None - '),
698
		]);
699
700
		return $fields;
701
	}
702
703
	/**
704
	 * If there isn't a capistrano env project folder, show options to create one
705
	 *
706
	 * @param FieldList $fields
707
	 */
708
	public function setCreateProjectFolderField(&$fields) {
709
		// Check if the capistrano project folder exists
710
		if (!$this->Name) {
711
			return;
712
		}
713
714
		if ($this->projectFolderExists()) {
715
			return;
716
		}
717
718
		$createFolderNotice = new LabelField('CreateEnvFolderNotice', 'Warning: No Capistrano project folder exists');
719
		$createFolderNotice->addExtraClass('message warning');
720
		$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...
721
		$createFolderField = new CheckboxField('CreateEnvFolder', 'Create folder');
722
		$createFolderField->setDescription('Would you like to create the capistrano project folder?');
723
		$fields->insertAfter($createFolderField, 'CreateEnvFolderNotice');
724
	}
725
726
	/**
727
	 * @return boolean
728
	 */
729
	public function projectFolderExists() {
730
		return file_exists($this->getProjectFolderPath());
731
	}
732
733
	/**
734
	 * @return bool
735
	 */
736
	public function repoExists() {
737
		return file_exists(sprintf('%s/HEAD', $this->getLocalCVSPath()));
738
	}
739
740
	/**
741
	 * Setup a job to clone a git repository.
742
	 * @return string resque token
743
	 */
744
	public function cloneRepo() {
745
		// Avoid this being called multiple times in the same request
746
		if (!isset(self::$has_cloned_cache[$this->ID])) {
747
			$fetch = DNGitFetch::create();
748
			$fetch->ProjectID = $this->ID;
749
			$fetch->write();
750
751
			// passing true here tells DNGitFetch to force a git clone, otherwise
752
			// it will just update the repo if it already exists. We want to ensure
753
			// we're always cloning a new repo in this case, as the git URL may have changed.
754
			$fetch->start(true);
755
756
			self::$has_cloned_cache[$this->ID] = true;
757
		}
758
	}
759
760
	/**
761
	 * @return string
762
	 */
763
	public function getLocalCVSPath() {
764
		return sprintf('%s/%s', DEPLOYNAUT_LOCAL_VCS_PATH, $this->Name);
765
	}
766
767
	public function onBeforeWrite() {
768
		parent::onBeforeWrite();
769
770
		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...
771
			mkdir($this->getProjectFolderPath());
772
		}
773
	}
774
775
	public function onAfterWrite() {
776
		parent::onAfterWrite();
777
778
		if (!$this->CVSPath) {
779
			return;
780
		}
781
782
		$changedFields = $this->getChangedFields(true, 2);
783
		if (isset($changedFields['CVSPath']) || isset($changedFields['Name'])) {
784
			$this->cloneRepo();
785
		}
786
	}
787
788
	/**
789
	 * Delete related environments and folders
790
	 */
791
	public function onAfterDelete() {
792
		parent::onAfterDelete();
793
794
		$environments = $this->Environments();
795
		if ($environments && $environments->exists()) {
796
			foreach ($environments as $env) {
797
				$env->delete();
798
			}
799
		}
800
801
		$fetches = $this->Fetches();
0 ignored issues
show
Documentation Bug introduced by
The method Fetches does not exist on object<DNProject>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
802
		if ($fetches && $fetches->exists()) {
803
			foreach ($fetches as $fetch) {
804
				$fetch->delete();
805
			}
806
		}
807
808
		// Delete local repository
809
		if (file_exists($this->getLocalCVSPath())) {
810
			Filesystem::removeFolder($this->getLocalCVSPath());
811
		}
812
813
		// Delete project template
814
		if (file_exists($this->getProjectFolderPath()) && Config::inst()->get('DNEnvironment', 'allow_web_editing')) {
815
			Filesystem::removeFolder($this->getProjectFolderPath());
816
		}
817
818
		// Delete the deploy key
819
		if (file_exists($this->getKeyDir())) {
820
			Filesystem::removeFolder($this->getKeyDir());
821
		}
822
	}
823
824
	/**
825
	 * Fetch the public key for this project.
826
	 *
827
	 * @return string|void
828
	 */
829
	public function getPublicKey() {
830
		$key = $this->getPublicKeyPath();
831
832
		if (file_exists($key)) {
833
			return trim(file_get_contents($key));
834
		}
835
	}
836
837
	/**
838
	 * This returns that path of the public key if a key directory is set. It doesn't check whether the file exists.
839
	 *
840
	 * @return string|null
841
	 */
842
	public function getPublicKeyPath() {
843
		if ($privateKey = $this->getPrivateKeyPath()) {
844
			return $privateKey . '.pub';
845
		}
846
		return null;
847
	}
848
849
	/**
850
	 * This returns that path of the private key if a key directory is set. It doesn't check whether the file exists.
851
	 *
852
	 * @return string|null
853
	 */
854
	public function getPrivateKeyPath() {
855
		$keyDir = $this->getKeyDir();
856
		if (!empty($keyDir)) {
857
			$filter = FileNameFilter::create();
858
			$name = $filter->filter($this->Name);
859
			return $keyDir . '/' . $name;
860
		}
861
		return null;
862
	}
863
864
	/**
865
	 * Returns the location of the projects key dir if one exists.
866
	 *
867
	 * @return string|null
868
	 */
869
	public function getKeyDir() {
870
		$keyDir = $this->DNData()->getKeyDir();
871
		if (!$keyDir) {
872
			return null;
873
		}
874
875
		$filter = FileNameFilter::create();
876
		$name = $filter->filter($this->Name);
877
878
		return $this->DNData()->getKeyDir() . '/' . $name;
879
	}
880
881
	/**
882
	 * Provide current repository URL to the users.
883
	 *
884
	 * @return void|string
885
	 */
886
	public function getRepositoryURL() {
887
		$showUrl = Config::inst()->get($this->class, 'show_repository_url');
888
		if ($showUrl) {
889
			return $this->CVSPath;
890
		}
891
	}
892
893
	/**
894
	 * Get a ViewableData structure describing the UI tool that lets the user view the repository code
895
	 *
896
	 * @return ArrayData
897
	 */
898
	public function getRepositoryInterface() {
899
		$interfaces = $this->config()->repository_interfaces;
900
901
		/* Look for each whitelisted hostname */
902
		foreach ($interfaces as $host => $interface) {
903
			/* See if the CVS Path is for this hostname, followed by some junk (maybe a port), then the path */
904
			if (preg_match('{^[^.]*' . $host . '(.*?)([/a-zA-Z].+)}', $this->CVSPath, $match)) {
905
906
				$path = $match[2];
907
908
				$scheme = isset($interface['scheme']) ? $interface['scheme'] : 'https';
909
				$host = isset($interface['host']) ? $interface['host'] : $host;
910
				$regex = isset($interface['regex']) ? $interface['regex'] : ['\.git$' => ''];
911
912
				$components = explode('.', $host);
913
914
				foreach ($regex as $pattern => $replacement) {
915
					$path = preg_replace('/' . $pattern . '/', $replacement, $path);
916
				}
917
918
				$uxurl = Controller::join_links($scheme . '://', $host, $path);
919
920
				if (array_key_exists('commit', $interface) && $interface['commit'] == false) {
921
					$commiturl = false;
922
				} else {
923
					$commiturl = Controller::join_links(
924
						$uxurl,
925
						isset($interface['commit']) ? $interface['commit'] : 'commit'
926
					);
927
				}
928
929
				return new ArrayData([
930
					'Name' => isset($interface['name']) ? $interface['name'] : ucfirst($components[0]),
931
					'Icon' => isset($interface['icon']) ? $interface['icon'] : 'deploynaut/img/git.png',
932
					'URL' => $uxurl,
933
					'CommitURL' => $commiturl
934
				]);
935
			}
936
		}
937
	}
938
939
	/**
940
	 * Convenience wrapper for a single permission code.
941
	 *
942
	 * @param string $code
943
	 * @return SS_List
944
	 */
945
	public function whoIsAllowed($code) {
946
		return $this->whoIsAllowedAny([$code]);
947
	}
948
949
	/**
950
	 * List members who have $codes on this project.
951
	 * Does not support Permission::DENY_PERMISSION malarky, same as Permission::get_groups_by_permission anyway...
952
	 *
953
	 * @param array|string $codes
954
	 * @return SS_List
955
	 */
956
	public function whoIsAllowedAny($codes) {
957
		if (!is_array($codes)) {
958
			$codes = [$codes];
959
		}
960
961
		$SQLa_codes = Convert::raw2sql($codes);
962
		$SQL_codes = join("','", $SQLa_codes);
963
964
		return DataObject::get('Member')
965
			->where("\"PermissionRoleCode\".\"Code\" IN ('$SQL_codes') OR \"Permission\".\"Code\" IN ('$SQL_codes')")
966
			->filter("DNProject_Viewers.DNProjectID", $this->ID)
967
			->leftJoin('Group_Members', "\"Group_Members\".\"MemberID\" = \"Member\".\"ID\"")
968
			->leftJoin('Group', "\"Group_Members\".\"GroupID\" = \"Group\".\"ID\"")
969
			->leftJoin('DNProject_Viewers', "\"DNProject_Viewers\".\"GroupID\" = \"Group\".\"ID\"")
970
			->leftJoin('Permission', "\"Permission\".\"GroupID\" = \"Group\".\"ID\"")
971
			->leftJoin('Group_Roles', "\"Group_Roles\".\"GroupID\" = \"Group\".\"ID\"")
972
			->leftJoin('PermissionRole', "\"Group_Roles\".\"PermissionRoleID\" = \"PermissionRole\".\"ID\"")
973
			->leftJoin('PermissionRoleCode', "\"PermissionRoleCode\".\"RoleID\" = \"PermissionRole\".\"ID\"");
974
	}
975
976
	/**
977
	 * Convenience wrapper for a single permission code.
978
	 *
979
	 * @param string $code
980
	 * @param Member|null $member
981
	 *
982
	 * @return bool
983
	 */
984
	public function allowed($code, $member = null) {
985
		return $this->allowedAny([$code], $member);
986
	}
987
988
	/**
989
	 * Checks if a group is allowed to the project and the permission code
990
	 *
991
	 * @param string $permissionCode
992
	 * @param Group $group
993
	 *
994
	 * @return bool
995
	 */
996
	public function groupAllowed($permissionCode, Group $group) {
997
		$viewers = $this->Viewers();
998
		if (!$viewers->find('ID', $group->ID)) {
999
			return false;
1000
		}
1001
		$groups = Permission::get_groups_by_permission($permissionCode);
1002
		if (!$groups->find('ID', $group->ID)) {
1003
			return false;
1004
		}
1005
		return true;
1006
	}
1007
1008
	/**
1009
	 * Check if member has a permission code in this project.
1010
	 *
1011
	 * @param array|string $codes
1012
	 * @param Member|null $member
1013
	 *
1014
	 * @return bool
1015
	 */
1016
	public function allowedAny($codes, $member = null) {
1017
		if (!$member) {
1018
			$member = Member::currentUser();
1019
		}
1020
1021
		if (Permission::checkMember($member, 'ADMIN')) {
1022
			return true;
1023
		}
1024
1025
		$hits = $this->whoIsAllowedAny($codes)->filter('Member.ID', $member->ID)->count();
1026
		return ($hits > 0 ? true : false);
1027
	}
1028
1029
	/**
1030
	 * Checks if the environment has been fully built.
1031
	 *
1032
	 * @return bool
1033
	 */
1034
	public function isProjectReady() {
1035
		if ($this->getRunningInitialEnvironmentCreations()->count() > 0) {
1036
			// We're still creating the initial environments for this project so we're
1037
			// not quite done
1038
			return false;
1039
		}
1040
1041
		// Provide a hook for further checks. Logic stolen from
1042
		// {@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...
1043
		$isDone = $this->extend('isProjectReady');
1044
		if ($isDone && is_array($isDone)) {
1045
			$isDone = array_filter($isDone, function ($val) {
1046
				return !is_null($val);
1047
			});
1048
1049
			// If anything returns false then we're not ready.
1050
			if ($isDone) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $isDone of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1051
				return min($isDone);
1052
			}
1053
		}
1054
1055
		return true;
1056
	}
1057
1058
	/**
1059
	 * Returns a list of environments still being created.
1060
	 *
1061
	 * @return SS_List
1062
	 */
1063
	public function getRunningEnvironmentCreations() {
1064
		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...
1065
			->filter('Status', ['Queued', 'Started']);
1066
	}
1067
1068
	/**
1069
	 * Returns a list of initial environments created for this project.
1070
	 *
1071
	 * @return DataList
1072
	 */
1073
	public function getInitialEnvironmentCreations() {
1074
		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...
1075
	}
1076
1077
	/**
1078
	 * Only returns initial environments that are being created.
1079
	 *
1080
	 * @return DataList
1081
	 */
1082
	public function getRunningInitialEnvironmentCreations() {
1083
		return $this->getInitialEnvironmentCreations()
1084
			->filter('Status', ['Queued', 'Started']);
1085
	}
1086
1087
	/**
1088
	 * Returns a list of completed initial environment creations. This includes failed tasks.
1089
	 *
1090
	 * @return DataList
1091
	 */
1092
	public function getCompleteInitialEnvironmentCreations() {
1093
		return $this->getInitialEnvironmentCreations()
1094
			->exclude('Status', ['Queued', 'Started']);
1095
	}
1096
1097
	/**
1098
	 * @param Member $member
1099
	 *
1100
	 * @return bool
1101
	 */
1102
	public function canCreate($member = null) {
1103
		if (!$member) {
1104
			$member = Member::currentUser();
1105
		}
1106
		if (!$member) {
1107
			return false;
1108
		}
1109
1110
		if (Permission::checkMember($member, 'ADMIN')) {
1111
			return true;
1112
		}
1113
1114
		// This calls canCreate on extensions.
1115
		return parent::canCreate($member);
1116
	}
1117
1118
	/**
1119
	 * This is a proxy call to gitonmy that caches the information per project and sha
1120
	 *
1121
	 * @param string $sha
1122
	 * @return false|\Gitonomy\Git\Commit
1123
	 */
1124
	public function getCommit($sha) {
1125
		$repo = $this->getRepository();
1126
		if (!$repo) {
1127
			return false;
1128
		}
1129
1130
		$cachekey = $this->ID . '_commit_' . $sha;
1131
		$cache = self::get_git_cache();
1132
		if (!($result = $cache->load($cachekey))) {
1133
			try {
1134
				$result = $repo->getCommit($sha);
1135
			} catch (\Gitonomy\Git\Exception\ReferenceNotFoundException $e) {
1136
				return false;
1137
			}
1138
			$cache->save($result, $cachekey, ['gitonomy', 'commit', 'project_' . $this->ID]);
1139
		}
1140
		return $result;
1141
	}
1142
1143
	/**
1144
	 * @param \Gitonomy\Git\Commit $commit
1145
	 * @return string
1146
	 */
1147 View Code Duplication
	public function getCommitMessage(\Gitonomy\Git\Commit $commit) {
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...
1148
		$cachekey = $this->ID . '_message_' . $commit->getRevision();
1149
		$cache = self::get_git_cache();
1150
		if (!($result = $cache->load($cachekey))) {
1151
			$result = $commit->getMessage();
1152
			$cache->save($result, $cachekey, ['gitonomy', 'message', 'project_' . $this->ID]);
1153
		}
1154
		return $result;
1155
	}
1156
1157
	/**
1158
	 * get the commit "subject", getCommitMessage get the full message
1159
	 *
1160
	 * @param \Gitonomy\Git\Commit $commit
1161
	 * @return string
1162
	 */
1163 View Code Duplication
	public function getCommitSubjectMessage(\Gitonomy\Git\Commit $commit) {
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...
1164
		$cachekey = $this->ID . '_message_subject' . $commit->getRevision();
1165
		$cache = self::get_git_cache();
1166
		if (!($result = $cache->load($cachekey))) {
1167
			$result = $commit->getSubjectMessage();
1168
			$cache->save($result, $cachekey, ['gitonomy', 'message', 'project_' . $this->ID]);
1169
		}
1170
		return $result;
1171
	}
1172
1173
	/**
1174
	 * @param \Gitonomy\Git\Commit $commit
1175
	 * @return mixed
1176
	 */
1177
	public function getCommitTags(\Gitonomy\Git\Commit $commit) {
1178
		$cachekey = $this->ID . '_tags_' . $commit->getRevision();
1179
		$cache = self::get_git_cache();
1180
		$result = $cache->load($cachekey);
1181
		// we check against false, because in many cases the tag list is an empty array
1182
		if ($result === false) {
1183
			$repo = $this->getRepository();
1184
			$result = $repo->getReferences()->resolveTags($commit->getRevision());
1185
			$cache->save($result, $cachekey, ['gitonomy', 'tags', 'project_' . $this->ID]);
1186
		}
1187
		return $result;
1188
	}
1189
1190
	/**
1191
	 * Setup a gridfield for the environment configs
1192
	 *
1193
	 * @param FieldList $fields
1194
	 * @param GridField $environments
1195
	 */
1196
	protected function setEnvironmentFields(&$fields, $environments) {
1197
		if (!$environments) {
1198
			return;
1199
		}
1200
1201
		$environments->getConfig()->addComponent(new GridFieldAddNewMultiClass());
1202
		$environments->getConfig()->removeComponentsByType('GridFieldAddNewButton');
1203
		$environments->getConfig()->removeComponentsByType('GridFieldAddExistingAutocompleter');
1204
		$environments->getConfig()->removeComponentsByType('GridFieldDeleteAction');
1205
		$environments->getConfig()->removeComponentsByType('GridFieldPageCount');
1206
		if (Config::inst()->get('DNEnvironment', 'allow_web_editing')) {
1207
			$addNewRelease = new GridFieldAddNewButton('toolbar-header-right');
1208
			$addNewRelease->setButtonName('Add');
1209
			$environments->getConfig()->addComponent($addNewRelease);
1210
		}
1211
1212
		$fields->addFieldToTab("Root.Main", $environments);
1213
	}
1214
1215
	/**
1216
	 * @return string
1217
	 */
1218
	protected function getProjectFolderPath() {
1219
		return sprintf('%s/%s', $this->DNData()->getEnvironmentDir(), $this->Name);
1220
	}
1221
1222
	/**
1223
	 * @return ValidationResult
1224
	 */
1225
	protected function validate() {
1226
		$validation = parent::validate();
1227
		if ($validation->valid()) {
1228
			if (empty($this->Name)) {
1229
				return $validation->error('The stack must have a name.');
1230
			}
1231
1232
			// The name is used to build filepaths so should be restricted
1233
			if (!preg_match('/^[a-zA-Z0-9][a-zA-Z0-9\-\_]+$/', $this->Name)) {
1234
				return $validation->error('Project name can only contain alphanumeric, hyphens and underscores.');
1235
			}
1236
1237
			if (empty($this->CVSPath)) {
1238
				return $validation->error('You must provide a repository URL.');
1239
			}
1240
1241
			$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...
1242
			if ($this->ID) {
1243
				$existing = $existing->exclude('ID', $this->ID);
1244
			}
1245
			if ($existing->count() > 0) {
1246
				return $validation->error('A stack already exists with that name.');
1247
			}
1248
		}
1249
		return $validation;
1250
	}
1251
1252
}
1253
1254