-7
appview/db/db.go
-7
appview/db/db.go
···
393
393
db.Exec("pragma foreign_keys = off;")
394
394
runMigration(db, "recreate-pulls-column-for-stacking-support", func(tx *sql.Tx) error {
395
395
_, err := tx.Exec(`
396
-
-- disable fk to not delete submissions table
397
-
pragma foreign_keys = off;
398
-
399
396
create table pulls_new (
400
397
-- identifiers
401
398
id integer primary key autoincrement,
···
446
443
447
444
drop table pulls;
448
445
alter table pulls_new rename to pulls;
449
-
450
-
-- reenable fk
451
-
pragma foreign_keys = on;
452
446
`)
453
447
return err
454
448
})
455
449
db.Exec("pragma foreign_keys = on;")
456
450
457
-
>>>>>>> Conflict 1 of 1 ends
458
451
return &DB{db}, nil
459
452
}
460
453
+37
-2
appview/db/pulls.go
+37
-2
appview/db/pulls.go
···
49
49
func (p PullState) IsClosed() bool {
50
50
return p == PullClosed
51
51
}
52
-
func (p PullState) IsDelete() bool {
52
+
func (p PullState) IsDeleted() bool {
53
53
return p == PullDeleted
54
54
}
55
55
···
885
885
886
886
func SetPullState(e Execer, repoAt syntax.ATURI, pullId int, pullState PullState) error {
887
887
_, err := e.Exec(
888
-
`update pulls set state = ? where repo_at = ? and pull_id = ? and state <> ?`,
888
+
`update pulls set state = ? where repo_at = ? and pull_id = ? and (state <> ? or state <> ?)`,
889
889
pullState,
890
890
repoAt,
891
891
pullId,
892
892
PullDeleted, // only update state of non-deleted pulls
893
+
PullMerged, // only update state of non-merged pulls
893
894
)
894
895
return err
895
896
}
···
1032
1033
return pulls, nil
1033
1034
}
1034
1035
1036
+
func GetAbandonedPulls(e Execer, stackId string) ([]*Pull, error) {
1037
+
pulls, err := GetPulls(
1038
+
e,
1039
+
FilterEq("stack_id", stackId),
1040
+
FilterEq("state", PullDeleted),
1041
+
)
1042
+
if err != nil {
1043
+
return nil, err
1044
+
}
1045
+
1046
+
return pulls, nil
1047
+
}
1048
+
1035
1049
// position of this pull in the stack
1036
1050
func (stack Stack) Position(pull *Pull) int {
1037
1051
return slices.IndexFunc(stack, func(p *Pull) bool {
···
1096
1110
}
1097
1111
return combined.String()
1098
1112
}
1113
+
1114
+
// filter out PRs that are "active"
1115
+
//
1116
+
// PRs that are still open are active
1117
+
func (stack Stack) Mergeable() Stack {
1118
+
var mergeable Stack
1119
+
1120
+
for _, p := range stack {
1121
+
// stop at the first merged PR
1122
+
if p.State == PullMerged || p.State == PullClosed {
1123
+
break
1124
+
}
1125
+
1126
+
// skip over deleted PRs
1127
+
if p.State != PullDeleted {
1128
+
mergeable = append(mergeable, p)
1129
+
}
1130
+
}
1131
+
1132
+
return mergeable
1133
+
}
+10
-8
appview/pages/pages.go
+10
-8
appview/pages/pages.go
···
738
738
}
739
739
740
740
type RepoSinglePullParams struct {
741
-
LoggedInUser *oauth.User
742
-
RepoInfo repoinfo.RepoInfo
743
-
Active string
744
-
DidHandleMap map[string]string
745
-
Pull *db.Pull
746
-
Stack db.Stack
747
-
MergeCheck types.MergeCheckResponse
748
-
ResubmitCheck ResubmitResult
741
+
LoggedInUser *oauth.User
742
+
RepoInfo repoinfo.RepoInfo
743
+
Active string
744
+
DidHandleMap map[string]string
745
+
Pull *db.Pull
746
+
Stack db.Stack
747
+
AbandonedPulls []*db.Pull
748
+
MergeCheck types.MergeCheckResponse
749
+
ResubmitCheck ResubmitResult
749
750
}
750
751
751
752
func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error {
···
837
838
RoundNumber int
838
839
MergeCheck types.MergeCheckResponse
839
840
ResubmitCheck ResubmitResult
841
+
Stack db.Stack
840
842
}
841
843
842
844
func (p *Pages) PullActionsFragment(w io.Writer, params PullActionsParams) error {
+12
-1
appview/pages/templates/repo/pulls/fragments/pullActions.html
+12
-1
appview/pages/templates/repo/pulls/fragments/pullActions.html
···
1
1
{{ define "repo/pulls/fragments/pullActions" }}
2
2
{{ $lastIdx := sub (len .Pull.Submissions) 1 }}
3
3
{{ $roundNumber := .RoundNumber }}
4
+
{{ $stack := .Stack }}
5
+
6
+
{{ $totalPulls := sub 0 1 }}
7
+
{{ $below := sub 0 1 }}
8
+
{{ $stackCount := "" }}
9
+
{{ if .Pull.IsStacked }}
10
+
{{ $totalPulls = len $stack }}
11
+
{{ $below = $stack.Below .Pull }}
12
+
{{ $mergeable := len $below.Mergeable }}
13
+
{{ $stackCount = printf "%d/%d" $mergeable $totalPulls }}
14
+
{{ end }}
4
15
5
16
{{ $isPushAllowed := .RepoInfo.Roles.IsPushAllowed }}
6
17
{{ $isMerged := .Pull.State.IsMerged }}
···
33
44
hx-confirm="Are you sure you want to merge pull #{{ .Pull.PullId }} into the `{{ .Pull.TargetBranch }}` branch?"
34
45
class="btn p-2 flex items-center gap-2 group" {{ $disabled }}>
35
46
{{ i "git-merge" "w-4 h-4" }}
36
-
<span>merge</span>
47
+
<span>merge{{if $stackCount}} {{$stackCount}}{{end}}</span>
37
48
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
38
49
</button>
39
50
{{ end }}
+4
-1
appview/pages/templates/repo/pulls/fragments/pullHeader.html
+4
-1
appview/pages/templates/repo/pulls/fragments/pullHeader.html
···
2
2
<header class="flex items-center gap-2 pb-2">
3
3
{{ block "pullState" .Pull }} {{ end }}
4
4
<h1 class="text-2xl dark:text-white">
5
-
<span class="text-gray-500 dark:text-gray-400">#{{ .Pull.PullId }}</span>
6
5
{{ .Pull.Title }}
6
+
<span class="text-gray-500 dark:text-gray-400">#{{ .Pull.PullId }}</span>
7
7
</h1>
8
8
</header>
9
9
···
96
96
{{ else if .State.IsMerged }}
97
97
{{ $bgColor = "bg-purple-600 dark:bg-purple-700" }}
98
98
{{ $icon = "git-merge" }}
99
+
{{ else if .State.IsDeleted }}
100
+
{{ $bgColor = "bg-red-600 dark:bg-red-700" }}
101
+
{{ $icon = "git-pull-request-closed" }}
99
102
{{ end }}
100
103
101
104
<div id="state" class="inline-flex items-center rounded px-3 py-1 {{ $bgColor }}" >
+40
-21
appview/pages/templates/repo/pulls/fragments/pullStack.html
+40
-21
appview/pages/templates/repo/pulls/fragments/pullStack.html
···
1
1
{{ define "repo/pulls/fragments/pullStack" }}
2
-
<div class="grid grid-cols-1 rounded border border-gray-200 dark:border-gray-700 divide-y divide-gray-200 dark:divide-gray-700">
3
-
{{ range $pull := .Stack }}
4
-
{{ $isCurrent := false }}
5
-
{{ with $.Pull }}
6
-
{{ $isCurrent = eq $pull.PullId $.Pull.PullId }}
7
-
{{ end }}
8
-
<a href="/{{ $.RepoInfo.FullName }}/pulls/{{ $pull.PullId }}" class="no-underline hover:no-underline hover:bg-gray-100 hover:dark:bg-gray-700">
9
-
<div class="flex gap-2 items-center px-2 {{ if $isCurrent }}bg-gray-100 dark:bg-gray-700{{ end }}">
10
-
{{ if $isCurrent }}
11
-
{{ i "arrow-right" "w-4 h-4" }}
12
-
{{ end }}
13
-
<div class="{{ if not $isCurrent }} ml-6 {{ end }} w-full py-2">
14
-
{{ block "summarizedHeader" $pull }} {{ end }}
15
-
</div>
16
-
</div>
17
-
</a>
18
-
{{ end }}
19
-
</div>
2
+
<p class="text-sm font-bold p-2 dark:text-white">STACK</p>
3
+
{{ block "pullList" (list .Stack $) }} {{ end }}
4
+
5
+
{{ if gt (len .AbandonedPulls) 0 }}
6
+
<p class="text-sm font-bold p-2 dark:text-white">ABANDONED PULLS</p>
7
+
{{ block "pullList" (list .AbandonedPulls $) }} {{ end }}
8
+
{{ end }}
20
9
{{ end }}
21
10
22
11
{{ define "summarizedHeader" }}
23
12
<div class="flex text-sm items-center justify-between w-full">
24
-
<div class="flex items-center gap-2">
13
+
<div class="flex items-center gap-2 text-ellipsis">
25
14
{{ block "summarizedPullState" . }} {{ end }}
26
15
<span>
27
16
<span class="text-gray-500 dark:text-gray-400">#{{ .PullId }}</span>
···
35
24
{{ $commentCount := len $lastSubmission.Comments }}
36
25
<span>
37
26
<div class="inline-flex items-center gap-2">
27
+
{{ i "message-square" "w-3 h-3 md:hidden" }}
38
28
{{ $commentCount }}
39
-
comment{{if ne $commentCount 1}}s{{end}}
29
+
<span class="hidden md:inline">comment{{if ne $commentCount 1}}s{{end}}</span>
40
30
</div>
41
31
</span>
42
32
<span class="mx-2 before:content-['·'] before:select-none"></span>
43
-
<span>round <span class="font-mono">#{{ $latestRound }}</span></span>
33
+
<span>
34
+
<span class="hidden md:inline">round</span>
35
+
<span class="font-mono">#{{ $latestRound }}</span>
36
+
</span>
44
37
</div>
45
38
</div>
46
39
{{ end }}
···
55
48
{{ else if .State.IsMerged }}
56
49
{{ $fgColor = "text-purple-600 dark:text-purple-500" }}
57
50
{{ $icon = "git-merge" }}
51
+
{{ else if .State.IsDeleted }}
52
+
{{ $fgColor = "text-red-600 dark:text-red-500" }}
53
+
{{ $icon = "git-pull-request-closed" }}
58
54
{{ end }}
59
55
60
56
{{ $style := printf "w-4 h-4 %s" $fgColor }}
61
57
62
58
{{ i $icon $style }}
63
59
{{ end }}
60
+
61
+
{{ define "pullList" }}
62
+
{{ $list := index . 0 }}
63
+
{{ $root := index . 1 }}
64
+
<div class="grid grid-cols-1 rounded border border-gray-200 dark:border-gray-700 divide-y divide-gray-200 dark:divide-gray-700">
65
+
{{ range $pull := $list }}
66
+
{{ $isCurrent := false }}
67
+
{{ with $root.Pull }}
68
+
{{ $isCurrent = eq $pull.PullId $root.Pull.PullId }}
69
+
{{ end }}
70
+
<a href="/{{ $root.RepoInfo.FullName }}/pulls/{{ $pull.PullId }}" class="no-underline hover:no-underline hover:bg-gray-100/25 hover:dark:bg-gray-700/25">
71
+
<div class="flex gap-2 items-center px-2 {{ if $isCurrent }}bg-gray-100/50 dark:bg-gray-700/50{{ end }}">
72
+
{{ if $isCurrent }}
73
+
{{ i "arrow-right" "w-4 h-4" }}
74
+
{{ end }}
75
+
<div class="{{ if not $isCurrent }} ml-6 {{ end }} w-full py-2">
76
+
{{ block "summarizedHeader" $pull }} {{ end }}
77
+
</div>
78
+
</div>
79
+
</a>
80
+
{{ end }}
81
+
</div>
82
+
{{ end }}
+9
-3
appview/pages/templates/repo/pulls/pull.html
+9
-3
appview/pages/templates/repo/pulls/pull.html
···
15
15
16
16
{{ if .Pull.IsStacked }}
17
17
<div class="mt-8">
18
-
<p class="text-sm font-bold p-2 dark:text-white">STACK</p>
19
18
{{ template "repo/pulls/fragments/pullStack" . }}
20
19
</div>
21
20
{{ end }}
···
85
84
{{ end }}
86
85
</div>
87
86
</summary>
88
-
87
+
89
88
{{ if .IsFormatPatch }}
90
89
{{ $patches := .AsFormatPatch }}
91
90
{{ $round := .RoundNumber }}
···
169
168
{{ end }}
170
169
171
170
{{ if $.LoggedInUser }}
172
-
{{ template "repo/pulls/fragments/pullActions" (dict "LoggedInUser" $.LoggedInUser "Pull" $.Pull "RepoInfo" $.RepoInfo "RoundNumber" .RoundNumber "MergeCheck" $.MergeCheck "ResubmitCheck" $.ResubmitCheck) }}
171
+
{{ template "repo/pulls/fragments/pullActions" (dict "LoggedInUser" $.LoggedInUser "Pull" $.Pull "RepoInfo" $.RepoInfo "RoundNumber" .RoundNumber "MergeCheck" $.MergeCheck "ResubmitCheck" $.ResubmitCheck "Stack" $.Stack) }}
173
172
{{ else }}
174
173
<div class="bg-white dark:bg-gray-800 rounded drop-shadow-sm px-6 py-4 w-fit dark:text-white">
175
174
<div class="absolute left-8 -top-2 w-px h-2 bg-gray-300 dark:bg-gray-600"></div>
···
198
197
{{ i "git-merge" "w-4 h-4" }}
199
198
<span class="font-medium">pull request successfully merged</span
200
199
>
200
+
</div>
201
+
</div>
202
+
{{ else if .Pull.State.IsDeleted }}
203
+
<div class="bg-red-50 dark:bg-red-900 border border-red-500 rounded drop-shadow-sm px-6 py-2 relative w-fit">
204
+
<div class="flex items-center gap-2 text-red-500 dark:text-red-300">
205
+
{{ i "git-pull-request-closed" "w-4 h-4" }}
206
+
<span class="font-medium">This pull has been deleted (possibly by jj abandon or jj squash)</span>
201
207
</div>
202
208
</div>
203
209
{{ else if and .MergeCheck .MergeCheck.Error }}
+6
appview/state/middleware.go
+6
appview/state/middleware.go
···
178
178
log.Println("failed to get stack", err)
179
179
return
180
180
}
181
+
abandonedPulls, err := db.GetAbandonedPulls(s.db, pr.StackId)
182
+
if err != nil {
183
+
log.Println("failed to get abandoned pulls", err)
184
+
return
185
+
}
181
186
182
187
ctx = context.WithValue(ctx, "stack", stack)
188
+
ctx = context.WithValue(ctx, "abandonedPulls", abandonedPulls)
183
189
}
184
190
185
191
next.ServeHTTP(w, r.WithContext(ctx))
+25
-44
appview/state/pull.go
+25
-44
appview/state/pull.go
···
75
75
RoundNumber: roundNumber,
76
76
MergeCheck: mergeCheckResponse,
77
77
ResubmitCheck: resubmitResult,
78
+
Stack: stack,
78
79
})
79
80
return
80
81
}
···
97
98
98
99
// can be nil if this pull is not stacked
99
100
stack, _ := r.Context().Value("stack").(db.Stack)
101
+
abandonedPulls, _ := r.Context().Value("abandonedPulls").([]*db.Pull)
100
102
101
103
totalIdents := 1
102
104
for _, submission := range pull.Submissions {
···
132
134
}
133
135
134
136
s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{
135
-
LoggedInUser: user,
136
-
RepoInfo: f.RepoInfo(s, user),
137
-
DidHandleMap: didHandleMap,
138
-
Pull: pull,
139
-
Stack: stack,
140
-
MergeCheck: mergeCheckResponse,
141
-
ResubmitCheck: resubmitResult,
137
+
LoggedInUser: user,
138
+
RepoInfo: f.RepoInfo(s, user),
139
+
DidHandleMap: didHandleMap,
140
+
Pull: pull,
141
+
Stack: stack,
142
+
AbandonedPulls: abandonedPulls,
143
+
MergeCheck: mergeCheckResponse,
144
+
ResubmitCheck: resubmitResult,
142
145
})
143
146
}
144
147
···
167
170
if pull.IsStacked() {
168
171
// combine patches of substack
169
172
subStack := stack.Below(pull)
170
-
171
173
// collect the portion of the stack that is mergeable
172
-
var mergeable db.Stack
173
-
for _, p := range subStack {
174
-
// stop at the first merged PR
175
-
if p.State == db.PullMerged || p.State == db.PullClosed {
176
-
break
177
-
}
178
-
179
-
// skip over deleted PRs
180
-
if p.State != db.PullDeleted {
181
-
mergeable = append(mergeable, p)
182
-
}
183
-
}
184
-
174
+
mergeable := subStack.Mergeable()
175
+
// combine each patch
185
176
patch = mergeable.CombinedPatch()
186
177
}
187
178
···
225
216
}
226
217
227
218
func (s *State) resubmitCheck(f *FullyResolvedRepo, pull *db.Pull, stack db.Stack) pages.ResubmitResult {
228
-
if pull.State == db.PullMerged || pull.PullSource == nil {
219
+
if pull.State == db.PullMerged || pull.State == db.PullDeleted || pull.PullSource == nil {
229
220
return pages.Unknown
230
221
}
231
222
···
903
894
return
904
895
}
905
896
897
+
client, err := s.oauth.AuthorizedClient(r)
898
+
if err != nil {
899
+
log.Println("failed to get authorized client", err)
900
+
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
901
+
return
902
+
}
903
+
906
904
tx, err := s.db.BeginTx(r.Context(), nil)
907
905
if err != nil {
908
906
log.Println("failed to start tx")
···
947
945
})
948
946
if err != nil {
949
947
log.Println("failed to create pull request", err)
950
-
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
951
-
return
952
-
}
953
-
client, err := s.oauth.AuthorizedClient(r)
954
-
if err != nil {
955
-
log.Println("failed to get authorized client", err)
956
948
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
957
949
return
958
950
}
···
1820
1812
1821
1813
// combine patches of substack
1822
1814
subStack := stack.Below(pull)
1823
-
1824
1815
// collect the portion of the stack that is mergeable
1825
-
for _, p := range subStack {
1826
-
// stop at the first merged/closed PR
1827
-
if p.State == db.PullMerged || p.State == db.PullClosed {
1828
-
break
1829
-
}
1830
-
1831
-
// skip over deleted PRs
1832
-
if p.State == db.PullDeleted {
1833
-
continue
1834
-
}
1835
-
1836
-
pullsToMerge = append(pullsToMerge, p)
1837
-
}
1816
+
mergeable := subStack.Mergeable()
1817
+
// add to total patch
1818
+
pullsToMerge = append(pullsToMerge, mergeable...)
1838
1819
}
1839
1820
1840
1821
patch := pullsToMerge.CombinedPatch()
···
2014
1995
var pullsToReopen []*db.Pull
2015
1996
pullsToReopen = append(pullsToReopen, pull)
2016
1997
2017
-
// if this PR is stacked, then we want to reopen all PRs below this one on the stack
1998
+
// if this PR is stacked, then we want to reopen all PRs above this one on the stack
2018
1999
if pull.IsStacked() {
2019
2000
stack := r.Context().Value("stack").(db.Stack)
2020
-
subStack := stack.StrictlyBelow(pull)
2001
+
subStack := stack.StrictlyAbove(pull)
2021
2002
pullsToReopen = append(pullsToReopen, subStack...)
2022
2003
}
2023
2004
+1
-1
flake.nix
+1
-1
flake.nix
···
435
435
g = config.services.tangled-knotserver.gitUser;
436
436
in [
437
437
"d /var/lib/knotserver 0770 ${u} ${g} - -" # Create the directory first
438
-
"f+ /var/lib/knotserver/secret 0660 ${u} ${g} - KNOT_SERVER_SECRET=679f15000084699abc6a20d3ef449efa3656583f38e456a08f0638250688ff2e"
438
+
"f+ /var/lib/knotserver/secret 0660 ${u} ${g} - KNOT_SERVER_SECRET=38a7c3237c2a585807e06a5bcfac92eb39442063f3da306b7acb15cfdc51d19d"
439
439
];
440
440
services.tangled-knotserver = {
441
441
enable = true;