code.gitea.io/gitea@v1.22.3/services/pull/patch.go (about) 1 // Copyright 2019 The Gitea Authors. 2 // All rights reserved. 3 // SPDX-License-Identifier: MIT 4 5 package pull 6 7 import ( 8 "bufio" 9 "context" 10 "fmt" 11 "io" 12 "os" 13 "path/filepath" 14 "strings" 15 16 "code.gitea.io/gitea/models" 17 git_model "code.gitea.io/gitea/models/git" 18 issues_model "code.gitea.io/gitea/models/issues" 19 "code.gitea.io/gitea/models/unit" 20 "code.gitea.io/gitea/modules/container" 21 "code.gitea.io/gitea/modules/git" 22 "code.gitea.io/gitea/modules/gitrepo" 23 "code.gitea.io/gitea/modules/graceful" 24 "code.gitea.io/gitea/modules/log" 25 "code.gitea.io/gitea/modules/process" 26 "code.gitea.io/gitea/modules/setting" 27 "code.gitea.io/gitea/modules/util" 28 29 "github.com/gobwas/glob" 30 ) 31 32 // DownloadDiffOrPatch will write the patch for the pr to the writer 33 func DownloadDiffOrPatch(ctx context.Context, pr *issues_model.PullRequest, w io.Writer, patch, binary bool) error { 34 if err := pr.LoadBaseRepo(ctx); err != nil { 35 log.Error("Unable to load base repository ID %d for pr #%d [%d]", pr.BaseRepoID, pr.Index, pr.ID) 36 return err 37 } 38 39 gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, pr.BaseRepo) 40 if err != nil { 41 return fmt.Errorf("OpenRepository: %w", err) 42 } 43 defer closer.Close() 44 45 if err := gitRepo.GetDiffOrPatch(pr.MergeBase, pr.GetGitRefName(), w, patch, binary); err != nil { 46 log.Error("Unable to get patch file from %s to %s in %s Error: %v", pr.MergeBase, pr.HeadBranch, pr.BaseRepo.FullName(), err) 47 return fmt.Errorf("Unable to get patch file from %s to %s in %s Error: %w", pr.MergeBase, pr.HeadBranch, pr.BaseRepo.FullName(), err) 48 } 49 return nil 50 } 51 52 var patchErrorSuffices = []string{ 53 ": already exists in index", 54 ": patch does not apply", 55 ": already exists in working directory", 56 "unrecognized input", 57 ": No such file or directory", 58 ": does not exist in index", 59 } 60 61 // TestPatch will test whether a simple patch will apply 62 func TestPatch(pr *issues_model.PullRequest) error { 63 ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("TestPatch: %s", pr)) 64 defer finished() 65 66 prCtx, cancel, err := createTemporaryRepoForPR(ctx, pr) 67 if err != nil { 68 if !git_model.IsErrBranchNotExist(err) { 69 log.Error("CreateTemporaryRepoForPR %-v: %v", pr, err) 70 } 71 return err 72 } 73 defer cancel() 74 75 return testPatch(ctx, prCtx, pr) 76 } 77 78 func testPatch(ctx context.Context, prCtx *prContext, pr *issues_model.PullRequest) error { 79 gitRepo, err := git.OpenRepository(ctx, prCtx.tmpBasePath) 80 if err != nil { 81 return fmt.Errorf("OpenRepository: %w", err) 82 } 83 defer gitRepo.Close() 84 85 // 1. update merge base 86 pr.MergeBase, _, err = git.NewCommand(ctx, "merge-base", "--", "base", "tracking").RunStdString(&git.RunOpts{Dir: prCtx.tmpBasePath}) 87 if err != nil { 88 var err2 error 89 pr.MergeBase, err2 = gitRepo.GetRefCommitID(git.BranchPrefix + "base") 90 if err2 != nil { 91 return fmt.Errorf("GetMergeBase: %v and can't find commit ID for base: %w", err, err2) 92 } 93 } 94 pr.MergeBase = strings.TrimSpace(pr.MergeBase) 95 if pr.HeadCommitID, err = gitRepo.GetRefCommitID(git.BranchPrefix + "tracking"); err != nil { 96 return fmt.Errorf("GetBranchCommitID: can't find commit ID for head: %w", err) 97 } 98 99 if pr.HeadCommitID == pr.MergeBase { 100 pr.Status = issues_model.PullRequestStatusAncestor 101 return nil 102 } 103 104 // 2. Check for conflicts 105 if conflicts, err := checkConflicts(ctx, pr, gitRepo, prCtx.tmpBasePath); err != nil || conflicts || pr.Status == issues_model.PullRequestStatusEmpty { 106 return err 107 } 108 109 // 3. Check for protected files changes 110 if err = checkPullFilesProtection(ctx, pr, gitRepo); err != nil { 111 return fmt.Errorf("pr.CheckPullFilesProtection(): %v", err) 112 } 113 114 if len(pr.ChangedProtectedFiles) > 0 { 115 log.Trace("Found %d protected files changed", len(pr.ChangedProtectedFiles)) 116 } 117 118 pr.Status = issues_model.PullRequestStatusMergeable 119 120 return nil 121 } 122 123 type errMergeConflict struct { 124 filename string 125 } 126 127 func (e *errMergeConflict) Error() string { 128 return fmt.Sprintf("conflict detected at: %s", e.filename) 129 } 130 131 func attemptMerge(ctx context.Context, file *unmergedFile, tmpBasePath string, filesToRemove *[]string, filesToAdd *[]git.IndexObjectInfo) error { 132 log.Trace("Attempt to merge:\n%v", file) 133 134 switch { 135 case file.stage1 != nil && (file.stage2 == nil || file.stage3 == nil): 136 // 1. Deleted in one or both: 137 // 138 // Conflict <==> the stage1 !SameAs to the undeleted one 139 if (file.stage2 != nil && !file.stage1.SameAs(file.stage2)) || (file.stage3 != nil && !file.stage1.SameAs(file.stage3)) { 140 // Conflict! 141 return &errMergeConflict{file.stage1.path} 142 } 143 144 // Not a genuine conflict and we can simply remove the file from the index 145 *filesToRemove = append(*filesToRemove, file.stage1.path) 146 return nil 147 case file.stage1 == nil && file.stage2 != nil && (file.stage3 == nil || file.stage2.SameAs(file.stage3)): 148 // 2. Added in ours but not in theirs or identical in both 149 // 150 // Not a genuine conflict just add to the index 151 *filesToAdd = append(*filesToAdd, git.IndexObjectInfo{Mode: file.stage2.mode, Object: git.MustIDFromString(file.stage2.sha), Filename: file.stage2.path}) 152 return nil 153 case file.stage1 == nil && file.stage2 != nil && file.stage3 != nil && file.stage2.sha == file.stage3.sha && file.stage2.mode != file.stage3.mode: 154 // 3. Added in both with the same sha but the modes are different 155 // 156 // Conflict! (Not sure that this can actually happen but we should handle) 157 return &errMergeConflict{file.stage2.path} 158 case file.stage1 == nil && file.stage2 == nil && file.stage3 != nil: 159 // 4. Added in theirs but not ours: 160 // 161 // Not a genuine conflict just add to the index 162 *filesToAdd = append(*filesToAdd, git.IndexObjectInfo{Mode: file.stage3.mode, Object: git.MustIDFromString(file.stage3.sha), Filename: file.stage3.path}) 163 return nil 164 case file.stage1 == nil: 165 // 5. Created by new in both 166 // 167 // Conflict! 168 return &errMergeConflict{file.stage2.path} 169 case file.stage2 != nil && file.stage3 != nil: 170 // 5. Modified in both - we should try to merge in the changes but first: 171 // 172 if file.stage2.mode == "120000" || file.stage3.mode == "120000" { 173 // 5a. Conflicting symbolic link change 174 return &errMergeConflict{file.stage2.path} 175 } 176 if file.stage2.mode == "160000" || file.stage3.mode == "160000" { 177 // 5b. Conflicting submodule change 178 return &errMergeConflict{file.stage2.path} 179 } 180 if file.stage2.mode != file.stage3.mode { 181 // 5c. Conflicting mode change 182 return &errMergeConflict{file.stage2.path} 183 } 184 185 // Need to get the objects from the object db to attempt to merge 186 root, _, err := git.NewCommand(ctx, "unpack-file").AddDynamicArguments(file.stage1.sha).RunStdString(&git.RunOpts{Dir: tmpBasePath}) 187 if err != nil { 188 return fmt.Errorf("unable to get root object: %s at path: %s for merging. Error: %w", file.stage1.sha, file.stage1.path, err) 189 } 190 root = strings.TrimSpace(root) 191 defer func() { 192 _ = util.Remove(filepath.Join(tmpBasePath, root)) 193 }() 194 195 base, _, err := git.NewCommand(ctx, "unpack-file").AddDynamicArguments(file.stage2.sha).RunStdString(&git.RunOpts{Dir: tmpBasePath}) 196 if err != nil { 197 return fmt.Errorf("unable to get base object: %s at path: %s for merging. Error: %w", file.stage2.sha, file.stage2.path, err) 198 } 199 base = strings.TrimSpace(filepath.Join(tmpBasePath, base)) 200 defer func() { 201 _ = util.Remove(base) 202 }() 203 head, _, err := git.NewCommand(ctx, "unpack-file").AddDynamicArguments(file.stage3.sha).RunStdString(&git.RunOpts{Dir: tmpBasePath}) 204 if err != nil { 205 return fmt.Errorf("unable to get head object:%s at path: %s for merging. Error: %w", file.stage3.sha, file.stage3.path, err) 206 } 207 head = strings.TrimSpace(head) 208 defer func() { 209 _ = util.Remove(filepath.Join(tmpBasePath, head)) 210 }() 211 212 // now git merge-file annoyingly takes a different order to the merge-tree ... 213 _, _, conflictErr := git.NewCommand(ctx, "merge-file").AddDynamicArguments(base, root, head).RunStdString(&git.RunOpts{Dir: tmpBasePath}) 214 if conflictErr != nil { 215 return &errMergeConflict{file.stage2.path} 216 } 217 218 // base now contains the merged data 219 hash, _, err := git.NewCommand(ctx, "hash-object", "-w", "--path").AddDynamicArguments(file.stage2.path, base).RunStdString(&git.RunOpts{Dir: tmpBasePath}) 220 if err != nil { 221 return err 222 } 223 hash = strings.TrimSpace(hash) 224 *filesToAdd = append(*filesToAdd, git.IndexObjectInfo{Mode: file.stage2.mode, Object: git.MustIDFromString(hash), Filename: file.stage2.path}) 225 return nil 226 default: 227 if file.stage1 != nil { 228 return &errMergeConflict{file.stage1.path} 229 } else if file.stage2 != nil { 230 return &errMergeConflict{file.stage2.path} 231 } else if file.stage3 != nil { 232 return &errMergeConflict{file.stage3.path} 233 } 234 } 235 return nil 236 } 237 238 // AttemptThreeWayMerge will attempt to three way merge using git read-tree and then follow the git merge-one-file algorithm to attempt to resolve basic conflicts 239 func AttemptThreeWayMerge(ctx context.Context, gitPath string, gitRepo *git.Repository, base, ours, theirs, description string) (bool, []string, error) { 240 ctx, cancel := context.WithCancel(ctx) 241 defer cancel() 242 243 // First we use read-tree to do a simple three-way merge 244 if _, _, err := git.NewCommand(ctx, "read-tree", "-m").AddDynamicArguments(base, ours, theirs).RunStdString(&git.RunOpts{Dir: gitPath}); err != nil { 245 log.Error("Unable to run read-tree -m! Error: %v", err) 246 return false, nil, fmt.Errorf("unable to run read-tree -m! Error: %w", err) 247 } 248 249 var filesToRemove []string 250 var filesToAdd []git.IndexObjectInfo 251 252 // Then we use git ls-files -u to list the unmerged files and collate the triples in unmergedfiles 253 unmerged := make(chan *unmergedFile) 254 go unmergedFiles(ctx, gitPath, unmerged) 255 256 defer func() { 257 cancel() 258 for range unmerged { 259 // empty the unmerged channel 260 } 261 }() 262 263 numberOfConflicts := 0 264 conflict := false 265 conflictedFiles := make([]string, 0, 5) 266 267 for file := range unmerged { 268 if file == nil { 269 break 270 } 271 if file.err != nil { 272 cancel() 273 return false, nil, file.err 274 } 275 276 // OK now we have the unmerged file triplet attempt to merge it 277 if err := attemptMerge(ctx, file, gitPath, &filesToRemove, &filesToAdd); err != nil { 278 if conflictErr, ok := err.(*errMergeConflict); ok { 279 log.Trace("Conflict: %s in %s", conflictErr.filename, description) 280 conflict = true 281 if numberOfConflicts < 10 { 282 conflictedFiles = append(conflictedFiles, conflictErr.filename) 283 } 284 numberOfConflicts++ 285 continue 286 } 287 return false, nil, err 288 } 289 } 290 291 // Add and remove files in one command, as this is slow with many files otherwise 292 if err := gitRepo.RemoveFilesFromIndex(filesToRemove...); err != nil { 293 return false, nil, err 294 } 295 if err := gitRepo.AddObjectsToIndex(filesToAdd...); err != nil { 296 return false, nil, err 297 } 298 299 return conflict, conflictedFiles, nil 300 } 301 302 func checkConflicts(ctx context.Context, pr *issues_model.PullRequest, gitRepo *git.Repository, tmpBasePath string) (bool, error) { 303 // 1. checkConflicts resets the conflict status - therefore - reset the conflict status 304 pr.ConflictedFiles = nil 305 306 // 2. AttemptThreeWayMerge first - this is much quicker than plain patch to base 307 description := fmt.Sprintf("PR[%d] %s/%s#%d", pr.ID, pr.BaseRepo.OwnerName, pr.BaseRepo.Name, pr.Index) 308 conflict, conflictFiles, err := AttemptThreeWayMerge(ctx, 309 tmpBasePath, gitRepo, pr.MergeBase, "base", "tracking", description) 310 if err != nil { 311 return false, err 312 } 313 314 if !conflict { 315 // No conflicts detected so we need to check if the patch is empty... 316 // a. Write the newly merged tree and check the new tree-hash 317 var treeHash string 318 treeHash, _, err = git.NewCommand(ctx, "write-tree").RunStdString(&git.RunOpts{Dir: tmpBasePath}) 319 if err != nil { 320 lsfiles, _, _ := git.NewCommand(ctx, "ls-files", "-u").RunStdString(&git.RunOpts{Dir: tmpBasePath}) 321 return false, fmt.Errorf("unable to write unconflicted tree: %w\n`git ls-files -u`:\n%s", err, lsfiles) 322 } 323 treeHash = strings.TrimSpace(treeHash) 324 baseTree, err := gitRepo.GetTree("base") 325 if err != nil { 326 return false, err 327 } 328 329 // b. compare the new tree-hash with the base tree hash 330 if treeHash == baseTree.ID.String() { 331 log.Debug("PullRequest[%d]: Patch is empty - ignoring", pr.ID) 332 pr.Status = issues_model.PullRequestStatusEmpty 333 } 334 335 return false, nil 336 } 337 338 // 3. OK the three-way merge method has detected conflicts 339 // 3a. Are still testing with GitApply? If not set the conflict status and move on 340 if !setting.Repository.PullRequest.TestConflictingPatchesWithGitApply { 341 pr.Status = issues_model.PullRequestStatusConflict 342 pr.ConflictedFiles = conflictFiles 343 344 log.Trace("Found %d files conflicted: %v", len(pr.ConflictedFiles), pr.ConflictedFiles) 345 return true, nil 346 } 347 348 // 3b. Create a plain patch from head to base 349 tmpPatchFile, err := os.CreateTemp("", "patch") 350 if err != nil { 351 log.Error("Unable to create temporary patch file! Error: %v", err) 352 return false, fmt.Errorf("unable to create temporary patch file! Error: %w", err) 353 } 354 defer func() { 355 _ = util.Remove(tmpPatchFile.Name()) 356 }() 357 358 if err := gitRepo.GetDiffBinary(pr.MergeBase, "tracking", tmpPatchFile); err != nil { 359 tmpPatchFile.Close() 360 log.Error("Unable to get patch file from %s to %s in %s Error: %v", pr.MergeBase, pr.HeadBranch, pr.BaseRepo.FullName(), err) 361 return false, fmt.Errorf("unable to get patch file from %s to %s in %s Error: %w", pr.MergeBase, pr.HeadBranch, pr.BaseRepo.FullName(), err) 362 } 363 stat, err := tmpPatchFile.Stat() 364 if err != nil { 365 tmpPatchFile.Close() 366 return false, fmt.Errorf("unable to stat patch file: %w", err) 367 } 368 patchPath := tmpPatchFile.Name() 369 tmpPatchFile.Close() 370 371 // 3c. if the size of that patch is 0 - there can be no conflicts! 372 if stat.Size() == 0 { 373 log.Debug("PullRequest[%d]: Patch is empty - ignoring", pr.ID) 374 pr.Status = issues_model.PullRequestStatusEmpty 375 return false, nil 376 } 377 378 log.Trace("PullRequest[%d].testPatch (patchPath): %s", pr.ID, patchPath) 379 380 // 4. Read the base branch in to the index of the temporary repository 381 _, _, err = git.NewCommand(gitRepo.Ctx, "read-tree", "base").RunStdString(&git.RunOpts{Dir: tmpBasePath}) 382 if err != nil { 383 return false, fmt.Errorf("git read-tree %s: %w", pr.BaseBranch, err) 384 } 385 386 // 5. Now get the pull request configuration to check if we need to ignore whitespace 387 prUnit, err := pr.BaseRepo.GetUnit(ctx, unit.TypePullRequests) 388 if err != nil { 389 return false, err 390 } 391 prConfig := prUnit.PullRequestsConfig() 392 393 // 6. Prepare the arguments to apply the patch against the index 394 cmdApply := git.NewCommand(gitRepo.Ctx, "apply", "--check", "--cached") 395 if prConfig.IgnoreWhitespaceConflicts { 396 cmdApply.AddArguments("--ignore-whitespace") 397 } 398 is3way := false 399 if git.DefaultFeatures().CheckVersionAtLeast("2.32.0") { 400 cmdApply.AddArguments("--3way") 401 is3way = true 402 } 403 cmdApply.AddDynamicArguments(patchPath) 404 405 // 7. Prep the pipe: 406 // - Here we could do the equivalent of: 407 // `git apply --check --cached patch_file > conflicts` 408 // Then iterate through the conflicts. However, that means storing all the conflicts 409 // in memory - which is very wasteful. 410 // - alternatively we can do the equivalent of: 411 // `git apply --check ... | grep ...` 412 // meaning we don't store all of the conflicts unnecessarily. 413 stderrReader, stderrWriter, err := os.Pipe() 414 if err != nil { 415 log.Error("Unable to open stderr pipe: %v", err) 416 return false, fmt.Errorf("unable to open stderr pipe: %w", err) 417 } 418 defer func() { 419 _ = stderrReader.Close() 420 _ = stderrWriter.Close() 421 }() 422 423 // 8. Run the check command 424 conflict = false 425 err = cmdApply.Run(&git.RunOpts{ 426 Dir: tmpBasePath, 427 Stderr: stderrWriter, 428 PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error { 429 // Close the writer end of the pipe to begin processing 430 _ = stderrWriter.Close() 431 defer func() { 432 // Close the reader on return to terminate the git command if necessary 433 _ = stderrReader.Close() 434 }() 435 436 const prefix = "error: patch failed:" 437 const errorPrefix = "error: " 438 const threewayFailed = "Failed to perform three-way merge..." 439 const appliedPatchPrefix = "Applied patch to '" 440 const withConflicts = "' with conflicts." 441 442 conflicts := make(container.Set[string]) 443 444 // Now scan the output from the command 445 scanner := bufio.NewScanner(stderrReader) 446 for scanner.Scan() { 447 line := scanner.Text() 448 log.Trace("PullRequest[%d].testPatch: stderr: %s", pr.ID, line) 449 if strings.HasPrefix(line, prefix) { 450 conflict = true 451 filepath := strings.TrimSpace(strings.Split(line[len(prefix):], ":")[0]) 452 conflicts.Add(filepath) 453 } else if is3way && line == threewayFailed { 454 conflict = true 455 } else if strings.HasPrefix(line, errorPrefix) { 456 conflict = true 457 for _, suffix := range patchErrorSuffices { 458 if strings.HasSuffix(line, suffix) { 459 filepath := strings.TrimSpace(strings.TrimSuffix(line[len(errorPrefix):], suffix)) 460 if filepath != "" { 461 conflicts.Add(filepath) 462 } 463 break 464 } 465 } 466 } else if is3way && strings.HasPrefix(line, appliedPatchPrefix) && strings.HasSuffix(line, withConflicts) { 467 conflict = true 468 filepath := strings.TrimPrefix(strings.TrimSuffix(line, withConflicts), appliedPatchPrefix) 469 if filepath != "" { 470 conflicts.Add(filepath) 471 } 472 } 473 // only list 10 conflicted files 474 if len(conflicts) >= 10 { 475 break 476 } 477 } 478 479 if len(conflicts) > 0 { 480 pr.ConflictedFiles = make([]string, 0, len(conflicts)) 481 for key := range conflicts { 482 pr.ConflictedFiles = append(pr.ConflictedFiles, key) 483 } 484 } 485 486 return nil 487 }, 488 }) 489 490 // 9. Check if the found conflictedfiles is non-zero, "err" could be non-nil, so we should ignore it if we found conflicts. 491 // Note: `"err" could be non-nil` is due that if enable 3-way merge, it doesn't return any error on found conflicts. 492 if len(pr.ConflictedFiles) > 0 { 493 if conflict { 494 pr.Status = issues_model.PullRequestStatusConflict 495 log.Trace("Found %d files conflicted: %v", len(pr.ConflictedFiles), pr.ConflictedFiles) 496 497 return true, nil 498 } 499 } else if err != nil { 500 return false, fmt.Errorf("git apply --check: %w", err) 501 } 502 return false, nil 503 } 504 505 // CheckFileProtection check file Protection 506 func CheckFileProtection(repo *git.Repository, branchName, oldCommitID, newCommitID string, patterns []glob.Glob, limit int, env []string) ([]string, error) { 507 if len(patterns) == 0 { 508 return nil, nil 509 } 510 affectedFiles, err := git.GetAffectedFiles(repo, branchName, oldCommitID, newCommitID, env) 511 if err != nil { 512 return nil, err 513 } 514 changedProtectedFiles := make([]string, 0, limit) 515 for _, affectedFile := range affectedFiles { 516 lpath := strings.ToLower(affectedFile) 517 for _, pat := range patterns { 518 if pat.Match(lpath) { 519 changedProtectedFiles = append(changedProtectedFiles, lpath) 520 break 521 } 522 } 523 if len(changedProtectedFiles) >= limit { 524 break 525 } 526 } 527 if len(changedProtectedFiles) > 0 { 528 err = models.ErrFilePathProtected{ 529 Path: changedProtectedFiles[0], 530 } 531 } 532 return changedProtectedFiles, err 533 } 534 535 // CheckUnprotectedFiles check if the commit only touches unprotected files 536 func CheckUnprotectedFiles(repo *git.Repository, branchName, oldCommitID, newCommitID string, patterns []glob.Glob, env []string) (bool, error) { 537 if len(patterns) == 0 { 538 return false, nil 539 } 540 affectedFiles, err := git.GetAffectedFiles(repo, branchName, oldCommitID, newCommitID, env) 541 if err != nil { 542 return false, err 543 } 544 for _, affectedFile := range affectedFiles { 545 lpath := strings.ToLower(affectedFile) 546 unprotected := false 547 for _, pat := range patterns { 548 if pat.Match(lpath) { 549 unprotected = true 550 break 551 } 552 } 553 if !unprotected { 554 return false, nil 555 } 556 } 557 return true, nil 558 } 559 560 // checkPullFilesProtection check if pr changed protected files and save results 561 func checkPullFilesProtection(ctx context.Context, pr *issues_model.PullRequest, gitRepo *git.Repository) error { 562 if pr.Status == issues_model.PullRequestStatusEmpty { 563 pr.ChangedProtectedFiles = nil 564 return nil 565 } 566 567 pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch) 568 if err != nil { 569 return err 570 } 571 572 if pb == nil { 573 pr.ChangedProtectedFiles = nil 574 return nil 575 } 576 577 pr.ChangedProtectedFiles, err = CheckFileProtection(gitRepo, pr.HeadBranch, pr.MergeBase, "tracking", pb.GetProtectedFilePatterns(), 10, os.Environ()) 578 if err != nil && !models.IsErrFilePathProtected(err) { 579 return err 580 } 581 return nil 582 }