Monorepo for Tangled tangled.org
854
fork

Configure Feed

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

knotmirror/xrpc: git.{compareRevs,formatPatch,interdiffRevs} #310

open opened by boltless.me targeting master from sl/suwkskkoykun
Labels

None yet.

assignee

None yet.

Participants 1
AT URI
at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.pull/3mkxi53xr4o22
+456 -2
Diff #0
+5
knotmirror/config/config.go
··· 11 11 PlcUrl string `env:"MIRROR_PLC_URL, default=https://plc.directory"` 12 12 TapUrl string `env:"MIRROR_TAP_URL, default=http://localhost:2480"` 13 13 DbUrl string `env:"MIRROR_DB_URL, required"` 14 + Redis RedisConfig `env:",prefix=MIRROR_REDIS_"` 14 15 KnotUseSSL bool `env:"MIRROR_KNOT_USE_SSL, default=false"` // use SSL for Knot when not scheme is not specified 15 16 KnotSSRF bool `env:"MIRROR_KNOT_SSRF, default=false"` 16 17 GitRepoBasePath string `env:"MIRROR_GIT_BASEPATH, default=repos"` ··· 31 32 return "http://" + c.Hostname 32 33 } 33 34 35 + type RedisConfig struct { 36 + Addr string `env:"ADDR, default=localhost:6379"` 37 + } 38 + 34 39 type SlurperConfig struct { 35 40 PersistCursorPeriod time.Duration `env:"PERSIST_CURSOR_PERIOD, default=4s"` 36 41 ConcurrencyPerHost int `env:"CONCURRENCY, default=4"`
+9 -1
knotmirror/knotmirror.go
··· 9 9 10 10 "github.com/go-chi/chi/v5" 11 11 "github.com/prometheus/client_golang/prometheus/promhttp" 12 + "github.com/redis/go-redis/v9" 12 13 "tangled.org/core/idresolver" 13 14 "tangled.org/core/knotmirror/config" 14 15 "tangled.org/core/knotmirror/db" ··· 30 31 return fmt.Errorf("initializing db: %w", err) 31 32 } 32 33 34 + var rdb *redis.Client 35 + if cfg.Redis.Addr != "" { 36 + rdb = redis.NewClient(&redis.Options{ 37 + Addr: cfg.Redis.Addr, 38 + }) 39 + } 40 + 33 41 resolver := idresolver.DefaultResolver(cfg.PlcUrl) 34 42 35 43 // NOTE: using plain git-cli for clone/fetch as go-git is too memory-intensive. ··· 53 61 crawler := NewCrawler(logger, db) 54 62 resyncer := NewResyncer(logger, db, gitm, cfg) 55 63 adminpage := NewAdminServer(logger, db, resyncer) 56 - xrpc := xrpc.New(logger, cfg, db, resolver, knotstream) 64 + xrpc := xrpc.New(logger, cfg, db, rdb, resolver, knotstream) 57 65 58 66 // maintain repository list with tap 59 67 // NOTE: this can be removed once we introduce did-for-repo because then we can just listen to KnotStream for #identity events.
+128
knotmirror/xrpc/git_compare_revs.go
··· 1 + package xrpc 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "net/http" 7 + "time" 8 + 9 + "github.com/bluesky-social/indigo/atproto/atclient" 10 + "github.com/bluesky-social/indigo/atproto/syntax" 11 + "github.com/go-git/go-git/v5/plumbing/object" 12 + "tangled.org/core/api/tangled" 13 + "tangled.org/core/knotserver/git" 14 + ) 15 + 16 + const ( 17 + RdbCompareRevs = "compare_revs:%s:%s" 18 + RdbCompareRevsTTL = 24 * time.Hour 19 + ) 20 + 21 + func (x *Xrpc) CompareRevs(w http.ResponseWriter, r *http.Request) { 22 + var ( 23 + repoQuery = r.URL.Query().Get("repo") 24 + rev1 = r.URL.Query().Get("rev1") 25 + rev2 = r.URL.Query().Get("rev2") 26 + ) 27 + 28 + l := x.logger.With("method", "git.compareRevs") 29 + ctx := r.Context() 30 + 31 + repo, err := syntax.ParseATURI(repoQuery) 32 + if err != nil || repo.RecordKey() == "" { 33 + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("repo1 parameter invalid: %s", repoQuery)}) 34 + return 35 + } 36 + 37 + if rev1 == "" { 38 + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: "missing rev1 parameter"}) 39 + return 40 + } 41 + if rev2 == "" { 42 + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: "missing rev2 parameter"}) 43 + return 44 + } 45 + 46 + l = l.With("repo1", repo, "rev1", rev1, "rev2", rev2) 47 + 48 + repoPath, err := x.makeRepoPath(ctx, repo) 49 + if err != nil { 50 + l.Error("error building repo path", "err", err) 51 + writeJson(w, http.StatusNotFound, atclient.ErrorBody{Name: "RepoNotFound", Message: "repository not found"}) 52 + return 53 + } 54 + 55 + gr, err := git.PlainOpen(repoPath) 56 + if err != nil { 57 + l.Error("failed opening git repo", "err", err) 58 + writeJson(w, http.StatusNotFound, atclient.ErrorBody{Name: "RepoNotFound", Message: "repository not found"}) 59 + return 60 + } 61 + 62 + commit1, err := gr.ResolveRevision(rev1) 63 + if err != nil { 64 + l.Error("error resolving revision 1", "err", err) 65 + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "RevisionNotFound", Message: fmt.Sprintf("error resolving revision %s", rev1)}) 66 + return 67 + } 68 + 69 + commit2, err := gr.ResolveRevision(rev2) 70 + if err != nil { 71 + l.Error("error resolving revision 2", "err", err) 72 + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "RevisionNotFound", Message: fmt.Sprintf("error resolving revision %s", rev2)}) 73 + return 74 + } 75 + 76 + rawPatch, err := x.compareRevs(ctx, gr, commit1, commit2) 77 + if err != nil { 78 + l.Error("error comparing revisions", "err", err.Error()) 79 + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "CompareError", Message: "error comparing revisions"}) 80 + return 81 + } 82 + 83 + rev1Hash := commit1.Hash.String() 84 + rev2Hash := commit2.Hash.String() 85 + writeJson(w, http.StatusOK, &tangled.GitTempCompareRevs_Output{ 86 + Rev1: &rev1Hash, 87 + Rev2: &rev2Hash, 88 + Patch: rawPatch, 89 + }) 90 + } 91 + 92 + func (x *Xrpc) compareRevs(ctx context.Context, gr *git.GitRepo, commit1, commit2 *object.Commit) (string, error) { 93 + l := x.logger 94 + mergeBaseCommit, err := gr.MergeBase(commit1, commit2) 95 + if err != nil { 96 + return "", err 97 + } 98 + 99 + if x.rdb != nil { 100 + rawPatch, err := x.rdb.Get(ctx, fmt.Sprintf(RdbCompareRevs, mergeBaseCommit.Hash, commit2.Hash)).Result() 101 + if err != nil { 102 + // no-op 103 + } else { 104 + l.Debug("using cached patch") 105 + return rawPatch, nil 106 + } 107 + } 108 + 109 + diffTree, err := gr.DiffTree(mergeBaseCommit, commit2) 110 + if err != nil { 111 + return "", err 112 + } 113 + 114 + if x.rdb != nil { 115 + go func() { 116 + key := fmt.Sprintf(RdbCompareRevs, mergeBaseCommit.Hash, commit2.Hash) 117 + l.Debug("caching compare patch", "key", key) 118 + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) 119 + defer cancel() 120 + _, err := x.rdb.Set(ctx, key, diffTree.Patch, RdbCompareRevsTTL).Result() 121 + if err != nil { 122 + l.Error("failed to cache compare result", "err", err) 123 + } 124 + }() 125 + } 126 + 127 + return diffTree.Patch, nil 128 + }
+150
knotmirror/xrpc/git_format_patch.go
··· 1 + package xrpc 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "net/http" 7 + "net/url" 8 + "os/exec" 9 + "time" 10 + 11 + "github.com/bluesky-social/indigo/atproto/atclient" 12 + "github.com/bluesky-social/indigo/atproto/syntax" 13 + "tangled.org/core/api/tangled" 14 + "tangled.org/core/knotserver/git" 15 + ) 16 + 17 + const ( 18 + RdbFormatPatch = "format_patch:%s:%s" 19 + RdbFormatPatchTTL = 24 * time.Hour 20 + ) 21 + 22 + func (x *Xrpc) FormatPatch(w http.ResponseWriter, r *http.Request) { 23 + var ( 24 + repo1Query = r.URL.Query().Get("repo1") 25 + rev1 = r.URL.Query().Get("rev1") 26 + repo2Query = r.URL.Query().Get("repo2") // optional 27 + rev2 = r.URL.Query().Get("rev2") 28 + ) 29 + 30 + l := x.logger.With("method", "git.formatPatch") 31 + ctx := r.Context() 32 + 33 + repo1, err := syntax.ParseATURI(repo1Query) 34 + if err != nil || repo1.RecordKey() == "" { 35 + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("repo1 parameter invalid: %s", repo1Query)}) 36 + return 37 + } 38 + 39 + var repo2 syntax.ATURI 40 + if repo2Query == "" { 41 + repo2 = repo1 42 + } else { 43 + repo2, err = syntax.ParseATURI(repo2Query) 44 + if err != nil || repo2.RecordKey() == "" { 45 + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("repo2 parameter invalid: %s", repo2Query)}) 46 + return 47 + } 48 + } 49 + 50 + if rev1 == "" { 51 + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: "missing rev1 parameter"}) 52 + return 53 + } 54 + if rev2 == "" { 55 + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: "missing rev2 parameter"}) 56 + return 57 + } 58 + 59 + l = l.With("repo1", repo1, "repo2", repo2, "rev1", rev1, "rev2", rev2) 60 + 61 + repo2Path, err := x.makeRepoPath(ctx, repo2) 62 + if err != nil { 63 + l.Error("error building repo path", "err", err) 64 + writeJson(w, http.StatusNotFound, atclient.ErrorBody{Name: "RepoNotFound", Message: "repository not found"}) 65 + return 66 + } 67 + 68 + gr, err := git.PlainOpen(repo2Path) 69 + if err != nil { 70 + l.Error("failed opening git repo", "err", err) 71 + writeJson(w, http.StatusNotFound, atclient.ErrorBody{Name: "RepoNotFound", Message: "repository not found"}) 72 + return 73 + } 74 + 75 + repo1Path, err := x.makeRepoPath(ctx, repo1) 76 + if err != nil { 77 + l.Error("error building repo path", "err", err) 78 + writeJson(w, http.StatusNotFound, atclient.ErrorBody{Name: "RepoNotFound", Message: "repository not found"}) 79 + return 80 + } 81 + if repo2Path != repo1Path { 82 + // fetch commit1 from repo1 to repo2 83 + repo1Remote := &url.URL{Scheme: "file", Path: repo1Path} // TODO: ensure repo1Path is absolute path 84 + fetchCmd := exec.Command( 85 + "git", 86 + "-C", repo2Path, 87 + "fetch", "--depth=1", 88 + repo1Remote.String(), rev1, 89 + ) 90 + if err := fetchCmd.Run(); err != nil { 91 + l.Error("error fetching rev1", "err", err) 92 + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "RevisionNotFound", Message: fmt.Sprintf("error resolving revision %s", rev1)}) 93 + return 94 + } 95 + } 96 + 97 + commit1, err := gr.ResolveRevision(rev1) 98 + if err != nil { 99 + l.Error("error resolving revision 1", "err", err) 100 + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "RevisionNotFound", Message: fmt.Sprintf("error resolving revision %s", rev1)}) 101 + return 102 + } 103 + 104 + commit2, err := gr.ResolveRevision(rev2) 105 + if err != nil { 106 + l.Error("error resolving revision 2", "err", err) 107 + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "RevisionNotFound", Message: fmt.Sprintf("error resolving revision %s", rev2)}) 108 + return 109 + } 110 + 111 + rev1Hash := commit1.Hash.String() 112 + rev2Hash := commit2.Hash.String() 113 + 114 + if x.rdb != nil { 115 + rawPatch, err := x.rdb.Get(ctx, fmt.Sprintf(RdbFormatPatch, commit1.Hash, commit2.Hash)).Result() 116 + if err != nil { 117 + // no-op 118 + } else { 119 + writeJson(w, http.StatusOK, &tangled.GitTempFormatPatch_Output{ 120 + Rev1: &rev1Hash, 121 + Rev2: &rev2Hash, 122 + Patch: rawPatch, 123 + }) 124 + return 125 + } 126 + } 127 + 128 + rawPatch, _, err := gr.FormatPatch(commit1, commit2) 129 + if err != nil { 130 + l.Error("error running format-patch", "err", err) 131 + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "CompareError", Message: "error comparing revisions"}) 132 + return 133 + } 134 + 135 + writeJson(w, http.StatusOK, &tangled.GitTempFormatPatch_Output{ 136 + Rev1: &rev1Hash, 137 + Rev2: &rev2Hash, 138 + Patch: rawPatch, 139 + }) 140 + 141 + if x.rdb != nil { 142 + go func() { 143 + ctx := context.Background() 144 + _, err := x.rdb.Set(ctx, fmt.Sprintf(RdbFormatPatch, commit1.Hash, commit2.Hash), rawPatch, RdbFormatPatchTTL).Result() 145 + if err != nil { 146 + l.Error("failed to cache compare result", "err", err) 147 + } 148 + }() 149 + } 150 + }
+153
knotmirror/xrpc/git_interdiff_revs.go
··· 1 + package xrpc 2 + 3 + import ( 4 + "fmt" 5 + "net/http" 6 + "net/url" 7 + "os/exec" 8 + 9 + "github.com/bluesky-social/indigo/atproto/atclient" 10 + "github.com/bluesky-social/indigo/atproto/syntax" 11 + "tangled.org/core/knotserver/git" 12 + ) 13 + 14 + func (x *Xrpc) InterdiffRevs(w http.ResponseWriter, r *http.Request) { 15 + var ( 16 + targetRepoQuery = r.URL.Query().Get("targetRepo") 17 + base = r.URL.Query().Get("base") // target branch 18 + sourceRepoQuery = r.URL.Query().Get("sourceRepo") // optional 19 + rev1 = r.URL.Query().Get("rev1") 20 + rev2 = r.URL.Query().Get("rev2") 21 + ) 22 + 23 + l := x.logger.With("method", "git.interdiffRevs") 24 + ctx := r.Context() 25 + 26 + targetRepo, err := syntax.ParseATURI(targetRepoQuery) 27 + if err != nil || targetRepo.RecordKey() == "" { 28 + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("repo1 parameter invalid: %s", targetRepoQuery)}) 29 + return 30 + } 31 + 32 + var sourceRepo syntax.ATURI 33 + if sourceRepoQuery == "" { 34 + sourceRepo = targetRepo 35 + } else { 36 + sourceRepo, err = syntax.ParseATURI(sourceRepoQuery) 37 + if err != nil || sourceRepo.RecordKey() == "" { 38 + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("repo2 parameter invalid: %s", sourceRepoQuery)}) 39 + return 40 + } 41 + } 42 + 43 + if base == "" { 44 + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: "missing base parameter"}) 45 + return 46 + } 47 + if rev1 == "" { 48 + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: "missing rev1 parameter"}) 49 + return 50 + } 51 + if rev2 == "" { 52 + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: "missing rev2 parameter"}) 53 + return 54 + } 55 + 56 + l = l.With("target", targetRepo, "base", base, "source", sourceRepo, "rev1", rev1, "rev2", rev2) 57 + 58 + sourceRepoPath, err := x.makeRepoPath(ctx, sourceRepo) 59 + if err != nil { 60 + l.Error("error building repo path", "err", err) 61 + writeJson(w, http.StatusNotFound, atclient.ErrorBody{Name: "RepoNotFound", Message: "repository not found"}) 62 + return 63 + } 64 + 65 + gr, err := git.PlainOpen(sourceRepoPath) 66 + if err != nil { 67 + l.Error("failed opening git repo", "err", err) 68 + writeJson(w, http.StatusNotFound, atclient.ErrorBody{Name: "RepoNotFound", Message: "repository not found"}) 69 + return 70 + } 71 + 72 + targetRepoPath, err := x.makeRepoPath(ctx, targetRepo) 73 + if err != nil { 74 + l.Error("error building repo path", "err", err) 75 + writeJson(w, http.StatusNotFound, atclient.ErrorBody{Name: "RepoNotFound", Message: "repository not found"}) 76 + return 77 + } 78 + if sourceRepoPath != targetRepoPath { 79 + // fetch `base` from targetRepo to sourceRepo 80 + targetRemote := &url.URL{Scheme: "file", Path: targetRepoPath} // TODO: ensure targetRepoPath is absolute path 81 + fetchCmd := exec.Command( 82 + "git", 83 + "-C", sourceRepoPath, 84 + "fetch", 85 + targetRemote.String(), base, 86 + ) 87 + if err := fetchCmd.Run(); err != nil { 88 + l.Error("error fetching base", "err", err) 89 + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "RevisionNotFound", Message: fmt.Sprintf("error resolving revision %s", rev1)}) 90 + return 91 + } 92 + } 93 + 94 + baseCommit, err := gr.ResolveRevision(base) 95 + if err != nil { 96 + l.Error("error resolving base rev", "err", err) 97 + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "RevisionNotFound", Message: fmt.Sprintf("error resolving revision %s", rev1)}) 98 + return 99 + } 100 + 101 + commit1, err := gr.ResolveRevision(rev1) 102 + if err != nil { 103 + l.Error("error resolving revision 1", "err", err) 104 + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "RevisionNotFound", Message: fmt.Sprintf("error resolving revision %s", rev1)}) 105 + return 106 + } 107 + 108 + commit2, err := gr.ResolveRevision(rev2) 109 + if err != nil { 110 + l.Error("error resolving revision 2", "err", err) 111 + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "RevisionNotFound", Message: fmt.Sprintf("error resolving revision %s", rev2)}) 112 + return 113 + } 114 + 115 + l = l.With("base", baseCommit.Hash, "rev1", commit1.Hash, "rev2", commit2.Hash) 116 + l.Debug("interdiff") 117 + 118 + rev1Patch, err := x.compareRevs(ctx, gr, baseCommit, commit1) 119 + if err != nil { 120 + l.Error("error comparing base...rev1", "err", err.Error()) 121 + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "CompareError", Message: "error comparing revisions"}) 122 + } 123 + rev2Patch, err := x.compareRevs(ctx, gr, baseCommit, commit2) 124 + if err != nil { 125 + l.Error("error comparing base...rev2", "err", err.Error()) 126 + writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "CompareError", Message: "error comparing revisions"}) 127 + } 128 + 129 + // NOTE: we can't json-decode `patchutil.InterdiffResult`. 130 + // So we just pass the unfinished values instead. 131 + writeJson(w, http.StatusOK, struct{ 132 + Patch1 string 133 + Patch2 string 134 + }{ 135 + Patch1: rev1Patch, 136 + Patch2: rev2Patch, 137 + }) 138 + 139 + // TODO(boltless): run interdiff from knotmirror & pass diff to appview 140 + 141 + // rev1Diff, err := patchutil.AsDiff(rev1Patch) 142 + // if err != nil { 143 + // l.Error("error parsing base...rev1", "err", err.Error()) 144 + // writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "CompareError", Message: "error comparing revisions"}) 145 + // } 146 + // rev2Diff, err := patchutil.AsDiff(rev2Patch) 147 + // if err != nil { 148 + // l.Error("error parsing base...rev2", "err", err.Error()) 149 + // writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "CompareError", Message: "error comparing revisions"}) 150 + // } 151 + // 152 + // writeJson(w, http.StatusOK, patchutil.Interdiff(rev1Diff, rev2Diff)) 153 + }
+7 -1
knotmirror/xrpc/xrpc.go
··· 10 10 11 11 "github.com/bluesky-social/indigo/atproto/atclient" 12 12 "github.com/go-chi/chi/v5" 13 + "github.com/redis/go-redis/v9" 13 14 "tangled.org/core/api/tangled" 14 15 "tangled.org/core/idresolver" 15 16 "tangled.org/core/knotmirror/config" ··· 20 21 type Xrpc struct { 21 22 cfg *config.Config 22 23 db *sql.DB 24 + rdb *redis.Client 23 25 resolver *idresolver.Resolver 24 26 ks *knotstream.KnotStream 25 27 logger *slog.Logger 26 28 httpClient *http.Client 27 29 } 28 30 29 - func New(logger *slog.Logger, cfg *config.Config, db *sql.DB, resolver *idresolver.Resolver, ks *knotstream.KnotStream) *Xrpc { 31 + func New(logger *slog.Logger, cfg *config.Config, db *sql.DB, rdb *redis.Client, resolver *idresolver.Resolver, ks *knotstream.KnotStream) *Xrpc { 30 32 return &Xrpc{ 31 33 cfg: cfg, 32 34 db: db, 35 + rdb: rdb, 33 36 resolver: resolver, 34 37 ks: ks, 35 38 logger: log.SubLogger(logger, "xrpc"), ··· 55 58 r.Get("/"+tangled.GitTempListCommitsNSID, x.ListCommits) 56 59 r.Get("/"+tangled.GitTempListLanguagesNSID, x.ListLanguages) 57 60 r.Get("/"+tangled.GitTempListTagsNSID, x.ListTags) 61 + r.Get("/"+tangled.GitTempCompareRevsNSID, x.CompareRevs) 62 + r.Get("/"+tangled.GitTempFormatPatchNSID, x.FormatPatch) 63 + r.Get("/"+tangled.GitTempInterdiffRevsNSID, x.InterdiffRevs) 58 64 r.Get("/"+tangled.RepoBlobNSID, x.RepoBlob) 59 65 r.Post("/"+tangled.SyncRequestCrawlNSID, x.RequestCrawl) 60 66
+4
nix/vm.nix
··· 148 148 host all tnglr 127.0.0.1/32 trust 149 149 ''; 150 150 }; 151 + services.redis.servers.km = { 152 + enable = true; 153 + port = 6379; 154 + }; 151 155 services.tangled.knotmirror = { 152 156 enable = true; 153 157 listenAddr = "0.0.0.0:7000";

History

1 round 0 comments
sign up or login to add to the discussion
boltless.me submitted #0
1 commit
expand
knotmirror/xrpc: git.{compareRevs,formatPatch,interdiffRevs}
merge conflicts detected
expand
  • knotmirror/config/config.go:11
  • knotmirror/knotmirror.go:9
  • knotmirror/xrpc/xrpc.go:10
  • nix/vm.nix:148
expand 0 comments