1// Copyright 2021 The Gitea Authors. All rights reserved. 2// Use of this source code is governed by a MIT-style 3// license that can be found in the LICENSE file. 4 5package mirror 6 7import ( 8 "context" 9 "fmt" 10 "strings" 11 "time" 12 13 "code.gitea.io/gitea/models" 14 admin_model "code.gitea.io/gitea/models/admin" 15 "code.gitea.io/gitea/models/db" 16 repo_model "code.gitea.io/gitea/models/repo" 17 "code.gitea.io/gitea/modules/cache" 18 "code.gitea.io/gitea/modules/git" 19 "code.gitea.io/gitea/modules/lfs" 20 "code.gitea.io/gitea/modules/log" 21 "code.gitea.io/gitea/modules/notification" 22 "code.gitea.io/gitea/modules/process" 23 repo_module "code.gitea.io/gitea/modules/repository" 24 "code.gitea.io/gitea/modules/setting" 25 "code.gitea.io/gitea/modules/timeutil" 26 "code.gitea.io/gitea/modules/util" 27) 28 29// gitShortEmptySha Git short empty SHA 30const gitShortEmptySha = "0000000" 31 32// UpdateAddress writes new address to Git repository and database 33func UpdateAddress(m *repo_model.Mirror, addr string) error { 34 remoteName := m.GetRemoteName() 35 repoPath := m.Repo.RepoPath() 36 // Remove old remote 37 _, err := git.NewCommand("remote", "rm", remoteName).RunInDir(repoPath) 38 if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") { 39 return err 40 } 41 42 _, err = git.NewCommand("remote", "add", remoteName, "--mirror=fetch", addr).RunInDir(repoPath) 43 if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") { 44 return err 45 } 46 47 if m.Repo.HasWiki() { 48 wikiPath := m.Repo.WikiPath() 49 wikiRemotePath := repo_module.WikiRemoteURL(addr) 50 // Remove old remote of wiki 51 _, err := git.NewCommand("remote", "rm", remoteName).RunInDir(wikiPath) 52 if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") { 53 return err 54 } 55 56 _, err = git.NewCommand("remote", "add", remoteName, "--mirror=fetch", wikiRemotePath).RunInDir(wikiPath) 57 if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") { 58 return err 59 } 60 } 61 62 m.Repo.OriginalURL = addr 63 return repo_model.UpdateRepositoryCols(m.Repo, "original_url") 64} 65 66// mirrorSyncResult contains information of a updated reference. 67// If the oldCommitID is "0000000", it means a new reference, the value of newCommitID is empty. 68// If the newCommitID is "0000000", it means the reference is deleted, the value of oldCommitID is empty. 69type mirrorSyncResult struct { 70 refName string 71 oldCommitID string 72 newCommitID string 73} 74 75// parseRemoteUpdateOutput detects create, update and delete operations of references from upstream. 76func parseRemoteUpdateOutput(output string) []*mirrorSyncResult { 77 results := make([]*mirrorSyncResult, 0, 3) 78 lines := strings.Split(output, "\n") 79 for i := range lines { 80 // Make sure reference name is presented before continue 81 idx := strings.Index(lines[i], "-> ") 82 if idx == -1 { 83 continue 84 } 85 86 refName := lines[i][idx+3:] 87 88 switch { 89 case strings.HasPrefix(lines[i], " * "): // New reference 90 if strings.HasPrefix(lines[i], " * [new tag]") { 91 refName = git.TagPrefix + refName 92 } else if strings.HasPrefix(lines[i], " * [new branch]") { 93 refName = git.BranchPrefix + refName 94 } 95 results = append(results, &mirrorSyncResult{ 96 refName: refName, 97 oldCommitID: gitShortEmptySha, 98 }) 99 case strings.HasPrefix(lines[i], " - "): // Delete reference 100 results = append(results, &mirrorSyncResult{ 101 refName: refName, 102 newCommitID: gitShortEmptySha, 103 }) 104 case strings.HasPrefix(lines[i], " + "): // Force update 105 if idx := strings.Index(refName, " "); idx > -1 { 106 refName = refName[:idx] 107 } 108 delimIdx := strings.Index(lines[i][3:], " ") 109 if delimIdx == -1 { 110 log.Error("SHA delimiter not found: %q", lines[i]) 111 continue 112 } 113 shas := strings.Split(lines[i][3:delimIdx+3], "...") 114 if len(shas) != 2 { 115 log.Error("Expect two SHAs but not what found: %q", lines[i]) 116 continue 117 } 118 results = append(results, &mirrorSyncResult{ 119 refName: refName, 120 oldCommitID: shas[0], 121 newCommitID: shas[1], 122 }) 123 case strings.HasPrefix(lines[i], " "): // New commits of a reference 124 delimIdx := strings.Index(lines[i][3:], " ") 125 if delimIdx == -1 { 126 log.Error("SHA delimiter not found: %q", lines[i]) 127 continue 128 } 129 shas := strings.Split(lines[i][3:delimIdx+3], "..") 130 if len(shas) != 2 { 131 log.Error("Expect two SHAs but not what found: %q", lines[i]) 132 continue 133 } 134 results = append(results, &mirrorSyncResult{ 135 refName: refName, 136 oldCommitID: shas[0], 137 newCommitID: shas[1], 138 }) 139 140 default: 141 log.Warn("parseRemoteUpdateOutput: unexpected update line %q", lines[i]) 142 } 143 } 144 return results 145} 146 147func pruneBrokenReferences(ctx context.Context, 148 m *repo_model.Mirror, 149 repoPath string, 150 timeout time.Duration, 151 stdoutBuilder, stderrBuilder *strings.Builder, 152 sanitizer *strings.Replacer, 153 isWiki bool) error { 154 155 wiki := "" 156 if isWiki { 157 wiki = "Wiki " 158 } 159 160 stderrBuilder.Reset() 161 stdoutBuilder.Reset() 162 pruneErr := git.NewCommandContext(ctx, "remote", "prune", m.GetRemoteName()). 163 SetDescription(fmt.Sprintf("Mirror.runSync %ssPrune references: %s ", wiki, m.Repo.FullName())). 164 RunInDirTimeoutPipeline(timeout, repoPath, stdoutBuilder, stderrBuilder) 165 if pruneErr != nil { 166 stdout := stdoutBuilder.String() 167 stderr := stderrBuilder.String() 168 169 // sanitize the output, since it may contain the remote address, which may 170 // contain a password 171 stderrMessage := sanitizer.Replace(stderr) 172 stdoutMessage := sanitizer.Replace(stdout) 173 174 log.Error("Failed to prune mirror repository %s%-v references:\nStdout: %s\nStderr: %s\nErr: %v", wiki, m.Repo, stdoutMessage, stderrMessage, pruneErr) 175 desc := fmt.Sprintf("Failed to prune mirror repository %s'%s' references: %s", wiki, repoPath, stderrMessage) 176 if err := admin_model.CreateRepositoryNotice(desc); err != nil { 177 log.Error("CreateRepositoryNotice: %v", err) 178 } 179 // this if will only be reached on a successful prune so try to get the mirror again 180 } 181 return pruneErr 182} 183 184// runSync returns true if sync finished without error. 185func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bool) { 186 repoPath := m.Repo.RepoPath() 187 wikiPath := m.Repo.WikiPath() 188 timeout := time.Duration(setting.Git.Timeout.Mirror) * time.Second 189 190 log.Trace("SyncMirrors [repo: %-v]: running git remote update...", m.Repo) 191 gitArgs := []string{"remote", "update"} 192 if m.EnablePrune { 193 gitArgs = append(gitArgs, "--prune") 194 } 195 gitArgs = append(gitArgs, m.GetRemoteName()) 196 197 remoteAddr, remoteErr := git.GetRemoteAddress(ctx, repoPath, m.GetRemoteName()) 198 if remoteErr != nil { 199 log.Error("SyncMirrors [repo: %-v]: GetRemoteAddress Error %v", m.Repo, remoteErr) 200 } 201 202 stdoutBuilder := strings.Builder{} 203 stderrBuilder := strings.Builder{} 204 if err := git.NewCommandContext(ctx, gitArgs...). 205 SetDescription(fmt.Sprintf("Mirror.runSync: %s", m.Repo.FullName())). 206 RunInDirTimeoutPipeline(timeout, repoPath, &stdoutBuilder, &stderrBuilder); err != nil { 207 stdout := stdoutBuilder.String() 208 stderr := stderrBuilder.String() 209 210 // sanitize the output, since it may contain the remote address, which may 211 // contain a password 212 sanitizer := util.NewURLSanitizer(remoteAddr, true) 213 stderrMessage := sanitizer.Replace(stderr) 214 stdoutMessage := sanitizer.Replace(stdout) 215 216 // Now check if the error is a resolve reference due to broken reference 217 if strings.Contains(stderr, "unable to resolve reference") && strings.Contains(stderr, "reference broken") { 218 log.Warn("SyncMirrors [repo: %-v]: failed to update mirror repository due to broken references:\nStdout: %s\nStderr: %s\nErr: %v\nAttempting Prune", m.Repo, stdoutMessage, stderrMessage, err) 219 err = nil 220 221 // Attempt prune 222 pruneErr := pruneBrokenReferences(ctx, m, repoPath, timeout, &stdoutBuilder, &stderrBuilder, sanitizer, false) 223 if pruneErr == nil { 224 // Successful prune - reattempt mirror 225 stderrBuilder.Reset() 226 stdoutBuilder.Reset() 227 if err = git.NewCommandContext(ctx, gitArgs...). 228 SetDescription(fmt.Sprintf("Mirror.runSync: %s", m.Repo.FullName())). 229 RunInDirTimeoutPipeline(timeout, repoPath, &stdoutBuilder, &stderrBuilder); err != nil { 230 stdout := stdoutBuilder.String() 231 stderr := stderrBuilder.String() 232 233 // sanitize the output, since it may contain the remote address, which may 234 // contain a password 235 stderrMessage = sanitizer.Replace(stderr) 236 stdoutMessage = sanitizer.Replace(stdout) 237 } 238 } 239 } 240 241 // If there is still an error (or there always was an error) 242 if err != nil { 243 log.Error("SyncMirrors [repo: %-v]: failed to update mirror repository:\nStdout: %s\nStderr: %s\nErr: %v", m.Repo, stdoutMessage, stderrMessage, err) 244 desc := fmt.Sprintf("Failed to update mirror repository '%s': %s", repoPath, stderrMessage) 245 if err = admin_model.CreateRepositoryNotice(desc); err != nil { 246 log.Error("CreateRepositoryNotice: %v", err) 247 } 248 return nil, false 249 } 250 } 251 output := stderrBuilder.String() 252 253 gitRepo, err := git.OpenRepository(repoPath) 254 if err != nil { 255 log.Error("SyncMirrors [repo: %-v]: failed to OpenRepository: %v", m.Repo, err) 256 return nil, false 257 } 258 259 log.Trace("SyncMirrors [repo: %-v]: syncing releases with tags...", m.Repo) 260 if err = repo_module.SyncReleasesWithTags(m.Repo, gitRepo); err != nil { 261 log.Error("SyncMirrors [repo: %-v]: failed to synchronize tags to releases: %v", m.Repo, err) 262 } 263 264 if m.LFS && setting.LFS.StartServer { 265 log.Trace("SyncMirrors [repo: %-v]: syncing LFS objects...", m.Repo) 266 endpoint := lfs.DetermineEndpoint(remoteAddr.String(), m.LFSEndpoint) 267 lfsClient := lfs.NewClient(endpoint, nil) 268 if err = repo_module.StoreMissingLfsObjectsInRepository(ctx, m.Repo, gitRepo, lfsClient); err != nil { 269 log.Error("SyncMirrors [repo: %-v]: failed to synchronize LFS objects for repository: %v", m.Repo, err) 270 } 271 } 272 gitRepo.Close() 273 274 log.Trace("SyncMirrors [repo: %-v]: updating size of repository", m.Repo) 275 if err := models.UpdateRepoSize(db.DefaultContext, m.Repo); err != nil { 276 log.Error("SyncMirrors [repo: %-v]: failed to update size for mirror repository: %v", m.Repo, err) 277 } 278 279 if m.Repo.HasWiki() { 280 log.Trace("SyncMirrors [repo: %-v Wiki]: running git remote update...", m.Repo) 281 stderrBuilder.Reset() 282 stdoutBuilder.Reset() 283 if err := git.NewCommandContext(ctx, "remote", "update", "--prune", m.GetRemoteName()). 284 SetDescription(fmt.Sprintf("Mirror.runSync Wiki: %s ", m.Repo.FullName())). 285 RunInDirTimeoutPipeline(timeout, wikiPath, &stdoutBuilder, &stderrBuilder); err != nil { 286 stdout := stdoutBuilder.String() 287 stderr := stderrBuilder.String() 288 289 // sanitize the output, since it may contain the remote address, which may 290 // contain a password 291 292 remoteAddr, remoteErr := git.GetRemoteAddress(ctx, wikiPath, m.GetRemoteName()) 293 if remoteErr != nil { 294 log.Error("SyncMirrors [repo: %-v Wiki]: unable to get GetRemoteAddress Error %v", m.Repo, remoteErr) 295 } 296 297 // sanitize the output, since it may contain the remote address, which may 298 // contain a password 299 sanitizer := util.NewURLSanitizer(remoteAddr, true) 300 stderrMessage := sanitizer.Replace(stderr) 301 stdoutMessage := sanitizer.Replace(stdout) 302 303 // Now check if the error is a resolve reference due to broken reference 304 if strings.Contains(stderrMessage, "unable to resolve reference") && strings.Contains(stderrMessage, "reference broken") { 305 log.Warn("SyncMirrors [repo: %-v Wiki]: failed to update mirror wiki repository due to broken references:\nStdout: %s\nStderr: %s\nErr: %v\nAttempting Prune", m.Repo, stdoutMessage, stderrMessage, err) 306 err = nil 307 308 // Attempt prune 309 pruneErr := pruneBrokenReferences(ctx, m, repoPath, timeout, &stdoutBuilder, &stderrBuilder, sanitizer, true) 310 if pruneErr == nil { 311 // Successful prune - reattempt mirror 312 stderrBuilder.Reset() 313 stdoutBuilder.Reset() 314 315 if err = git.NewCommandContext(ctx, "remote", "update", "--prune", m.GetRemoteName()). 316 SetDescription(fmt.Sprintf("Mirror.runSync Wiki: %s ", m.Repo.FullName())). 317 RunInDirTimeoutPipeline(timeout, wikiPath, &stdoutBuilder, &stderrBuilder); err != nil { 318 stdout := stdoutBuilder.String() 319 stderr := stderrBuilder.String() 320 stderrMessage = sanitizer.Replace(stderr) 321 stdoutMessage = sanitizer.Replace(stdout) 322 } 323 } 324 } 325 326 // If there is still an error (or there always was an error) 327 if err != nil { 328 log.Error("SyncMirrors [repo: %-v Wiki]: failed to update mirror repository wiki:\nStdout: %s\nStderr: %s\nErr: %v", m.Repo, stdoutMessage, stderrMessage, err) 329 desc := fmt.Sprintf("Failed to update mirror repository wiki '%s': %s", wikiPath, stderrMessage) 330 if err = admin_model.CreateRepositoryNotice(desc); err != nil { 331 log.Error("CreateRepositoryNotice: %v", err) 332 } 333 return nil, false 334 } 335 } 336 log.Trace("SyncMirrors [repo: %-v Wiki]: git remote update complete", m.Repo) 337 } 338 339 log.Trace("SyncMirrors [repo: %-v]: invalidating mirror branch caches...", m.Repo) 340 branches, _, err := git.GetBranchesByPath(m.Repo.RepoPath(), 0, 0) 341 if err != nil { 342 log.Error("SyncMirrors [repo: %-v]: failed to GetBranches: %v", m.Repo, err) 343 return nil, false 344 } 345 346 for _, branch := range branches { 347 cache.Remove(m.Repo.GetCommitsCountCacheKey(branch.Name, true)) 348 } 349 350 m.UpdatedUnix = timeutil.TimeStampNow() 351 return parseRemoteUpdateOutput(output), true 352} 353 354// SyncPullMirror starts the sync of the pull mirror and schedules the next run. 355func SyncPullMirror(ctx context.Context, repoID int64) bool { 356 log.Trace("SyncMirrors [repo_id: %v]", repoID) 357 defer func() { 358 err := recover() 359 if err == nil { 360 return 361 } 362 // There was a panic whilst syncMirrors... 363 log.Error("PANIC whilst SyncMirrors[repo_id: %d] Panic: %v\nStacktrace: %s", repoID, err, log.Stack(2)) 364 }() 365 366 m, err := repo_model.GetMirrorByRepoID(repoID) 367 if err != nil { 368 log.Error("SyncMirrors [repo_id: %v]: unable to GetMirrorByRepoID: %v", repoID, err) 369 return false 370 } 371 372 ctx, _, finished := process.GetManager().AddContext(ctx, fmt.Sprintf("Syncing Mirror %s/%s", m.Repo.OwnerName, m.Repo.Name)) 373 defer finished() 374 375 log.Trace("SyncMirrors [repo: %-v]: Running Sync", m.Repo) 376 results, ok := runSync(ctx, m) 377 if !ok { 378 return false 379 } 380 381 log.Trace("SyncMirrors [repo: %-v]: Scheduling next update", m.Repo) 382 m.ScheduleNextUpdate() 383 if err = repo_model.UpdateMirror(m); err != nil { 384 log.Error("SyncMirrors [repo: %-v]: failed to UpdateMirror with next update date: %v", m.Repo, err) 385 return false 386 } 387 388 var gitRepo *git.Repository 389 if len(results) == 0 { 390 log.Trace("SyncMirrors [repo: %-v]: no branches updated", m.Repo) 391 } else { 392 log.Trace("SyncMirrors [repo: %-v]: %d branches updated", m.Repo, len(results)) 393 gitRepo, err = git.OpenRepositoryCtx(ctx, m.Repo.RepoPath()) 394 if err != nil { 395 log.Error("SyncMirrors [repo: %-v]: unable to OpenRepository: %v", m.Repo, err) 396 return false 397 } 398 defer gitRepo.Close() 399 400 if ok := checkAndUpdateEmptyRepository(m, gitRepo, results); !ok { 401 return false 402 } 403 } 404 405 for _, result := range results { 406 // Discard GitHub pull requests, i.e. refs/pull/* 407 if strings.HasPrefix(result.refName, git.PullPrefix) { 408 continue 409 } 410 411 tp, _ := git.SplitRefName(result.refName) 412 413 // Create reference 414 if result.oldCommitID == gitShortEmptySha { 415 if tp == git.TagPrefix { 416 tp = "tag" 417 } else if tp == git.BranchPrefix { 418 tp = "branch" 419 } 420 commitID, err := gitRepo.GetRefCommitID(result.refName) 421 if err != nil { 422 log.Error("SyncMirrors [repo: %-v]: unable to GetRefCommitID [ref_name: %s]: %v", m.Repo, result.refName, err) 423 continue 424 } 425 notification.NotifySyncPushCommits(m.Repo.MustOwner(), m.Repo, &repo_module.PushUpdateOptions{ 426 RefFullName: result.refName, 427 OldCommitID: git.EmptySHA, 428 NewCommitID: commitID, 429 }, repo_module.NewPushCommits()) 430 notification.NotifySyncCreateRef(m.Repo.MustOwner(), m.Repo, tp, result.refName) 431 continue 432 } 433 434 // Delete reference 435 if result.newCommitID == gitShortEmptySha { 436 notification.NotifySyncDeleteRef(m.Repo.MustOwner(), m.Repo, tp, result.refName) 437 continue 438 } 439 440 // Push commits 441 oldCommitID, err := git.GetFullCommitID(gitRepo.Path, result.oldCommitID) 442 if err != nil { 443 log.Error("SyncMirrors [repo: %-v]: unable to get GetFullCommitID[%s]: %v", m.Repo, result.oldCommitID, err) 444 continue 445 } 446 newCommitID, err := git.GetFullCommitID(gitRepo.Path, result.newCommitID) 447 if err != nil { 448 log.Error("SyncMirrors [repo: %-v]: unable to get GetFullCommitID [%s]: %v", m.Repo, result.newCommitID, err) 449 continue 450 } 451 commits, err := gitRepo.CommitsBetweenIDs(newCommitID, oldCommitID) 452 if err != nil { 453 log.Error("SyncMirrors [repo: %-v]: unable to get CommitsBetweenIDs [new_commit_id: %s, old_commit_id: %s]: %v", m.Repo, newCommitID, oldCommitID, err) 454 continue 455 } 456 457 theCommits := repo_module.GitToPushCommits(commits) 458 if len(theCommits.Commits) > setting.UI.FeedMaxCommitNum { 459 theCommits.Commits = theCommits.Commits[:setting.UI.FeedMaxCommitNum] 460 } 461 462 theCommits.CompareURL = m.Repo.ComposeCompareURL(oldCommitID, newCommitID) 463 464 notification.NotifySyncPushCommits(m.Repo.MustOwner(), m.Repo, &repo_module.PushUpdateOptions{ 465 RefFullName: result.refName, 466 OldCommitID: oldCommitID, 467 NewCommitID: newCommitID, 468 }, theCommits) 469 } 470 log.Trace("SyncMirrors [repo: %-v]: done notifying updated branches/tags - now updating last commit time", m.Repo) 471 472 // Get latest commit date and update to current repository updated time 473 commitDate, err := git.GetLatestCommitTime(m.Repo.RepoPath()) 474 if err != nil { 475 log.Error("SyncMirrors [repo: %-v]: unable to GetLatestCommitDate: %v", m.Repo, err) 476 return false 477 } 478 479 if err = repo_model.UpdateRepositoryUpdatedTime(m.RepoID, commitDate); err != nil { 480 log.Error("SyncMirrors [repo: %-v]: unable to update repository 'updated_unix': %v", m.Repo, err) 481 return false 482 } 483 484 log.Trace("SyncMirrors [repo: %-v]: Successfully updated", m.Repo) 485 486 return true 487} 488 489func checkAndUpdateEmptyRepository(m *repo_model.Mirror, gitRepo *git.Repository, results []*mirrorSyncResult) bool { 490 if !m.Repo.IsEmpty { 491 return true 492 } 493 494 hasDefault := false 495 hasMaster := false 496 hasMain := false 497 defaultBranchName := m.Repo.DefaultBranch 498 if len(defaultBranchName) == 0 { 499 defaultBranchName = setting.Repository.DefaultBranch 500 } 501 firstName := "" 502 for _, result := range results { 503 if strings.HasPrefix(result.refName, git.PullPrefix) { 504 continue 505 } 506 tp, name := git.SplitRefName(result.refName) 507 if len(tp) > 0 && tp != git.BranchPrefix { 508 continue 509 } 510 if len(firstName) == 0 { 511 firstName = name 512 } 513 514 hasDefault = hasDefault || name == defaultBranchName 515 hasMaster = hasMaster || name == "master" 516 hasMain = hasMain || name == "main" 517 } 518 519 if len(firstName) > 0 { 520 if hasDefault { 521 m.Repo.DefaultBranch = defaultBranchName 522 } else if hasMaster { 523 m.Repo.DefaultBranch = "master" 524 } else if hasMain { 525 m.Repo.DefaultBranch = "main" 526 } else { 527 m.Repo.DefaultBranch = firstName 528 } 529 // Update the git repository default branch 530 if err := gitRepo.SetDefaultBranch(m.Repo.DefaultBranch); err != nil { 531 if !git.IsErrUnsupportedVersion(err) { 532 log.Error("Failed to update default branch of underlying git repository %-v. Error: %v", m.Repo, err) 533 desc := fmt.Sprintf("Failed to uupdate default branch of underlying git repository '%s': %v", m.Repo.RepoPath(), err) 534 if err = admin_model.CreateRepositoryNotice(desc); err != nil { 535 log.Error("CreateRepositoryNotice: %v", err) 536 } 537 return false 538 } 539 } 540 m.Repo.IsEmpty = false 541 // Update the is empty and default_branch columns 542 if err := repo_model.UpdateRepositoryCols(m.Repo, "default_branch", "is_empty"); err != nil { 543 log.Error("Failed to update default branch of repository %-v. Error: %v", m.Repo, err) 544 desc := fmt.Sprintf("Failed to uupdate default branch of repository '%s': %v", m.Repo.RepoPath(), err) 545 if err = admin_model.CreateRepositoryNotice(desc); err != nil { 546 log.Error("CreateRepositoryNotice: %v", err) 547 } 548 return false 549 } 550 } 551 return true 552} 553