Fossil SCM

Add the --import-marks and --export-marks options to "fossil import" when the --git option is used.

drh 2016-05-16 18:00 trunk merge
Commit b3acfa2e4acd84bf9fac3017e2bdb4b17115eabd
2 files changed +237 -25 +50 -2
+237 -25
--- src/export.c
+++ src/export.c
@@ -19,10 +19,28 @@
1919
*/
2020
#include "config.h"
2121
#include "export.h"
2222
#include <assert.h>
2323
24
+#if INTERFACE
25
+/*
26
+** struct mark_t
27
+** holds information for translating between git commits
28
+** and fossil commits.
29
+** -git_name: This is the mark name that identifies the commit to git.
30
+** It will always begin with a ':'.
31
+** -rid: The unique object ID that identifies this commit within the
32
+** repository database.
33
+** -uuid: The SHA-1 of artifact corresponding to rid.
34
+*/
35
+struct mark_t{
36
+ char *name;
37
+ int rid;
38
+ char uuid[41];
39
+};
40
+#endif
41
+
2442
/*
2543
** Output a "committer" record for the given user.
2644
*/
2745
static void print_person(const char *zUser){
2846
static Stmt q;
@@ -96,10 +114,199 @@
96114
db_reset(&q);
97115
}
98116
99117
#define BLOBMARK(rid) ((rid) * 2)
100118
#define COMMITMARK(rid) ((rid) * 2 + 1)
119
+
120
+/*
121
+** insert_commit_xref()
122
+** Insert a new (mark,rid,uuid) entry into the 'xmark' table.
123
+** zName and zUuid must be non-null and must point to NULL-terminated strings.
124
+*/
125
+void insert_commit_xref(int rid, const char *zName, const char *zUuid){
126
+ db_multi_exec(
127
+ "INSERT OR IGNORE INTO xmark(tname, trid, tuuid)"
128
+ "VALUES(%Q,%d,%Q)",
129
+ zName, rid, zUuid
130
+ );
131
+}
132
+
133
+/*
134
+** create_mark()
135
+** Create a new (mark,rid,uuid) entry for the given rid in the 'xmark' table,
136
+** and return that information as a struct mark_t in *mark.
137
+** This function returns -1 in the case where 'rid' does not exist, otherwise
138
+** it returns 0.
139
+** mark->name is dynamically allocated and is owned by the caller upon return.
140
+*/
141
+int create_mark(int rid, struct mark_t *mark){
142
+ char sid[13];
143
+ char *zUuid = rid_to_uuid(rid);
144
+ if(!zUuid){
145
+ fossil_trace("Undefined rid=%d\n", rid);
146
+ return -1;
147
+ }
148
+ mark->rid = rid;
149
+ sprintf(sid, ":%d", COMMITMARK(rid));
150
+ mark->name = fossil_strdup(sid);
151
+ strcpy(mark->uuid, zUuid);
152
+ free(zUuid);
153
+ insert_commit_xref(mark->rid, mark->name, mark->uuid);
154
+ return 0;
155
+}
156
+
157
+/*
158
+** mark_name_from_rid()
159
+** Find the mark associated with the given rid. Mark names always start
160
+** with ':', and are pulled from the 'xmark' temporary table.
161
+** This function returns NULL if the rid does not exist in the 'xmark' table.
162
+** Otherwise, it returns the name of the mark, which is dynamically allocated
163
+** and is owned by the caller of this function.
164
+*/
165
+char * mark_name_from_rid(int rid){
166
+ char *zMark = db_text(0, "SELECT tname FROM xmark WHERE trid=%d", rid);
167
+ if(zMark==NULL){
168
+ struct mark_t mark;
169
+ if(create_mark(rid, &mark)==0){
170
+ zMark = mark.name;
171
+ }else{
172
+ return NULL;
173
+ }
174
+ }
175
+ return zMark;
176
+}
177
+
178
+/*
179
+** parse_mark()
180
+** Create a new (mark,rid,uuid) entry in the 'xmark' table given a line
181
+** from a marks file. Return the cross-ref information as a struct mark_t
182
+** in *mark.
183
+** This function returns -1 in the case that the line is blank, malformed, or
184
+** the rid/uuid named in 'line' does not match what is in the repository
185
+** database. Otherwise, 0 is returned.
186
+** mark->name is dynamically allocated, and owned by the caller.
187
+*/
188
+int parse_mark(char *line, struct mark_t *mark){
189
+ char *cur_tok;
190
+ char type;
191
+ cur_tok = strtok(line, " \t");
192
+ if(!cur_tok||strlen(cur_tok)<2){
193
+ return -1;
194
+ }
195
+ mark->rid = atoi(&cur_tok[1]);
196
+ if(cur_tok[0]!='c'){
197
+ /* This is probably a blob mark */
198
+ mark->name = NULL;
199
+ return 0;
200
+ }
201
+
202
+ cur_tok = strtok(NULL, " \t");
203
+ if(!cur_tok){
204
+ /* This mark was generated by an older version of Fossil and doesn't
205
+ ** include the mark name and uuid. create_mark() will name the new mark
206
+ ** exactly as it was when exported to git, so that we should have a
207
+ ** valid mapping from git sha1<->mark name<->fossil sha1. */
208
+ return create_mark(mark->rid, mark);
209
+ }else{
210
+ mark->name = fossil_strdup(cur_tok);
211
+ }
212
+
213
+ cur_tok = strtok(NULL, "\n");
214
+ if(!cur_tok||strlen(cur_tok)!=40){
215
+ free(mark->name);
216
+ fossil_trace("Invalid SHA-1 in marks file: %s\n", cur_tok);
217
+ return -1;
218
+ }else{
219
+ strcpy(mark->uuid, cur_tok);
220
+ }
221
+
222
+ /* make sure that rid corresponds to UUID */
223
+ if(fast_uuid_to_rid(mark->uuid)!=mark->rid){
224
+ free(mark->name);
225
+ fossil_trace("Non-existent SHA-1 in marks file: %s\n", mark->uuid);
226
+ return -1;
227
+ }
228
+
229
+ /* insert a cross-ref into the 'xmark' table */
230
+ insert_commit_xref(mark->rid, mark->name, mark->uuid);
231
+ return 0;
232
+}
233
+
234
+/*
235
+** import_marks()
236
+** Import the marks specified in file 'f' into the 'xmark' table.
237
+** If 'blobs' is non-null, insert all blob marks into it.
238
+** If 'vers' is non-null, insert all commit marks into it.
239
+** Each line in the file must be at most 100 characters in length. This
240
+** seems like a reasonable maximum for a 40-character uuid, and 1-13
241
+** character rid.
242
+** The function returns -1 if any of the lines in file 'f' are malformed,
243
+** or the rid/uuid information doesn't match what is in the repository
244
+** database. Otherwise, 0 is returned.
245
+*/
246
+int import_marks(FILE* f, Bag *blobs, Bag *vers){
247
+ char line[101];
248
+ size_t len;
249
+ while(fgets(line, sizeof(line), f)){
250
+ struct mark_t mark;
251
+ if(strlen(line)==100&&line[99]!='\n'){
252
+ /* line too long */
253
+ return -1;
254
+ }
255
+ if( parse_mark(line, &mark)<0 ){
256
+ return -1;
257
+ }else if( line[0]=='b' ){
258
+ /* Don't import blob marks into 'xmark' table--git doesn't use them,
259
+ ** so they need to be left free for git to reuse. */
260
+ if(blobs!=NULL){
261
+ bag_insert(blobs, mark.rid);
262
+ }
263
+ }else if( vers!=NULL ){
264
+ bag_insert(vers, mark.rid);
265
+ }
266
+ free(mark.name);
267
+ }
268
+ return 0;
269
+}
270
+
271
+/*
272
+** If 'blobs' is non-null, it must point to a Bag of blob rids to be
273
+** written to disk. Blob rids are written as 'b<rid>'.
274
+** If 'vers' is non-null, it must point to a Bag of commit rids to be
275
+** written to disk. Commit rids are written as 'c<rid> :<mark> <uuid>'.
276
+** All commit (mark,rid,uuid) tuples are stored in 'xmark' table.
277
+** This function does not fail, but may produce errors if a uuid cannot
278
+** be found for an rid in 'vers'.
279
+*/
280
+void export_marks(FILE* f, Bag *blobs, Bag *vers){
281
+ int rid;
282
+ if( blobs!=NULL ){
283
+ rid = bag_first(blobs);
284
+ if(rid!=0){
285
+ do{
286
+ fprintf(f, "b%d\n", rid);
287
+ }while((rid = bag_next(blobs, rid))!=0);
288
+ }
289
+ }
290
+ if( vers!=NULL ){
291
+ rid = bag_first(vers);
292
+ if( rid!=0 ){
293
+ do{
294
+ char *zUuid = rid_to_uuid(rid);
295
+ char *zMark;
296
+ if(zUuid==NULL){
297
+ fossil_trace("No uuid matching rid=%d when exporting marks\n", rid);
298
+ continue;
299
+ }
300
+ zMark = mark_name_from_rid(rid);
301
+ fprintf(f, "c%d %s %s\n", rid, zMark, zUuid);
302
+ free(zMark);
303
+ free(zUuid);
304
+ }while( (rid = bag_next(vers, rid))!=0 );
305
+ }
306
+ }
307
+}
101308
102309
/*
103310
** COMMAND: export
104311
**
105312
** Usage: %fossil export --git ?OPTIONS? ?REPOSITORY?
@@ -147,35 +354,41 @@
147354
verify_all_options();
148355
if( g.argc!=2 && g.argc!=3 ){ usage("--git ?REPOSITORY?"); }
149356
150357
db_multi_exec("CREATE TEMPORARY TABLE oldblob(rid INTEGER PRIMARY KEY)");
151358
db_multi_exec("CREATE TEMPORARY TABLE oldcommit(rid INTEGER PRIMARY KEY)");
359
+ db_multi_exec("CREATE TEMP TABLE xmark(tname TEXT UNIQUE, trid INT, tuuid TEXT)");
152360
if( markfile_in!=0 ){
153361
Stmt qb,qc;
154362
char line[100];
155363
FILE *f;
364
+ int rid;
156365
157366
f = fossil_fopen(markfile_in, "r");
158367
if( f==0 ){
159368
fossil_fatal("cannot open %s for reading", markfile_in);
160369
}
370
+ if(import_marks(f, &blobs, &vers)<0){
371
+ fossil_fatal("error importing marks from file: %s\n", markfile_in);
372
+ }
161373
db_prepare(&qb, "INSERT OR IGNORE INTO oldblob VALUES (:rid)");
162374
db_prepare(&qc, "INSERT OR IGNORE INTO oldcommit VALUES (:rid)");
163
- while( fgets(line, sizeof(line), f)!=0 ){
164
- if( *line == 'b' ){
165
- db_bind_text(&qb, ":rid", line + 1);
375
+ rid = bag_first(&blobs);
376
+ if(rid!=0){
377
+ do{
378
+ db_bind_int(&qb, ":rid", rid);
166379
db_step(&qb);
167380
db_reset(&qb);
168
- bag_insert(&blobs, atoi(line + 1));
169
- }else if( *line == 'c' ){
170
- db_bind_text(&qc, ":rid", line + 1);
381
+ }while((rid = bag_next(&blobs, rid))!=0);
382
+ }
383
+ rid = bag_first(&vers);
384
+ if(rid!=0){
385
+ do{
386
+ db_bind_int(&qc, ":rid", rid);
171387
db_step(&qc);
172388
db_reset(&qc);
173
- bag_insert(&vers, atoi(line + 1));
174
- }else{
175
- fossil_fatal("bad input from %s: %s", markfile_in, line);
176
- }
389
+ }while((rid = bag_next(&vers, rid))!=0);
177390
}
178391
db_finalize(&qb);
179392
db_finalize(&qc);
180393
fclose(f);
181394
}
@@ -249,10 +462,11 @@
249462
int ckinId = db_column_int(&q, 1);
250463
const char *zComment = db_column_text(&q, 2);
251464
const char *zUser = db_column_text(&q, 3);
252465
const char *zBranch = db_column_text(&q, 4);
253466
char *zBr;
467
+ char *zMark;
254468
255469
bag_insert(&vers, ckinId);
256470
db_bind_int(&q2, ":rid", ckinId);
257471
db_step(&q2);
258472
db_reset(&q2);
@@ -259,11 +473,13 @@
259473
if( zBranch==0 ) zBranch = "trunk";
260474
zBr = mprintf("%s", zBranch);
261475
for(i=0; zBr[i]; i++){
262476
if( !fossil_isalnum(zBr[i]) ) zBr[i] = '_';
263477
}
264
- printf("commit refs/heads/%s\nmark :%d\n", zBr, COMMITMARK(ckinId));
478
+ zMark = mark_name_from_rid(ckinId);
479
+ printf("commit refs/heads/%s\nmark %s\n", zBr, zMark);
480
+ free(zMark);
265481
free(zBr);
266482
printf("committer");
267483
print_person(zUser);
268484
printf(" %s +0000\n", zSecondsSince1970);
269485
if( zComment==0 ) zComment = "null comment";
@@ -273,19 +489,24 @@
273489
" WHERE cid=%d AND isprim"
274490
" AND pid IN (SELECT objid FROM event)",
275491
ckinId
276492
);
277493
if( db_step(&q3) == SQLITE_ROW ){
278
- printf("from :%d\n", COMMITMARK(db_column_int(&q3, 0)));
494
+ int pid = db_column_int(&q3, 0);
495
+ zMark = mark_name_from_rid(pid);
496
+ printf("from %s\n", zMark);
497
+ free(zMark);
279498
db_prepare(&q4,
280499
"SELECT pid FROM plink"
281500
" WHERE cid=%d AND NOT isprim"
282501
" AND NOT EXISTS(SELECT 1 FROM phantom WHERE rid=pid)"
283502
" ORDER BY pid",
284503
ckinId);
285504
while( db_step(&q4)==SQLITE_ROW ){
286
- printf("merge :%d\n", COMMITMARK(db_column_int(&q4,0)));
505
+ zMark = mark_name_from_rid(db_column_int(&q4, 0));
506
+ printf("merge %s\n", zMark);
507
+ free(zMark);
287508
}
288509
db_finalize(&q4);
289510
}else{
290511
printf("deleteall\n");
291512
}
@@ -316,11 +537,10 @@
316537
db_finalize(&q3);
317538
printf("\n");
318539
}
319540
db_finalize(&q2);
320541
db_finalize(&q);
321
- bag_clear(&blobs);
322542
manifest_cache_clear();
323543
324544
325545
/* Output tags */
326546
db_prepare(&q,
@@ -345,28 +565,20 @@
345565
printf("tagger <tagger> %s +0000\n", zSecSince1970);
346566
printf("data 0\n");
347567
fossil_free(zEncoded);
348568
}
349569
db_finalize(&q);
350
- bag_clear(&vers);
351570
352571
if( markfile_out!=0 ){
353572
FILE *f;
354573
f = fossil_fopen(markfile_out, "w");
355574
if( f == 0 ){
356575
fossil_fatal("cannot open %s for writing", markfile_out);
357576
}
358
- db_prepare(&q, "SELECT rid FROM oldblob");
359
- while( db_step(&q)==SQLITE_ROW ){
360
- fprintf(f, "b%d\n", db_column_int(&q, 0));
361
- }
362
- db_finalize(&q);
363
- db_prepare(&q, "SELECT rid FROM oldcommit");
364
- while( db_step(&q)==SQLITE_ROW ){
365
- fprintf(f, "c%d\n", db_column_int(&q, 0));
366
- }
367
- db_finalize(&q);
577
+ export_marks(f, &blobs, &vers);
368578
if( ferror(f)!=0 || fclose(f)!=0 ) {
369579
fossil_fatal("error while writing %s", markfile_out);
370580
}
371581
}
582
+ bag_clear(&blobs);
583
+ bag_clear(&vers);
372584
}
373585
--- src/export.c
+++ src/export.c
@@ -19,10 +19,28 @@
19 */
20 #include "config.h"
21 #include "export.h"
22 #include <assert.h>
23
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24 /*
25 ** Output a "committer" record for the given user.
26 */
27 static void print_person(const char *zUser){
28 static Stmt q;
@@ -96,10 +114,199 @@
96 db_reset(&q);
97 }
98
99 #define BLOBMARK(rid) ((rid) * 2)
100 #define COMMITMARK(rid) ((rid) * 2 + 1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
102 /*
103 ** COMMAND: export
104 **
105 ** Usage: %fossil export --git ?OPTIONS? ?REPOSITORY?
@@ -147,35 +354,41 @@
147 verify_all_options();
148 if( g.argc!=2 && g.argc!=3 ){ usage("--git ?REPOSITORY?"); }
149
150 db_multi_exec("CREATE TEMPORARY TABLE oldblob(rid INTEGER PRIMARY KEY)");
151 db_multi_exec("CREATE TEMPORARY TABLE oldcommit(rid INTEGER PRIMARY KEY)");
 
152 if( markfile_in!=0 ){
153 Stmt qb,qc;
154 char line[100];
155 FILE *f;
 
156
157 f = fossil_fopen(markfile_in, "r");
158 if( f==0 ){
159 fossil_fatal("cannot open %s for reading", markfile_in);
160 }
 
 
 
161 db_prepare(&qb, "INSERT OR IGNORE INTO oldblob VALUES (:rid)");
162 db_prepare(&qc, "INSERT OR IGNORE INTO oldcommit VALUES (:rid)");
163 while( fgets(line, sizeof(line), f)!=0 ){
164 if( *line == 'b' ){
165 db_bind_text(&qb, ":rid", line + 1);
 
166 db_step(&qb);
167 db_reset(&qb);
168 bag_insert(&blobs, atoi(line + 1));
169 }else if( *line == 'c' ){
170 db_bind_text(&qc, ":rid", line + 1);
 
 
 
171 db_step(&qc);
172 db_reset(&qc);
173 bag_insert(&vers, atoi(line + 1));
174 }else{
175 fossil_fatal("bad input from %s: %s", markfile_in, line);
176 }
177 }
178 db_finalize(&qb);
179 db_finalize(&qc);
180 fclose(f);
181 }
@@ -249,10 +462,11 @@
249 int ckinId = db_column_int(&q, 1);
250 const char *zComment = db_column_text(&q, 2);
251 const char *zUser = db_column_text(&q, 3);
252 const char *zBranch = db_column_text(&q, 4);
253 char *zBr;
 
254
255 bag_insert(&vers, ckinId);
256 db_bind_int(&q2, ":rid", ckinId);
257 db_step(&q2);
258 db_reset(&q2);
@@ -259,11 +473,13 @@
259 if( zBranch==0 ) zBranch = "trunk";
260 zBr = mprintf("%s", zBranch);
261 for(i=0; zBr[i]; i++){
262 if( !fossil_isalnum(zBr[i]) ) zBr[i] = '_';
263 }
264 printf("commit refs/heads/%s\nmark :%d\n", zBr, COMMITMARK(ckinId));
 
 
265 free(zBr);
266 printf("committer");
267 print_person(zUser);
268 printf(" %s +0000\n", zSecondsSince1970);
269 if( zComment==0 ) zComment = "null comment";
@@ -273,19 +489,24 @@
273 " WHERE cid=%d AND isprim"
274 " AND pid IN (SELECT objid FROM event)",
275 ckinId
276 );
277 if( db_step(&q3) == SQLITE_ROW ){
278 printf("from :%d\n", COMMITMARK(db_column_int(&q3, 0)));
 
 
 
279 db_prepare(&q4,
280 "SELECT pid FROM plink"
281 " WHERE cid=%d AND NOT isprim"
282 " AND NOT EXISTS(SELECT 1 FROM phantom WHERE rid=pid)"
283 " ORDER BY pid",
284 ckinId);
285 while( db_step(&q4)==SQLITE_ROW ){
286 printf("merge :%d\n", COMMITMARK(db_column_int(&q4,0)));
 
 
287 }
288 db_finalize(&q4);
289 }else{
290 printf("deleteall\n");
291 }
@@ -316,11 +537,10 @@
316 db_finalize(&q3);
317 printf("\n");
318 }
319 db_finalize(&q2);
320 db_finalize(&q);
321 bag_clear(&blobs);
322 manifest_cache_clear();
323
324
325 /* Output tags */
326 db_prepare(&q,
@@ -345,28 +565,20 @@
345 printf("tagger <tagger> %s +0000\n", zSecSince1970);
346 printf("data 0\n");
347 fossil_free(zEncoded);
348 }
349 db_finalize(&q);
350 bag_clear(&vers);
351
352 if( markfile_out!=0 ){
353 FILE *f;
354 f = fossil_fopen(markfile_out, "w");
355 if( f == 0 ){
356 fossil_fatal("cannot open %s for writing", markfile_out);
357 }
358 db_prepare(&q, "SELECT rid FROM oldblob");
359 while( db_step(&q)==SQLITE_ROW ){
360 fprintf(f, "b%d\n", db_column_int(&q, 0));
361 }
362 db_finalize(&q);
363 db_prepare(&q, "SELECT rid FROM oldcommit");
364 while( db_step(&q)==SQLITE_ROW ){
365 fprintf(f, "c%d\n", db_column_int(&q, 0));
366 }
367 db_finalize(&q);
368 if( ferror(f)!=0 || fclose(f)!=0 ) {
369 fossil_fatal("error while writing %s", markfile_out);
370 }
371 }
 
 
372 }
373
--- src/export.c
+++ src/export.c
@@ -19,10 +19,28 @@
19 */
20 #include "config.h"
21 #include "export.h"
22 #include <assert.h>
23
24 #if INTERFACE
25 /*
26 ** struct mark_t
27 ** holds information for translating between git commits
28 ** and fossil commits.
29 ** -git_name: This is the mark name that identifies the commit to git.
30 ** It will always begin with a ':'.
31 ** -rid: The unique object ID that identifies this commit within the
32 ** repository database.
33 ** -uuid: The SHA-1 of artifact corresponding to rid.
34 */
35 struct mark_t{
36 char *name;
37 int rid;
38 char uuid[41];
39 };
40 #endif
41
42 /*
43 ** Output a "committer" record for the given user.
44 */
45 static void print_person(const char *zUser){
46 static Stmt q;
@@ -96,10 +114,199 @@
114 db_reset(&q);
115 }
116
117 #define BLOBMARK(rid) ((rid) * 2)
118 #define COMMITMARK(rid) ((rid) * 2 + 1)
119
120 /*
121 ** insert_commit_xref()
122 ** Insert a new (mark,rid,uuid) entry into the 'xmark' table.
123 ** zName and zUuid must be non-null and must point to NULL-terminated strings.
124 */
125 void insert_commit_xref(int rid, const char *zName, const char *zUuid){
126 db_multi_exec(
127 "INSERT OR IGNORE INTO xmark(tname, trid, tuuid)"
128 "VALUES(%Q,%d,%Q)",
129 zName, rid, zUuid
130 );
131 }
132
133 /*
134 ** create_mark()
135 ** Create a new (mark,rid,uuid) entry for the given rid in the 'xmark' table,
136 ** and return that information as a struct mark_t in *mark.
137 ** This function returns -1 in the case where 'rid' does not exist, otherwise
138 ** it returns 0.
139 ** mark->name is dynamically allocated and is owned by the caller upon return.
140 */
141 int create_mark(int rid, struct mark_t *mark){
142 char sid[13];
143 char *zUuid = rid_to_uuid(rid);
144 if(!zUuid){
145 fossil_trace("Undefined rid=%d\n", rid);
146 return -1;
147 }
148 mark->rid = rid;
149 sprintf(sid, ":%d", COMMITMARK(rid));
150 mark->name = fossil_strdup(sid);
151 strcpy(mark->uuid, zUuid);
152 free(zUuid);
153 insert_commit_xref(mark->rid, mark->name, mark->uuid);
154 return 0;
155 }
156
157 /*
158 ** mark_name_from_rid()
159 ** Find the mark associated with the given rid. Mark names always start
160 ** with ':', and are pulled from the 'xmark' temporary table.
161 ** This function returns NULL if the rid does not exist in the 'xmark' table.
162 ** Otherwise, it returns the name of the mark, which is dynamically allocated
163 ** and is owned by the caller of this function.
164 */
165 char * mark_name_from_rid(int rid){
166 char *zMark = db_text(0, "SELECT tname FROM xmark WHERE trid=%d", rid);
167 if(zMark==NULL){
168 struct mark_t mark;
169 if(create_mark(rid, &mark)==0){
170 zMark = mark.name;
171 }else{
172 return NULL;
173 }
174 }
175 return zMark;
176 }
177
178 /*
179 ** parse_mark()
180 ** Create a new (mark,rid,uuid) entry in the 'xmark' table given a line
181 ** from a marks file. Return the cross-ref information as a struct mark_t
182 ** in *mark.
183 ** This function returns -1 in the case that the line is blank, malformed, or
184 ** the rid/uuid named in 'line' does not match what is in the repository
185 ** database. Otherwise, 0 is returned.
186 ** mark->name is dynamically allocated, and owned by the caller.
187 */
188 int parse_mark(char *line, struct mark_t *mark){
189 char *cur_tok;
190 char type;
191 cur_tok = strtok(line, " \t");
192 if(!cur_tok||strlen(cur_tok)<2){
193 return -1;
194 }
195 mark->rid = atoi(&cur_tok[1]);
196 if(cur_tok[0]!='c'){
197 /* This is probably a blob mark */
198 mark->name = NULL;
199 return 0;
200 }
201
202 cur_tok = strtok(NULL, " \t");
203 if(!cur_tok){
204 /* This mark was generated by an older version of Fossil and doesn't
205 ** include the mark name and uuid. create_mark() will name the new mark
206 ** exactly as it was when exported to git, so that we should have a
207 ** valid mapping from git sha1<->mark name<->fossil sha1. */
208 return create_mark(mark->rid, mark);
209 }else{
210 mark->name = fossil_strdup(cur_tok);
211 }
212
213 cur_tok = strtok(NULL, "\n");
214 if(!cur_tok||strlen(cur_tok)!=40){
215 free(mark->name);
216 fossil_trace("Invalid SHA-1 in marks file: %s\n", cur_tok);
217 return -1;
218 }else{
219 strcpy(mark->uuid, cur_tok);
220 }
221
222 /* make sure that rid corresponds to UUID */
223 if(fast_uuid_to_rid(mark->uuid)!=mark->rid){
224 free(mark->name);
225 fossil_trace("Non-existent SHA-1 in marks file: %s\n", mark->uuid);
226 return -1;
227 }
228
229 /* insert a cross-ref into the 'xmark' table */
230 insert_commit_xref(mark->rid, mark->name, mark->uuid);
231 return 0;
232 }
233
234 /*
235 ** import_marks()
236 ** Import the marks specified in file 'f' into the 'xmark' table.
237 ** If 'blobs' is non-null, insert all blob marks into it.
238 ** If 'vers' is non-null, insert all commit marks into it.
239 ** Each line in the file must be at most 100 characters in length. This
240 ** seems like a reasonable maximum for a 40-character uuid, and 1-13
241 ** character rid.
242 ** The function returns -1 if any of the lines in file 'f' are malformed,
243 ** or the rid/uuid information doesn't match what is in the repository
244 ** database. Otherwise, 0 is returned.
245 */
246 int import_marks(FILE* f, Bag *blobs, Bag *vers){
247 char line[101];
248 size_t len;
249 while(fgets(line, sizeof(line), f)){
250 struct mark_t mark;
251 if(strlen(line)==100&&line[99]!='\n'){
252 /* line too long */
253 return -1;
254 }
255 if( parse_mark(line, &mark)<0 ){
256 return -1;
257 }else if( line[0]=='b' ){
258 /* Don't import blob marks into 'xmark' table--git doesn't use them,
259 ** so they need to be left free for git to reuse. */
260 if(blobs!=NULL){
261 bag_insert(blobs, mark.rid);
262 }
263 }else if( vers!=NULL ){
264 bag_insert(vers, mark.rid);
265 }
266 free(mark.name);
267 }
268 return 0;
269 }
270
271 /*
272 ** If 'blobs' is non-null, it must point to a Bag of blob rids to be
273 ** written to disk. Blob rids are written as 'b<rid>'.
274 ** If 'vers' is non-null, it must point to a Bag of commit rids to be
275 ** written to disk. Commit rids are written as 'c<rid> :<mark> <uuid>'.
276 ** All commit (mark,rid,uuid) tuples are stored in 'xmark' table.
277 ** This function does not fail, but may produce errors if a uuid cannot
278 ** be found for an rid in 'vers'.
279 */
280 void export_marks(FILE* f, Bag *blobs, Bag *vers){
281 int rid;
282 if( blobs!=NULL ){
283 rid = bag_first(blobs);
284 if(rid!=0){
285 do{
286 fprintf(f, "b%d\n", rid);
287 }while((rid = bag_next(blobs, rid))!=0);
288 }
289 }
290 if( vers!=NULL ){
291 rid = bag_first(vers);
292 if( rid!=0 ){
293 do{
294 char *zUuid = rid_to_uuid(rid);
295 char *zMark;
296 if(zUuid==NULL){
297 fossil_trace("No uuid matching rid=%d when exporting marks\n", rid);
298 continue;
299 }
300 zMark = mark_name_from_rid(rid);
301 fprintf(f, "c%d %s %s\n", rid, zMark, zUuid);
302 free(zMark);
303 free(zUuid);
304 }while( (rid = bag_next(vers, rid))!=0 );
305 }
306 }
307 }
308
309 /*
310 ** COMMAND: export
311 **
312 ** Usage: %fossil export --git ?OPTIONS? ?REPOSITORY?
@@ -147,35 +354,41 @@
354 verify_all_options();
355 if( g.argc!=2 && g.argc!=3 ){ usage("--git ?REPOSITORY?"); }
356
357 db_multi_exec("CREATE TEMPORARY TABLE oldblob(rid INTEGER PRIMARY KEY)");
358 db_multi_exec("CREATE TEMPORARY TABLE oldcommit(rid INTEGER PRIMARY KEY)");
359 db_multi_exec("CREATE TEMP TABLE xmark(tname TEXT UNIQUE, trid INT, tuuid TEXT)");
360 if( markfile_in!=0 ){
361 Stmt qb,qc;
362 char line[100];
363 FILE *f;
364 int rid;
365
366 f = fossil_fopen(markfile_in, "r");
367 if( f==0 ){
368 fossil_fatal("cannot open %s for reading", markfile_in);
369 }
370 if(import_marks(f, &blobs, &vers)<0){
371 fossil_fatal("error importing marks from file: %s\n", markfile_in);
372 }
373 db_prepare(&qb, "INSERT OR IGNORE INTO oldblob VALUES (:rid)");
374 db_prepare(&qc, "INSERT OR IGNORE INTO oldcommit VALUES (:rid)");
375 rid = bag_first(&blobs);
376 if(rid!=0){
377 do{
378 db_bind_int(&qb, ":rid", rid);
379 db_step(&qb);
380 db_reset(&qb);
381 }while((rid = bag_next(&blobs, rid))!=0);
382 }
383 rid = bag_first(&vers);
384 if(rid!=0){
385 do{
386 db_bind_int(&qc, ":rid", rid);
387 db_step(&qc);
388 db_reset(&qc);
389 }while((rid = bag_next(&vers, rid))!=0);
 
 
 
390 }
391 db_finalize(&qb);
392 db_finalize(&qc);
393 fclose(f);
394 }
@@ -249,10 +462,11 @@
462 int ckinId = db_column_int(&q, 1);
463 const char *zComment = db_column_text(&q, 2);
464 const char *zUser = db_column_text(&q, 3);
465 const char *zBranch = db_column_text(&q, 4);
466 char *zBr;
467 char *zMark;
468
469 bag_insert(&vers, ckinId);
470 db_bind_int(&q2, ":rid", ckinId);
471 db_step(&q2);
472 db_reset(&q2);
@@ -259,11 +473,13 @@
473 if( zBranch==0 ) zBranch = "trunk";
474 zBr = mprintf("%s", zBranch);
475 for(i=0; zBr[i]; i++){
476 if( !fossil_isalnum(zBr[i]) ) zBr[i] = '_';
477 }
478 zMark = mark_name_from_rid(ckinId);
479 printf("commit refs/heads/%s\nmark %s\n", zBr, zMark);
480 free(zMark);
481 free(zBr);
482 printf("committer");
483 print_person(zUser);
484 printf(" %s +0000\n", zSecondsSince1970);
485 if( zComment==0 ) zComment = "null comment";
@@ -273,19 +489,24 @@
489 " WHERE cid=%d AND isprim"
490 " AND pid IN (SELECT objid FROM event)",
491 ckinId
492 );
493 if( db_step(&q3) == SQLITE_ROW ){
494 int pid = db_column_int(&q3, 0);
495 zMark = mark_name_from_rid(pid);
496 printf("from %s\n", zMark);
497 free(zMark);
498 db_prepare(&q4,
499 "SELECT pid FROM plink"
500 " WHERE cid=%d AND NOT isprim"
501 " AND NOT EXISTS(SELECT 1 FROM phantom WHERE rid=pid)"
502 " ORDER BY pid",
503 ckinId);
504 while( db_step(&q4)==SQLITE_ROW ){
505 zMark = mark_name_from_rid(db_column_int(&q4, 0));
506 printf("merge %s\n", zMark);
507 free(zMark);
508 }
509 db_finalize(&q4);
510 }else{
511 printf("deleteall\n");
512 }
@@ -316,11 +537,10 @@
537 db_finalize(&q3);
538 printf("\n");
539 }
540 db_finalize(&q2);
541 db_finalize(&q);
 
542 manifest_cache_clear();
543
544
545 /* Output tags */
546 db_prepare(&q,
@@ -345,28 +565,20 @@
565 printf("tagger <tagger> %s +0000\n", zSecSince1970);
566 printf("data 0\n");
567 fossil_free(zEncoded);
568 }
569 db_finalize(&q);
 
570
571 if( markfile_out!=0 ){
572 FILE *f;
573 f = fossil_fopen(markfile_out, "w");
574 if( f == 0 ){
575 fossil_fatal("cannot open %s for writing", markfile_out);
576 }
577 export_marks(f, &blobs, &vers);
 
 
 
 
 
 
 
 
 
578 if( ferror(f)!=0 || fclose(f)!=0 ) {
579 fossil_fatal("error while writing %s", markfile_out);
580 }
581 }
582 bag_clear(&blobs);
583 bag_clear(&vers);
584 }
585
+50 -2
--- src/import.c
+++ src/import.c
@@ -1494,10 +1494,13 @@
14941494
** data is read from standard input.
14951495
**
14961496
** The following formats are currently understood by this command
14971497
**
14981498
** --git Import from the git-fast-export file format (default)
1499
+** Options:
1500
+** --import-marks FILE Restore marks table from FILE
1501
+** --export-marks FILE Save marks table to FILE
14991502
**
15001503
** --svn Import from the svnadmin-dump file format. The default
15011504
** behaviour (unless overridden by --flat) is to treat 3
15021505
** folders in the SVN root as special, following the
15031506
** common layout of SVN repositories. These are (by
@@ -1525,19 +1528,24 @@
15251528
char *zPassword;
15261529
FILE *pIn;
15271530
Stmt q;
15281531
int forceFlag = find_option("force", "f", 0)!=0;
15291532
int svnFlag = find_option("svn", 0, 0)!=0;
1533
+ int gitFlag = find_option("git", 0, 0)!=0;
15301534
int omitRebuild = find_option("no-rebuild",0,0)!=0;
15311535
int omitVacuum = find_option("no-vacuum",0,0)!=0;
15321536
15331537
/* Options common to all input formats */
15341538
int incrFlag = find_option("incremental", "i", 0)!=0;
15351539
15361540
/* Options for --svn only */
15371541
const char *zBase="";
15381542
int flatFlag=0;
1543
+
1544
+ /* Options for --git only */
1545
+ const char *markfile_in;
1546
+ const char *markfile_out;
15391547
15401548
if( svnFlag ){
15411549
/* Get --svn related options here, so verify_all_options() fail when svn
15421550
* only option are specify with --git
15431551
*/
@@ -1545,12 +1553,13 @@
15451553
flatFlag = find_option("flat", 0, 0)!=0;
15461554
gsvn.zTrunk = find_option("trunk", 0, 1);
15471555
gsvn.zBranches = find_option("branches", 0, 1);
15481556
gsvn.zTags = find_option("tags", 0, 1);
15491557
gsvn.incrFlag = incrFlag;
1550
- }else{
1551
- find_option("git",0,0); /* Skip the --git option for now */
1558
+ }else if( gitFlag ){
1559
+ markfile_in = find_option("import-marks", 0, 1);
1560
+ markfile_out = find_option("export-marks", 0, 1);
15521561
}
15531562
verify_all_options();
15541563
15551564
if( g.argc!=3 && g.argc!=4 ){
15561565
usage("--git|--svn ?OPTIONS? NEW-REPOSITORY ?INPUT-FILE?");
@@ -1624,10 +1633,13 @@
16241633
gsvn.lenTags++;
16251634
}
16261635
}
16271636
svn_dump_import(pIn);
16281637
}else{
1638
+ Bag blobs, vers;
1639
+ bag_init(&blobs);
1640
+ bag_init(&vers);
16291641
/* The following temp-tables are used to hold information needed for
16301642
** the import.
16311643
**
16321644
** The XMARK table provides a mapping from fast-import "marks" and symbols
16331645
** into artifact ids (UUIDs - the 40-byte hex SHA1 hash of artifacts).
@@ -1648,10 +1660,21 @@
16481660
db_multi_exec(
16491661
"CREATE TEMP TABLE xmark(tname TEXT UNIQUE, trid INT, tuuid TEXT);"
16501662
"CREATE TEMP TABLE xbranch(tname TEXT UNIQUE, brnm TEXT);"
16511663
"CREATE TEMP TABLE xtag(tname TEXT UNIQUE, tcontent TEXT);"
16521664
);
1665
+
1666
+ if(markfile_in){
1667
+ FILE *f = fossil_fopen(markfile_in, "r");
1668
+ if(!f){
1669
+ fossil_fatal("cannot open %s for reading\n", markfile_in);
1670
+ }
1671
+ if(import_marks(f, &blobs, NULL)<0){
1672
+ fossil_fatal("error importing marks from file: %s\n", markfile_in);
1673
+ }
1674
+ fclose(f);
1675
+ }
16531676
16541677
manifest_crosslink_begin();
16551678
git_fast_import(pIn);
16561679
db_prepare(&q, "SELECT tcontent FROM xtag");
16571680
while( db_step(&q)==SQLITE_ROW ){
@@ -1659,10 +1682,35 @@
16591682
db_ephemeral_blob(&q, 0, &record);
16601683
fast_insert_content(&record, 0, 0, 1);
16611684
import_reset(0);
16621685
}
16631686
db_finalize(&q);
1687
+ if(markfile_out){
1688
+ int rid;
1689
+ Stmt q_marks;
1690
+ FILE *f;
1691
+ db_prepare(&q_marks, "SELECT DISTINCT trid FROM xmark");
1692
+ while( db_step(&q_marks)==SQLITE_ROW){
1693
+ rid = db_column_int(&q_marks, 0);
1694
+ if(db_int(0, "SELECT count(objid) FROM event WHERE objid=%d AND type='ci'", rid)==0){
1695
+ if(bag_find(&blobs, rid)==0){
1696
+ bag_insert(&blobs, rid);
1697
+ }
1698
+ }else{
1699
+ bag_insert(&vers, rid);
1700
+ }
1701
+ }
1702
+ db_finalize(&q_marks);
1703
+ f = fossil_fopen(markfile_out, "w");
1704
+ if(!f){
1705
+ fossil_fatal("cannot open %s for writing\n", markfile_out);
1706
+ }
1707
+ export_marks(f, &blobs, &vers);
1708
+ fclose(f);
1709
+ bag_clear(&blobs);
1710
+ bag_clear(&vers);
1711
+ }
16641712
manifest_crosslink_end(MC_NONE);
16651713
}
16661714
16671715
verify_cancel();
16681716
db_end_transaction(0);
16691717
--- src/import.c
+++ src/import.c
@@ -1494,10 +1494,13 @@
1494 ** data is read from standard input.
1495 **
1496 ** The following formats are currently understood by this command
1497 **
1498 ** --git Import from the git-fast-export file format (default)
 
 
 
1499 **
1500 ** --svn Import from the svnadmin-dump file format. The default
1501 ** behaviour (unless overridden by --flat) is to treat 3
1502 ** folders in the SVN root as special, following the
1503 ** common layout of SVN repositories. These are (by
@@ -1525,19 +1528,24 @@
1525 char *zPassword;
1526 FILE *pIn;
1527 Stmt q;
1528 int forceFlag = find_option("force", "f", 0)!=0;
1529 int svnFlag = find_option("svn", 0, 0)!=0;
 
1530 int omitRebuild = find_option("no-rebuild",0,0)!=0;
1531 int omitVacuum = find_option("no-vacuum",0,0)!=0;
1532
1533 /* Options common to all input formats */
1534 int incrFlag = find_option("incremental", "i", 0)!=0;
1535
1536 /* Options for --svn only */
1537 const char *zBase="";
1538 int flatFlag=0;
 
 
 
 
1539
1540 if( svnFlag ){
1541 /* Get --svn related options here, so verify_all_options() fail when svn
1542 * only option are specify with --git
1543 */
@@ -1545,12 +1553,13 @@
1545 flatFlag = find_option("flat", 0, 0)!=0;
1546 gsvn.zTrunk = find_option("trunk", 0, 1);
1547 gsvn.zBranches = find_option("branches", 0, 1);
1548 gsvn.zTags = find_option("tags", 0, 1);
1549 gsvn.incrFlag = incrFlag;
1550 }else{
1551 find_option("git",0,0); /* Skip the --git option for now */
 
1552 }
1553 verify_all_options();
1554
1555 if( g.argc!=3 && g.argc!=4 ){
1556 usage("--git|--svn ?OPTIONS? NEW-REPOSITORY ?INPUT-FILE?");
@@ -1624,10 +1633,13 @@
1624 gsvn.lenTags++;
1625 }
1626 }
1627 svn_dump_import(pIn);
1628 }else{
 
 
 
1629 /* The following temp-tables are used to hold information needed for
1630 ** the import.
1631 **
1632 ** The XMARK table provides a mapping from fast-import "marks" and symbols
1633 ** into artifact ids (UUIDs - the 40-byte hex SHA1 hash of artifacts).
@@ -1648,10 +1660,21 @@
1648 db_multi_exec(
1649 "CREATE TEMP TABLE xmark(tname TEXT UNIQUE, trid INT, tuuid TEXT);"
1650 "CREATE TEMP TABLE xbranch(tname TEXT UNIQUE, brnm TEXT);"
1651 "CREATE TEMP TABLE xtag(tname TEXT UNIQUE, tcontent TEXT);"
1652 );
 
 
 
 
 
 
 
 
 
 
 
1653
1654 manifest_crosslink_begin();
1655 git_fast_import(pIn);
1656 db_prepare(&q, "SELECT tcontent FROM xtag");
1657 while( db_step(&q)==SQLITE_ROW ){
@@ -1659,10 +1682,35 @@
1659 db_ephemeral_blob(&q, 0, &record);
1660 fast_insert_content(&record, 0, 0, 1);
1661 import_reset(0);
1662 }
1663 db_finalize(&q);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1664 manifest_crosslink_end(MC_NONE);
1665 }
1666
1667 verify_cancel();
1668 db_end_transaction(0);
1669
--- src/import.c
+++ src/import.c
@@ -1494,10 +1494,13 @@
1494 ** data is read from standard input.
1495 **
1496 ** The following formats are currently understood by this command
1497 **
1498 ** --git Import from the git-fast-export file format (default)
1499 ** Options:
1500 ** --import-marks FILE Restore marks table from FILE
1501 ** --export-marks FILE Save marks table to FILE
1502 **
1503 ** --svn Import from the svnadmin-dump file format. The default
1504 ** behaviour (unless overridden by --flat) is to treat 3
1505 ** folders in the SVN root as special, following the
1506 ** common layout of SVN repositories. These are (by
@@ -1525,19 +1528,24 @@
1528 char *zPassword;
1529 FILE *pIn;
1530 Stmt q;
1531 int forceFlag = find_option("force", "f", 0)!=0;
1532 int svnFlag = find_option("svn", 0, 0)!=0;
1533 int gitFlag = find_option("git", 0, 0)!=0;
1534 int omitRebuild = find_option("no-rebuild",0,0)!=0;
1535 int omitVacuum = find_option("no-vacuum",0,0)!=0;
1536
1537 /* Options common to all input formats */
1538 int incrFlag = find_option("incremental", "i", 0)!=0;
1539
1540 /* Options for --svn only */
1541 const char *zBase="";
1542 int flatFlag=0;
1543
1544 /* Options for --git only */
1545 const char *markfile_in;
1546 const char *markfile_out;
1547
1548 if( svnFlag ){
1549 /* Get --svn related options here, so verify_all_options() fail when svn
1550 * only option are specify with --git
1551 */
@@ -1545,12 +1553,13 @@
1553 flatFlag = find_option("flat", 0, 0)!=0;
1554 gsvn.zTrunk = find_option("trunk", 0, 1);
1555 gsvn.zBranches = find_option("branches", 0, 1);
1556 gsvn.zTags = find_option("tags", 0, 1);
1557 gsvn.incrFlag = incrFlag;
1558 }else if( gitFlag ){
1559 markfile_in = find_option("import-marks", 0, 1);
1560 markfile_out = find_option("export-marks", 0, 1);
1561 }
1562 verify_all_options();
1563
1564 if( g.argc!=3 && g.argc!=4 ){
1565 usage("--git|--svn ?OPTIONS? NEW-REPOSITORY ?INPUT-FILE?");
@@ -1624,10 +1633,13 @@
1633 gsvn.lenTags++;
1634 }
1635 }
1636 svn_dump_import(pIn);
1637 }else{
1638 Bag blobs, vers;
1639 bag_init(&blobs);
1640 bag_init(&vers);
1641 /* The following temp-tables are used to hold information needed for
1642 ** the import.
1643 **
1644 ** The XMARK table provides a mapping from fast-import "marks" and symbols
1645 ** into artifact ids (UUIDs - the 40-byte hex SHA1 hash of artifacts).
@@ -1648,10 +1660,21 @@
1660 db_multi_exec(
1661 "CREATE TEMP TABLE xmark(tname TEXT UNIQUE, trid INT, tuuid TEXT);"
1662 "CREATE TEMP TABLE xbranch(tname TEXT UNIQUE, brnm TEXT);"
1663 "CREATE TEMP TABLE xtag(tname TEXT UNIQUE, tcontent TEXT);"
1664 );
1665
1666 if(markfile_in){
1667 FILE *f = fossil_fopen(markfile_in, "r");
1668 if(!f){
1669 fossil_fatal("cannot open %s for reading\n", markfile_in);
1670 }
1671 if(import_marks(f, &blobs, NULL)<0){
1672 fossil_fatal("error importing marks from file: %s\n", markfile_in);
1673 }
1674 fclose(f);
1675 }
1676
1677 manifest_crosslink_begin();
1678 git_fast_import(pIn);
1679 db_prepare(&q, "SELECT tcontent FROM xtag");
1680 while( db_step(&q)==SQLITE_ROW ){
@@ -1659,10 +1682,35 @@
1682 db_ephemeral_blob(&q, 0, &record);
1683 fast_insert_content(&record, 0, 0, 1);
1684 import_reset(0);
1685 }
1686 db_finalize(&q);
1687 if(markfile_out){
1688 int rid;
1689 Stmt q_marks;
1690 FILE *f;
1691 db_prepare(&q_marks, "SELECT DISTINCT trid FROM xmark");
1692 while( db_step(&q_marks)==SQLITE_ROW){
1693 rid = db_column_int(&q_marks, 0);
1694 if(db_int(0, "SELECT count(objid) FROM event WHERE objid=%d AND type='ci'", rid)==0){
1695 if(bag_find(&blobs, rid)==0){
1696 bag_insert(&blobs, rid);
1697 }
1698 }else{
1699 bag_insert(&vers, rid);
1700 }
1701 }
1702 db_finalize(&q_marks);
1703 f = fossil_fopen(markfile_out, "w");
1704 if(!f){
1705 fossil_fatal("cannot open %s for writing\n", markfile_out);
1706 }
1707 export_marks(f, &blobs, &vers);
1708 fclose(f);
1709 bag_clear(&blobs);
1710 bag_clear(&vers);
1711 }
1712 manifest_crosslink_end(MC_NONE);
1713 }
1714
1715 verify_cancel();
1716 db_end_transaction(0);
1717

Keyboard Shortcuts

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