• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

CPS-IT / migrator / 9970941466

17 Jul 2024 08:36AM UTC coverage: 99.277% (-0.2%) from 99.515%
9970941466

push

github

web-flow
Merge pull request #291 from CPS-IT/task/destruct-coverage

[TASK] Add test case for destructor of `GitDiffer`

412 of 415 relevant lines covered (99.28%)

8.87 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.53
/src/Diff/Differ/GitDiffer.php
1
<?php
2

3
declare(strict_types=1);
4

5
/*
6
 * This file is part of the Composer package "cpsit/migrator".
7
 *
8
 * Copyright (C) 2023 Elias Häußler <e.haeussler@familie-redlich.de>
9
 *
10
 * This program is free software: you can redistribute it and/or modify
11
 * it under the terms of the GNU General Public License as published by
12
 * the Free Software Foundation, either version 3 of the License, or
13
 * (at your option) any later version.
14
 *
15
 * This program is distributed in the hope that it will be useful,
16
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18
 * GNU General Public License for more details.
19
 *
20
 * You should have received a copy of the GNU General Public License
21
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
22
 */
23

24
namespace CPSIT\Migrator\Diff\Differ;
25

26
use CPSIT\Migrator\Diff;
27
use CPSIT\Migrator\Exception;
28
use CPSIT\Migrator\Helper;
29
use CPSIT\Migrator\Resource;
30
use GitElephant\Objects;
31
use GitElephant\Repository;
32
use GitElephant\Status;
33
use Symfony\Component\Filesystem;
34

35
use function end;
36
use function explode;
37

38
/**
39
 * GitDiffer.
40
 *
41
 * @author Elias Häußler <e.haeussler@familie-redlich.de>
42
 * @license GPL-3.0-or-later
43
 */
44
final class GitDiffer implements Differ
45
{
46
    private const MAIN_BRANCH = 'main';
47
    private const BASE_BRANCH = 'base';
48

49
    private const DIFF_MODES = [
50
        Status\StatusFile::ADDED => Diff\DiffMode::Added,
51
        Status\StatusFile::COPIED => Diff\DiffMode::Copied,
52
        Status\StatusFile::DELETED => Diff\DiffMode::Deleted,
53
        Status\StatusFile::IGNORED => Diff\DiffMode::Ignored,
54
        Status\StatusFile::MODIFIED => Diff\DiffMode::Modified,
55
        Status\StatusFile::RENAMED => Diff\DiffMode::Renamed,
56
        Status\StatusFile::UNTRACKED => Diff\DiffMode::Untracked,
57
        Status\StatusFile::UPDATED_BUT_UNMERGED => Diff\DiffMode::Conflicted,
58
    ];
59

60
    private readonly Filesystem\Filesystem $filesystem;
61
    private readonly Repository $repository;
62

63
    public function __construct()
20✔
64
    {
65
        $this->filesystem = new Filesystem\Filesystem();
20✔
66
        $this->repository = $this->initializeRepository();
20✔
67
    }
68

69
    public function generateDiff(
17✔
70
        Resource\Collector\CollectorInterface $source,
71
        Resource\Collector\CollectorInterface $target,
72
        Resource\Collector\DirectoryCollector $base,
73
    ): Diff\DiffResult {
74
        // Generate Git tree
75
        $this->commitSourceFiles($source);
17✔
76
        $this->commitBaseFiles($base);
17✔
77
        $this->commitTargetFiles($target);
17✔
78

79
        // Perform three-way merge
80
        $outcome = $this->merge();
17✔
81

82
        // Calculate diff objects from current status
83
        $status = $this->repository->stage()->getStatus();
17✔
84
        $diffObjects = $this->calculateDiffObjects($status);
17✔
85

86
        // Generate applicable patch
87
        $patch = $this->repository->getCaller()->execute('diff --cached')->getOutput();
17✔
88

89
        return new Diff\DiffResult($diffObjects, $patch, $outcome);
17✔
90
    }
91

92
    public function applyDiff(
2✔
93
        Diff\DiffResult $diffResult,
94
        Resource\Collector\DirectoryCollector $base,
95
    ): bool {
96
        if (!$diffResult->getOutcome()->isSuccessful()) {
2✔
97
            throw Exception\PatchFailureException::forConflictedDiff($diffResult);
1✔
98
        }
99

100
        $repo = new Resource\Collector\DirectoryCollector($this->repository->getPath());
1✔
101

102
        $this->filesystem->remove($base->collectFiles());
1✔
103
        $this->filesystem->mirror($this->repository->getPath(), $base->getBaseDirectory(), $repo->collectFiles());
1✔
104

105
        return true;
1✔
106
    }
107

108
    private function commitSourceFiles(Resource\Collector\CollectorInterface $source): void
17✔
109
    {
110
        foreach ($source as $path => $contents) {
17✔
111
            $this->filesystem->dumpFile(
17✔
112
                Filesystem\Path::join($this->repository->getPath(), $path),
17✔
113
                $contents,
17✔
114
            );
17✔
115
        }
116

117
        $this->repository->commit('Add source files', true, allowEmpty: true);
17✔
118
    }
119

120
    private function commitBaseFiles(Resource\Collector\DirectoryCollector $base): void
17✔
121
    {
122
        $repo = new Resource\Collector\DirectoryCollector($this->repository->getPath());
17✔
123

124
        // Go to base branch
125
        $this->repository->checkout(self::BASE_BRANCH, true);
17✔
126

127
        // Add base files
128
        $this->filesystem->remove($repo->collectFiles());
17✔
129
        $this->filesystem->mirror($base->getBaseDirectory(), $this->repository->getPath(), $base->collectFiles());
17✔
130

131
        // Commit base files
132
        $this->repository->commit('Add base files', true, allowEmpty: true);
17✔
133

134
        // Go back to main branch
135
        $this->repository->checkout(self::MAIN_BRANCH);
17✔
136
    }
137

138
    private function commitTargetFiles(Resource\Collector\CollectorInterface $target): void
17✔
139
    {
140
        $repo = new Resource\Collector\DirectoryCollector($this->repository->getPath());
17✔
141

142
        // Clean up repository
143
        $this->filesystem->remove($repo->collectFiles());
17✔
144

145
        // Add target files
146
        foreach ($target as $path => $contents) {
17✔
147
            $this->filesystem->dumpFile(
17✔
148
                Filesystem\Path::join($this->repository->getPath(), $path),
17✔
149
                $contents,
17✔
150
            );
17✔
151
        }
152

153
        // Commit target files
154
        $this->repository->commit('Add target files', true, allowEmpty: true);
17✔
155
    }
156

157
    private function merge(): Diff\Outcome
17✔
158
    {
159
        $mainBranch = $this->repository->getBranch(self::MAIN_BRANCH) ?? $this->repository->getMainBranch();
17✔
160

161
        try {
162
            $this->repository->checkout(self::BASE_BRANCH);
17✔
163
            $this->repository->merge($mainBranch, 'Migrate base files', 'no-ff');
17✔
164

165
            // Enforce working directory to be dirty (we need this to correctly
166
            // determine the current Git status in the DiffResult class)
167
            $this->repository->reset('HEAD~1', []);
16✔
168

169
            $outcome = Diff\Outcome::successful();
16✔
170
        } catch (\Exception $exception) {
1✔
171
            $errorMessage = explode('with reason: ', $exception->getMessage());
1✔
172
            $outcome = Diff\Outcome::failed(end($errorMessage));
1✔
173
        }
174

175
        return $outcome;
17✔
176
    }
177

178
    /**
179
     * @return list<Diff\DiffObject>
180
     */
181
    private function calculateDiffObjects(Status\Status $status): array
17✔
182
    {
183
        $diff = $this->repository->commit('Add changed files', allowEmpty: true)->getDiff('HEAD', 'HEAD~1');
17✔
184
        $diffObjects = [];
17✔
185

186
        /** @var Status\StatusFile $statusFile */
187
        foreach ($status->all() as $statusFile) {
17✔
188
            $diffMode = self::DIFF_MODES[$statusFile->getWorkingTreeStatus()] ?? self::DIFF_MODES[$statusFile->getIndexStatus()] ?? null;
17✔
189
            $destinationPath = $statusFile->getRenamed() ?? $statusFile->getName();
17✔
190

191
            // Skip irrelevant statuses
192
            if (null === $diffMode) {
17✔
193
                continue;
×
194
            }
195

196
            /** @var Objects\Diff\DiffObject $diffObject */
197
            foreach ($diff as $diffObject) {
17✔
198
                if ($destinationPath === $diffObject->getDestinationPath()) {
17✔
199
                    $diffObjects[] = new Diff\DiffObject(
17✔
200
                        $diffMode,
17✔
201
                        $diffObject->getOriginalPath(),
17✔
202
                        $diffObject->getDestinationPath(),
17✔
203
                        $diffObject->getChunks(),
17✔
204
                    );
17✔
205
                }
206
            }
207
        }
208

209
        return $diffObjects;
17✔
210
    }
211

212
    private function initializeRepository(): Repository
20✔
213
    {
214
        $repoDir = Helper\FilesystemHelper::getNewTemporaryDirectory();
20✔
215

216
        // Create temporary directory for Git repository
217
        $this->filesystem->mkdir($repoDir);
20✔
218

219
        // Create repository
220
        $repository = new Repository($repoDir);
20✔
221

222
        // Initialize repository
223
        $repository->addGlobalConfig('commit.gpgsign', false);
20✔
224
        $repository->addGlobalConfig('user.name', 'CPS Migrator');
20✔
225
        $repository->addGlobalConfig('user.email', '');
20✔
226
        $repository->init(false, self::MAIN_BRANCH);
20✔
227

228
        return $repository;
20✔
229
    }
230

231
    public function __destruct()
15✔
232
    {
233
        try {
234
            $this->filesystem->remove($this->repository->getPath());
15✔
235
        } catch (Filesystem\Exception\IOException) {
×
236
            // Ignore failures
237
        }
238
    }
239
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc