Fossil SCM

Add --import-marks and --export-marks options to `fossil import' subcommand. This allows one to save all git-generated marks following an import. PPreviously, performing an incremental import from git resulted in new commits being imported as orphans, and trying to perform an export to git at some later time resultedd in git complaining of undefined marks. To remedy this, the format of the marks file is amended to include the rid, mark, and UUID of each commit. Now, fossil and git agree on the mapping between commits and marks, so updates in either direction can be performed easily.

nick.lloyd 2016-05-14 21:41 nick.lloyd-git-interop
Commit c26213be47b980847c85bac250cfe45e52f6154b
2 files changed +237 -25 +49 -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
+ if(strlen(line)==100&&line[99]!='\n'){
251
+ /* line too long */
252
+ return -1;
253
+ }
254
+ struct mark_t mark;
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
+** export_marks()
273
+** If `blobs' is non-null, it must point to a Bag of blob rids to be
274
+** written to disk. Blob rids are written as 'b<rid>'.
275
+** If `vers' is non-null, it must point to a Bag of commit rids to be
276
+** written to disk. Commit rids are written as 'c<rid> :<mark> <uuid>'.
277
+** All commit (mark,rid,uuid) tuples are stored in `xmark' table.
278
+** This function does not fail, but may produce errors if a uuid cannot
279
+** be found for an rid in `vers'.
280
+*/
281
+void export_marks(FILE* f, Bag *blobs, Bag *vers){
282
+ int rid;
283
+ if(blobs!=NULL){
284
+ rid = bag_first(blobs);
285
+ if(rid!=0){
286
+ do{
287
+ fprintf(f, "b%d\n", rid);
288
+ }while((rid = bag_next(blobs, rid))!=0);
289
+ }
290
+ }
291
+ if(vers!=NULL){
292
+ rid = bag_first(vers);
293
+ if(rid!=0){
294
+ do{
295
+ char *zUuid = rid_to_uuid(rid);
296
+ if(zUuid==NULL){
297
+ fossil_trace("No uuid matching rid=%d when exporting marks\n", rid);
298
+ continue;
299
+ }
300
+ char *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 if(strlen(line)==100&&line[99]!='\n'){
251 /* line too long */
252 return -1;
253 }
254 struct mark_t mark;
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 ** export_marks()
273 ** If `blobs' is non-null, it must point to a Bag of blob rids to be
274 ** written to disk. Blob rids are written as 'b<rid>'.
275 ** If `vers' is non-null, it must point to a Bag of commit rids to be
276 ** written to disk. Commit rids are written as 'c<rid> :<mark> <uuid>'.
277 ** All commit (mark,rid,uuid) tuples are stored in `xmark' table.
278 ** This function does not fail, but may produce errors if a uuid cannot
279 ** be found for an rid in `vers'.
280 */
281 void export_marks(FILE* f, Bag *blobs, Bag *vers){
282 int rid;
283 if(blobs!=NULL){
284 rid = bag_first(blobs);
285 if(rid!=0){
286 do{
287 fprintf(f, "b%d\n", rid);
288 }while((rid = bag_next(blobs, rid))!=0);
289 }
290 }
291 if(vers!=NULL){
292 rid = bag_first(vers);
293 if(rid!=0){
294 do{
295 char *zUuid = rid_to_uuid(rid);
296 if(zUuid==NULL){
297 fossil_trace("No uuid matching rid=%d when exporting marks\n", rid);
298 continue;
299 }
300 char *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
+49 -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,34 @@
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
+ db_prepare(&q_marks, "SELECT DISTINCT trid FROM xmark");
1691
+ while( db_step(&q_marks)==SQLITE_ROW){
1692
+ rid = db_column_int(&q_marks, 0);
1693
+ if(db_int(0, "SELECT count(objid) FROM event WHERE objid=%d AND type='ci'", rid)==0){
1694
+ if(bag_find(&blobs, rid)==0){
1695
+ bag_insert(&blobs, rid);
1696
+ }
1697
+ }else{
1698
+ bag_insert(&vers, rid);
1699
+ }
1700
+ }
1701
+ db_finalize(&q_marks);
1702
+ FILE* f = fossil_fopen(markfile_out, "w");
1703
+ if(!f){
1704
+ fossil_fatal("cannot open %s for writing\n", markfile_out);
1705
+ }
1706
+ export_marks(f, &blobs, &vers);
1707
+ fclose(f);
1708
+ bag_clear(&blobs);
1709
+ bag_clear(&vers);
1710
+ }
16641711
manifest_crosslink_end(MC_NONE);
16651712
}
16661713
16671714
verify_cancel();
16681715
db_end_transaction(0);
16691716
--- 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,34 @@
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,34 @@
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 db_prepare(&q_marks, "SELECT DISTINCT trid FROM xmark");
1691 while( db_step(&q_marks)==SQLITE_ROW){
1692 rid = db_column_int(&q_marks, 0);
1693 if(db_int(0, "SELECT count(objid) FROM event WHERE objid=%d AND type='ci'", rid)==0){
1694 if(bag_find(&blobs, rid)==0){
1695 bag_insert(&blobs, rid);
1696 }
1697 }else{
1698 bag_insert(&vers, rid);
1699 }
1700 }
1701 db_finalize(&q_marks);
1702 FILE* f = fossil_fopen(markfile_out, "w");
1703 if(!f){
1704 fossil_fatal("cannot open %s for writing\n", markfile_out);
1705 }
1706 export_marks(f, &blobs, &vers);
1707 fclose(f);
1708 bag_clear(&blobs);
1709 bag_clear(&vers);
1710 }
1711 manifest_crosslink_end(MC_NONE);
1712 }
1713
1714 verify_cancel();
1715 db_end_transaction(0);
1716

Keyboard Shortcuts

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