Fossil SCM

Allow shunning/unshunning multiple files at a time

baruch 2014-09-09 06:54 UTC trunk
Commit 9f6b1964fb884775c8dc44026604d04804423852
2 files changed +99 -46 +4
+99 -46
--- src/shun.c
+++ src/shun.c
@@ -40,12 +40,14 @@
4040
*/
4141
void shun_page(void){
4242
Stmt q;
4343
int cnt = 0;
4444
const char *zUuid = P("uuid");
45
+ const char *zShun = P("shun");
46
+ const char *zAccept = P("accept");
4547
int nUuid;
46
- char zCanonical[UUID_SIZE+1];
48
+ char *zCanonical = 0;
4749
4850
login_check_credentials();
4951
if( !g.perm.Admin ){
5052
login_needed();
5153
}
@@ -55,71 +57,113 @@
5557
db_begin_transaction();
5658
rebuild_db(0, 0, 0);
5759
db_end_transaction(0);
5860
}
5961
if( zUuid ){
60
- nUuid = strlen(zUuid);
61
- if( nUuid!=40 || !validate16(zUuid, nUuid) ){
62
- zUuid = 0;
63
- }else{
64
- memcpy(zCanonical, zUuid, UUID_SIZE+1);
65
- canonical16(zCanonical, UUID_SIZE);
66
- zUuid = zCanonical;
67
- }
62
+ char *p;
63
+ int i = 0;
64
+ int j = 0;
65
+ zCanonical = fossil_malloc(strlen(zUuid)+2);
66
+ while( zUuid[i] ){
67
+ if( fossil_isspace(zUuid[i]) ){
68
+ if( j && zCanonical[j-1] ){
69
+ zCanonical[j] = 0;
70
+ j++;
71
+ }
72
+ }else{
73
+ zCanonical[j] = zUuid[i];
74
+ j++;
75
+ }
76
+ i++;
77
+ }
78
+ zCanonical[j+1] = zCanonical[j] = 0;
79
+ p = zCanonical;
80
+ while( *p ){
81
+ nUuid = strlen(p);
82
+ if( nUuid!=UUID_SIZE || !validate16(p, nUuid) ){
83
+ @ <p class="generalError">Error: Bad artifact IDs.</p>
84
+ fossil_free(zCanonical);
85
+ zCanonical = 0;
86
+ break;
87
+ }else{
88
+ canonical16(p, UUID_SIZE);
89
+ p += UUID_SIZE+1;
90
+ }
91
+ }
92
+ zUuid = zCanonical;
6893
}
6994
style_header("Shunned Artifacts");
7095
if( zUuid && P("sub") ){
96
+ const char *p = zUuid;
97
+ int allExist = 1;
7198
login_verify_csrf_secret();
72
- db_multi_exec("DELETE FROM shun WHERE uuid='%s'", zUuid);
73
- if( db_exists("SELECT 1 FROM blob WHERE uuid='%s'", zUuid) ){
74
- @ <p class="noMoreShun">Artifact
75
- @ <a href="%s(g.zTop)/artifact/%s(zUuid)">%s(zUuid)</a> is no
76
- @ longer being shunned.</p>
99
+ while( *p ){
100
+ db_multi_exec("DELETE FROM shun WHERE uuid='%s'", p);
101
+ if( !db_exists("SELECT 1 FROM blob WHERE uuid='%s'", p) ){
102
+ allExist = 0;
103
+ }
104
+ p += UUID_SIZE+1;
105
+ }
106
+ if( allExist ){
107
+ @ <p class="noMoreShun">Artifact(s)<br />
108
+ for( p = zUuid ; *p ; p += UUID_SIZE+1 ){
109
+ @ <a href="%s(g.zTop)/artifact/%s(p)">%s(p)</a><br />
110
+ }
111
+ @ are no longer being shunned.</p>
77112
}else{
78
- @ <p class="noMoreShun">Artifact %s(zUuid) will no longer
79
- @ be shunned. But it does not exist in the repository. It
80
- @ may be necessary to rebuild the repository using the
113
+ @ <p class="noMoreShun">Artifact(s)<br />
114
+ for( p = zUuid ; *p ; p += UUID_SIZE+1 ){
115
+ @ %s(p)<br />
116
+ }
117
+ @ will no longer be shunned. But they may not exist in the repository.
118
+ @ It may be necessary to rebuild the repository using the
81119
@ <b>fossil rebuild</b> command-line before the artifact content
82120
@ can pulled in from other repositories.</p>
83121
}
84122
}
85123
if( zUuid && P("add") ){
124
+ const char *p = zUuid;
86125
int rid, tagid;
87126
login_verify_csrf_secret();
88
- db_multi_exec(
89
- "INSERT OR IGNORE INTO shun(uuid,mtime)"
90
- " VALUES('%s', now())", zUuid);
91
- @ <p class="shunned">Artifact
92
- @ <a href="%s(g.zTop)/artifact/%s(zUuid)">%s(zUuid)</a> has been
93
- @ shunned. It will no longer be pushed.
94
- @ It will be removed from the repository the next time the repository
95
- @ is rebuilt using the <b>fossil rebuild</b> command-line</p>
96
- db_multi_exec("DELETE FROM attachment WHERE src=%Q", zUuid);
97
- rid = db_int(0, "SELECT rid FROM blob WHERE uuid=%Q", zUuid);
98
- if( rid ){
99
- db_multi_exec("DELETE FROM event WHERE objid=%d", rid);
100
- }
101
- tagid = db_int(0, "SELECT tagid FROM tag WHERE tagname='tkt-%q'", zUuid);
102
- if( tagid ){
103
- db_multi_exec("DELETE FROM ticket WHERE tkt_uuid=%Q", zUuid);
104
- db_multi_exec("DELETE FROM tag WHERE tagid=%d", tagid);
105
- db_multi_exec("DELETE FROM tagxref WHERE tagid=%d", tagid);
106
- }
127
+ while( *p ){
128
+ db_multi_exec(
129
+ "INSERT OR IGNORE INTO shun(uuid,mtime)"
130
+ " VALUES('%s', now())", p);
131
+ db_multi_exec("DELETE FROM attachment WHERE src=%Q", p);
132
+ rid = db_int(0, "SELECT rid FROM blob WHERE uuid=%Q", p);
133
+ if( rid ){
134
+ db_multi_exec("DELETE FROM event WHERE objid=%d", rid);
135
+ }
136
+ tagid = db_int(0, "SELECT tagid FROM tag WHERE tagname='tkt-%q'", p);
137
+ if( tagid ){
138
+ db_multi_exec("DELETE FROM ticket WHERE tkt_uuid=%Q", p);
139
+ db_multi_exec("DELETE FROM tag WHERE tagid=%d", tagid);
140
+ db_multi_exec("DELETE FROM tagxref WHERE tagid=%d", tagid);
141
+ }
142
+ p += UUID_SIZE+1;
143
+ }
144
+ @ <p class="shunned">Artifact(s)<br />
145
+ for( p = zUuid ; *p ; p += UUID_SIZE+1 ){
146
+ @ <a href="%s(g.zTop)/artifact/%s(p)">%s(p)</a><br />
147
+ }
148
+ @ have been shunned. They will no longer be pushed.
149
+ @ They will be removed from the repository the next time the repository
150
+ @ is rebuilt using the <b>fossil rebuild</b> command-line</p>
107151
}
108152
@ <p>A shunned artifact will not be pushed nor accepted in a pull and the
109153
@ artifact content will be purged from the repository the next time the
110154
@ repository is rebuilt. A list of shunned artifacts can be seen at the
111155
@ bottom of this page.</p>
112156
@
113157
@ <a name="addshun"></a>
114
- @ <p>To shun an artifact, enter its artifact ID (the 40-character SHA1
115
- @ hash of the artifact) in the
116
- @ following box and press the "Shun" button. This will cause the artifact
117
- @ to be removed from the repository and will prevent the artifact from being
158
+ @ <p>To shun artifacts, enter their artifact IDs (the 40-character SHA1
159
+ @ hash of the artifacts) in the
160
+ @ following box and press the "Shun" button. This will cause the artifacts
161
+ @ to be removed from the repository and will prevent the artifacts from being
118162
@ readded to the repository by subsequent sync operation.</p>
119163
@
120
- @ <p>Note that you must enter the full 40-character artifact ID, not
164
+ @ <p>Note that you must enter the full 40-character artifact IDs, not
121165
@ an abbreviation or a symbolic tag.</p>
122166
@
123167
@ <p>Warning: Shunning should only be used to remove inappropriate content
124168
@ from the repository. Inappropriate content includes such things as
125169
@ spam added to Wiki, files that violate copyright or patent agreements,
@@ -128,26 +172,34 @@
128172
@ sight - set the "hidden" tag on such artifacts instead.</p>
129173
@
130174
@ <blockquote>
131175
@ <form method="post" action="%s(g.zTop)/%s(g.zPath)"><div>
132176
login_insert_csrf_secret();
133
- @ <input type="text" name="uuid" value="%h(PD("shun",""))" size="50" />
177
+ @ <textarea class="fullsize-text" cols="50" rows="3" name="uuid">
178
+ if( zShun ){
179
+ @ %h(zShun)
180
+ }
181
+ @ </textarea>
134182
@ <input type="submit" name="add" value="Shun" />
135183
@ </div></form>
136184
@ </blockquote>
137185
@
138186
@ <a name="delshun"></a>
139
- @ <p>Enter the UUID of a previous shunned artifact to cause it to be
140
- @ accepted again in the repository. The artifact content is not
187
+ @ <p>Enter the UUIDs of previously shunned artifacts to cause them to be
188
+ @ accepted again in the repository. The artifacts content is not
141189
@ restored because the content is unknown. The only change is that
142
- @ the formerly shunned artifact will be accepted on subsequent sync
190
+ @ the formerly shunned artifacts will be accepted on subsequent sync
143191
@ operations.</p>
144192
@
145193
@ <blockquote>
146194
@ <form method="post" action="%s(g.zTop)/%s(g.zPath)"><div>
147195
login_insert_csrf_secret();
148
- @ <input type="text" name="uuid" value="%h(PD("accept", ""))" size="50" />
196
+ @ <textarea class="fullsize-text" cols="50" rows="3" name="uuid">
197
+ if( zAccept ){
198
+ @ %h(zAccept)
199
+ }
200
+ @ </textarea>
149201
@ <input type="submit" name="sub" value="Accept" />
150202
@ </div></form>
151203
@ </blockquote>
152204
@
153205
@ <p>Press the Rebuild button below to rebuild the repository. The
@@ -181,10 +233,11 @@
181233
@ <i>no artifacts are shunned on this server</i>
182234
}
183235
db_finalize(&q);
184236
@ </p></blockquote>
185237
style_footer();
238
+ fossil_free(zCanonical);
186239
}
187240
188241
/*
189242
** Remove from the BLOB table all artifacts that are in the SHUN table.
190243
*/
191244
--- src/shun.c
+++ src/shun.c
@@ -40,12 +40,14 @@
40 */
41 void shun_page(void){
42 Stmt q;
43 int cnt = 0;
44 const char *zUuid = P("uuid");
 
 
45 int nUuid;
46 char zCanonical[UUID_SIZE+1];
47
48 login_check_credentials();
49 if( !g.perm.Admin ){
50 login_needed();
51 }
@@ -55,71 +57,113 @@
55 db_begin_transaction();
56 rebuild_db(0, 0, 0);
57 db_end_transaction(0);
58 }
59 if( zUuid ){
60 nUuid = strlen(zUuid);
61 if( nUuid!=40 || !validate16(zUuid, nUuid) ){
62 zUuid = 0;
63 }else{
64 memcpy(zCanonical, zUuid, UUID_SIZE+1);
65 canonical16(zCanonical, UUID_SIZE);
66 zUuid = zCanonical;
67 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68 }
69 style_header("Shunned Artifacts");
70 if( zUuid && P("sub") ){
 
 
71 login_verify_csrf_secret();
72 db_multi_exec("DELETE FROM shun WHERE uuid='%s'", zUuid);
73 if( db_exists("SELECT 1 FROM blob WHERE uuid='%s'", zUuid) ){
74 @ <p class="noMoreShun">Artifact
75 @ <a href="%s(g.zTop)/artifact/%s(zUuid)">%s(zUuid)</a> is no
76 @ longer being shunned.</p>
 
 
 
 
 
 
 
 
77 }else{
78 @ <p class="noMoreShun">Artifact %s(zUuid) will no longer
79 @ be shunned. But it does not exist in the repository. It
80 @ may be necessary to rebuild the repository using the
 
 
 
81 @ <b>fossil rebuild</b> command-line before the artifact content
82 @ can pulled in from other repositories.</p>
83 }
84 }
85 if( zUuid && P("add") ){
 
86 int rid, tagid;
87 login_verify_csrf_secret();
88 db_multi_exec(
89 "INSERT OR IGNORE INTO shun(uuid,mtime)"
90 " VALUES('%s', now())", zUuid);
91 @ <p class="shunned">Artifact
92 @ <a href="%s(g.zTop)/artifact/%s(zUuid)">%s(zUuid)</a> has been
93 @ shunned. It will no longer be pushed.
94 @ It will be removed from the repository the next time the repository
95 @ is rebuilt using the <b>fossil rebuild</b> command-line</p>
96 db_multi_exec("DELETE FROM attachment WHERE src=%Q", zUuid);
97 rid = db_int(0, "SELECT rid FROM blob WHERE uuid=%Q", zUuid);
98 if( rid ){
99 db_multi_exec("DELETE FROM event WHERE objid=%d", rid);
100 }
101 tagid = db_int(0, "SELECT tagid FROM tag WHERE tagname='tkt-%q'", zUuid);
102 if( tagid ){
103 db_multi_exec("DELETE FROM ticket WHERE tkt_uuid=%Q", zUuid);
104 db_multi_exec("DELETE FROM tag WHERE tagid=%d", tagid);
105 db_multi_exec("DELETE FROM tagxref WHERE tagid=%d", tagid);
106 }
 
 
 
 
 
107 }
108 @ <p>A shunned artifact will not be pushed nor accepted in a pull and the
109 @ artifact content will be purged from the repository the next time the
110 @ repository is rebuilt. A list of shunned artifacts can be seen at the
111 @ bottom of this page.</p>
112 @
113 @ <a name="addshun"></a>
114 @ <p>To shun an artifact, enter its artifact ID (the 40-character SHA1
115 @ hash of the artifact) in the
116 @ following box and press the "Shun" button. This will cause the artifact
117 @ to be removed from the repository and will prevent the artifact from being
118 @ readded to the repository by subsequent sync operation.</p>
119 @
120 @ <p>Note that you must enter the full 40-character artifact ID, not
121 @ an abbreviation or a symbolic tag.</p>
122 @
123 @ <p>Warning: Shunning should only be used to remove inappropriate content
124 @ from the repository. Inappropriate content includes such things as
125 @ spam added to Wiki, files that violate copyright or patent agreements,
@@ -128,26 +172,34 @@
128 @ sight - set the "hidden" tag on such artifacts instead.</p>
129 @
130 @ <blockquote>
131 @ <form method="post" action="%s(g.zTop)/%s(g.zPath)"><div>
132 login_insert_csrf_secret();
133 @ <input type="text" name="uuid" value="%h(PD("shun",""))" size="50" />
 
 
 
 
134 @ <input type="submit" name="add" value="Shun" />
135 @ </div></form>
136 @ </blockquote>
137 @
138 @ <a name="delshun"></a>
139 @ <p>Enter the UUID of a previous shunned artifact to cause it to be
140 @ accepted again in the repository. The artifact content is not
141 @ restored because the content is unknown. The only change is that
142 @ the formerly shunned artifact will be accepted on subsequent sync
143 @ operations.</p>
144 @
145 @ <blockquote>
146 @ <form method="post" action="%s(g.zTop)/%s(g.zPath)"><div>
147 login_insert_csrf_secret();
148 @ <input type="text" name="uuid" value="%h(PD("accept", ""))" size="50" />
 
 
 
 
149 @ <input type="submit" name="sub" value="Accept" />
150 @ </div></form>
151 @ </blockquote>
152 @
153 @ <p>Press the Rebuild button below to rebuild the repository. The
@@ -181,10 +233,11 @@
181 @ <i>no artifacts are shunned on this server</i>
182 }
183 db_finalize(&q);
184 @ </p></blockquote>
185 style_footer();
 
186 }
187
188 /*
189 ** Remove from the BLOB table all artifacts that are in the SHUN table.
190 */
191
--- src/shun.c
+++ src/shun.c
@@ -40,12 +40,14 @@
40 */
41 void shun_page(void){
42 Stmt q;
43 int cnt = 0;
44 const char *zUuid = P("uuid");
45 const char *zShun = P("shun");
46 const char *zAccept = P("accept");
47 int nUuid;
48 char *zCanonical = 0;
49
50 login_check_credentials();
51 if( !g.perm.Admin ){
52 login_needed();
53 }
@@ -55,71 +57,113 @@
57 db_begin_transaction();
58 rebuild_db(0, 0, 0);
59 db_end_transaction(0);
60 }
61 if( zUuid ){
62 char *p;
63 int i = 0;
64 int j = 0;
65 zCanonical = fossil_malloc(strlen(zUuid)+2);
66 while( zUuid[i] ){
67 if( fossil_isspace(zUuid[i]) ){
68 if( j && zCanonical[j-1] ){
69 zCanonical[j] = 0;
70 j++;
71 }
72 }else{
73 zCanonical[j] = zUuid[i];
74 j++;
75 }
76 i++;
77 }
78 zCanonical[j+1] = zCanonical[j] = 0;
79 p = zCanonical;
80 while( *p ){
81 nUuid = strlen(p);
82 if( nUuid!=UUID_SIZE || !validate16(p, nUuid) ){
83 @ <p class="generalError">Error: Bad artifact IDs.</p>
84 fossil_free(zCanonical);
85 zCanonical = 0;
86 break;
87 }else{
88 canonical16(p, UUID_SIZE);
89 p += UUID_SIZE+1;
90 }
91 }
92 zUuid = zCanonical;
93 }
94 style_header("Shunned Artifacts");
95 if( zUuid && P("sub") ){
96 const char *p = zUuid;
97 int allExist = 1;
98 login_verify_csrf_secret();
99 while( *p ){
100 db_multi_exec("DELETE FROM shun WHERE uuid='%s'", p);
101 if( !db_exists("SELECT 1 FROM blob WHERE uuid='%s'", p) ){
102 allExist = 0;
103 }
104 p += UUID_SIZE+1;
105 }
106 if( allExist ){
107 @ <p class="noMoreShun">Artifact(s)<br />
108 for( p = zUuid ; *p ; p += UUID_SIZE+1 ){
109 @ <a href="%s(g.zTop)/artifact/%s(p)">%s(p)</a><br />
110 }
111 @ are no longer being shunned.</p>
112 }else{
113 @ <p class="noMoreShun">Artifact(s)<br />
114 for( p = zUuid ; *p ; p += UUID_SIZE+1 ){
115 @ %s(p)<br />
116 }
117 @ will no longer be shunned. But they may not exist in the repository.
118 @ It may be necessary to rebuild the repository using the
119 @ <b>fossil rebuild</b> command-line before the artifact content
120 @ can pulled in from other repositories.</p>
121 }
122 }
123 if( zUuid && P("add") ){
124 const char *p = zUuid;
125 int rid, tagid;
126 login_verify_csrf_secret();
127 while( *p ){
128 db_multi_exec(
129 "INSERT OR IGNORE INTO shun(uuid,mtime)"
130 " VALUES('%s', now())", p);
131 db_multi_exec("DELETE FROM attachment WHERE src=%Q", p);
132 rid = db_int(0, "SELECT rid FROM blob WHERE uuid=%Q", p);
133 if( rid ){
134 db_multi_exec("DELETE FROM event WHERE objid=%d", rid);
135 }
136 tagid = db_int(0, "SELECT tagid FROM tag WHERE tagname='tkt-%q'", p);
137 if( tagid ){
138 db_multi_exec("DELETE FROM ticket WHERE tkt_uuid=%Q", p);
139 db_multi_exec("DELETE FROM tag WHERE tagid=%d", tagid);
140 db_multi_exec("DELETE FROM tagxref WHERE tagid=%d", tagid);
141 }
142 p += UUID_SIZE+1;
143 }
144 @ <p class="shunned">Artifact(s)<br />
145 for( p = zUuid ; *p ; p += UUID_SIZE+1 ){
146 @ <a href="%s(g.zTop)/artifact/%s(p)">%s(p)</a><br />
147 }
148 @ have been shunned. They will no longer be pushed.
149 @ They will be removed from the repository the next time the repository
150 @ is rebuilt using the <b>fossil rebuild</b> command-line</p>
151 }
152 @ <p>A shunned artifact will not be pushed nor accepted in a pull and the
153 @ artifact content will be purged from the repository the next time the
154 @ repository is rebuilt. A list of shunned artifacts can be seen at the
155 @ bottom of this page.</p>
156 @
157 @ <a name="addshun"></a>
158 @ <p>To shun artifacts, enter their artifact IDs (the 40-character SHA1
159 @ hash of the artifacts) in the
160 @ following box and press the "Shun" button. This will cause the artifacts
161 @ to be removed from the repository and will prevent the artifacts from being
162 @ readded to the repository by subsequent sync operation.</p>
163 @
164 @ <p>Note that you must enter the full 40-character artifact IDs, not
165 @ an abbreviation or a symbolic tag.</p>
166 @
167 @ <p>Warning: Shunning should only be used to remove inappropriate content
168 @ from the repository. Inappropriate content includes such things as
169 @ spam added to Wiki, files that violate copyright or patent agreements,
@@ -128,26 +172,34 @@
172 @ sight - set the "hidden" tag on such artifacts instead.</p>
173 @
174 @ <blockquote>
175 @ <form method="post" action="%s(g.zTop)/%s(g.zPath)"><div>
176 login_insert_csrf_secret();
177 @ <textarea class="fullsize-text" cols="50" rows="3" name="uuid">
178 if( zShun ){
179 @ %h(zShun)
180 }
181 @ </textarea>
182 @ <input type="submit" name="add" value="Shun" />
183 @ </div></form>
184 @ </blockquote>
185 @
186 @ <a name="delshun"></a>
187 @ <p>Enter the UUIDs of previously shunned artifacts to cause them to be
188 @ accepted again in the repository. The artifacts content is not
189 @ restored because the content is unknown. The only change is that
190 @ the formerly shunned artifacts will be accepted on subsequent sync
191 @ operations.</p>
192 @
193 @ <blockquote>
194 @ <form method="post" action="%s(g.zTop)/%s(g.zPath)"><div>
195 login_insert_csrf_secret();
196 @ <textarea class="fullsize-text" cols="50" rows="3" name="uuid">
197 if( zAccept ){
198 @ %h(zAccept)
199 }
200 @ </textarea>
201 @ <input type="submit" name="sub" value="Accept" />
202 @ </div></form>
203 @ </blockquote>
204 @
205 @ <p>Press the Rebuild button below to rebuild the repository. The
@@ -181,10 +233,11 @@
233 @ <i>no artifacts are shunned on this server</i>
234 }
235 db_finalize(&q);
236 @ </p></blockquote>
237 style_footer();
238 fossil_free(zCanonical);
239 }
240
241 /*
242 ** Remove from the BLOB table all artifacts that are in the SHUN table.
243 */
244
--- src/skins.c
+++ src/skins.c
@@ -874,10 +874,14 @@
874874
@ cursor: pointer;
875875
@ }
876876
@
877877
@ textarea {
878878
@ font-size: 1em;
879
+@ }
880
+@
881
+@ .fullsize-text {
882
+@ font-size: 1.25em;
879883
@ }');
880884
@ REPLACE INTO config(name,mtime,value) VALUES('header',now(),'<html>
881885
@ <head>
882886
@ <base href="$baseurl/$current_page" />
883887
@ <title>$<project_name>: $<title></title>
884888
--- src/skins.c
+++ src/skins.c
@@ -874,10 +874,14 @@
874 @ cursor: pointer;
875 @ }
876 @
877 @ textarea {
878 @ font-size: 1em;
 
 
 
 
879 @ }');
880 @ REPLACE INTO config(name,mtime,value) VALUES('header',now(),'<html>
881 @ <head>
882 @ <base href="$baseurl/$current_page" />
883 @ <title>$<project_name>: $<title></title>
884
--- src/skins.c
+++ src/skins.c
@@ -874,10 +874,14 @@
874 @ cursor: pointer;
875 @ }
876 @
877 @ textarea {
878 @ font-size: 1em;
879 @ }
880 @
881 @ .fullsize-text {
882 @ font-size: 1.25em;
883 @ }');
884 @ REPLACE INTO config(name,mtime,value) VALUES('header',now(),'<html>
885 @ <head>
886 @ <base href="$baseurl/$current_page" />
887 @ <title>$<project_name>: $<title></title>
888

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button