Federated via graft
0

Configure Feed

Select the types of activity you want to include in your feed.

graft-source / internal / sync / issues.go
3.1 kB 110 lines
1package sync 2 3import ( 4 "crypto/sha256" 5 "encoding/hex" 6 "fmt" 7 8 "graft/internal/forgejo" 9 "graft/internal/radicle" 10 "graft/internal/state" 11) 12 13// A marker prepended to mirrored items' bodies so a human browsing either 14// side can tell where an issue/patch actually originated. 15const mirrorNotice = "\n\n---\n_Mirrored automatically, do not edit here — edit at the source instead._" 16 17// IssueSyncer mirrors issues between one Forgejo repository and one Radicle 18// repository, create-only (see package doc for what that means). 19type IssueSyncer struct { 20 RepoPair string 21 Forgejo *forgejo.Client 22 Radicle *radicle.Client 23 State *state.Store 24 ForgejoWebURL string // https://host/owner/repo, for links in the UI 25 RadicleWebURL string // https://explorer/nodes/host/rid, for links in the UI 26} 27 28func (s *IssueSyncer) Sync() error { 29 forgejoIssues, err := s.Forgejo.ListIssues() 30 if err != nil { 31 return fmt.Errorf("list forgejo issues: %w", err) 32 } 33 radicleIssues, err := s.Radicle.ListIssues() 34 if err != nil { 35 return fmt.Errorf("list radicle issues: %w", err) 36 } 37 38 for _, fi := range forgejoIssues { 39 if err := s.mirrorForgejoToRadicle(fi); err != nil { 40 return fmt.Errorf("mirror forgejo issue #%d: %w", fi.Index, err) 41 } 42 } 43 for _, ri := range radicleIssues { 44 if err := s.mirrorRadicleToForgejo(ri); err != nil { 45 return fmt.Errorf("mirror radicle issue %s: %w", ri.ID, err) 46 } 47 } 48 return nil 49} 50 51func (s *IssueSyncer) mirrorForgejoToRadicle(fi forgejo.Issue) error { 52 existing, err := s.State.FindByForgejoID(s.RepoPair, "issue", fi.Index) 53 if err != nil { 54 return err 55 } 56 if existing != nil { 57 return nil // already mirrored; edits not yet synced (see package doc) 58 } 59 60 radicleID, err := s.Radicle.OpenIssue(fi.Title, fi.Body+mirrorNotice) 61 if err != nil { 62 return fmt.Errorf("open radicle issue: %w", err) 63 } 64 65 if err := s.State.Upsert(s.RepoPair, state.ItemMapping{ 66 Kind: "issue", 67 ForgejoID: fi.Index, 68 RadicleID: radicleID, 69 ContentHash: hashText(fi.Title, fi.Body), 70 }); err != nil { 71 return err 72 } 73 s.State.LogActivity(s.RepoPair, "issue", state.ForgejoToRadicle, fi.Title, s.RadicleWebURL+"/issues/"+radicleID) 74 return nil 75} 76 77func (s *IssueSyncer) mirrorRadicleToForgejo(ri radicle.Issue) error { 78 existing, err := s.State.FindByRadicleID(s.RepoPair, "issue", ri.ID) 79 if err != nil { 80 return err 81 } 82 if existing != nil { 83 return nil 84 } 85 86 fi, err := s.Forgejo.CreateIssue(ri.Title, ri.Body()+mirrorNotice) 87 if err != nil { 88 return fmt.Errorf("create forgejo issue: %w", err) 89 } 90 91 if err := s.State.Upsert(s.RepoPair, state.ItemMapping{ 92 Kind: "issue", 93 ForgejoID: fi.Index, 94 RadicleID: ri.ID, 95 ContentHash: hashText(ri.Title, ri.Body()), 96 }); err != nil { 97 return err 98 } 99 s.State.LogActivity(s.RepoPair, "issue", state.RadicleToForgejo, ri.Title, fmt.Sprintf("%s/issues/%d", s.ForgejoWebURL, fi.Index)) 100 return nil 101} 102 103func hashText(parts ...string) string { 104 h := sha256.New() 105 for _, p := range parts { 106 h.Write([]byte(p)) 107 h.Write([]byte{0}) 108 } 109 return hex.EncodeToString(h.Sum(nil)) 110}