Signed-off-by: Seongmin Lee git@boltless.me
+456
-2
Diff
round #0
+5
knotmirror/config/config.go
+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
-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
+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
+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
+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
+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
History
1 round
0 comments
boltless.me
submitted
#0
1 commit
expand
collapse
knotmirror/xrpc:
git.{compareRevs,formatPatch,interdiffRevs}
Signed-off-by: Seongmin Lee <git@boltless.me>
merge conflicts detected
expand
collapse
expand
collapse
- knotmirror/config/config.go:11
- knotmirror/knotmirror.go:9
- knotmirror/xrpc/xrpc.go:10
- nix/vm.nix:148