code.gitea.io/gitea@v1.22.3/services/mirror/mirror_push.go (about) 1 // Copyright 2021 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package mirror 5 6 import ( 7 "context" 8 "errors" 9 "fmt" 10 "io" 11 "regexp" 12 "strings" 13 "time" 14 15 "code.gitea.io/gitea/models/db" 16 repo_model "code.gitea.io/gitea/models/repo" 17 "code.gitea.io/gitea/modules/git" 18 "code.gitea.io/gitea/modules/gitrepo" 19 "code.gitea.io/gitea/modules/lfs" 20 "code.gitea.io/gitea/modules/log" 21 "code.gitea.io/gitea/modules/process" 22 "code.gitea.io/gitea/modules/repository" 23 "code.gitea.io/gitea/modules/setting" 24 "code.gitea.io/gitea/modules/timeutil" 25 "code.gitea.io/gitea/modules/util" 26 ) 27 28 var stripExitStatus = regexp.MustCompile(`exit status \d+ - `) 29 30 // AddPushMirrorRemote registers the push mirror remote. 31 func AddPushMirrorRemote(ctx context.Context, m *repo_model.PushMirror, addr string) error { 32 addRemoteAndConfig := func(addr, path string) error { 33 cmd := git.NewCommand(ctx, "remote", "add", "--mirror=push").AddDynamicArguments(m.RemoteName, addr) 34 if strings.Contains(addr, "://") && strings.Contains(addr, "@") { 35 cmd.SetDescription(fmt.Sprintf("remote add %s --mirror=push %s [repo_path: %s]", m.RemoteName, util.SanitizeCredentialURLs(addr), path)) 36 } else { 37 cmd.SetDescription(fmt.Sprintf("remote add %s --mirror=push %s [repo_path: %s]", m.RemoteName, addr, path)) 38 } 39 if _, _, err := cmd.RunStdString(&git.RunOpts{Dir: path}); err != nil { 40 return err 41 } 42 if _, _, err := git.NewCommand(ctx, "config", "--add").AddDynamicArguments("remote."+m.RemoteName+".push", "+refs/heads/*:refs/heads/*").RunStdString(&git.RunOpts{Dir: path}); err != nil { 43 return err 44 } 45 if _, _, err := git.NewCommand(ctx, "config", "--add").AddDynamicArguments("remote."+m.RemoteName+".push", "+refs/tags/*:refs/tags/*").RunStdString(&git.RunOpts{Dir: path}); err != nil { 46 return err 47 } 48 return nil 49 } 50 51 if err := addRemoteAndConfig(addr, m.Repo.RepoPath()); err != nil { 52 return err 53 } 54 55 if m.Repo.HasWiki() { 56 wikiRemoteURL := repository.WikiRemoteURL(ctx, addr) 57 if len(wikiRemoteURL) > 0 { 58 if err := addRemoteAndConfig(wikiRemoteURL, m.Repo.WikiPath()); err != nil { 59 return err 60 } 61 } 62 } 63 64 return nil 65 } 66 67 // RemovePushMirrorRemote removes the push mirror remote. 68 func RemovePushMirrorRemote(ctx context.Context, m *repo_model.PushMirror) error { 69 cmd := git.NewCommand(ctx, "remote", "rm").AddDynamicArguments(m.RemoteName) 70 _ = m.GetRepository(ctx) 71 72 if _, _, err := cmd.RunStdString(&git.RunOpts{Dir: m.Repo.RepoPath()}); err != nil { 73 return err 74 } 75 76 if m.Repo.HasWiki() { 77 if _, _, err := cmd.RunStdString(&git.RunOpts{Dir: m.Repo.WikiPath()}); err != nil { 78 // The wiki remote may not exist 79 log.Warn("Wiki Remote[%d] could not be removed: %v", m.ID, err) 80 } 81 } 82 83 return nil 84 } 85 86 // SyncPushMirror starts the sync of the push mirror and schedules the next run. 87 func SyncPushMirror(ctx context.Context, mirrorID int64) bool { 88 log.Trace("SyncPushMirror [mirror: %d]", mirrorID) 89 defer func() { 90 err := recover() 91 if err == nil { 92 return 93 } 94 // There was a panic whilst syncPushMirror... 95 log.Error("PANIC whilst syncPushMirror[%d] Panic: %v\nStacktrace: %s", mirrorID, err, log.Stack(2)) 96 }() 97 98 // TODO: Handle "!exist" better 99 m, exist, err := db.GetByID[repo_model.PushMirror](ctx, mirrorID) 100 if err != nil || !exist { 101 log.Error("GetPushMirrorByID [%d]: %v", mirrorID, err) 102 return false 103 } 104 105 _ = m.GetRepository(ctx) 106 107 m.LastError = "" 108 109 ctx, _, finished := process.GetManager().AddContext(ctx, fmt.Sprintf("Syncing PushMirror %s/%s to %s", m.Repo.OwnerName, m.Repo.Name, m.RemoteName)) 110 defer finished() 111 112 log.Trace("SyncPushMirror [mirror: %d][repo: %-v]: Running Sync", m.ID, m.Repo) 113 err = runPushSync(ctx, m) 114 if err != nil { 115 log.Error("SyncPushMirror [mirror: %d][repo: %-v]: %v", m.ID, m.Repo, err) 116 m.LastError = stripExitStatus.ReplaceAllLiteralString(err.Error(), "") 117 } 118 119 m.LastUpdateUnix = timeutil.TimeStampNow() 120 121 if err := repo_model.UpdatePushMirror(ctx, m); err != nil { 122 log.Error("UpdatePushMirror [%d]: %v", m.ID, err) 123 124 return false 125 } 126 127 log.Trace("SyncPushMirror [mirror: %d][repo: %-v]: Finished", m.ID, m.Repo) 128 129 return err == nil 130 } 131 132 func runPushSync(ctx context.Context, m *repo_model.PushMirror) error { 133 timeout := time.Duration(setting.Git.Timeout.Mirror) * time.Second 134 135 performPush := func(repo *repo_model.Repository, isWiki bool) error { 136 path := repo.RepoPath() 137 if isWiki { 138 path = repo.WikiPath() 139 } 140 remoteURL, err := git.GetRemoteURL(ctx, path, m.RemoteName) 141 if err != nil { 142 log.Error("GetRemoteAddress(%s) Error %v", path, err) 143 return errors.New("Unexpected error") 144 } 145 146 if setting.LFS.StartServer { 147 log.Trace("SyncMirrors [repo: %-v]: syncing LFS objects...", m.Repo) 148 149 var gitRepo *git.Repository 150 if isWiki { 151 gitRepo, err = gitrepo.OpenWikiRepository(ctx, repo) 152 } else { 153 gitRepo, err = gitrepo.OpenRepository(ctx, repo) 154 } 155 if err != nil { 156 log.Error("OpenRepository: %v", err) 157 return errors.New("Unexpected error") 158 } 159 defer gitRepo.Close() 160 161 endpoint := lfs.DetermineEndpoint(remoteURL.String(), "") 162 lfsClient := lfs.NewClient(endpoint, nil) 163 if err := pushAllLFSObjects(ctx, gitRepo, lfsClient); err != nil { 164 return util.SanitizeErrorCredentialURLs(err) 165 } 166 } 167 168 log.Trace("Pushing %s mirror[%d] remote %s", path, m.ID, m.RemoteName) 169 170 if err := git.Push(ctx, path, git.PushOptions{ 171 Remote: m.RemoteName, 172 Force: true, 173 Mirror: true, 174 Timeout: timeout, 175 }); err != nil { 176 log.Error("Error pushing %s mirror[%d] remote %s: %v", path, m.ID, m.RemoteName, err) 177 178 return util.SanitizeErrorCredentialURLs(err) 179 } 180 181 return nil 182 } 183 184 err := performPush(m.Repo, false) 185 if err != nil { 186 return err 187 } 188 189 if m.Repo.HasWiki() { 190 _, err := git.GetRemoteAddress(ctx, m.Repo.WikiPath(), m.RemoteName) 191 if err == nil { 192 err := performPush(m.Repo, true) 193 if err != nil { 194 return err 195 } 196 } else { 197 log.Trace("Skipping wiki: No remote configured") 198 } 199 } 200 201 return nil 202 } 203 204 func pushAllLFSObjects(ctx context.Context, gitRepo *git.Repository, lfsClient lfs.Client) error { 205 contentStore := lfs.NewContentStore() 206 207 pointerChan := make(chan lfs.PointerBlob) 208 errChan := make(chan error, 1) 209 go lfs.SearchPointerBlobs(ctx, gitRepo, pointerChan, errChan) 210 211 uploadObjects := func(pointers []lfs.Pointer) error { 212 err := lfsClient.Upload(ctx, pointers, func(p lfs.Pointer, objectError error) (io.ReadCloser, error) { 213 if objectError != nil { 214 return nil, objectError 215 } 216 217 content, err := contentStore.Get(p) 218 if err != nil { 219 log.Error("Error reading LFS object %v: %v", p, err) 220 } 221 return content, err 222 }) 223 if err != nil { 224 select { 225 case <-ctx.Done(): 226 return nil 227 default: 228 } 229 } 230 return err 231 } 232 233 var batch []lfs.Pointer 234 for pointerBlob := range pointerChan { 235 exists, err := contentStore.Exists(pointerBlob.Pointer) 236 if err != nil { 237 log.Error("Error checking if LFS object %v exists: %v", pointerBlob.Pointer, err) 238 return err 239 } 240 if !exists { 241 log.Trace("Skipping missing LFS object %v", pointerBlob.Pointer) 242 continue 243 } 244 245 batch = append(batch, pointerBlob.Pointer) 246 if len(batch) >= lfsClient.BatchSize() { 247 if err := uploadObjects(batch); err != nil { 248 return err 249 } 250 batch = nil 251 } 252 } 253 if len(batch) > 0 { 254 if err := uploadObjects(batch); err != nil { 255 return err 256 } 257 } 258 259 err, has := <-errChan 260 if has { 261 log.Error("Error enumerating LFS objects for repository: %v", err) 262 return err 263 } 264 265 return nil 266 } 267 268 func syncPushMirrorWithSyncOnCommit(ctx context.Context, repoID int64) { 269 pushMirrors, err := repo_model.GetPushMirrorsSyncedOnCommit(ctx, repoID) 270 if err != nil { 271 log.Error("repo_model.GetPushMirrorsSyncedOnCommit failed: %v", err) 272 return 273 } 274 275 for _, mirror := range pushMirrors { 276 AddPushMirrorToQueue(mirror.ID) 277 } 278 }