| 1 |
|
package graph |
| 2 |
|
|
| 3 |
|
import ( |
| 4 |
|
"context" |
| 5 |
|
"strconv" |
| 6 |
|
|
| 7 |
|
coremodel "sourcecraft.dev/bigbes/sr-ht-core/model" |
| 8 |
|
|
| 9 |
|
"sourcecraft.dev/bigbes/sr-ht-dolt/browse" |
| 10 |
|
"sourcecraft.dev/bigbes/sr-ht-dolt/core" |
| 11 |
|
"sourcecraft.dev/bigbes/sr-ht-dolt/graph/model" |
| 12 |
|
) |
| 13 |
|
|
| 14 |
|
// Page sizes for the database listings. The count travels in the cursor, as it |
| 15 |
|
// does on every other sr.ht service, so a client that asked for a page size |
| 16 |
|
// keeps it across pages without repeating itself. |
| 17 |
|
const ( |
| 18 |
|
defaultPageSize = 25 |
| 19 |
|
maxPageSize = 100 |
| 20 |
|
) |
| 21 |
|
|
| 22 |
|
// page slices one page out of a listing the store has already filtered to what |
| 23 |
|
// the caller may see, and mints the cursor for the next one. |
| 24 |
|
// |
| 25 |
|
// The slicing is in memory rather than in SQL, which is a real limit and not an |
| 26 |
|
// oversight: ListReposForViewer's rule spans three tables and the listings it |
| 27 |
|
// answers are instance-sized (tens of rows), so the whole set is fetched and cut |
| 28 |
|
// here. A listing that outgrows that wants a keyset query in db/, and this is |
| 29 |
|
// the function that would then become a thin wrapper over it. |
| 30 |
|
// |
| 31 |
|
// The cursor carries the id of the first row of the next page and the page size. |
| 32 |
|
// Resuming starts at the first row whose id is at or below it, so a cursor whose |
| 33 |
|
// row was deleted — or made invisible to this caller — between two pages resumes |
| 34 |
|
// at the next row instead of failing or, worse, silently starting over. |
| 35 |
13 |
func page(repos []*core.Repo, cursor *coremodel.Cursor, filter *coremodel.Filter) *model.DatabaseCursor { |
| 36 |
13 |
count := defaultPageSize |
| 37 |
13 |
start := 0 |
| 38 |
13 |
if cursor != nil { |
| 39 |
3 |
if cursor.Count > 0 { |
| 40 |
3 |
count = cursor.Count |
| 41 |
3 |
} |
| 42 |
3 |
if after, err := strconv.Atoi(cursor.Next); err == nil { |
| 43 |
3 |
start = len(repos) |
| 44 |
11 |
for i, repo := range repos { |
| 45 |
11 |
if repo.ID <= after { |
| 46 |
3 |
start = i |
| 47 |
3 |
break |
| 48 |
|
} |
| 49 |
|
} |
| 50 |
|
} |
| 51 |
|
} |
| 52 |
|
// A filter given on this call wins over what the cursor remembers, so a |
| 53 |
|
// client can change the page size mid-walk without minting a new cursor. |
| 54 |
13 |
if filter != nil && filter.Count != nil && *filter.Count > 0 { |
| 55 |
3 |
count = *filter.Count |
| 56 |
3 |
} |
| 57 |
13 |
if count > maxPageSize { |
| 58 |
1 |
count = maxPageSize |
| 59 |
1 |
} |
| 60 |
|
|
| 61 |
13 |
end := start + count |
| 62 |
13 |
if end > len(repos) { |
| 63 |
9 |
end = len(repos) |
| 64 |
9 |
} |
| 65 |
|
|
| 66 |
13 |
out := make([]*model.Database, 0, end-start) |
| 67 |
129 |
for _, repo := range repos[start:end] { |
| 68 |
129 |
out = append(out, model.NewDatabase(repo)) |
| 69 |
129 |
} |
| 70 |
|
|
| 71 |
13 |
page := &model.DatabaseCursor{Results: out} |
| 72 |
13 |
if end < len(repos) { |
| 73 |
4 |
page.Cursor = &coremodel.Cursor{ |
| 74 |
4 |
Next: strconv.Itoa(repos[end].ID), |
| 75 |
4 |
Count: count, |
| 76 |
4 |
} |
| 77 |
4 |
} |
| 78 |
13 |
return page |
| 79 |
|
} |
| 80 |
|
|
| 81 |
|
// logLimit bounds how many commits one Database.log answers. The cap is the |
| 82 |
|
// surface's, not the client's: a log page is a walk of the on-disk store. |
| 83 |
1 |
func logLimit(limit *int) int { |
| 84 |
1 |
if limit == nil || *limit <= 0 { |
| 85 |
1 |
return defaultLogLimit |
| 86 |
1 |
} |
| 87 |
0 |
if *limit > maxLogLimit { |
| 88 |
0 |
return maxLogLimit |
| 89 |
0 |
} |
| 90 |
0 |
return *limit |
| 91 |
|
} |
| 92 |
|
|
| 93 |
|
// cursorNext is the commit hash a log page resumes from, or "" to start at the |
| 94 |
|
// ref's head. |
| 95 |
1 |
func cursorNext(from *coremodel.Cursor) string { |
| 96 |
1 |
if from == nil { |
| 97 |
1 |
return "" |
| 98 |
1 |
} |
| 99 |
0 |
return from.Next |
| 100 |
|
} |
| 101 |
|
|
| 102 |
|
// nextCursor wraps browse's "next hash" in the schema's cursor, or nil when the |
| 103 |
|
// walk reached the root commit. |
| 104 |
1 |
func nextCursor(next string) *coremodel.Cursor { |
| 105 |
1 |
if next == "" { |
| 106 |
0 |
return nil |
| 107 |
0 |
} |
| 108 |
1 |
return &coremodel.Cursor{Next: next} |
| 109 |
|
} |
| 110 |
|
|
| 111 |
|
// resolveRef answers the ref a field should read: the one the query named, or |
| 112 |
|
// the database's default branch. It returns "" — and no error — for a store |
| 113 |
|
// with no branches at all, which is a database nothing has been pushed to yet; |
| 114 |
|
// the caller turns that into the empty answer for its own field. |
| 115 |
4 |
func resolveRef(ctx context.Context, sess BrowseSession, ref *string) (string, error) { |
| 116 |
4 |
if ref != nil && *ref != "" { |
| 117 |
0 |
return *ref, nil |
| 118 |
0 |
} |
| 119 |
4 |
branches, err := sess.Branches(ctx) |
| 120 |
4 |
if err != nil { |
| 121 |
0 |
return "", internalError("Database.ref", err) |
| 122 |
0 |
} |
| 123 |
4 |
if len(branches) == 0 { |
| 124 |
2 |
return "", nil |
| 125 |
2 |
} |
| 126 |
2 |
return browse.DefaultBranch(branches), nil |
| 127 |
|
} |