Completed
Pull Request — master (#838)
by Stig
03:36
created

DNProject::get_git_cache()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 6
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 4
nc 1
nop 0
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
	 * Provides a list of environments found in this project.
530
	 * CAUTION: filterByCallback will change this into an ArrayList!
531
	 *
532
	 * @return ArrayList
533
	 */
534
	public function DNEnvironmentList() {
535
536
		if (!self::$_current_member_cache) {
537
			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...
538
		}
539
540
		if (self::$_current_member_cache === false) {
541
			return new ArrayList();
542
		}
543
544
		$currentMember = self::$_current_member_cache;
545
		return $this->Environments()
546
			->filterByCallBack(function ($item) use ($currentMember) {
547
				return $item->canView($currentMember);
548
			});
549
	}
550
551
	/**
552
	 * @param string $usage
553
	 * @return ArrayList
554
	 */
555
	public function EnvironmentsByUsage($usage) {
556
		return $this->DNEnvironmentList()->filter('Usage', $usage);
557
	}
558
559
	/**
560
	 * Returns a map of envrionment name to build name
561
	 *
562
	 * @return false|DNDeployment
563
	 */
564
	public function currentBuilds() {
565
		if (!isset(self::$relation_cache['currentBuilds.' . $this->ID])) {
566
			$currentBuilds = [];
567
			foreach ($this->Environments() as $env) {
568
				$currentBuilds[$env->Name] = $env->CurrentBuild();
569
			}
570
			self::$relation_cache['currentBuilds.' . $this->ID] = $currentBuilds;
571
		}
572
		return self::$relation_cache['currentBuilds.' . $this->ID];
573
	}
574
575
	/**
576
	 * @param string
577
	 * @return string
578
	 */
579
	public function Link($action = '') {
580
		return Controller::join_links("naut", "project", $this->Name, $action);
581
	}
582
583
	/**
584
	 * @return string|null
585
	 */
586
	public function CreateEnvironmentLink() {
587
		if ($this->canCreateEnvironments()) {
588
			return $this->Link('createenv');
589
		}
590
		return null;
591
	}
592
593
	/**
594
	 * @return string
595
	 */
596
	public function ToggleStarLink() {
597
		return $this->Link('/star');
598
	}
599
600
	/**
601
	 * @return bool
602
	 */
603
	public function IsStarred() {
604
		$member = Member::currentUser();
605
		if ($member === null) {
606
			return false;
607
		}
608
		$favourited = $this->StarredBy()->filter('MemberID', $member->ID);
609
		if ($favourited->count() == 0) {
610
			return false;
611
		}
612
		return true;
613
	}
614
615
	/**
616
	 * @param string $action
617
	 * @return string
618
	 */
619
	public function APILink($action) {
620
		return Controller::join_links("naut", "api", $this->Name, $action);
621
	}
622
623
	/**
624
	 * @return FieldList
625
	 */
626
	public function getCMSFields() {
627
		$fields = parent::getCMSFields();
628
629
		/** @var GridField $environments */
630
		$environments = $fields->dataFieldByName("Environments");
631
632
		$fields->fieldByName("Root")->removeByName("Viewers");
633
		$fields->fieldByName("Root")->removeByName("Environments");
634
		$fields->fieldByName("Root")->removeByName("LocalCVSPath");
635
636
		$diskQuotaDesc = 'This is the maximum amount of disk space (in megabytes) that all environments within this '
637
			. 'project can use for stored snapshots';
638
		$fields->dataFieldByName('DiskQuotaMB')->setDescription($diskQuotaDesc);
639
640
		$projectNameDesc = 'Changing the name will <strong>reset</strong> the deploy configuration and avoid using non'
641
			. 'alphanumeric characters';
642
		$fields->fieldByName('Root.Main.Name')
643
			->setTitle('Project name')
644
			->setDescription($projectNameDesc);
645
646
		$fields->fieldByName('Root.Main.IsNewDeployEnabled')
647
			->setTitle('New deploy form enabled for this project')
648
			->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...
649
650
		$fields->fieldByName('Root.Main.CVSPath')
651
			->setTitle('Git repository')
652
			->setDescription('E.g. [email protected]:silverstripe/silverstripe-installer.git');
653
654
		$workspaceField = new ReadonlyField('LocalWorkspace', 'Git workspace', $this->getLocalCVSPath());
655
		$workspaceField->setDescription('This is where the GIT repository are located on this server');
656
		$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...
657
658
		$readAccessGroups = ListboxField::create('Viewers', 'Project viewers', Group::get()->map()->toArray())
659
			->setMultiple(true)
660
			->setDescription('These groups can view the project in the front-end.');
661
		$fields->addFieldToTab("Root.Main", $readAccessGroups);
662
663
		$this->setCreateProjectFolderField($fields);
664
		$this->setEnvironmentFields($fields, $environments);
665
666
		$environmentTypes = ClassInfo::implementorsOf('EnvironmentCreateBackend');
667
		$types = [];
668
		foreach ($environmentTypes as $type) {
669
			$types[$type] = $type;
670
		}
671
672
		$fields->addFieldsToTab('Root.Main', [
673
			DropdownField::create(
674
				'AllowedEnvironmentType',
675
				'Allowed Environment Type',
676
				$types
677
			)->setDescription('This defined which form to show on the front end for '
678
				. 'environment creation. This will not affect backend functionality.')
679
				->setEmptyString(' - None - '),
680
		]);
681
682
		return $fields;
683
	}
684
685
	/**
686
	 * If there isn't a capistrano env project folder, show options to create one
687
	 *
688
	 * @param FieldList $fields
689
	 */
690
	public function setCreateProjectFolderField(&$fields) {
691
		// Check if the capistrano project folder exists
692
		if (!$this->Name) {
693
			return;
694
		}
695
696
		if ($this->projectFolderExists()) {
697
			return;
698
		}
699
700
		$createFolderNotice = new LabelField('CreateEnvFolderNotice', 'Warning: No Capistrano project folder exists');
701
		$createFolderNotice->addExtraClass('message warning');
702
		$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...
703
		$createFolderField = new CheckboxField('CreateEnvFolder', 'Create folder');
704
		$createFolderField->setDescription('Would you like to create the capistrano project folder?');
705
		$fields->insertAfter($createFolderField, 'CreateEnvFolderNotice');
706
	}
707
708
	/**
709
	 * @return boolean
710
	 */
711
	public function projectFolderExists() {
712
		return file_exists($this->getProjectFolderPath());
713
	}
714
715
	/**
716
	 * @return bool
717
	 */
718
	public function repoExists() {
719
		return file_exists(sprintf('%s/HEAD', $this->getLocalCVSPath()));
720
	}
721
722
	/**
723
	 * Setup a job to clone a git repository.
724
	 * @return string resque token
725
	 */
726
	public function cloneRepo() {
727
		// Avoid this being called multiple times in the same request
728
		if (!isset(self::$has_cloned_cache[$this->ID])) {
729
			$fetch = DNGitFetch::create();
730
			$fetch->ProjectID = $this->ID;
731
			$fetch->write();
732
733
			// passing true here tells DNGitFetch to force a git clone, otherwise
734
			// it will just update the repo if it already exists. We want to ensure
735
			// we're always cloning a new repo in this case, as the git URL may have changed.
736
			$fetch->start(true);
737
738
			self::$has_cloned_cache[$this->ID] = true;
739
		}
740
	}
741
742
	/**
743
	 * @return string
744
	 */
745
	public function getLocalCVSPath() {
746
		return sprintf('%s/%s', DEPLOYNAUT_LOCAL_VCS_PATH, $this->Name);
747
	}
748
749
	public function onBeforeWrite() {
750
		parent::onBeforeWrite();
751
752
		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...
753
			mkdir($this->getProjectFolderPath());
754
		}
755
	}
756
757
	public function onAfterWrite() {
758
		parent::onAfterWrite();
759
760
		if (!$this->CVSPath) {
761
			return;
762
		}
763
764
		$changedFields = $this->getChangedFields(true, 2);
765
		if (isset($changedFields['CVSPath']) || isset($changedFields['Name'])) {
766
			$this->cloneRepo();
767
		}
768
	}
769
770
	/**
771
	 * Delete related environments and folders
772
	 */
773
	public function onAfterDelete() {
774
		parent::onAfterDelete();
775
776
		$environments = $this->Environments();
777
		if ($environments && $environments->exists()) {
778
			foreach ($environments as $env) {
779
				$env->delete();
780
			}
781
		}
782
783
		$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...
784
		if ($fetches && $fetches->exists()) {
785
			foreach ($fetches as $fetch) {
786
				$fetch->delete();
787
			}
788
		}
789
790
		// Delete local repository
791
		if (file_exists($this->getLocalCVSPath())) {
792
			Filesystem::removeFolder($this->getLocalCVSPath());
793
		}
794
795
		// Delete project template
796
		if (file_exists($this->getProjectFolderPath()) && Config::inst()->get('DNEnvironment', 'allow_web_editing')) {
797
			Filesystem::removeFolder($this->getProjectFolderPath());
798
		}
799
800
		// Delete the deploy key
801
		if (file_exists($this->getKeyDir())) {
802
			Filesystem::removeFolder($this->getKeyDir());
803
		}
804
	}
805
806
	/**
807
	 * Fetch the public key for this project.
808
	 *
809
	 * @return string|void
810
	 */
811
	public function getPublicKey() {
812
		$key = $this->getPublicKeyPath();
813
814
		if (file_exists($key)) {
815
			return trim(file_get_contents($key));
816
		}
817
	}
818
819
	/**
820
	 * This returns that path of the public key if a key directory is set. It doesn't check whether the file exists.
821
	 *
822
	 * @return string|null
823
	 */
824
	public function getPublicKeyPath() {
825
		if ($privateKey = $this->getPrivateKeyPath()) {
826
			return $privateKey . '.pub';
827
		}
828
		return null;
829
	}
830
831
	/**
832
	 * This returns that path of the private key if a key directory is set. It doesn't check whether the file exists.
833
	 *
834
	 * @return string|null
835
	 */
836
	public function getPrivateKeyPath() {
837
		$keyDir = $this->getKeyDir();
838
		if (!empty($keyDir)) {
839
			$filter = FileNameFilter::create();
840
			$name = $filter->filter($this->Name);
841
			return $keyDir . '/' . $name;
842
		}
843
		return null;
844
	}
845
846
	/**
847
	 * Returns the location of the projects key dir if one exists.
848
	 *
849
	 * @return string|null
850
	 */
851
	public function getKeyDir() {
852
		$keyDir = $this->DNData()->getKeyDir();
853
		if (!$keyDir) {
854
			return null;
855
		}
856
857
		$filter = FileNameFilter::create();
858
		$name = $filter->filter($this->Name);
859
860
		return $this->DNData()->getKeyDir() . '/' . $name;
861
	}
862
863
	/**
864
	 * Provide current repository URL to the users.
865
	 *
866
	 * @return void|string
867
	 */
868
	public function getRepositoryURL() {
869
		$showUrl = Config::inst()->get($this->class, 'show_repository_url');
870
		if ($showUrl) {
871
			return $this->CVSPath;
872
		}
873
	}
874
875
	/**
876
	 * Get a ViewableData structure describing the UI tool that lets the user view the repository code
877
	 *
878
	 * @return ArrayData
879
	 */
880
	public function getRepositoryInterface() {
881
		$interfaces = $this->config()->repository_interfaces;
882
883
		/* Look for each whitelisted hostname */
884
		foreach ($interfaces as $host => $interface) {
885
			/* See if the CVS Path is for this hostname, followed by some junk (maybe a port), then the path */
886
			if (preg_match('{^[^.]*' . $host . '(.*?)([/a-zA-Z].+)}', $this->CVSPath, $match)) {
887
888
				$path = $match[2];
889
890
				$scheme = isset($interface['scheme']) ? $interface['scheme'] : 'https';
891
				$host = isset($interface['host']) ? $interface['host'] : $host;
892
				$regex = isset($interface['regex']) ? $interface['regex'] : ['\.git$' => ''];
893
894
				$components = explode('.', $host);
895
896
				foreach ($regex as $pattern => $replacement) {
897
					$path = preg_replace('/' . $pattern . '/', $replacement, $path);
898
				}
899
900
				$uxurl = Controller::join_links($scheme . '://', $host, $path);
901
902
				if (array_key_exists('commit', $interface) && $interface['commit'] == false) {
903
					$commiturl = false;
904
				} else {
905
					$commiturl = Controller::join_links(
906
						$uxurl,
907
						isset($interface['commit']) ? $interface['commit'] : 'commit'
908
					);
909
				}
910
911
				return new ArrayData([
912
					'Name' => isset($interface['name']) ? $interface['name'] : ucfirst($components[0]),
913
					'Icon' => isset($interface['icon']) ? $interface['icon'] : 'deploynaut/img/git.png',
914
					'URL' => $uxurl,
915
					'CommitURL' => $commiturl
916
				]);
917
			}
918
		}
919
	}
920
921
	/**
922
	 * Convenience wrapper for a single permission code.
923
	 *
924
	 * @param string $code
925
	 * @return SS_List
926
	 */
927
	public function whoIsAllowed($code) {
928
		return $this->whoIsAllowedAny([$code]);
929
	}
930
931
	/**
932
	 * List members who have $codes on this project.
933
	 * Does not support Permission::DENY_PERMISSION malarky, same as Permission::get_groups_by_permission anyway...
934
	 *
935
	 * @param array|string $codes
936
	 * @return SS_List
937
	 */
938
	public function whoIsAllowedAny($codes) {
939
		if (!is_array($codes)) {
940
			$codes = [$codes];
941
		}
942
943
		$SQLa_codes = Convert::raw2sql($codes);
944
		$SQL_codes = join("','", $SQLa_codes);
945
946
		return DataObject::get('Member')
947
			->where("\"PermissionRoleCode\".\"Code\" IN ('$SQL_codes') OR \"Permission\".\"Code\" IN ('$SQL_codes')")
948
			->filter("DNProject_Viewers.DNProjectID", $this->ID)
949
			->leftJoin('Group_Members', "\"Group_Members\".\"MemberID\" = \"Member\".\"ID\"")
950
			->leftJoin('Group', "\"Group_Members\".\"GroupID\" = \"Group\".\"ID\"")
951
			->leftJoin('DNProject_Viewers', "\"DNProject_Viewers\".\"GroupID\" = \"Group\".\"ID\"")
952
			->leftJoin('Permission', "\"Permission\".\"GroupID\" = \"Group\".\"ID\"")
953
			->leftJoin('Group_Roles', "\"Group_Roles\".\"GroupID\" = \"Group\".\"ID\"")
954
			->leftJoin('PermissionRole', "\"Group_Roles\".\"PermissionRoleID\" = \"PermissionRole\".\"ID\"")
955
			->leftJoin('PermissionRoleCode', "\"PermissionRoleCode\".\"RoleID\" = \"PermissionRole\".\"ID\"");
956
	}
957
958
	/**
959
	 * Convenience wrapper for a single permission code.
960
	 *
961
	 * @param string $code
962
	 * @param Member|null $member
963
	 *
964
	 * @return bool
965
	 */
966
	public function allowed($code, $member = null) {
967
		return $this->allowedAny([$code], $member);
968
	}
969
970
	/**
971
	 * Checks if a group is allowed to the project and the permission code
972
	 *
973
	 * @param string $permissionCode
974
	 * @param Group $group
975
	 *
976
	 * @return bool
977
	 */
978
	public function groupAllowed($permissionCode, Group $group) {
979
		$viewers = $this->Viewers();
980
		if (!$viewers->find('ID', $group->ID)) {
981
			return false;
982
		}
983
		$groups = Permission::get_groups_by_permission($permissionCode);
984
		if (!$groups->find('ID', $group->ID)) {
985
			return false;
986
		}
987
		return true;
988
	}
989
990
	/**
991
	 * Check if member has a permission code in this project.
992
	 *
993
	 * @param array|string $codes
994
	 * @param Member|null $member
995
	 *
996
	 * @return bool
997
	 */
998
	public function allowedAny($codes, $member = null) {
999
		if (!$member) {
1000
			$member = Member::currentUser();
1001
		}
1002
1003
		if (Permission::checkMember($member, 'ADMIN')) {
1004
			return true;
1005
		}
1006
1007
		$hits = $this->whoIsAllowedAny($codes)->filter('Member.ID', $member->ID)->count();
1008
		return ($hits > 0 ? true : false);
1009
	}
1010
1011
	/**
1012
	 * Checks if the environment has been fully built.
1013
	 *
1014
	 * @return bool
1015
	 */
1016
	public function isProjectReady() {
1017
		if ($this->getRunningInitialEnvironmentCreations()->count() > 0) {
1018
			// We're still creating the initial environments for this project so we're
1019
			// not quite done
1020
			return false;
1021
		}
1022
1023
		// Provide a hook for further checks. Logic stolen from
1024
		// {@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...
1025
		$isDone = $this->extend('isProjectReady');
1026
		if ($isDone && is_array($isDone)) {
1027
			$isDone = array_filter($isDone, function ($val) {
1028
				return !is_null($val);
1029
			});
1030
1031
			// If anything returns false then we're not ready.
1032
			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...
1033
				return min($isDone);
1034
			}
1035
		}
1036
1037
		return true;
1038
	}
1039
1040
	/**
1041
	 * Returns a list of environments still being created.
1042
	 *
1043
	 * @return SS_List
1044
	 */
1045
	public function getRunningEnvironmentCreations() {
1046
		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...
1047
			->filter('Status', ['Queued', 'Started']);
1048
	}
1049
1050
	/**
1051
	 * Returns a list of initial environments created for this project.
1052
	 *
1053
	 * @return DataList
1054
	 */
1055
	public function getInitialEnvironmentCreations() {
1056
		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...
1057
	}
1058
1059
	/**
1060
	 * Only returns initial environments that are being created.
1061
	 *
1062
	 * @return DataList
1063
	 */
1064
	public function getRunningInitialEnvironmentCreations() {
1065
		return $this->getInitialEnvironmentCreations()
1066
			->filter('Status', ['Queued', 'Started']);
1067
	}
1068
1069
	/**
1070
	 * Returns a list of completed initial environment creations. This includes failed tasks.
1071
	 *
1072
	 * @return DataList
1073
	 */
1074
	public function getCompleteInitialEnvironmentCreations() {
1075
		return $this->getInitialEnvironmentCreations()
1076
			->exclude('Status', ['Queued', 'Started']);
1077
	}
1078
1079
	/**
1080
	 * @param Member $member
1081
	 *
1082
	 * @return bool
1083
	 */
1084
	public function canCreate($member = null) {
1085
		if (!$member) {
1086
			$member = Member::currentUser();
1087
		}
1088
		if (!$member) {
1089
			return false;
1090
		}
1091
1092
		if (Permission::checkMember($member, 'ADMIN')) {
1093
			return true;
1094
		}
1095
1096
		// This calls canCreate on extensions.
1097
		return parent::canCreate($member);
1098
	}
1099
1100
	/**
1101
	 * This is a proxy call to gitonmy that caches the information per project and sha
1102
	 *
1103
	 * @param string $sha
1104
	 * @return false|\Gitonomy\Git\Commit
1105
	 */
1106
	public function getCommit($sha) {
1107
		$repo = $this->getRepository();
1108
		if (!$repo) {
1109
			return false;
1110
		}
1111
1112
		$cachekey = $this->ID . '_commit_' . $sha;
1113
		$cache = self::get_git_cache();
1114
		if (!($result = $cache->load($cachekey))) {
1115
			$result = $repo->getCommit($sha);
1116
			$cache->save($result, $cachekey, ['gitonomy', 'commit', 'project_' . $this->ID]);
1117
		}
1118
		return $result;
1119
	}
1120
1121
	/**
1122
	 * @param \Gitonomy\Git\Commit $commit
1123
	 * @return string
1124
	 */
1125 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...
1126
		$cachekey = $this->ID . '_message_' . $commit->getRevision();
1127
		$cache = self::get_git_cache();
1128
		if (!($result = $cache->load($cachekey))) {
1129
			$result = $commit->getMessage();
1130
			$cache->save($result, $cachekey, ['gitonomy', 'message', 'project_' . $this->ID]);
1131
		}
1132
		return $result;
1133
	}
1134
1135
	/**
1136
	 * get the commit "subject", getCommitMessage get the full message
1137
	 *
1138
	 * @param \Gitonomy\Git\Commit $commit
1139
	 * @return string
1140
	 */
1141 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...
1142
		$cachekey = $this->ID . '_message_subject' . $commit->getRevision();
1143
		$cache = self::get_git_cache();
1144
		if (!($result = $cache->load($cachekey))) {
1145
			$result = $commit->getSubjectMessage();
1146
			$cache->save($result, $cachekey, ['gitonomy', 'message', 'project_' . $this->ID]);
1147
		}
1148
		return $result;
1149
	}
1150
1151
	/**
1152
	 * @param \Gitonomy\Git\Commit $commit
1153
	 * @return mixed
1154
	 */
1155
	public function getCommitTags(\Gitonomy\Git\Commit $commit) {
1156
		$cachekey = $this->ID . '_tags_' . $commit->getRevision();
1157
		$cache = self::get_git_cache();
1158
		$result = $cache->load($cachekey);
1159
		// we check against false, because in many cases the tag list is an empty array
1160
		if ($result === false) {
1161
			$repo = $this->getRepository();
1162
			$result = $repo->getReferences()->resolveTags($commit->getRevision());
1163
			$cache->save($result, $cachekey, ['gitonomy', 'tags', 'project_' . $this->ID]);
1164
		}
1165
		return $result;
1166
	}
1167
1168
	/**
1169
	 * Setup a gridfield for the environment configs
1170
	 *
1171
	 * @param FieldList $fields
1172
	 * @param GridField $environments
1173
	 */
1174
	protected function setEnvironmentFields(&$fields, $environments) {
1175
		if (!$environments) {
1176
			return;
1177
		}
1178
1179
		$environments->getConfig()->addComponent(new GridFieldAddNewMultiClass());
1180
		$environments->getConfig()->removeComponentsByType('GridFieldAddNewButton');
1181
		$environments->getConfig()->removeComponentsByType('GridFieldAddExistingAutocompleter');
1182
		$environments->getConfig()->removeComponentsByType('GridFieldDeleteAction');
1183
		$environments->getConfig()->removeComponentsByType('GridFieldPageCount');
1184
		if (Config::inst()->get('DNEnvironment', 'allow_web_editing')) {
1185
			$addNewRelease = new GridFieldAddNewButton('toolbar-header-right');
1186
			$addNewRelease->setButtonName('Add');
1187
			$environments->getConfig()->addComponent($addNewRelease);
1188
		}
1189
1190
		$fields->addFieldToTab("Root.Main", $environments);
1191
	}
1192
1193
	/**
1194
	 * @return string
1195
	 */
1196
	protected function getProjectFolderPath() {
1197
		return sprintf('%s/%s', $this->DNData()->getEnvironmentDir(), $this->Name);
1198
	}
1199
1200
	/**
1201
	 * @return ValidationResult
1202
	 */
1203
	protected function validate() {
1204
		$validation = parent::validate();
1205
		if ($validation->valid()) {
1206
			if (empty($this->Name)) {
1207
				return $validation->error('The stack must have a name.');
1208
			}
1209
1210
			// The name is used to build filepaths so should be restricted
1211
			if (!preg_match('/^[a-zA-Z0-9][a-zA-Z0-9\-\_]+$/', $this->Name)) {
1212
				return $validation->error('Project name can only contain alphanumeric, hyphens and underscores.');
1213
			}
1214
1215
			if (empty($this->CVSPath)) {
1216
				return $validation->error('You must provide a repository URL.');
1217
			}
1218
1219
			$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...
1220
			if ($this->ID) {
1221
				$existing = $existing->exclude('ID', $this->ID);
1222
			}
1223
			if ($existing->count() > 0) {
1224
				return $validation->error('A stack already exists with that name.');
1225
			}
1226
		}
1227
		return $validation;
1228
	}
1229
1230
}
1231
1232