com.liferay.jenkins.results.parser.RootCauseAnalysisToolTopLevelBuildRunner Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of com.liferay.jenkins.results.parser
Show all versions of com.liferay.jenkins.results.parser
Liferay Jenkins Results Parser
The newest version!
/**
* SPDX-FileCopyrightText: (c) 2000 Liferay, Inc. https://liferay.com
* SPDX-License-Identifier: LGPL-2.1-or-later OR LicenseRef-Liferay-DXP-EULA-2.0.0-2023-06
*/
package com.liferay.jenkins.results.parser;
import com.liferay.jenkins.results.parser.test.clazz.FunctionalTestClass;
import com.liferay.jenkins.results.parser.test.clazz.JUnitTestClass;
import com.liferay.jenkins.results.parser.test.clazz.ModulesTestClass;
import com.liferay.jenkins.results.parser.test.clazz.PlaywrightJUnitTestClass;
import com.liferay.jenkins.results.parser.test.clazz.TestClass;
import com.liferay.jenkins.results.parser.test.clazz.TestClassMethod;
import com.liferay.jenkins.results.parser.test.clazz.group.BatchTestClassGroup;
import com.liferay.jenkins.results.parser.test.clazz.group.TestClassGroupFactory;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang.StringUtils;
import org.dom4j.Element;
/**
* @author Michael Hashimoto
*/
public class RootCauseAnalysisToolTopLevelBuildRunner
extends PortalTopLevelBuildRunner {
@Override
public void tearDown() {
cleanUpHostServices();
tearDownWorkspace();
}
protected RootCauseAnalysisToolTopLevelBuildRunner(
PortalTopLevelBuildData portalTopLevelBuildData) {
super(portalTopLevelBuildData);
}
@Override
protected Element getJenkinsReportElement() {
PortalTopLevelBuildData portalTopLevelBuildData = getBuildData();
Workspace workspace = getWorkspace();
if (workspace == null) {
return Dom4JUtil.getNewElement(
"html", null,
Dom4JUtil.getNewElement(
"h1", null, "Report building in progress for ",
Dom4JUtil.getNewAnchorElement(
portalTopLevelBuildData.getBuildURL(),
portalTopLevelBuildData.getBuildURL())));
}
RootCauseAnalysisToolBuild rootCauseAnalysisToolBuild =
(RootCauseAnalysisToolBuild)getTopLevelBuild();
List downstreamPortalBuildDataList = new ArrayList<>();
for (BuildData downstreamBuildData :
portalTopLevelBuildData.getDownstreamBuildDataList()) {
if (downstreamBuildData instanceof PortalBuildData) {
downstreamPortalBuildDataList.add(
(PortalBuildData)downstreamBuildData);
}
}
rootCauseAnalysisToolBuild.setDownstreamPortalBuildDataList(
downstreamPortalBuildDataList);
rootCauseAnalysisToolBuild.setWorkspaceGitRepository(
workspace.getPrimaryWorkspaceGitRepository());
return super.getJenkinsReportElement();
}
@Override
protected void prepareInvocationBuildDataList() {
PortalTopLevelBuildData portalTopLevelBuildData = getBuildData();
String downstreamJobName =
portalTopLevelBuildData.getJobName() + "-batch";
for (String portalBranchSHA : _getPortalBranchSHAs()) {
int retestCount = _getRetestCount();
for (int i = 0; i < retestCount; i++) {
BatchBuildData batchBuildData =
BuildDataFactory.newBatchBuildData(
null, downstreamJobName, null);
if (!(batchBuildData instanceof PortalBatchBuildData)) {
throw new RuntimeException("Invalid build data");
}
PortalBatchBuildData portalBatchBuildData =
(PortalBatchBuildData)batchBuildData;
portalBatchBuildData.setBuildDescription(
_getDownstreamBuildDescription(portalBranchSHA));
portalBatchBuildData.setBatchName(_getBatchName());
portalBatchBuildData.setPortalBranchSHA(portalBranchSHA);
portalBatchBuildData.setTestList(_getTestList());
addInvocationBuildData(portalBatchBuildData);
}
}
}
@Override
protected void setUpWorkspace() {
super.setUpWorkspace();
Workspace workspace = getWorkspace();
WorkspaceGitRepository workspaceGitRepository =
workspace.getPrimaryWorkspaceGitRepository();
GitWorkingDirectory gitWorkingDirectory =
workspaceGitRepository.getGitWorkingDirectory();
List portalBranchSHAs = _getPortalBranchSHAs();
for (String portalBranchSHA : portalBranchSHAs) {
if (gitWorkingDirectory.localSHAExists(portalBranchSHA)) {
continue;
}
String portalGitHubURL = _getPortalGitHubURL();
failBuildRunner(
JenkinsResultsParserUtil.combine(
_NAME_BUILD_PARAMETER_PORTAL_BRANCH_SHAS,
" has SHAs that are not be found within the latest ",
String.valueOf(
WorkspaceGitRepository.COMMITS_HISTORY_SIZE_MAX),
" commits of ",
portalGitHubURL, ""));
return;
}
List portalCherryPickSHAs = _getPortalCherryPickSHAs();
for (String portalCherryPickSHA : portalCherryPickSHAs) {
if (gitWorkingDirectory.localSHAExists(portalCherryPickSHA)) {
continue;
}
String portalGitHubURL = _getPortalGitHubURL();
failBuildRunner(
JenkinsResultsParserUtil.combine(
_NAME_BUILD_PARAMETER_PORTAL_CHERRY_PICK_SHAS,
" has SHAs that are not be found within the latest ",
String.valueOf(
WorkspaceGitRepository.COMMITS_HISTORY_SIZE_MAX),
" commits of ",
portalGitHubURL, ""));
return;
}
List commitSHAs = new ArrayList<>();
commitSHAs.addAll(portalBranchSHAs);
commitSHAs.addAll(portalCherryPickSHAs);
try {
workspaceGitRepository.storeCommitHistory(commitSHAs);
}
catch (Exception exception) {
failBuildRunner("Unable to store the commit history", exception);
}
}
@Override
protected void validateBuildParameters() {
_validateBuildParameterJenkinsGitHubURL();
_validateBuildParameterPortalBatchName();
_validateBuildParameterPortalBatchTestSelector();
_validateBuildParameterPortalBranchSHAs();
_validateBuildParameterPortalGitHubURL();
_validateBuildParameterPortalUpstreamBranchName();
_validateBuildParameterRetestCherryPickSHA();
_validateBuildParameterRetestCount();
}
private void _failInvalidPortalRepositoryName(
String buildParameter, String portalUpstreamBranchName) {
String portalRepositoryName = "liferay-portal";
if (!portalUpstreamBranchName.equals("master")) {
portalRepositoryName += "-ee";
}
failBuildRunner(
JenkinsResultsParserUtil.combine(
buildParameter, " should point to a ", portalRepositoryName,
" GitHub URL"));
}
private int _getAllowedPortalBranchSHACount() {
String allowedPortalBranchSHACount = getJobPropertyValue(
"allowed.portal.branch.shas");
if ((allowedPortalBranchSHACount == null) ||
allowedPortalBranchSHACount.isEmpty()) {
return -1;
}
return Integer.valueOf(allowedPortalBranchSHACount);
}
private String _getBatchName() {
BuildData buildData = getBuildData();
return JenkinsResultsParserUtil.getBuildParameter(
buildData.getBuildURL(), "PORTAL_BATCH_NAME");
}
private String _getDownstreamBuildDescription(String portalBranchSHA) {
PortalTopLevelBuildData portalTopLevelBuildData = getBuildData();
StringBuilder sb = new StringBuilder();
sb.append(portalBranchSHA);
sb.append(" - ");
sb.append(_getBatchName());
sb.append(" - ");
sb.append("Jenkins Report");
sb.append("");
for (String test : _getTestList()) {
sb.append("- ");
sb.append(test);
sb.append("
");
}
sb.append("
");
return sb.toString();
}
private int _getMaxCommitGroupCount() {
int maxCommitGroupCount = _getAllowedPortalBranchSHACount();
if (maxCommitGroupCount != -1) {
return maxCommitGroupCount;
}
return _COMMITS_GROUP_SIZE_MAX_DEFAULT;
}
private int _getMaxRetestCount() {
String maxRetestCount = getJobPropertyValue("maximum.retest.count");
if ((maxRetestCount == null) || maxRetestCount.isEmpty()) {
return -1;
}
try {
return Integer.valueOf(maxRetestCount);
}
catch (NumberFormatException numberFormatException) {
numberFormatException.printStackTrace();
return -1;
}
}
private List _getPortalBranchSHAs() {
String portalBranchSHAsString = getBuildParameter(
_NAME_BUILD_PARAMETER_PORTAL_BRANCH_SHAS);
if ((portalBranchSHAsString == null) ||
portalBranchSHAsString.isEmpty()) {
Workspace workspace = getWorkspace();
WorkspaceGitRepository workspaceGitRepository =
workspace.getPrimaryWorkspaceGitRepository();
GitWorkingDirectory gitWorkingDirectory =
workspaceGitRepository.getGitWorkingDirectory();
List localGitCommits = gitWorkingDirectory.log(1);
LocalGitCommit localGitCommit = localGitCommits.get(0);
return Collections.singletonList(localGitCommit.getSHA());
}
Matcher matcher = _compareURLPattern.matcher(portalBranchSHAsString);
if (matcher.find()) {
Workspace workspace = getWorkspace();
WorkspaceGitRepository workspaceGitRepository =
workspace.getPrimaryWorkspaceGitRepository();
List rangeLocalGitCommits = new ArrayList<>();
try {
rangeLocalGitCommits =
workspaceGitRepository.getRangeLocalGitCommits(
matcher.group("earliestSHA"),
matcher.group("latestSHA"));
}
catch (Exception exception) {
failBuildRunner(
"Unable to store the commit history", exception);
}
List> localGitCommitsLists =
workspaceGitRepository.partitionLocalGitCommits(
rangeLocalGitCommits, _getMaxCommitGroupCount());
List portalBranchSHAs = new ArrayList<>();
for (List localGitCommits : localGitCommitsLists) {
LocalGitCommit localGitCommit = localGitCommits.get(0);
portalBranchSHAs.add(localGitCommit.getSHA());
}
return portalBranchSHAs;
}
List portalBranchSHAs = new ArrayList<>();
for (String portalBranchSHA : portalBranchSHAsString.split(",")) {
portalBranchSHAs.add(portalBranchSHA.trim());
}
return portalBranchSHAs;
}
private List _getPortalCherryPickSHAs() {
List portalCherryPickSHAList = new ArrayList<>();
String portalCherryPickSHAsString = getBuildParameter(
_NAME_BUILD_PARAMETER_PORTAL_CHERRY_PICK_SHAS);
if (JenkinsResultsParserUtil.isNullOrEmpty(
portalCherryPickSHAsString)) {
return portalCherryPickSHAList;
}
for (String portalCherryPickSHA :
portalCherryPickSHAsString.split(",")) {
portalCherryPickSHAList.add(portalCherryPickSHA.trim());
}
return portalCherryPickSHAList;
}
private String _getPortalGitHubURL() {
return getBuildParameter(_NAME_BUILD_PARAMETER_PORTAL_GITHUB_URL);
}
private int _getRetestCount() {
String retestCount = getBuildParameter(
_NAME_BUILD_PARAMETER_RETEST_COUNT);
if ((retestCount == null) || retestCount.isEmpty()) {
return 1;
}
try {
return Integer.parseInt(retestCount);
}
catch (NumberFormatException numberFormatException) {
numberFormatException.printStackTrace();
return 1;
}
}
private List _getTestList() {
String portalBatchTestSelector = getBuildParameter(
_NAME_BUILD_PARAMETER_PORTAL_BATCH_TEST_SELECTOR);
List list = new ArrayList<>();
if (JenkinsResultsParserUtil.isNullOrEmpty(portalBatchTestSelector)) {
if (!_isModulesBatch()) {
return list;
}
BuildDatabase buildDatabase = BuildDatabaseUtil.getBuildDatabase();
buildDatabase.putProperty(
"start.properties", "PORTAL_BATCH_TEST_SELECTOR", "**/*");
}
BatchTestClassGroup batchTestClassGroup =
TestClassGroupFactory.newBatchTestClassGroup(
_getBatchName(), getJob());
for (TestClass testClass : batchTestClassGroup.getTestClasses()) {
if (testClass instanceof FunctionalTestClass) {
FunctionalTestClass functionalTestClass =
(FunctionalTestClass)testClass;
list.add(functionalTestClass.getTestClassMethodName());
}
else if ((testClass instanceof JUnitTestClass) &&
!(testClass instanceof PlaywrightJUnitTestClass)) {
String testClassFilePath =
JenkinsResultsParserUtil.getCanonicalPath(
testClass.getTestClassFile());
list.add(
testClassFilePath.replaceAll(
".*/(com/.*)\\.java", "$1.class"));
}
else if (testClass instanceof ModulesTestClass) {
for (TestClassMethod testClassMethod :
testClass.getTestClassMethods()) {
list.add(testClassMethod.getName());
}
}
else if (testClass instanceof PlaywrightJUnitTestClass) {
list.add(testClass.getName());
}
}
return list;
}
private boolean _isFunctionalBatch() {
String portalBatchName = getBuildParameter(
_NAME_BUILD_PARAMETER_PORTAL_BATCH);
if (portalBatchName.startsWith("functional")) {
return true;
}
return false;
}
private boolean _isJUnitBatch() {
String portalBatchName = getBuildParameter(
_NAME_BUILD_PARAMETER_PORTAL_BATCH);
if (portalBatchName.startsWith("integration") ||
portalBatchName.startsWith("modules-integration") ||
portalBatchName.startsWith("modules-unit") ||
portalBatchName.startsWith("unit")) {
return true;
}
return false;
}
private boolean _isModulesBatch() {
String portalBatchName = getBuildParameter(
_NAME_BUILD_PARAMETER_PORTAL_BATCH);
if (portalBatchName.startsWith("js-unit") ||
portalBatchName.startsWith("modules-compile") ||
portalBatchName.startsWith("modules-semantic-versioning") ||
portalBatchName.startsWith("rest-builder") ||
portalBatchName.startsWith("service-builder")) {
return true;
}
return false;
}
private void _validateBuildParameterJenkinsGitHubURL() {
String jenkinsGitHubURL = getBuildParameter(
_NAME_BUILD_PARAMETER_JENKINS_GITHUB_URL);
if ((jenkinsGitHubURL == null) || jenkinsGitHubURL.isEmpty()) {
return;
}
String failureMessage = JenkinsResultsParserUtil.combine(
_NAME_BUILD_PARAMETER_JENKINS_GITHUB_URL,
" has an invalid Jenkins GitHub URL ", jenkinsGitHubURL, "");
Matcher matcher = _portalURLPattern.matcher(jenkinsGitHubURL);
if (!matcher.find()) {
failBuildRunner(failureMessage);
}
String repositoryName = matcher.group("repositoryName");
if (!repositoryName.equals("liferay-jenkins-ee")) {
failBuildRunner(failureMessage);
}
}
private void _validateBuildParameterPortalBatchName() {
String portalBatchName = getBuildParameter(
_NAME_BUILD_PARAMETER_PORTAL_BATCH);
if ((portalBatchName == null) || portalBatchName.isEmpty()) {
failBuildRunner(_NAME_BUILD_PARAMETER_PORTAL_BATCH + " is null");
}
String allowedPortalBatchNames = getJobPropertyValue(
JenkinsResultsParserUtil.combine(
"allowed.portal.batch.names[",
getBuildParameter(
_NAME_BUILD_PARAMETER_PORTAL_UPSTREAM_BRANCH_NAME),
"]"));
if ((allowedPortalBatchNames == null) ||
allowedPortalBatchNames.isEmpty()) {
return;
}
List allowedPortalBatchNamesList = Arrays.asList(
allowedPortalBatchNames.split(","));
if (!allowedPortalBatchNamesList.contains(portalBatchName)) {
StringBuilder sb = new StringBuilder();
sb.append(_NAME_BUILD_PARAMETER_PORTAL_BATCH);
sb.append(" must match one of the following: ");
sb.append("");
for (String allowedPortalBatchName : allowedPortalBatchNamesList) {
sb.append("- ");
sb.append(allowedPortalBatchName);
sb.append("
");
}
sb.append("
");
failBuildRunner(sb.toString());
}
}
private void _validateBuildParameterPortalBatchTestSelector() {
if (!_isFunctionalBatch() && !_isJUnitBatch()) {
return;
}
String portalBatchTestSelector = getBuildParameter(
_NAME_BUILD_PARAMETER_PORTAL_BATCH_TEST_SELECTOR);
if (!JenkinsResultsParserUtil.isNullOrEmpty(portalBatchTestSelector)) {
return;
}
String portalBatchName = getBuildParameter(
_NAME_BUILD_PARAMETER_PORTAL_BATCH);
failBuildRunner(
JenkinsResultsParserUtil.combine(
_NAME_BUILD_PARAMETER_PORTAL_BATCH_TEST_SELECTOR,
" is required for ", portalBatchName));
}
private void _validateBuildParameterPortalBranchSHAs() {
String portalBranchSHAs = getBuildParameter(
_NAME_BUILD_PARAMETER_PORTAL_BRANCH_SHAS);
if ((portalBranchSHAs == null) || portalBranchSHAs.isEmpty()) {
return;
}
int allowedPortalBranchSHACount = _getAllowedPortalBranchSHACount();
if (allowedPortalBranchSHACount == -1) {
return;
}
int portalBranchSHACount =
StringUtils.countMatches(portalBranchSHAs, ",") + 1;
int retestCount = _getRetestCount();
if (retestCount != 1) {
allowedPortalBranchSHACount = 1;
}
if (portalBranchSHACount > allowedPortalBranchSHACount) {
failBuildRunner(
JenkinsResultsParserUtil.combine(
_NAME_BUILD_PARAMETER_PORTAL_BRANCH_SHAS,
" may only reference ",
String.valueOf(allowedPortalBranchSHACount),
" portal branch SHAs"));
}
Matcher matcher = _compareURLPattern.matcher(portalBranchSHAs);
if (matcher.find()) {
String portalUpstreamBranchName = getBuildParameter(
_NAME_BUILD_PARAMETER_PORTAL_UPSTREAM_BRANCH_NAME);
String repositoryName = matcher.group("repositoryName");
if ((repositoryName.equals("liferay-portal") &&
!portalUpstreamBranchName.equals("master")) ||
(repositoryName.equals("liferay-portal-ee") &&
portalUpstreamBranchName.equals("master"))) {
_failInvalidPortalRepositoryName(
_NAME_BUILD_PARAMETER_PORTAL_BRANCH_SHAS,
portalUpstreamBranchName);
}
}
}
private void _validateBuildParameterPortalGitHubURL() {
String portalGitHubURL = _getPortalGitHubURL();
if ((portalGitHubURL == null) || portalGitHubURL.isEmpty()) {
failBuildRunner(
_NAME_BUILD_PARAMETER_PORTAL_GITHUB_URL + " is null");
}
String failureMessage = JenkinsResultsParserUtil.combine(
_NAME_BUILD_PARAMETER_PORTAL_GITHUB_URL,
" has an invalid Portal GitHub URL ", portalGitHubURL, "");
Matcher matcher = _portalURLPattern.matcher(portalGitHubURL);
if (!matcher.find()) {
failBuildRunner(failureMessage);
}
String repositoryName = matcher.group("repositoryName");
if (!repositoryName.equals("liferay-portal") &&
!repositoryName.equals("liferay-portal-ee")) {
failBuildRunner(failureMessage);
}
String portalUpstreamBranchName = getBuildParameter(
_NAME_BUILD_PARAMETER_PORTAL_UPSTREAM_BRANCH_NAME);
if ((repositoryName.equals("liferay-portal") &&
!portalUpstreamBranchName.equals("master")) ||
(repositoryName.equals("liferay-portal-ee") &&
portalUpstreamBranchName.equals("master"))) {
_failInvalidPortalRepositoryName(
_NAME_BUILD_PARAMETER_PORTAL_GITHUB_URL,
portalUpstreamBranchName);
}
}
private void _validateBuildParameterPortalUpstreamBranchName() {
String portalUpstreamBranchName = getBuildParameter(
_NAME_BUILD_PARAMETER_PORTAL_UPSTREAM_BRANCH_NAME);
if ((portalUpstreamBranchName == null) ||
portalUpstreamBranchName.isEmpty()) {
failBuildRunner(
_NAME_BUILD_PARAMETER_PORTAL_UPSTREAM_BRANCH_NAME + " is null");
}
if (portalUpstreamBranchName.matches("release-\\d{4}.q\\d+")) {
return;
}
String allowedPortalUpstreamBranchNames = getJobPropertyValue(
"allowed.portal.upstream.branch.names");
if ((allowedPortalUpstreamBranchNames == null) ||
allowedPortalUpstreamBranchNames.isEmpty()) {
return;
}
List allowedPortalUpstreamBranchNamesList = Arrays.asList(
allowedPortalUpstreamBranchNames.split(","));
if (!allowedPortalUpstreamBranchNamesList.contains(
portalUpstreamBranchName)) {
StringBuilder sb = new StringBuilder();
sb.append(_NAME_BUILD_PARAMETER_PORTAL_UPSTREAM_BRANCH_NAME);
sb.append(" must match one of the following: ");
sb.append("");
for (String allowedPortalUpstreamBranchName :
allowedPortalUpstreamBranchNamesList) {
sb.append("- ");
sb.append(allowedPortalUpstreamBranchName);
sb.append("
");
}
sb.append("
");
sb.append("or is not a valid release branch.");
failBuildRunner(sb.toString());
}
}
private void _validateBuildParameterRetestCherryPickSHA() {
String cherryPickSHAs = getBuildParameter(
_NAME_BUILD_PARAMETER_PORTAL_CHERRY_PICK_SHAS);
if ((cherryPickSHAs == null) || cherryPickSHAs.isEmpty()) {
return;
}
int retestCount = _getRetestCount();
if (retestCount != 1) {
failBuildRunner(
JenkinsResultsParserUtil.combine(
"Cherry-picked SHAs may not be used when retesting."));
}
}
private void _validateBuildParameterRetestCount() {
String retestCount = getBuildParameter(
_NAME_BUILD_PARAMETER_RETEST_COUNT);
if ((retestCount == null) || retestCount.isEmpty()) {
return;
}
int retestCountInt = 0;
try {
retestCountInt = Integer.parseInt(retestCount);
}
catch (NumberFormatException numberFormatException) {
failBuildRunner(
JenkinsResultsParserUtil.combine(
_NAME_BUILD_PARAMETER_RETEST_COUNT, " parameter value: \"",
retestCount, "\" is not a number."));
}
int maxRetestCount = _getMaxRetestCount();
if ((retestCountInt < 0) || (retestCountInt > maxRetestCount)) {
failBuildRunner(
JenkinsResultsParserUtil.combine(
_NAME_BUILD_PARAMETER_RETEST_COUNT,
" must be between 0 and ", String.valueOf(maxRetestCount),
"."));
}
}
private static final int _COMMITS_GROUP_SIZE_MAX_DEFAULT = 5;
private static final String _NAME_BUILD_PARAMETER_JENKINS_GITHUB_URL =
"JENKINS_GITHUB_URL";
private static final String _NAME_BUILD_PARAMETER_PORTAL_BATCH =
"PORTAL_BATCH_NAME";
private static final String
_NAME_BUILD_PARAMETER_PORTAL_BATCH_TEST_SELECTOR =
"PORTAL_BATCH_TEST_SELECTOR";
private static final String _NAME_BUILD_PARAMETER_PORTAL_BRANCH_SHAS =
"PORTAL_BRANCH_SHAS";
private static final String _NAME_BUILD_PARAMETER_PORTAL_CHERRY_PICK_SHAS =
"PORTAL_CHERRY_PICK_SHAS";
private static final String _NAME_BUILD_PARAMETER_PORTAL_GITHUB_URL =
"PORTAL_GITHUB_URL";
private static final String
_NAME_BUILD_PARAMETER_PORTAL_UPSTREAM_BRANCH_NAME =
"PORTAL_UPSTREAM_BRANCH_NAME";
private static final String _NAME_BUILD_PARAMETER_RETEST_COUNT =
"RETEST_COUNT";
private static final Pattern _compareURLPattern = Pattern.compile(
JenkinsResultsParserUtil.combine(
"https://github.com/(?[^/]+)/(?[^/]+)",
"/compare/(?[0-9a-f]{5,40})\\.{3}",
"(?[0-9a-f]{5,40})"));
private static final Pattern _portalURLPattern = Pattern.compile(
"https://github.com/[^/]+/(?[^/]+)/tree/.+");
}