diff --git a/.build/license.php b/.build/license.php new file mode 100644 index 00000000..df3f078c --- /dev/null +++ b/.build/license.php @@ -0,0 +1,464 @@ +licenseText = <<. + * + */ +EOD; + $this->licenseTextLegacy = << + * + */ +EOD; + $this->licenseTextLegacy = str_replace('@YEAR@', date('Y'), $this->licenseTextLegacy); + } + + /** + * @param string|string[] $folder + * @param string|bool $gitRoot + */ + public function exec($folder, $gitRoot = false) { + if (is_array($folder)) { + foreach ($folder as $f) { + $this->exec($f, $gitRoot); + } + return; + } + + if ($gitRoot !== false && substr($gitRoot, -1) !== '/') { + $gitRoot .= '/'; + } + + if (is_file($folder)) { + $this->handleFile($folder, $gitRoot); + $this->printFilesToCheck(); + return; + } + + $excludes = array_map(function ($item) use ($folder) { + return $folder . '/' . $item; + }, ['vendor', '3rdparty', '.git', 'l10n', 'js', 'node_modules']); + + $iterator = new RecursiveDirectoryIterator($folder, RecursiveDirectoryIterator::SKIP_DOTS); + $iterator = new RecursiveCallbackFilterIterator($iterator, function ($item) use ($folder, $excludes) { + /** @var SplFileInfo $item */ + foreach ($excludes as $exclude) { + if (substr($item->getPath(), 0, strlen($exclude)) === $exclude) { + return false; + } + } + return true; + }); + $iterator = new RecursiveIteratorIterator($iterator); + $iterator = new RegexIterator($iterator, '/^.+\.(js|php|md|twig|xml|Dockerfile|Caddyfile|sh|conf|script|cfg|motd|yml|yaml|css)$/i'); + + foreach ($iterator as $file) { + /** @var SplFileInfo $file */ + $this->handleFile($file, $gitRoot); + } + + $this->printFilesToCheck(); + } + + public function writeAuthorsFile() { + ksort($this->authors); + $template = ' +# Authors +@AUTHORS@ + +'; + $authors = implode(PHP_EOL, array_map(function ($author) { + return ' - ' . $author; + }, $this->authors)); + $template = str_replace('@AUTHORS@', $authors, $template); + file_put_contents(__DIR__ . '/../AUTHORS', $template); + } + + public function handleFile($path, $gitRoot) { + $isPhp = preg_match('/^.+\.php$/i', $path); + $isShell = preg_match('/^.+\.sh$/i', $path); + + $source = file_get_contents($path); + if ($this->isMITLicensed($source)) { + echo "MIT licensed file: $path" . PHP_EOL; + return; + } + $copyrightNotices = $this->getCopyrightNotices($path, $source); + $authors = $this->getAuthors($path, $gitRoot); + if ($this->isOwnCloudLicensed($source)) { + $license = str_replace('@AUTHORS@', $authors, $this->licenseTextLegacy); + $this->checkCopyrightState($path, $gitRoot); + } else { + $license = str_replace('@AUTHORS@', $authors, $this->licenseText); + } + + if ($copyrightNotices === '') { + $creator = $this->getCreatorCopyright($path, $gitRoot); + $license = str_replace('@COPYRIGHT@', $creator, $license); + } else { + $license = str_replace('@COPYRIGHT@', $copyrightNotices, $license); + } + + [$source, $isStrict] = $this->eatOldLicense($source); + + if ($isPhp) { + if ($isStrict) { + $source = 'getTimestamp(); + + $buildDir = getcwd(); + if ($gitRoot) { + chdir($gitRoot); + $path = substr($path, strlen($gitRoot)); + } + $out = shell_exec("git --no-pager blame --line-porcelain $path | sed -n 's/^author-time //p'"); + if ($gitRoot) { + chdir($buildDir); + } + $timestampChanges = explode(PHP_EOL, $out); + $timestampChanges = array_slice($timestampChanges, 0, count($timestampChanges) - 1); + foreach ($timestampChanges as $timestamp) { + if ((int)$timestamp < $deadlineTimestamp) { + return; + } + } + + //all changes after the deadline + $this->checkFiles[] = $path; + } + + private function printFilesToCheck() { + if (!empty($this->checkFiles)) { + print "\n"; + print 'For following files all lines changed since the Nextcloud fork.' . PHP_EOL; + print 'Please check if these files can be moved over to AGPLv3 or later' . PHP_EOL; + print "\n"; + foreach ($this->checkFiles as $file) { + print $file . PHP_EOL; + } + print "\n"; + } + } + + private function filterAuthors($authors = []) { + $authors = array_filter($authors, function ($author) { + return !in_array($author, [ + '', + 'Not Committed Yet ', + 'Jenkins for ownCloud ', + 'Scrutinizer Auto-Fixer ', + ]); + }); + + // Strip out dependabot + $authors = array_filter($authors, function ($author) { + return strpos($author, 'dependabot') === false; + }); + + return $authors; + } + + private function getCreatorCopyright($file, $gitRoot) { + $buildDir = getcwd(); + + if ($gitRoot) { + chdir($gitRoot); + $file = substr($file, strlen($gitRoot)); + } + + $year = date('Y'); + $blame = shell_exec("git blame --line-porcelain $file | sed -n 's/^author //p;s/^author-mail //p' | sed 'N;s/\\n/ /'"); + $authors = explode(PHP_EOL, $blame); + + if ($gitRoot) { + chdir($buildDir); + } + + $authors = $this->filterAuthors($authors); + + if ($gitRoot) { + $authors = array_map([$this, 'checkCoreMailMap'], $authors); + $authors = array_unique($authors); + } + + $creator = array_key_exists(0, $authors) + ? $this->fixInvalidEmail($authors[0]) + : ''; + return " * @copyright Copyright (c) $year $creator"; + } + + private function getAuthors($file, $gitRoot) { + // only add authors that changed code and not the license header + $licenseHeaderEndsAtLine = trim(shell_exec("grep -n '*/' $file | head -n 1 | cut -d ':' -f 1")); + $buildDir = getcwd(); + + if ($gitRoot) { + chdir($gitRoot); + $file = substr($file, strlen($gitRoot)); + } + $out = shell_exec("git blame --line-porcelain -L $licenseHeaderEndsAtLine, $file | sed -n 's/^author //p;s/^author-mail //p' | sed 'N;s/\\n/ /' | sort -f | uniq"); + + if ($gitRoot) { + chdir($buildDir); + } + + $authors = explode(PHP_EOL, $out); + $authors = $this->filterAuthors($authors); + + if ($gitRoot) { + $authors = array_map([$this, 'checkCoreMailMap'], $authors); + $authors = array_unique($authors); + } + + $authors = array_map(function ($author) { + $author = $this->fixInvalidEmail($author); + $this->authors[$author] = $author; + return " * @author $author"; + }, $authors); + + return implode(PHP_EOL, $authors); + } + + private function checkCoreMailMap($author) { + if (empty($this->mailMap)) { + $content = file_get_contents(__DIR__ . '/../.mailmap'); + $entries = explode("\n", $content); + foreach ($entries as $entry) { + if (strpos($entry, '> ') === false) { + $this->mailMap[$entry] = $entry; + } else { + [$use, $actual] = explode('> ', $entry); + $this->mailMap[$actual] = $use . '>'; + } + } + } + + if (isset($this->mailMap[$author])) { + return $this->mailMap[$author]; + } + return $author; + } + + private function fixInvalidEmail($author) { + preg_match('/<(.*)>/', $author, $mailMatch); + if (count($mailMatch) === 2 && !filter_var($mailMatch[1], FILTER_VALIDATE_EMAIL)) { + $author = str_replace('<' . $mailMatch[1] . '>', '"' . $mailMatch[1] . '"', $author); + } + return $author; + } +} + +$licenses = new Licenses; +if (isset($argv[1])) { + $licenses->exec($argv[1], isset($argv[2]) ? $argv[1] : false); +} else { + $licenses->exec([ + // '../.github', // Not possible because of workflow restrictions + '../app', + '../community-containers', + '../Containers', + '../manual-install', + '../nextcloud-aio-helm-chart', + '../php', + '../tests', + '../compose.yaml', + '../develop.md', + '../docker-ipv6-support.md', + '../docker-rootless.md', + '../local-instance.md', + '../manual-upgrade.md', + '../migration.md', + '../multiple-instances.md', + '../readme.md', + '../reverse-proxy.md', + ]); + $licenses->writeAuthorsFile(); +} diff --git a/.github/workflows/update-copyright.yml b/.github/workflows/update-copyright.yml index 42502027..574eb9fe 100644 --- a/.github/workflows/update-copyright.yml +++ b/.github/workflows/update-copyright.yml @@ -11,3 +11,43 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - name: Fetch history + run: git fetch --prune --unshallow + - name: Set up php + uses: shivammathur/setup-php@v2 + with: + php-version: 8.1 + coverage: none + + - name: Run script + run: | + set -x + cd .build + php license.php + cd ../ + + - name: Run script 2 + run: | + set -x + cd ../ + # todo: remove the single branch below and clone master + git clone https://github.com/nextcloud/github_helper.git --depth 1 --single-branch --branch enh/noid/fix-exit + ls -la + cd github_helper/spdx-convertor + composer install + cd ../ + cd ../ + ls -la + php github_helper/spdx-convertor/convert.php ./all-in-one + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v7 + with: + commit-message: Update copyright + signoff: true + title: Update copyright + body: Automated updating copyright headers + labels: dependencies, 3. to review + milestone: next + branch: aio-copyright-update + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 00000000..3efaf6ad --- /dev/null +++ b/AUTHORS @@ -0,0 +1,5 @@ + +# Authors