Changeset View
Changeset View
Standalone View
Standalone View
src/land/engine/ArcanistMercurialLandEngine.php
<?php | <?php | ||||
final class ArcanistMercurialLandEngine | final class ArcanistMercurialLandEngine | ||||
extends ArcanistLandEngine { | extends ArcanistLandEngine { | ||||
private $ontoBranchMarker; | private $ontoBranchMarker; | ||||
private $ontoMarkers; | private $ontoMarkers; | ||||
private $rebasedActiveCommit; | |||||
protected function getDefaultSymbols() { | protected function getDefaultSymbols() { | ||||
$api = $this->getRepositoryAPI(); | $api = $this->getRepositoryAPI(); | ||||
$log = $this->getLogEngine(); | $log = $this->getLogEngine(); | ||||
// TODO: In Mercurial, you normally can not create a branch and a bookmark | // TODO: In Mercurial, you normally can not create a branch and a bookmark | ||||
// with the same name. However, you can fetch a branch or bookmark from | // with the same name. However, you can fetch a branch or bookmark from | ||||
// a remote that has the same name as a local branch or bookmark of the | // a remote that has the same name as a local branch or bookmark of the | ||||
// other type, and end up with a local branch and bookmark with the same | // other type, and end up with a local branch and bookmark with the same | ||||
▲ Show 20 Lines • Show All 676 Lines • ▼ Show 20 Lines | final class ArcanistMercurialLandEngine | ||||
protected function selectCommits($into_commit, array $symbols) { | protected function selectCommits($into_commit, array $symbols) { | ||||
assert_instances_of($symbols, 'ArcanistLandSymbol'); | assert_instances_of($symbols, 'ArcanistLandSymbol'); | ||||
$api = $this->getRepositoryAPI(); | $api = $this->getRepositoryAPI(); | ||||
$commit_map = array(); | $commit_map = array(); | ||||
foreach ($symbols as $symbol) { | foreach ($symbols as $symbol) { | ||||
$symbol_commit = $symbol->getCommit(); | $symbol_commit = $symbol->getCommit(); | ||||
$template = '{node}-{parents}-'; | $template = '{node}-{parents % \'{node} \'}-{desc|firstline}\\n'; | ||||
// The returned array of commits is expected to be ordered by max to min | |||||
// where the max commit has no descendants in the range and the min | |||||
// commit has no ancestors in the range. Use 'reverse()' in the template | |||||
// so the output is ordered with the max commit as the first line. The | |||||
// max/min terms are used in a topological sense as chronological terms | |||||
// for commits may be misleading or incorrect in some situations. | |||||
if ($into_commit === null) { | if ($into_commit === null) { | ||||
list($commits) = $api->execxLocal( | list($commits) = $api->execxLocal( | ||||
'log --rev %s --template %s --', | 'log --rev %s --template %s --', | ||||
hgsprintf('reverse(ancestors(%s))', $into_commit), | hgsprintf('reverse(ancestors(%s))', $into_commit), | ||||
$template); | $template); | ||||
} else { | } else { | ||||
list($commits) = $api->execxLocal( | list($commits) = $api->execxLocal( | ||||
'log --rev %s --template %s --', | 'log --rev %s --template %s --', | ||||
▲ Show 20 Lines • Show All 73 Lines • ▼ Show 20 Lines | protected function executeMerge(ArcanistLandCommitSet $set, $into_commit) { | ||||
// TODO: Add a Mercurial version check requiring 2.1.1 or newer. | // TODO: Add a Mercurial version check requiring 2.1.1 or newer. | ||||
$api->execxLocal( | $api->execxLocal( | ||||
'update --rev %s', | 'update --rev %s', | ||||
hgsprintf('%s', $into_commit)); | hgsprintf('%s', $into_commit)); | ||||
$commits = $set->getCommits(); | $commits = $set->getCommits(); | ||||
$min_commit = last($commits)->getHash(); | // confirmCommits() reverses the order of the commits as they're ordered | ||||
$max_commit = head($commits)->getHash(); | // above in selectCommits(). Now the head of the list is the min commit and | ||||
// the last is the max commit, where within the range the max commit has no | |||||
// descendants and the min commit has no ancestors. The min/max terms are | |||||
// used in a topological sense as chronological terms for commits can be | |||||
// misleading or incorrect in certain situations. | |||||
$min_commit = head($commits)->getHash(); | |||||
$max_commit = last($commits)->getHash(); | |||||
$revision_ref = $set->getRevisionRef(); | $revision_ref = $set->getRevisionRef(); | ||||
$commit_message = $revision_ref->getCommitMessage(); | $commit_message = $revision_ref->getCommitMessage(); | ||||
// If we're landing "--onto" a branch, set that as the branch marker | // If we're landing "--onto" a branch, set that as the branch marker | ||||
// before creating the new commit. | // before creating the new commit. | ||||
// TODO: We could skip this if we know that the "$into_commit" already | // TODO: We could skip this if we know that the "$into_commit" already | ||||
Show All 13 Lines | try { | ||||
$argv[] = hgsprintf('%s..%s', $min_commit, $max_commit); | $argv[] = hgsprintf('%s..%s', $min_commit, $max_commit); | ||||
$argv[] = '--logfile'; | $argv[] = '--logfile'; | ||||
$argv[] = '-'; | $argv[] = '-'; | ||||
$argv[] = '--keep'; | $argv[] = '--keep'; | ||||
$argv[] = '--collapse'; | $argv[] = '--collapse'; | ||||
$future = $api->execFutureLocal('rebase %Ls', $argv); | $future = $api->execFutureLocalWithExtension( | ||||
'rebase', | |||||
'rebase %Ls', | |||||
$argv); | |||||
$future->write($commit_message); | $future->write($commit_message); | ||||
$future->resolvex(); | $future->resolvex(); | ||||
} catch (CommandException $ex) { | } catch (CommandException $ex) { | ||||
// TODO | // Aborting the rebase should restore the same state prior to running the | ||||
// $api->execManualLocal('rebase --abort'); | // rebase command. | ||||
$api->execManualLocalWithExtension( | |||||
'rebase', | |||||
'rebase --abort'); | |||||
throw $ex; | throw $ex; | ||||
} | } | ||||
// Find all the bookmarks which pointed at commits we just rebased, and | // Find all the bookmarks which pointed at commits we just rebased, and | ||||
// put them back the way they were before rebasing moved them. We aren't | // put them back the way they were before rebasing moved them. We aren't | ||||
// deleting the old commits yet and don't want to move the bookmarks. | // deleting the old commits yet and don't want to move the bookmarks. | ||||
$obsolete_map = array(); | $obsolete_map = array(); | ||||
Show All 12 Lines | foreach ($bookmark_refs as $bookmark_ref) { | ||||
'bookmark --force --rev %s -- %s', | 'bookmark --force --rev %s -- %s', | ||||
$bookmark_hash, | $bookmark_hash, | ||||
$bookmark_ref->getName()); | $bookmark_ref->getName()); | ||||
} | } | ||||
list($stdout) = $api->execxLocal('log --rev tip --template %s', '{node}'); | list($stdout) = $api->execxLocal('log --rev tip --template %s', '{node}'); | ||||
$new_cursor = trim($stdout); | $new_cursor = trim($stdout); | ||||
// If any of the commits that were rebased was the active commit before the | |||||
// workflow started, track the new commit so it can be used as the working | |||||
// directory after the land has succeeded. | |||||
if (isset($obsolete_map[$this->getLocalState()->getLocalCommit()])) { | |||||
$this->rebasedActiveCommit = $new_cursor; | |||||
} | |||||
return $new_cursor; | return $new_cursor; | ||||
} | } | ||||
protected function pushChange($into_commit) { | protected function pushChange($into_commit) { | ||||
$api = $this->getRepositoryAPI(); | $api = $this->getRepositoryAPI(); | ||||
list($head, $body, $tail) = $this->newPushCommands($into_commit); | list($head, $body, $tail_pass, $tail_fail) = $this->newPushCommands( | ||||
$into_commit); | |||||
foreach ($head as $command) { | foreach ($head as $command) { | ||||
$api->execxLocal('%Ls', $command); | $api->execxLocal('%Ls', $command); | ||||
} | } | ||||
try { | try { | ||||
foreach ($body as $command) { | foreach ($body as $command) { | ||||
$err = $this->newPassthru('%Ls', $command); | $err = $this->newPassthru('%Ls', $command); | ||||
if ($err) { | if ($err) { | ||||
throw new ArcanistLandPushFailureException( | throw new ArcanistLandPushFailureException( | ||||
pht( | pht( | ||||
'Push failed! Fix the error and run "arc land" again.')); | 'Push failed! Fix the error and run "arc land" again.')); | ||||
} | } | ||||
} | } | ||||
} finally { | |||||
foreach ($tail as $command) { | foreach ($tail_pass as $command) { | ||||
$api->execxLocal('%Ls', $command); | $api->execxLocal('%Ls', $command); | ||||
} | } | ||||
} catch (Exception $ex) { | |||||
foreach ($tail_fail as $command) { | |||||
$api->execxLocal('%Ls', $command); | |||||
} | |||||
throw $ex; | |||||
} catch (Throwable $ex) { | |||||
foreach ($tail_fail as $command) { | |||||
$api->execxLocal('%Ls', $command); | |||||
} | |||||
throw $ex; | |||||
} | } | ||||
} | } | ||||
private function newPushCommands($into_commit) { | private function newPushCommands($into_commit) { | ||||
$api = $this->getRepositoryAPI(); | $api = $this->getRepositoryAPI(); | ||||
$head_commands = array(); | $head_commands = array(); | ||||
$body_commands = array(); | $body_commands = array(); | ||||
$tail_commands = array(); | $tail_pass_commands = array(); | ||||
$tail_fail_commands = array(); | |||||
$bookmarks = array(); | $bookmarks = array(); | ||||
foreach ($this->ontoMarkers as $onto_marker) { | foreach ($this->ontoMarkers as $onto_marker) { | ||||
if (!$onto_marker->isBookmark()) { | if (!$onto_marker->isBookmark()) { | ||||
continue; | continue; | ||||
} | } | ||||
$bookmarks[] = $onto_marker; | $bookmarks[] = $onto_marker; | ||||
} | } | ||||
// If we're pushing to bookmarks, move all the bookmarks we want to push | // If we're pushing to bookmarks, move all the bookmarks we want to push | ||||
// to the merge commit. (There doesn't seem to be any way to specify | // to the merge commit. (There doesn't seem to be any way to specify | ||||
// "push commit X as bookmark Y" in Mercurial.) | // "push commit X as bookmark Y" in Mercurial.) | ||||
$restore = array(); | $restore_bookmarks = array(); | ||||
if ($bookmarks) { | if ($bookmarks) { | ||||
$markers = $api->newMarkerRefQuery() | $markers = $api->newMarkerRefQuery() | ||||
->withNames(mpull($bookmarks, 'getName')) | ->withNames(mpull($bookmarks, 'getName')) | ||||
->withMarkerTypes(array(ArcanistMarkerRef::TYPE_BOOKMARK)) | ->withMarkerTypes(array(ArcanistMarkerRef::TYPE_BOOKMARK)) | ||||
->execute(); | ->execute(); | ||||
$markers = mpull($markers, 'getCommitHash', 'getName'); | $markers = mpull($markers, 'getCommitHash', 'getName'); | ||||
foreach ($bookmarks as $bookmark) { | foreach ($bookmarks as $bookmark) { | ||||
Show All 15 Lines | if ($bookmarks) { | ||||
$bookmark_name, | $bookmark_name, | ||||
); | ); | ||||
$api->execxLocal( | $api->execxLocal( | ||||
'bookmark --force --rev %s -- %s', | 'bookmark --force --rev %s -- %s', | ||||
hgsprintf('%s', $new_position), | hgsprintf('%s', $new_position), | ||||
$bookmark_name); | $bookmark_name); | ||||
$restore[$bookmark_name] = $old_position; | if ($old_position !== null) { | ||||
$restore_bookmarks[$bookmark_name] = $old_position; | |||||
} | |||||
} | } | ||||
} | } | ||||
// Now, prepare the actual push. | // Now, prepare the actual push. | ||||
$argv = array(); | $argv = array(); | ||||
$argv[] = 'push'; | $argv[] = 'push'; | ||||
Show All 12 Lines | private function newPushCommands($into_commit) { | ||||
$argv[] = '--'; | $argv[] = '--'; | ||||
$argv[] = $this->getOntoRemote(); | $argv[] = $this->getOntoRemote(); | ||||
$body_commands[] = $argv; | $body_commands[] = $argv; | ||||
// Finally, restore the bookmarks. | // Finally, restore the bookmarks. | ||||
foreach ($restore as $bookmark_name => $old_position) { | if ($restore_bookmarks) { | ||||
$tail = array(); | // Instead of restoring the previous state, assume landing onto bookmarks | ||||
$tail[] = 'bookmark'; | // also updates those bookmarks in the remote. After pushing, pull the | ||||
// latest state of these bookmarks. Mercurial allows pulling multiple | |||||
if ($old_position === null) { | // bookmarks in a single pull command which will be faster than pulling | ||||
$tail[] = '--delete'; | // them from a remote individually. | ||||
} else { | $tail = array( | ||||
$tail[] = '--force'; | 'pull', | ||||
$tail[] = '--rev'; | ); | ||||
$tail[] = hgsprintf('%s', $api->getDisplayHash($old_position)); | |||||
} | |||||
$tail[] = '--'; | foreach ($restore_bookmarks as $bookmark_name => $old_position) { | ||||
$tail[] = '--bookmark'; | |||||
$tail[] = $bookmark_name; | $tail[] = $bookmark_name; | ||||
$tail_commands[] = $tail; | // In the failure case restore the state of the bookmark. Mercurial | ||||
// does not provide a way to move multiple bookmarks in a single | |||||
// command however these commands do not involve the remote. | |||||
$tail_fail_commands[] = array( | |||||
'bookmark', | |||||
'--force', | |||||
'--rev', | |||||
hgsprintf('%s', $api->getDisplayHash($old_position)), | |||||
); | |||||
} | |||||
if ($tail) { | |||||
$tail_pass_commands[] = $tail; | |||||
} | |||||
} | } | ||||
return array( | return array( | ||||
$head_commands, | $head_commands, | ||||
$body_commands, | $body_commands, | ||||
$tail_commands, | $tail_pass_commands, | ||||
$tail_fail_commands, | |||||
); | ); | ||||
} | } | ||||
protected function cascadeState(ArcanistLandCommitSet $set, $into_commit) { | protected function cascadeState(ArcanistLandCommitSet $set, $into_commit) { | ||||
$api = $this->getRepositoryAPI(); | $api = $this->getRepositoryAPI(); | ||||
$log = $this->getLogEngine(); | $log = $this->getLogEngine(); | ||||
// This has no effect when we're executing a merge strategy. | // This has no effect when we're executing a merge strategy. | ||||
Show All 14 Lines | foreach ($child_hashes as $child_hash) { | ||||
if (!strlen($child_hash)) { | if (!strlen($child_hash)) { | ||||
continue; | continue; | ||||
} | } | ||||
// TODO: If the only heads which are descendants of this child will | // TODO: If the only heads which are descendants of this child will | ||||
// be deleted, we can skip this rebase? | // be deleted, we can skip this rebase? | ||||
try { | try { | ||||
$api->execxLocal( | $api->execxLocalWithExtension( | ||||
'rebase', | |||||
'rebase --source %s --dest %s --keep --keepbranches', | 'rebase --source %s --dest %s --keep --keepbranches', | ||||
$child_hash, | $child_hash, | ||||
$new_commit); | $new_commit); | ||||
} catch (CommandException $ex) { | } catch (CommandException $ex) { | ||||
// TODO: Recover state. | // Aborting the rebase should restore the same state prior to running | ||||
// the rebase command. | |||||
$api->execManualLocalWithExtension( | |||||
'rebase', | |||||
'rebase --abort'); | |||||
throw $ex; | throw $ex; | ||||
} | } | ||||
} | } | ||||
} | } | ||||
protected function pruneBranches(array $sets) { | protected function pruneBranches(array $sets) { | ||||
assert_instances_of($sets, 'ArcanistLandCommitSet'); | assert_instances_of($sets, 'ArcanistLandCommitSet'); | ||||
$api = $this->getRepositoryAPI(); | $api = $this->getRepositoryAPI(); | ||||
$log = $this->getLogEngine(); | $log = $this->getLogEngine(); | ||||
// This has no effect when we're executing a merge strategy. | // This has no effect when we're executing a merge strategy. | ||||
if (!$this->isSquashStrategy()) { | if (!$this->isSquashStrategy()) { | ||||
return; | return; | ||||
} | } | ||||
$revs = array(); | $revs = array(); | ||||
$obsolete_map = array(); | $obsolete_map = array(); | ||||
$using_evolve = $api->getMercurialFeature('evolve'); | |||||
// We've rebased all descendants already, so we can safely delete all | // We've rebased all descendants already, so we can safely delete all | ||||
// of these commits. | // of these commits. | ||||
$sets = array_reverse($sets); | $sets = array_reverse($sets); | ||||
foreach ($sets as $set) { | foreach ($sets as $set) { | ||||
$commits = $set->getCommits(); | $commits = $set->getCommits(); | ||||
// In the commit set the min commit should be the commit with no | |||||
// ancestors and the max commit should be the commit with no descendants. | |||||
// The min/max terms are used in a toplogical sense as chronological | |||||
// terms for commits may be misleading or incorrect in some situations. | |||||
$min_commit = head($commits)->getHash(); | $min_commit = head($commits)->getHash(); | ||||
$max_commit = last($commits)->getHash(); | $max_commit = last($commits)->getHash(); | ||||
if ($using_evolve) { | |||||
// If non-head series of commits are rebased while the evolve extension | |||||
// is in use, the rebase leaves behind the entire series of descendants | |||||
// in which case the entire chain needs removed, not just a section. | |||||
// Otherwise this results in the prune leaving behind orphaned commits. | |||||
$revs[] = hgsprintf('%s::', $min_commit); | |||||
} else { | |||||
$revs[] = hgsprintf('%s::%s', $min_commit, $max_commit); | $revs[] = hgsprintf('%s::%s', $min_commit, $max_commit); | ||||
} | |||||
foreach ($commits as $commit) { | foreach ($commits as $commit) { | ||||
$obsolete_map[$commit->getHash()] = true; | $obsolete_map[$commit->getHash()] = true; | ||||
} | } | ||||
} | } | ||||
$rev_set = '('.implode(') or (', $revs).')'; | $rev_set = '('.implode(') or (', $revs).')'; | ||||
// See PHI45. If we have "hg evolve", get rid of old commits using | // See PHI45. If we have "hg evolve", get rid of old commits using | ||||
// "hg prune" instead of "hg strip". | // "hg prune" instead of "hg strip". | ||||
// If we "hg strip" a commit which has an obsolete predecessor, it | // If we "hg strip" a commit which has an obsolete predecessor, it | ||||
// removes the obsolescence marker and revives the predecessor. This is | // removes the obsolescence marker and revives the predecessor. This is | ||||
// not desirable: we want to destroy all predecessors of these commits. | // not desirable: we want to destroy all predecessors of these commits. | ||||
// See PHI1808. Both "hg strip" and "hg prune" move bookmarks backwards in | // See PHI1808. Both "hg strip" and "hg prune" move bookmarks backwards in | ||||
// history rather than destroying them. Instead, we want to destroy any | // history rather than destroying them. Instead, we want to destroy any | ||||
// bookmarks which point at these now-obsoleted commits. | // bookmarks which point at these now-obsoleted commits. | ||||
$bookmark_refs = $api->newMarkerRefQuery() | $bookmark_refs = $api->newMarkerRefQuery() | ||||
->withMarkerTypes( | ->withMarkerTypes(array(ArcanistMarkerRef::TYPE_BOOKMARK)) | ||||
array( | |||||
ArcanistMarkerRef::TYPE_BOOKMARK, | |||||
)) | |||||
->execute(); | ->execute(); | ||||
foreach ($bookmark_refs as $bookmark_ref) { | foreach ($bookmark_refs as $bookmark_ref) { | ||||
$bookmark_hash = $bookmark_ref->getCommitHash(); | $bookmark_hash = $bookmark_ref->getCommitHash(); | ||||
$bookmark_name = $bookmark_ref->getName(); | $bookmark_name = $bookmark_ref->getName(); | ||||
if (!isset($obsolete_map[$bookmark_hash])) { | if (!isset($obsolete_map[$bookmark_hash])) { | ||||
continue; | continue; | ||||
} | } | ||||
$log->writeStatus( | $log->writeStatus( | ||||
pht('CLEANUP'), | pht('CLEANUP'), | ||||
pht('Deleting bookmark "%s".', $bookmark_name)); | pht('Deleting bookmark "%s".', $bookmark_name)); | ||||
$api->execxLocal( | $api->execxLocal( | ||||
'bookmark --delete -- %s', | 'bookmark --delete -- %s', | ||||
$bookmark_name); | $bookmark_name); | ||||
} | } | ||||
if ($api->getMercurialFeature('evolve')) { | if ($using_evolve) { | ||||
$api->execxLocal( | $api->execxLocal( | ||||
'prune --rev %s', | 'prune --rev %s', | ||||
$rev_set); | $rev_set); | ||||
} else { | } else { | ||||
$api->execxLocal( | $api->execxLocalWithExtension( | ||||
'--config extensions.strip= strip --rev %s', | 'strip', | ||||
'strip --rev %s', | |||||
$rev_set); | $rev_set); | ||||
} | } | ||||
} | } | ||||
protected function reconcileLocalState( | protected function reconcileLocalState( | ||||
$into_commit, | $into_commit, | ||||
ArcanistRepositoryLocalState $state) { | ArcanistRepositoryLocalState $state) { | ||||
// TODO: For now, just leave users wherever they ended up. | $api = $this->getRepositoryAPI(); | ||||
// If the starting working state was not part of land process just update | |||||
// to that original working state. | |||||
if ($this->rebasedActiveCommit === null) { | |||||
$update_marker = $this->getLocalState()->getLocalCommit(); | |||||
if ($this->getLocalState()->getLocalBookmark() !== null) { | |||||
$update_marker = $this->getLocalState()->getLocalBookmark(); | |||||
} | |||||
$api->execxLocal( | |||||
'update -- %s', | |||||
$update_marker); | |||||
$state->discardLocalState(); | |||||
return; | |||||
} | |||||
// If the working state was landed into multiple destinations then the | |||||
// resulting working state is ambiguous. | |||||
if (count($this->ontoMarkers) != 1) { | |||||
$state->discardLocalState(); | |||||
return; | |||||
} | |||||
// Get the current state of bookmarks | |||||
$bookmark_refs = $api->newMarkerRefQuery() | |||||
->withMarkerTypes(array(ArcanistMarkerRef::TYPE_BOOKMARK)) | |||||
->execute(); | |||||
$update_marker = $this->rebasedActiveCommit; | |||||
// Find any bookmarks which exist on the commit which is the result of the | |||||
// starting working directory's rebase. If any of those bookmarks are also | |||||
// the destination marker then we use that bookmark as the update in order | |||||
// for it to become active. | |||||
$onto_marker = $this->ontoMarkers[0]->getName(); | |||||
foreach ($bookmark_refs as $bookmark_ref) { | |||||
if ($bookmark_ref->getCommitHash() == $this->rebasedActiveCommit && | |||||
$bookmark_ref->getName() == $onto_marker) { | |||||
$update_marker = $onto_marker; | |||||
break; | |||||
} | |||||
} | |||||
$api->execxLocal( | |||||
'update -- %s', | |||||
$update_marker); | |||||
$state->discardLocalState(); | $state->discardLocalState(); | ||||
} | } | ||||
protected function didHoldChanges($into_commit) { | protected function didHoldChanges($into_commit) { | ||||
$log = $this->getLogEngine(); | $log = $this->getLogEngine(); | ||||
$local_state = $this->getLocalState(); | $local_state = $this->getLocalState(); | ||||
$message = pht( | $message = pht( | ||||
'Holding changes locally, they have not been pushed.'); | 'Holding changes locally, they have not been pushed.'); | ||||
list($head, $body, $tail) = $this->newPushCommands($into_commit); | list($head, $body, $tail_pass, $tail_fail) = $this->newPushCommands( | ||||
$commands = array_merge($head, $body, $tail); | $into_commit); | ||||
$commands = array_merge($head, $body, $tail_pass); | |||||
echo tsprintf( | echo tsprintf( | ||||
"\n%!\n%s\n\n", | "\n%!\n%s\n\n", | ||||
pht('HOLD CHANGES'), | pht('HOLD CHANGES'), | ||||
$message); | $message); | ||||
echo tsprintf( | echo tsprintf( | ||||
"%s\n\n", | "%s\n\n", | ||||
Show All 33 Lines |
Content licensed under Creative Commons Attribution-ShareAlike 4.0 (CC-BY-SA) unless otherwise noted; code licensed under Apache 2.0 or other open source licenses. · CC BY-SA 4.0 · Apache 2.0