git-reviewer 2.2.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -12
- package/bin/reviewer.js +33 -3
- package/lib/browse.js +129 -0
- package/lib/cli.js +17 -3
- package/lib/paths.js +25 -0
- package/lib/recents.js +123 -0
- package/lib/repo.js +53 -0
- package/package.json +4 -3
- package/public/app.js +443 -60
- package/public/index.html +60 -6
- package/public/style.css +317 -2
- package/server.js +78 -5
package/public/index.html
CHANGED
|
@@ -12,12 +12,35 @@
|
|
|
12
12
|
<div class="header-content">
|
|
13
13
|
<h1>Code Reviewer</h1>
|
|
14
14
|
<div class="header-actions">
|
|
15
|
-
<
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
<button id="repoButton" class="repo-button" onclick="showPicker()">
|
|
16
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
|
|
17
|
+
<path d="M1.75 1.5a.25.25 0 00-.25.25v11.5c0 .138.112.25.25.25h12.5a.25.25 0 00.25-.25v-8.5a.25.25 0 00-.25-.25H7.5a.75.75 0 01-.6-.3L5.55 2.4a.25.25 0 00-.2-.1H1.75z"></path>
|
|
18
|
+
</svg>
|
|
19
|
+
<span id="repoButtonLabel">Open a repository</span>
|
|
20
|
+
</button>
|
|
21
|
+
|
|
22
|
+
<button
|
|
23
|
+
id="pathToggle"
|
|
24
|
+
class="icon-button"
|
|
25
|
+
onclick="togglePathEntry()"
|
|
26
|
+
title="Type or paste a path instead"
|
|
27
|
+
aria-label="Type or paste a path instead"
|
|
28
|
+
aria-expanded="false">
|
|
29
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
|
|
30
|
+
<path d="M1.75 3h12.5c.966 0 1.75.784 1.75 1.75v6.5A1.75 1.75 0 0114.25 13H1.75A1.75 1.75 0 010 11.25v-6.5C0 3.784.784 3 1.75 3zm0 1.5a.25.25 0 00-.25.25v6.5c0 .138.112.25.25.25h12.5a.25.25 0 00.25-.25v-6.5a.25.25 0 00-.25-.25H1.75zM3 6h1.5v1.5H3V6zm2.75 0h1.5v1.5h-1.5V6zM8.5 6H10v1.5H8.5V6zm2.75 0h1.5v1.5h-1.5V6zM3 8.75h1.5v1.5H3v-1.5zm2.75 0h4.5v1.5h-4.5v-1.5zm5.75 0H13v1.5h-1.5v-1.5z"></path>
|
|
31
|
+
</svg>
|
|
32
|
+
</button>
|
|
33
|
+
|
|
34
|
+
<div id="pathEntry" class="path-entry hidden">
|
|
35
|
+
<input
|
|
36
|
+
type="text"
|
|
37
|
+
id="repoPath"
|
|
38
|
+
placeholder="/path/to/repository"
|
|
39
|
+
spellcheck="false"
|
|
40
|
+
autocomplete="off"
|
|
41
|
+
value="">
|
|
42
|
+
<button id="loadBtn" onclick="loadRepo()">Load</button>
|
|
43
|
+
</div>
|
|
21
44
|
<button id="submitReviewBtn" onclick="submitReview()" class="submit-btn hidden">
|
|
22
45
|
Submit Review
|
|
23
46
|
</button>
|
|
@@ -27,6 +50,37 @@
|
|
|
27
50
|
</header>
|
|
28
51
|
|
|
29
52
|
<div class="main-container">
|
|
53
|
+
<section id="picker" class="picker">
|
|
54
|
+
<div class="picker-card">
|
|
55
|
+
<div class="picker-heading">
|
|
56
|
+
<h2>Open a repository</h2>
|
|
57
|
+
<button id="pickerClose" class="icon-button hidden" onclick="closePicker()"
|
|
58
|
+
title="Keep the review open" aria-label="Keep the review open">
|
|
59
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
|
|
60
|
+
<path d="M3.72 3.72a.75.75 0 011.06 0L8 6.94l3.22-3.22a.75.75 0 111.06 1.06L9.06 8l3.22 3.22a.75.75 0 11-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 01-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 010-1.06z"></path>
|
|
61
|
+
</svg>
|
|
62
|
+
</button>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<section id="recentSection" class="picker-section hidden">
|
|
66
|
+
<h3>Recent</h3>
|
|
67
|
+
<div id="recentList" class="picker-list"></div>
|
|
68
|
+
</section>
|
|
69
|
+
|
|
70
|
+
<section class="picker-section picker-browse">
|
|
71
|
+
<h3>Browse</h3>
|
|
72
|
+
<div class="picker-toolbar">
|
|
73
|
+
<button id="browseUpBtn" class="picker-up" onclick="browseUp()">Up</button>
|
|
74
|
+
<span id="browsePath" class="picker-current"></span>
|
|
75
|
+
<button id="browseOpenBtn" class="picker-open hidden" onclick="openBrowsed()">
|
|
76
|
+
Open this repository
|
|
77
|
+
</button>
|
|
78
|
+
</div>
|
|
79
|
+
<div id="browseList" class="picker-list picker-list-scroll"></div>
|
|
80
|
+
</section>
|
|
81
|
+
</div>
|
|
82
|
+
</section>
|
|
83
|
+
|
|
30
84
|
<aside id="sidebar" class="sidebar hidden">
|
|
31
85
|
<div class="sidebar-header">
|
|
32
86
|
<h2>Changed Files</h2>
|
package/public/style.css
CHANGED
|
@@ -44,9 +44,9 @@ body {
|
|
|
44
44
|
.header-actions {
|
|
45
45
|
display: flex;
|
|
46
46
|
align-items: center;
|
|
47
|
-
gap:
|
|
47
|
+
gap: 8px;
|
|
48
48
|
flex: 1;
|
|
49
|
-
|
|
49
|
+
min-width: 0;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
#repoPath {
|
|
@@ -1004,3 +1004,318 @@ button:disabled {
|
|
|
1004
1004
|
font-size: 13px;
|
|
1005
1005
|
opacity: 0.8;
|
|
1006
1006
|
}
|
|
1007
|
+
|
|
1008
|
+
/* Repository picker
|
|
1009
|
+
*
|
|
1010
|
+
* What the page shows before a repository is open. It used to show nothing at
|
|
1011
|
+
* all but an empty path box in the header, which asked you to already know the
|
|
1012
|
+
* path you were looking for.
|
|
1013
|
+
*/
|
|
1014
|
+
.picker {
|
|
1015
|
+
flex: 1;
|
|
1016
|
+
overflow-y: auto;
|
|
1017
|
+
padding: 32px 24px;
|
|
1018
|
+
display: flex;
|
|
1019
|
+
justify-content: center;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
.picker-card {
|
|
1023
|
+
width: 100%;
|
|
1024
|
+
max-width: 720px;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
.picker-card h2 {
|
|
1028
|
+
font-size: 18px;
|
|
1029
|
+
font-weight: 600;
|
|
1030
|
+
color: #24292e;
|
|
1031
|
+
margin-bottom: 20px;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
.picker-section {
|
|
1035
|
+
margin-bottom: 28px;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
.picker-section h3 {
|
|
1039
|
+
font-size: 12px;
|
|
1040
|
+
font-weight: 600;
|
|
1041
|
+
text-transform: uppercase;
|
|
1042
|
+
letter-spacing: 0.6px;
|
|
1043
|
+
color: #6a737d;
|
|
1044
|
+
margin-bottom: 10px;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
.picker-list {
|
|
1048
|
+
background: white;
|
|
1049
|
+
border: 1px solid #e1e4e8;
|
|
1050
|
+
border-radius: 6px;
|
|
1051
|
+
overflow: hidden;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
.picker-list-scroll {
|
|
1055
|
+
max-height: 340px;
|
|
1056
|
+
overflow-y: auto;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
.picker-empty {
|
|
1060
|
+
padding: 16px;
|
|
1061
|
+
font-size: 13px;
|
|
1062
|
+
color: #6a737d;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
.picker-row {
|
|
1066
|
+
display: flex;
|
|
1067
|
+
align-items: center;
|
|
1068
|
+
gap: 10px;
|
|
1069
|
+
width: 100%;
|
|
1070
|
+
padding: 10px 14px;
|
|
1071
|
+
border: none;
|
|
1072
|
+
border-bottom: 1px solid #f0f2f4;
|
|
1073
|
+
background: white;
|
|
1074
|
+
font: inherit;
|
|
1075
|
+
font-size: 13px;
|
|
1076
|
+
color: #24292e;
|
|
1077
|
+
text-align: left;
|
|
1078
|
+
cursor: pointer;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
.picker-row:last-child {
|
|
1082
|
+
border-bottom: none;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
.picker-row:hover {
|
|
1086
|
+
background: #f6f8fa;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
.picker-row-icon {
|
|
1090
|
+
flex: none;
|
|
1091
|
+
width: 16px;
|
|
1092
|
+
color: #6a737d;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
.picker-row-name {
|
|
1096
|
+
font-weight: 500;
|
|
1097
|
+
white-space: nowrap;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
.picker-row-path {
|
|
1101
|
+
flex: 1;
|
|
1102
|
+
min-width: 0;
|
|
1103
|
+
color: #6a737d;
|
|
1104
|
+
font-size: 12px;
|
|
1105
|
+
overflow: hidden;
|
|
1106
|
+
text-overflow: ellipsis;
|
|
1107
|
+
white-space: nowrap;
|
|
1108
|
+
/* Truncate the front of a long path, not the end -- the last few segments
|
|
1109
|
+
* are the ones that identify it. `direction: rtl` is what puts the ellipsis
|
|
1110
|
+
* on the left; `unicode-bidi: plaintext` is what stops it also relocating
|
|
1111
|
+
* the leading slash to the far end, which spells the path wrong. */
|
|
1112
|
+
direction: rtl;
|
|
1113
|
+
unicode-bidi: plaintext;
|
|
1114
|
+
text-align: left;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
.picker-row-meta {
|
|
1118
|
+
flex: none;
|
|
1119
|
+
color: #6a737d;
|
|
1120
|
+
font-size: 12px;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
.picker-badge {
|
|
1124
|
+
flex: none;
|
|
1125
|
+
padding: 1px 6px;
|
|
1126
|
+
border-radius: 20px;
|
|
1127
|
+
background: #ddf4e4;
|
|
1128
|
+
color: #1a7f37;
|
|
1129
|
+
font-size: 11px;
|
|
1130
|
+
font-weight: 600;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
/* A repository that has been moved or deleted is still shown, because seeing
|
|
1134
|
+
* the path you remember is a better answer than a list that quietly shrinks. */
|
|
1135
|
+
.picker-row.missing {
|
|
1136
|
+
cursor: not-allowed;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
.picker-row.missing .picker-row-name,
|
|
1140
|
+
.picker-row.missing .picker-row-path {
|
|
1141
|
+
color: #a0a6ad;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
.picker-toolbar {
|
|
1145
|
+
display: flex;
|
|
1146
|
+
align-items: center;
|
|
1147
|
+
gap: 10px;
|
|
1148
|
+
margin-bottom: 10px;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
.picker-up {
|
|
1152
|
+
flex: none;
|
|
1153
|
+
padding: 5px 12px;
|
|
1154
|
+
border: 1px solid #d1d5da;
|
|
1155
|
+
border-radius: 6px;
|
|
1156
|
+
background: white;
|
|
1157
|
+
font: inherit;
|
|
1158
|
+
font-size: 12px;
|
|
1159
|
+
color: #24292e;
|
|
1160
|
+
cursor: pointer;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
.picker-up:hover:not(:disabled) {
|
|
1164
|
+
background: #f6f8fa;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
.picker-up:disabled {
|
|
1168
|
+
color: #a0a6ad;
|
|
1169
|
+
cursor: default;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
.picker-current {
|
|
1173
|
+
flex: 1;
|
|
1174
|
+
min-width: 0;
|
|
1175
|
+
font-size: 12px;
|
|
1176
|
+
color: #6a737d;
|
|
1177
|
+
overflow: hidden;
|
|
1178
|
+
text-overflow: ellipsis;
|
|
1179
|
+
white-space: nowrap;
|
|
1180
|
+
direction: rtl;
|
|
1181
|
+
unicode-bidi: plaintext;
|
|
1182
|
+
text-align: left;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
.picker-open {
|
|
1186
|
+
flex: none;
|
|
1187
|
+
padding: 5px 12px;
|
|
1188
|
+
border: none;
|
|
1189
|
+
border-radius: 6px;
|
|
1190
|
+
background: #0366d6;
|
|
1191
|
+
color: white;
|
|
1192
|
+
font: inherit;
|
|
1193
|
+
font-size: 12px;
|
|
1194
|
+
font-weight: 500;
|
|
1195
|
+
cursor: pointer;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
.picker-open:hover {
|
|
1199
|
+
background: #0256b9;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
/* The repository control
|
|
1203
|
+
*
|
|
1204
|
+
* The header used to be a text box saying "Enter local repository path...",
|
|
1205
|
+
* which asked the one thing a person opening this tool is least able to
|
|
1206
|
+
* supply: the exact path, typed correctly, from memory. It is a button now.
|
|
1207
|
+
* Typing a path is still there, behind the icon beside it, because pasting one
|
|
1208
|
+
* is a real thing to want and a navigator is slow when you already know.
|
|
1209
|
+
*/
|
|
1210
|
+
.repo-button {
|
|
1211
|
+
display: flex;
|
|
1212
|
+
align-items: center;
|
|
1213
|
+
gap: 8px;
|
|
1214
|
+
max-width: 380px;
|
|
1215
|
+
padding: 7px 12px;
|
|
1216
|
+
border: 1px solid #d1d5da;
|
|
1217
|
+
border-radius: 6px;
|
|
1218
|
+
background: white;
|
|
1219
|
+
font: inherit;
|
|
1220
|
+
font-size: 13px;
|
|
1221
|
+
font-weight: 500;
|
|
1222
|
+
color: #24292e;
|
|
1223
|
+
cursor: pointer;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
.repo-button:hover {
|
|
1227
|
+
background: #f6f8fa;
|
|
1228
|
+
border-color: #c6cbd1;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
.repo-button svg {
|
|
1232
|
+
flex: none;
|
|
1233
|
+
color: #6a737d;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
.repo-button span {
|
|
1237
|
+
overflow: hidden;
|
|
1238
|
+
text-overflow: ellipsis;
|
|
1239
|
+
white-space: nowrap;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
.icon-button {
|
|
1243
|
+
display: flex;
|
|
1244
|
+
align-items: center;
|
|
1245
|
+
justify-content: center;
|
|
1246
|
+
flex: none;
|
|
1247
|
+
width: 30px;
|
|
1248
|
+
height: 30px;
|
|
1249
|
+
padding: 0;
|
|
1250
|
+
border: 1px solid transparent;
|
|
1251
|
+
border-radius: 6px;
|
|
1252
|
+
background: transparent;
|
|
1253
|
+
color: #6a737d;
|
|
1254
|
+
cursor: pointer;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
.icon-button:hover {
|
|
1258
|
+
background: #f0f2f4;
|
|
1259
|
+
color: #24292e;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
.icon-button[aria-expanded='true'] {
|
|
1263
|
+
background: #eaeef2;
|
|
1264
|
+
border-color: #d1d5da;
|
|
1265
|
+
color: #24292e;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
.path-entry {
|
|
1269
|
+
display: flex;
|
|
1270
|
+
align-items: center;
|
|
1271
|
+
gap: 8px;
|
|
1272
|
+
flex: 1;
|
|
1273
|
+
min-width: 0;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
.path-entry input {
|
|
1277
|
+
flex: 1;
|
|
1278
|
+
min-width: 0;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
/* Pushed to the far end, so the review controls are not mixed in with the
|
|
1282
|
+
* controls for choosing what you are reviewing. */
|
|
1283
|
+
.submit-btn {
|
|
1284
|
+
margin-left: auto;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
.picker-heading {
|
|
1288
|
+
display: flex;
|
|
1289
|
+
align-items: center;
|
|
1290
|
+
justify-content: space-between;
|
|
1291
|
+
gap: 12px;
|
|
1292
|
+
margin-bottom: 20px;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
.picker-heading h2 {
|
|
1296
|
+
margin-bottom: 0;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
/* Over the review rather than instead of it: switching repositories with a
|
|
1300
|
+
* review open should not throw the review off the screen to ask. */
|
|
1301
|
+
.picker.as-overlay {
|
|
1302
|
+
position: fixed;
|
|
1303
|
+
top: 0;
|
|
1304
|
+
left: 0;
|
|
1305
|
+
width: 100%;
|
|
1306
|
+
height: 100%;
|
|
1307
|
+
z-index: 1000;
|
|
1308
|
+
background: rgba(0, 0, 0, 0.45);
|
|
1309
|
+
padding: 56px 24px;
|
|
1310
|
+
align-items: flex-start;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
.picker.as-overlay .picker-card {
|
|
1314
|
+
max-width: 640px;
|
|
1315
|
+
padding: 24px;
|
|
1316
|
+
background: white;
|
|
1317
|
+
border-radius: 8px;
|
|
1318
|
+
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
|
|
1319
|
+
max-height: calc(100vh - 112px);
|
|
1320
|
+
overflow-y: auto;
|
|
1321
|
+
}
|
package/server.js
CHANGED
|
@@ -18,6 +18,10 @@ const { normalizeComments } = require('./lib/comments');
|
|
|
18
18
|
const { formatReview, reviewFilename, commentsFilename } = require('./lib/review');
|
|
19
19
|
const { buildReviewDocument } = require('./lib/agent');
|
|
20
20
|
const { readSavedComments, ReviewOwnershipError } = require('./lib/store');
|
|
21
|
+
const { repoRoot } = require('./lib/repo');
|
|
22
|
+
const { browse } = require('./lib/browse');
|
|
23
|
+
const { recordRecent, describeRecents } = require('./lib/recents');
|
|
24
|
+
const { listReviews } = require('./lib/export');
|
|
21
25
|
|
|
22
26
|
const DEFAULT_PORT = 4500;
|
|
23
27
|
const DEFAULT_HOST = '127.0.0.1';
|
|
@@ -187,6 +191,61 @@ function createApp(options = {}) {
|
|
|
187
191
|
res.json({ status: 'ok', sessions: sessions.size });
|
|
188
192
|
});
|
|
189
193
|
|
|
194
|
+
/**
|
|
195
|
+
* Refuse a request a browser has marked as coming from somewhere else.
|
|
196
|
+
*
|
|
197
|
+
* `cors()` above is open, deliberately, and that is a settled decision this
|
|
198
|
+
* does not reopen. It is a rule for the two endpoints below, which differ
|
|
199
|
+
* from every other one in kind: they enumerate directories and name
|
|
200
|
+
* projects, rather than answering about a repository whose path the caller
|
|
201
|
+
* already had. A browser sends `Origin` on exactly the requests where that
|
|
202
|
+
* distinction matters and omits it on same-origin GETs, so the page keeps
|
|
203
|
+
* working and a page on another site does not. Anything that is not a
|
|
204
|
+
* browser can read these directories without asking this server anyway.
|
|
205
|
+
*/
|
|
206
|
+
function sameOriginOnly(req, res, next) {
|
|
207
|
+
const origin = req.get('origin');
|
|
208
|
+
|
|
209
|
+
if (origin && origin !== `http://${req.headers.host}`) {
|
|
210
|
+
return res.status(403).json({ error: 'Not available to another origin' });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
next();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** List the directories inside one, so a repository can be found by looking. */
|
|
217
|
+
app.get('/api/browse', sameOriginOnly, async (req, res) => {
|
|
218
|
+
try {
|
|
219
|
+
res.json(await browse(req.query.path));
|
|
220
|
+
} catch (error) {
|
|
221
|
+
if (error.code === 'ENOENT' || error.code === 'ENOTDIR') {
|
|
222
|
+
return res.status(400).json({ error: 'No such directory' });
|
|
223
|
+
}
|
|
224
|
+
if (error.code === 'EACCES' || error.code === 'EPERM') {
|
|
225
|
+
return res.status(403).json({ error: 'Not allowed to read that directory' });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
console.error('Browse error:', error);
|
|
229
|
+
res.status(500).json({ error: error.message });
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
/** Repositories opened before now, newest first. */
|
|
234
|
+
app.get('/api/recent', sameOriginOnly, async (req, res) => {
|
|
235
|
+
try {
|
|
236
|
+
// Annotated with what is saved against each, because "3 comments" is
|
|
237
|
+
// what tells you which of two checkouts you were in the middle of.
|
|
238
|
+
const counts = new Map(
|
|
239
|
+
(await listReviews(reviewsDir)).map(review => [review.repoPath, review.comments])
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
res.json({ projects: await describeRecents(reviewsDir, counts) });
|
|
243
|
+
} catch (error) {
|
|
244
|
+
console.error('Recent projects error:', error);
|
|
245
|
+
res.status(500).json({ error: error.message });
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
190
249
|
/** Open a repository and list what changed. */
|
|
191
250
|
app.post('/api/load-repo', async (req, res) => {
|
|
192
251
|
try {
|
|
@@ -202,11 +261,16 @@ function createApp(options = {}) {
|
|
|
202
261
|
return res.status(400).json({ error: 'Path does not exist' });
|
|
203
262
|
}
|
|
204
263
|
|
|
205
|
-
|
|
206
|
-
|
|
264
|
+
// Resolve to the root of the working tree before anything is keyed on
|
|
265
|
+
// this path. A subdirectory passes `checkIsRepo()` quite happily and
|
|
266
|
+
// then produces a file list whose diffs are all empty; see lib/repo.js.
|
|
267
|
+
const root = await repoRoot(repoPath, gitFactory);
|
|
268
|
+
if (!root) {
|
|
207
269
|
return res.status(400).json({ error: 'Not a valid git repository' });
|
|
208
270
|
}
|
|
209
271
|
|
|
272
|
+
const git = gitFactory(root);
|
|
273
|
+
|
|
210
274
|
let files = collectWorkingChanges(await git.status());
|
|
211
275
|
let mode = 'working';
|
|
212
276
|
let message = `Found ${files.length} changed file(s)`;
|
|
@@ -227,10 +291,19 @@ function createApp(options = {}) {
|
|
|
227
291
|
}
|
|
228
292
|
}
|
|
229
293
|
|
|
230
|
-
const repoId = sessions.create(
|
|
231
|
-
console.log(`Loaded repository: ${
|
|
294
|
+
const repoId = sessions.create(root, mode);
|
|
295
|
+
console.log(`Loaded repository: ${root} (mode: ${mode})`);
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
await recordRecent(reviewsDir, root);
|
|
299
|
+
} catch (error) {
|
|
300
|
+
// Being unable to remember this must not stop you opening it.
|
|
301
|
+
console.error('Could not record recent project:', error.message);
|
|
302
|
+
}
|
|
232
303
|
|
|
233
|
-
|
|
304
|
+
// `repoPath` in the response is the resolved root, not what was asked
|
|
305
|
+
// for, so the page can show which repository it actually opened.
|
|
306
|
+
res.json({ repoId, files, repoPath: root, mode, message });
|
|
234
307
|
} catch (error) {
|
|
235
308
|
console.error('Load repo error:', error);
|
|
236
309
|
res.status(500).json({ error: error.message });
|