Completed
Pull Request — master (#112)
by Pieter
04:24 queued 01:13
created

GitWrapper   B

Complexity

Total Complexity 42

Size/Duplication

Total Lines 538
Duplicated Lines 1.86 %

Coupling/Cohesion

Components 3
Dependencies 9

Test Coverage

Coverage 84.13%

Importance

Changes 29
Bugs 19 Features 22
Metric Value
wmc 42
c 29
b 19
f 22
lcom 3
cbo 9
dl 10
loc 538
ccs 106
cts 126
cp 0.8413
rs 8.295

27 Methods

Rating   Name   Duplication   Size   Complexity  
A setEnvVar() 0 5 1
A unsetEnvVar() 0 5 1
A getEnvVar() 0 4 2
A getEnvVars() 0 4 1
A setTimeout() 0 5 1
A getTimeout() 0 4 1
A setProcOptions() 0 5 1
A getProcOptions() 0 4 1
A setPrivateKey() 0 18 4
A unsetPrivateKey() 0 8 1
A addLoggerListener() 0 8 1
A workingCopy() 0 4 1
A parseRepositoryName() 0 14 2
A init() 0 7 1
A cloneRepository() 0 10 2
A git() 0 6 1
A __call() 10 10 2
A getDispatcher() 0 7 2
A getGitBinary() 0 4 1
A version() 0 4 1
A __construct() 0 14 3
A setDispatcher() 0 5 1
A setGitBinary() 0 5 1
A run() 0 10 2
A addOutputListener() 0 8 1
A removeOutputListener() 0 8 1
B streamOutput() 0 14 5

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like GitWrapper often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use GitWrapper, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace GitWrapper;
4
5
use Symfony\Component\Process\Process;
6
use Symfony\Component\Process\ExecutableFinder;
7
use Symfony\Component\EventDispatcher\EventDispatcher;
8
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
9
10
/**
11
 * A wrapper class around the Git binary.
12
 *
13
 * A GitWrapper object contains the necessary context to run Git commands such
14
 * as the path to the Git binary and environment variables. It also provides
15
 * helper methods to run Git commands as set up the connection to the GIT_SSH
16
 * wrapper script.
17
 */
18
class GitWrapper
19
{
20
    /**
21
     * Symfony event dispatcher object used by this library to dispatch events.
22
     *
23
     * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
24
     */
25
    private $dispatcher;
26
27
    /**
28
     * Path to the Git binary.
29
     *
30
     * @var string
31
     */
32
    protected $gitBinary;
33
34
    /**
35
     * Environment variables defined in the scope of the Git command.
36
     *
37
     * @var array
38
     */
39
    protected $env = array();
40
41
    /**
42
     * The timeout of the Git command in seconds, defaults to 60.
43
     *
44
     * @var int
45
     */
46
    protected $timeout = 60;
47
48
    /**
49
     * An array of options passed to the proc_open() function.
50
     *
51
     * @var array
52
     */
53
    protected $procOptions = array();
54
55
    /**
56
     * @var \GitWrapper\Event\GitOutputListenerInterface
57
     */
58
    protected $streamListener;
59
60
    /**
61
     * Constructs a GitWrapper object.
62
     *
63
     * @param string|null $gitBinary
64
     *   The path to the Git binary. Defaults to null, which uses Symfony's
65
     *   ExecutableFinder to resolve it automatically.
66
     *
67
     * @throws \GitWrapper\GitException
68
     *   Throws an exception if the path to the Git binary couldn't be resolved
69
     *   by the ExecutableFinder class.
70
     */
71 180
    public function __construct($gitBinary = null)
72
    {
73 180
        if (null === $gitBinary) {
74
            // @codeCoverageIgnoreStart
75
            $finder = new ExecutableFinder();
76
            $gitBinary = $finder->find('git');
77
            if (!$gitBinary) {
78
                throw new GitException('Unable to find the Git executable.');
79
            }
80
            // @codeCoverageIgnoreEnd
81 180
        }
82
83 180
        $this->setGitBinary($gitBinary);
84 180
    }
85
86
    /**
87
     * Gets the dispatcher used by this library to dispatch events.
88
     *
89
     * @return \Symfony\Component\EventDispatcher\EventDispatcherInterface
90
     */
91 88
    public function getDispatcher()
92
    {
93 88
        if (!isset($this->dispatcher)) {
94 84
            $this->dispatcher = new EventDispatcher();
95 84
        }
96 88
        return $this->dispatcher;
97
    }
98
99
    /**
100
     * Sets the dispatcher used by this library to dispatch events.
101
     *
102
     * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
103
     *   The Symfony event dispatcher object.
104
     *
105
     * @return \GitWrapper\GitWrapper
106
     */
107 4
    public function setDispatcher(EventDispatcherInterface $dispatcher)
108
    {
109 4
        $this->dispatcher = $dispatcher;
110 4
        return $this;
111
    }
112
113
    /**
114
     * Sets the path to the Git binary.
115
     *
116
     * @param string $gitBinary
117
     *   Path to the Git binary.
118
     *
119
     * @return \GitWrapper\GitWrapper
120
     */
121 180
    public function setGitBinary($gitBinary)
122
    {
123 180
        $this->gitBinary = $gitBinary;
124 180
        return $this;
125
    }
126
127
    /**
128
     * Returns the path to the Git binary.
129
     *
130
     * @return string
131
     */
132 92
    public function getGitBinary()
133
    {
134 92
        return $this->gitBinary;
135
    }
136
137
    /**
138
     * Sets an environment variable that is defined only in the scope of the Git
139
     * command.
140
     *
141
     * @param string $var
142
     *   The name of the environment variable, e.g. "HOME", "GIT_SSH".
143
     * @param mixed $default
0 ignored issues
show
Bug introduced by
There is no parameter named $default. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
144
     *   The value of the environment variable is not set, defaults to null.
145
     *
146
     * @return \GitWrapper\GitWrapper
147
     */
148 20
    public function setEnvVar($var, $value)
149
    {
150 20
        $this->env[$var] = $value;
151 20
        return $this;
152
    }
153
154
    /**
155
     * Unsets an environment variable that is defined only in the scope of the
156
     * Git command.
157
     *
158
     * @param string $var
159
     *   The name of the environment variable, e.g. "HOME", "GIT_SSH".
160
     *
161
     * @return \GitWrapper\GitWrapper
162
     */
163 8
    public function unsetEnvVar($var)
164
    {
165 8
        unset($this->env[$var]);
166 8
        return $this;
167
    }
168
169
    /**
170
     * Returns an environment variable that is defined only in the scope of the
171
     * Git command.
172
     *
173
     * @param string $var
174
     *   The name of the environment variable, e.g. "HOME", "GIT_SSH".
175
     * @param mixed $default
176
     *   The value returned if the environment variable is not set, defaults to
177
     *   null.
178
     *
179
     * @return mixed
180
     */
181 24
    public function getEnvVar($var, $default = null)
182
    {
183 24
        return isset($this->env[$var]) ? $this->env[$var] : $default;
184
    }
185
186
    /**
187
     * Returns the associative array of environment variables that are defined
188
     * only in the scope of the Git command.
189
     *
190
     * @return array
191
     */
192 88
    public function getEnvVars()
193
    {
194 88
        return $this->env;
195
    }
196
197
    /**
198
     * Sets the timeout of the Git command.
199
     *
200
     * @param int $timeout
201
     *   The timeout in seconds.
202
     *
203
     * @return \GitWrapper\GitWrapper
204
     */
205 4
    public function setTimeout($timeout)
206
    {
207 4
        $this->timeout = (int) $timeout;
208 4
        return $this;
209
    }
210
211
    /**
212
     * Gets the timeout of the Git command.
213
     *
214
     * @return int
215
     *   The timeout in seconds.
216
     */
217 88
    public function getTimeout()
218
    {
219 88
        return $this->timeout;
220
    }
221
222
    /**
223
     * Sets the options passed to proc_open() when executing the Git command.
224
     *
225
     * @param array $timeout
0 ignored issues
show
Bug introduced by
There is no parameter named $timeout. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
226
     *   The options passed to proc_open().
227
     *
228
     * @return \GitWrapper\GitWrapper
229
     */
230 4
    public function setProcOptions(array $options)
231
    {
232 4
        $this->procOptions = $options;
233 4
        return $this;
234
    }
235
236
    /**
237
     * Gets the options passed to proc_open() when executing the Git command.
238
     *
239
     * @return array
240
     */
241 88
    public function getProcOptions()
242
    {
243 88
        return $this->procOptions;
244
    }
245
246
    /**
247
     * Set an alternate private key used to connect to the repository.
248
     *
249
     * This method sets the GIT_SSH environment variable to use the wrapper
250
     * script included with this library. It also sets the custom GIT_SSH_KEY
251
     * and GIT_SSH_PORT environment variables that are used by the script.
252
     *
253
     * @param string $privateKey
254
     *   Path to the private key.
255
     * @param int $port
256
     *   Port that the SSH server being connected to listens on, defaults to 22.
257
     * @param string|null $wrapper
258
     *   Path the the GIT_SSH wrapper script, defaults to null which uses the
259
     *   script included with this library.
260
     *
261
     * @return \GitWrapper\GitWrapper
262
     *
263
     * @throws \GitWrapper\GitException
264
     *   Thrown when any of the paths cannot be resolved.
265
     */
266 24
    public function setPrivateKey($privateKey, $port = 22, $wrapper = null)
267
    {
268 24
        if (null === $wrapper) {
269 12
            $wrapper = __DIR__ . '/../../bin/git-ssh-wrapper.sh';
270 12
        }
271 24
        if (!$wrapperPath = realpath($wrapper)) {
272 4
            throw new GitException('Path to GIT_SSH wrapper script could not be resolved: ' . $wrapper);
273
        }
274 20
        if (!$privateKeyPath = realpath($privateKey)) {
275 4
            throw new GitException('Path private key could not be resolved: ' . $privateKey);
276
        }
277
278 16
        return $this
279 16
            ->setEnvVar('GIT_SSH', $wrapperPath)
280 16
            ->setEnvVar('GIT_SSH_KEY', $privateKeyPath)
281 16
            ->setEnvVar('GIT_SSH_PORT', (int) $port)
282 16
        ;
283
    }
284
285
    /**
286
     * Unsets the private key by removing the appropriate environment variables.
287
     *
288
     * @return \GitWrapper\GitWrapper
289
     */
290 4
    public function unsetPrivateKey()
291
    {
292 4
        return $this
293 4
            ->unsetEnvVar('GIT_SSH')
294 4
            ->unsetEnvVar('GIT_SSH_KEY')
295 4
            ->unsetEnvVar('GIT_SSH_PORT')
296 4
        ;
297
    }
298
299
    /**
300
     * Adds output listener.
301
     *
302
     * @param \GitWrapper\Event\GitOutputListenerInterface $listener
303
     *
304
     * @return \GitWrapper\GitWrapper
305
     */
306
    public function addOutputListener(Event\GitOutputListenerInterface $listener)
307
    {
308
        $this
309
            ->getDispatcher()
310
            ->addListener(Event\GitEvents::GIT_OUTPUT, array($listener, 'handleOutput'))
311
        ;
312
        return $this;
313
    }
314
315
    /**
316
     * Adds logger listener listener.
317
     *
318
     * @param Event\GitLoggerListener $listener
319
     *
320
     * @return GitWrapper
321
     */
322 8
    public function addLoggerListener(Event\GitLoggerListener $listener)
323
    {
324 8
        $this
325 8
            ->getDispatcher()
326 8
            ->addSubscriber($listener)
327
        ;
328 8
        return $this;
329
    }
330
331
    /**
332
     * Removes an output listener.
333
     *
334
     * @param \GitWrapper\Event\GitOutputListenerInterface $listener
335
     *
336
     * @return \GitWrapper\GitWrapper
337
     */
338
    public function removeOutputListener(Event\GitOutputListenerInterface $listener)
339
    {
340
        $this
341
            ->getDispatcher()
342
            ->removeListener(Event\GitEvents::GIT_OUTPUT, array($listener, 'handleOutput'))
343
        ;
344
        return $this;
345
    }
346
347
    /**
348
     * Set whether or not to stream real-time output to STDOUT and STDERR.
349
     *
350
     * @param boolean $streamOutput
351
     *
352
     * @return \GitWrapper\GitWrapper
353
     */
354
    public function streamOutput($streamOutput = true)
355
    {
356
        if ($streamOutput && !isset($this->streamListener)) {
357
            $this->streamListener = new Event\GitOutputStreamListener();
358
            $this->addOutputListener($this->streamListener);
359
        }
360
361
        if (!$streamOutput && isset($this->streamListener)) {
362
            $this->removeOutputListener($this->streamListener);
363
            unset($this->streamListener);
364
        }
365
366
        return $this;
367
    }
368
369
    /**
370
     * Returns an object that interacts with a working copy.
371
     *
372
     * @param string $directory
373
     *   Path to the directory containing the working copy.
374
     *
375
     * @return GitWorkingCopy
376
     */
377 56
    public function workingCopy($directory)
378
    {
379 56
        return new GitWorkingCopy($this, $directory);
380
    }
381
382
    /**
383
     * Returns the version of the installed Git client.
384
     *
385
     * @return string
386
     *
387
     * @throws \GitWrapper\GitException
388
     */
389 12
    public function version()
390
    {
391 12
        return $this->git('--version');
392
    }
393
394
    /**
395
     * Parses name of the repository from the path.
396
     *
397
     * For example, passing the "[email protected]:cpliakas/git-wrapper.git"
398
     * repository would return "git-wrapper".
399
     *
400
     * @param string $repository
401
     *   The repository URL.
402
     *
403
     * @return string
404
     */
405 8
    public static function parseRepositoryName($repository)
406
    {
407 8
        $scheme = parse_url($repository, PHP_URL_SCHEME);
408
409 8
        if (null === $scheme) {
410 4
            $parts = explode('/', $repository);
411 4
            $path = end($parts);
412 4
        } else {
413 8
            $strpos = strpos($repository, ':');
414 8
            $path = substr($repository, $strpos + 1);
415
        }
416
417 8
        return basename($path, '.git');
418
    }
419
420
    /**
421
     * Executes a `git init` command.
422
     *
423
     * Create an empty git repository or reinitialize an existing one.
424
     *
425
     * @param string $directory
426
     *   The directory being initialized.
427
     * @param array $options
428
     *   (optional) An associative array of command line options.
429
     *
430
     * @return \GitWrapper\GitWorkingCopy
431
     *
432
     * @throws \GitWrapper\GitException
433
     *
434
     * @see GitWorkingCopy::cloneRepository()
435
     *
436
     * @ingroup commands
437
     */
438 48
    public function init($directory, array $options = array())
439
    {
440 48
        $git = $this->workingCopy($directory);
441 48
        $git->init($options);
442 48
        $git->setCloned(true);
443 48
        return $git;
444
    }
445
446
    /**
447
     * Executes a `git clone` command and returns a working copy object.
448
     *
449
     * Clone a repository into a new directory. Use GitWorkingCopy::clone()
450
     * instead for more readable code.
451
     *
452
     * @param string $repository
453
     *   The Git URL of the repository being cloned.
454
     * @param string $directory
455
     *   The directory that the repository will be cloned into. If null is
456
     *   passed, the directory will automatically be generated from the URL via
457
     *   the GitWrapper::parseRepositoryName() method.
458
     * @param array $options
459
     *   (optional) An associative array of command line options.
460
     *
461
     * @return \GitWrapper\GitWorkingCopy
462
     *
463
     * @throws \GitWrapper\GitException
464
     *
465
     * @see GitWorkingCopy::cloneRepository()
466
     *
467
     * @ingroup commands
468
     */
469 48
    public function cloneRepository($repository, $directory = null, array $options = array())
470
    {
471 48
        if (null === $directory) {
472 4
            $directory = self::parseRepositoryName($repository);
473 4
        }
474 48
        $git = $this->workingCopy($directory);
475 48
        $git->clone($repository, $options);
0 ignored issues
show
Documentation Bug introduced by
The method clone does not exist on object<GitWrapper\GitWorkingCopy>? 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...
476 48
        $git->setCloned(true);
477 48
        return $git;
478
    }
479
480
    /**
481
     * Runs an arbitrary Git command.
482
     *
483
     * The command is simply a raw command line entry for everything after the
484
     * Git binary. For example, a `git config -l` command would be passed as
485
     * `config -l` via the first argument of this method.
486
     *
487
     * Note that no events are thrown by this method.
488
     *
489
     * @param string $commandLine
490
     *   The raw command containing the Git options and arguments. The Git
491
     *   binary should not be in the command, for example `git config -l` would
492
     *   translate to "config -l".
493
     * @param string|null $cwd
494
     *   The working directory of the Git process. Defaults to null which uses
495
     *   the current working directory of the PHP process.
496
     *
497
     * @return string
498
     *   The STDOUT returned by the Git command.
499
     *
500
     * @throws \GitWrapper\GitException
501
     *
502
     * @see GitWrapper::run()
503
     */
504 24
    public function git($commandLine, $cwd = null)
505
    {
506 24
        $command = GitCommand::getInstance($commandLine);
507 24
        $command->setDirectory($cwd);
508 24
        return $this->run($command);
509
    }
510
511
    /**
512
     * Runs a Git command.
513
     *
514
     * @param \GitWrapper\GitCommand $command
515
     *   The Git command being executed.
516
     * @param string|null $cwd
517
     *   Explicitly specify the working directory of the Git process. Defaults
518
     *   to null which automatically sets the working directory based on the
519
     *   command being executed relative to the working copy.
520
     *
521
     * @return string
522
     *   The STDOUT returned by the Git command.
523
     *
524
     * @throws \GitWrapper\GitException
525
     *
526
     * @see Process
527
     */
528 88
    public function run(GitCommand $command, $cwd = null)
529
    {
530 88
        $wrapper = $this;
531 88
        $process = new GitProcess($this, $command, $cwd);
532 84
        $process->run(function ($type, $buffer) use ($wrapper, $process, $command) {
533 72
            $event = new Event\GitOutputEvent($wrapper, $process, $command, $type, $buffer);
534 72
            $wrapper->getDispatcher()->dispatch(Event\GitEvents::GIT_OUTPUT, $event);
535 84
        });
536 76
        return $command->notBypassed() ? $process->getOutput() : '';
537
    }
538
539
    /**
540
     * Hackish, allows us to use "clone" as a method name.
541
     *
542
     * $throws \BadMethodCallException
543
     * @throws \GitWrapper\GitException
544
     */
545 52 View Code Duplication
    public function __call($method, $args)
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...
546
    {
547 52
        if ('clone' == $method) {
548 48
            return call_user_func_array(array($this, 'cloneRepository'), $args);
549
        } else {
550 4
            $class = get_called_class();
551 4
            $message = "Call to undefined method $class::$method()";
552 4
            throw new \BadMethodCallException($message);
553
        }
554
    }
555
}
556