diff --git a/server-core/src/main/java/io/onedev/server/data/migration/DataMigrator.java b/server-core/src/main/java/io/onedev/server/data/migration/DataMigrator.java index 7fe765fcb3..8b8cce80aa 100644 --- a/server-core/src/main/java/io/onedev/server/data/migration/DataMigrator.java +++ b/server-core/src/main/java/io/onedev/server/data/migration/DataMigrator.java @@ -8349,4 +8349,21 @@ public class DataMigrator { } } } + + private void migrate213(File dataDir, Stack versions) { + for (File file : dataDir.listFiles()) { + if (file.getName().startsWith("Projects.xml")) { + VersionedXmlDoc dom = VersionedXmlDoc.fromFile(file); + for (Element projectElement : dom.getRootElement().elements()) { + for (Element branchProtectionElement : projectElement.element("branchProtections").elements()) { + branchProtectionElement.addElement("disallowedFileTypes"); + } + for (Element tagProtectionElement : projectElement.element("tagProtections").elements()) { + tagProtectionElement.addElement("disallowedFileTypes"); + } + } + dom.writeToFile(file, false); + } + } + } } diff --git a/server-core/src/main/java/io/onedev/server/git/command/ListChangedFilesCommand.java b/server-core/src/main/java/io/onedev/server/git/command/ListChangedFilesCommand.java index 4823a09a65..e79f7eb706 100644 --- a/server-core/src/main/java/io/onedev/server/git/command/ListChangedFilesCommand.java +++ b/server-core/src/main/java/io/onedev/server/git/command/ListChangedFilesCommand.java @@ -6,6 +6,7 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; +import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.util.QuotedString; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -43,7 +44,10 @@ public class ListChangedFilesCommand { Commandline git = newGit().workingDir(workingDir); git.environments().putAll(envs); - git.addArgs("diff", "--name-only", "--no-renames", fromRev + ".." + toRev); + if (fromRev.equals(ObjectId.zeroId().name())) + git.addArgs("ls-tree", "--name-only", toRev); + else + git.addArgs("diff", "--name-only", "--no-renames", fromRev + ".." + toRev); git.execute(new LineConsumer() { diff --git a/server-core/src/main/java/io/onedev/server/git/hook/GitPreReceiveCallback.java b/server-core/src/main/java/io/onedev/server/git/hook/GitPreReceiveCallback.java index a9600d095b..7e2c87f2a3 100644 --- a/server-core/src/main/java/io/onedev/server/git/hook/GitPreReceiveCallback.java +++ b/server-core/src/main/java/io/onedev/server/git/hook/GitPreReceiveCallback.java @@ -158,6 +158,12 @@ public class GitPreReceiveCallback extends HttpServlet { if (commitMessageError != null) errorMessages.add(commitMessageError.toString()); } + if (errorMessages.isEmpty() && !protection.getDisallowedFileTypes().isEmpty()) { + var violatedFileTypes = protection.getViolatedFileTypes(project, oldObjectId, newObjectId, gitEnvs); + if (!violatedFileTypes.isEmpty()) { + errorMessages.add("Your push contains disallowed file type(s): " + StringUtils.join(violatedFileTypes, ", ")); + } + } if (errorMessages.isEmpty() && !oldObjectId.equals(ObjectId.zeroId()) && !newObjectId.equals(ObjectId.zeroId()) @@ -202,6 +208,12 @@ public class GitPreReceiveCallback extends HttpServlet { errorMessages.add("Can not update this tag as tag protection setting requires " + "valid tag signature"); } + if (errorMessages.isEmpty() && !protection.getDisallowedFileTypes().isEmpty()) { + var violatedFileTypes = protection.getViolatedFileTypes(project, newObjectId, gitEnvs); + if (!violatedFileTypes.isEmpty()) { + errorMessages.add("Your push contains disallowed file type(s): " + StringUtils.join(violatedFileTypes, ", ")); + } + } if (errorMessages.isEmpty()) { for (var preReceiveChecker : preReceiveCheckers) { var errorMessage = preReceiveChecker.check(project, user, refName, oldObjectId, newObjectId); diff --git a/server-core/src/main/java/io/onedev/server/git/service/GitService.java b/server-core/src/main/java/io/onedev/server/git/service/GitService.java index f5cc23ec85..f66d607e6a 100644 --- a/server-core/src/main/java/io/onedev/server/git/service/GitService.java +++ b/server-core/src/main/java/io/onedev/server/git/service/GitService.java @@ -148,7 +148,7 @@ public interface GitService { @Nullable CommitMessageError checkCommitMessages(BranchProtection protection, Project project, ObjectId oldId, ObjectId newId, Map envs); - + @Nullable byte[] getRawTag(Project project, ObjectId tagId, Map envs); diff --git a/server-core/src/main/java/io/onedev/server/model/Project.java b/server-core/src/main/java/io/onedev/server/model/Project.java index 368d8a38b3..f6481f91d5 100644 --- a/server-core/src/main/java/io/onedev/server/model/Project.java +++ b/server-core/src/main/java/io/onedev/server/model/Project.java @@ -1607,6 +1607,12 @@ public class Project extends AbstractEntity implements LabelSupport getViolatedFileTypes(String branch, User user, ObjectId oldCommitId, ObjectId newCommitId, + Map gitEnvs) { + var protection = getBranchProtection(branch, user); + return protection.getViolatedFileTypes(this, oldCommitId, newCommitId, gitEnvs); + } @Nullable public List readLines(BlobIdent blobIdent, WhitespaceOption whitespaceOption, boolean mustExist) { diff --git a/server-core/src/main/java/io/onedev/server/model/PullRequest.java b/server-core/src/main/java/io/onedev/server/model/PullRequest.java index 48b60ba5dc..29993946e0 100644 --- a/server-core/src/main/java/io/onedev/server/model/PullRequest.java +++ b/server-core/src/main/java/io/onedev/server/model/PullRequest.java @@ -455,6 +455,8 @@ public class PullRequest extends ProjectBelonging private transient Optional commitMessageErrorOpt; + private transient Optional> violatedFileTypesOpt; + public String getTitle() { return title; } @@ -1197,7 +1199,10 @@ public class PullRequest extends ProjectBelonging return _T("No valid signature for head commit"); var commitMessageError = checkCommitMessages(); if (commitMessageError != null) - return commitMessageError.toString(); + return commitMessageError.toString(); + var violatedFileTypes = getViolatedFileTypes(); + if (!violatedFileTypes.isEmpty()) + return MessageFormat.format(_T("Disallowed file type(s): {0}"), StringUtils.join(violatedFileTypes, ", ")); return null; } @@ -1318,6 +1323,15 @@ public class PullRequest extends ProjectBelonging return commitMessageErrorOpt.orElse(null); } + public Collection getViolatedFileTypes() { + if (violatedFileTypesOpt == null) { + var violatedFileTypes = getProject().getViolatedFileTypes(getTargetBranch(), getSubmitter(), + getBaseCommit().copy(), getLatestUpdate().getHeadCommit().copy(), new HashMap<>()); + violatedFileTypesOpt = Optional.of(violatedFileTypes); + } + return violatedFileTypesOpt.get(); + } + public static String getSerialLockName(Long requestId) { return "pull-request-" + requestId + "-serial"; } diff --git a/server-core/src/main/java/io/onedev/server/model/support/code/BranchProtection.java b/server-core/src/main/java/io/onedev/server/model/support/code/BranchProtection.java index 28631d5440..191b4882db 100644 --- a/server-core/src/main/java/io/onedev/server/model/support/code/BranchProtection.java +++ b/server-core/src/main/java/io/onedev/server/model/support/code/BranchProtection.java @@ -6,6 +6,7 @@ import static java.util.regex.Pattern.UNICODE_CHARACTER_CLASS; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; @@ -81,6 +82,8 @@ public class BranchProtection implements Serializable { private List commitTypesForFooterCheck = new ArrayList<>(); private Integer maxCommitMessageLineLength; + + private List disallowedFileTypes = new ArrayList<>(); private String reviewRequirement; @@ -262,6 +265,15 @@ public class BranchProtection implements Serializable { reviewRequirement = parsedReviewRequirement.toString(); } + @Editable(order=410, placeholder = "No disallowed file types", description = "Optionally specify disallowed file types by extensions (hit ENTER to add value), for instance exe, bin. Leave empty to allow all file types") + public List getDisallowedFileTypes() { + return disallowedFileTypes; + } + + public void setDisallowedFileTypes(List disallowedFileTypes) { + this.disallowedFileTypes = disallowedFileTypes; + } + @Editable(order=500, name="Required Builds", placeholder="No any", description="Optionally choose required builds. You may also " + "input jobs not listed here, and press ENTER to add them") @JobChoice(tagsMode=true) @@ -422,6 +434,18 @@ public class BranchProtection implements Serializable { return false; } + public Collection getViolatedFileTypes(Project project, ObjectId oldObjectId, ObjectId newObjectId, + Map gitEnvs) { + if (disallowedFileTypes.isEmpty()) { + return Collections.emptySet(); + } else { + var changedFiles = getGitService().getChangedFiles(project, oldObjectId, newObjectId, gitEnvs); + return getDisallowedFileTypes().stream() + .filter(type -> changedFiles.stream().anyMatch(file -> file.toLowerCase().endsWith("." + type.toLowerCase()))) + .collect(Collectors.toSet()); + } + } + public BuildRequirement getBuildRequirement(Project project, ObjectId oldObjectId, ObjectId newObjectId, Map gitEnvs) { Collection requiredJobs = new LinkedHashSet<>(getJobNames()); diff --git a/server-core/src/main/java/io/onedev/server/model/support/code/TagProtection.java b/server-core/src/main/java/io/onedev/server/model/support/code/TagProtection.java index 205b54909a..e91f0640df 100644 --- a/server-core/src/main/java/io/onedev/server/model/support/code/TagProtection.java +++ b/server-core/src/main/java/io/onedev/server/model/support/code/TagProtection.java @@ -1,8 +1,22 @@ package io.onedev.server.model.support.code; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import javax.validation.constraints.NotEmpty; + +import org.eclipse.jgit.lib.ObjectId; + import io.onedev.commons.codeassist.InputSuggestion; +import io.onedev.server.OneDev; import io.onedev.server.annotation.Editable; import io.onedev.server.annotation.Patterns; +import io.onedev.server.git.service.GitService; import io.onedev.server.model.Project; import io.onedev.server.util.patternset.PatternSet; import io.onedev.server.util.usage.Usage; @@ -10,11 +24,6 @@ import io.onedev.server.util.usermatch.Anyone; import io.onedev.server.util.usermatch.UserMatch; import io.onedev.server.web.util.SuggestionUtils; -import javax.validation.constraints.NotEmpty; -import java.io.Serializable; -import java.util.ArrayList; -import java.util.List; - @Editable public class TagProtection implements Serializable { @@ -33,6 +42,8 @@ public class TagProtection implements Serializable { private boolean preventCreation = true; private boolean commitSignatureRequired; + + private List disallowedFileTypes = new ArrayList<>(); public boolean isEnabled() { return enabled; @@ -115,6 +126,15 @@ public class TagProtection implements Serializable { this.commitSignatureRequired = commitSignatureRequired; } + @Editable(order=410, placeholder = "No disallowed file types", description = "Optionally specify disallowed file types by extensions (hit ENTER to add value), for instance exe, bin. Leave empty to allow all file types") + public List getDisallowedFileTypes() { + return disallowedFileTypes; + } + + public void setDisallowedFileTypes(List disallowedFileTypes) { + this.disallowedFileTypes = disallowedFileTypes; + } + public void onRenameGroup(String oldName, String newName) { userMatch = UserMatch.onRenameGroup(userMatch, oldName, newName); } @@ -145,4 +165,19 @@ public class TagProtection implements Serializable { return usage; } + public Collection getViolatedFileTypes(Project project, ObjectId objectId, Map gitEnvs) { + if (disallowedFileTypes.isEmpty()) { + return Collections.emptySet(); + } else { + var files = getGitService().getChangedFiles(project, ObjectId.zeroId(), objectId, gitEnvs); + return getDisallowedFileTypes().stream() + .filter(type -> files.stream().anyMatch(file -> file.toLowerCase().endsWith("." + type.toLowerCase()))) + .collect(Collectors.toSet()); + } + } + + private GitService getGitService() { + return OneDev.getInstance(GitService.class); + } + } diff --git a/server-core/src/main/java/io/onedev/server/util/reviewrequirement/ReviewRequirement.g4 b/server-core/src/main/java/io/onedev/server/util/reviewrequirement/ReviewRequirement.g4 index 817ad8532e..18b044cc52 100644 --- a/server-core/src/main/java/io/onedev/server/util/reviewrequirement/ReviewRequirement.g4 +++ b/server-core/src/main/java/io/onedev/server/util/reviewrequirement/ReviewRequirement.g4 @@ -4,8 +4,8 @@ requirement: WS* criteria (WS+ criteria)* WS* EOF; criteria: userCriteria | groupCriteria; -userCriteria: USER WS* Value; -groupCriteria: GROUP WS* Value (WS*':' WS* DIGIT)?; +userCriteria: USER Value; +groupCriteria: GROUP Value (':' DIGIT)?; DIGIT: [1-9][0-9]*; diff --git a/server-core/src/main/java/io/onedev/server/web/page/project/blob/ProjectBlobPage.java b/server-core/src/main/java/io/onedev/server/web/page/project/blob/ProjectBlobPage.java index df5c8168c5..a26275d929 100644 --- a/server-core/src/main/java/io/onedev/server/web/page/project/blob/ProjectBlobPage.java +++ b/server-core/src/main/java/io/onedev/server/web/page/project/blob/ProjectBlobPage.java @@ -1539,11 +1539,17 @@ public class ProjectBlobPage extends ProjectPage implements BlobRenderContext, String blobPath = FilenameUtils.sanitizeFileName(FileUpload.getFileName(item)); if (parentPath != null) blobPath = parentPath + "/" + blobPath; - + var blobType = StringUtils.substringAfterLast(blobPath, "."); + + var disallowedFileTypes = getProject().getBranchProtection(blobIdent.revision, user).getDisallowedFileTypes(); + if (disallowedFileTypes.stream().anyMatch(type -> blobType.equalsIgnoreCase(type))) { + throw new BlobEditException(MessageFormat.format(_T("Not allowed file type: {0}"), blobType)); + } + if (getProject().isReviewRequiredForModification(user, blobIdent.revision, blobPath)) - throw new BlobEditException("Review required for this change. Please submit pull request instead"); + throw new BlobEditException(_T("Review required for this change. Please submit pull request instead")); else if (getProject().isBuildRequiredForModification(user, blobIdent.revision, blobPath)) - throw new BlobEditException("Build required for this change. Please submit pull request instead"); + throw new BlobEditException(_T("Build required for this change. Please submit pull request instead")); else if (getProject().isCommitSignatureRequiredButNoSigningKey(user, blobIdent.revision)) signRequired = true; diff --git a/server-core/src/main/java/io/onedev/server/web/page/project/blob/render/commitoption/CommitOptionPanel.java b/server-core/src/main/java/io/onedev/server/web/page/project/blob/render/commitoption/CommitOptionPanel.java index 2ed62ae521..78b1bd3f6d 100644 --- a/server-core/src/main/java/io/onedev/server/web/page/project/blob/render/commitoption/CommitOptionPanel.java +++ b/server-core/src/main/java/io/onedev/server/web/page/project/blob/render/commitoption/CommitOptionPanel.java @@ -274,16 +274,25 @@ public class CommitOptionPanel extends Panel { Map newBlobs = new HashMap<>(); if (newContentProvider != null) { String newPath = context.getNewPath(); + var blobType = StringUtils.substringAfterLast(newPath, "."); + + var disallowedFileTypes = context.getProject().getBranchProtection(revision, user).getDisallowedFileTypes(); + if (disallowedFileTypes.stream().anyMatch(type -> blobType.equalsIgnoreCase(type))) { + form.error(MessageFormat.format(_T("Not allowed file type: {0}"), blobType)); + target.add(form); + return false; + } + if (context.getProject().isReviewRequiredForModification(user, revision, newPath)) { - form.error("Review required for this change. Please submit pull request instead"); + form.error(_T("Review required for this change. Please submit pull request instead")); target.add(form); return false; } else if (context.getProject().isBuildRequiredForModification(user, revision, newPath)) { - form.error("Build required for this change. Please submit pull request instead"); + form.error(_T("Build required for this change. Please submit pull request instead")); target.add(form); return false; } else if (context.getProject().isCommitSignatureRequiredButNoSigningKey(user, revision)) { - form.error("Signature required for this change, but no signing key is specified"); + form.error(_T("Signature required for this change, but no signing key is specified")); target.add(form); return false; } @@ -307,13 +316,11 @@ public class CommitOptionPanel extends Panel { ExceptionUtils.find(e, ObsoleteCommitException.class); if (objectAlreadyExistsException != null) { - form.error("A path with same name already exists. " - + "Please choose a different name and try again."); + form.error(_T("A path with same name already exists.Please choose a different name and try again.")); target.add(form); break; } else if (notTreeException != null) { - form.error("A file exists where you’re trying to create a subdirectory. " - + "Choose a new path and try again.."); + form.error(_T("A file exists where you’re trying to create a subdirectory. Choose a new path and try again..")); target.add(form); break; } else if (obsoleteCommitException != null) { @@ -333,14 +340,14 @@ public class CommitOptionPanel extends Panel { if (newContentProvider != null) { oldPaths.clear(); changesOfOthers = getBlobChange(path, pathChange, lastPrevCommitId, prevCommitId); - form.warn("Someone made below change since you started editing"); + form.warn(_T("Someone made below change since you started editing")); break; } else { newCommitId = obsoleteCommitException.getOldCommitId(); } } else { changesOfOthers = getBlobChange(path, pathChange, lastPrevCommitId, prevCommitId); - form.warn("Someone made below change since you started editing"); + form.warn(_T("Someone made below change since you started editing")); break; } } diff --git a/server-core/src/main/java/io/onedev/server/web/page/project/pullrequests/detail/PullRequestDetailPage.html b/server-core/src/main/java/io/onedev/server/web/page/project/pullrequests/detail/PullRequestDetailPage.html index 8ccee4f43d..dd01c3d550 100644 --- a/server-core/src/main/java/io/onedev/server/web/page/project/pullrequests/detail/PullRequestDetailPage.html +++ b/server-core/src/main/java/io/onedev/server/web/page/project/pullrequests/detail/PullRequestDetailPage.html @@ -94,6 +94,11 @@ + +
+ +
+
diff --git a/server-core/src/main/java/io/onedev/server/web/page/project/pullrequests/detail/PullRequestDetailPage.java b/server-core/src/main/java/io/onedev/server/web/page/project/pullrequests/detail/PullRequestDetailPage.java index 38a1b53967..877d5ba951 100644 --- a/server-core/src/main/java/io/onedev/server/web/page/project/pullrequests/detail/PullRequestDetailPage.java +++ b/server-core/src/main/java/io/onedev/server/web/page/project/pullrequests/detail/PullRequestDetailPage.java @@ -724,6 +724,27 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR } }.setEscapeModelStrings(false)); + summaryContainer.add(new Label("fileTypesCheckError", new LoadableDetachableModel<>() { + @Override + protected String load() { + var violatedFileTypes = getPullRequest().getViolatedFileTypes(); + if (violatedFileTypes.isEmpty()) + return null; + else + return MessageFormat.format(_T("The change contains disallowed file type(s): {0}"), StringUtils.join(violatedFileTypes, ", ")); + } + }) { + @Override + protected void onConfigure() { + super.onConfigure(); + PullRequest request = getPullRequest(); + if (request.isOpen()) + setVisible(getDefaultModelObject() != null); + else + setVisible(false); + } + }.setEscapeModelStrings(false)); + summaryContainer.add(new Label("requiredJobsMessage", new AbstractReadOnlyModel() { @Override public String getObject() {