|
1
|
/* |
|
2
|
** Copyright (c) 2010 D. Richard Hipp |
|
3
|
** |
|
4
|
** This program is free software; you can redistribute it and/or |
|
5
|
** modify it under the terms of the Simplified BSD License (also |
|
6
|
** known as the "2-Clause License" or "FreeBSD License".) |
|
7
|
|
|
8
|
** This program is distributed in the hope that it will be useful, |
|
9
|
** but without any warranty; without even the implied warranty of |
|
10
|
** merchantability or fitness for a particular purpose. |
|
11
|
** |
|
12
|
** Author contact information: |
|
13
|
** [email protected] |
|
14
|
** |
|
15
|
******************************************************************************* |
|
16
|
** |
|
17
|
** This file contains code used to export the content of a Fossil |
|
18
|
** repository in the git-fast-import format. |
|
19
|
*/ |
|
20
|
#include "config.h" |
|
21
|
#include "export.h" |
|
22
|
#include <assert.h> |
|
23
|
|
|
24
|
/* |
|
25
|
** State information common to all export types. |
|
26
|
*/ |
|
27
|
static struct { |
|
28
|
const char *zTrunkName; /* Name of trunk branch */ |
|
29
|
} gexport; |
|
30
|
|
|
31
|
#if INTERFACE |
|
32
|
/* |
|
33
|
** Each line in a git-fast-export "mark" file is an instance of |
|
34
|
** this object. |
|
35
|
*/ |
|
36
|
struct mark_t { |
|
37
|
char *name; /* Name of the mark. Also starts with ":" */ |
|
38
|
int rid; /* Corresponding object in the BLOB table */ |
|
39
|
char uuid[65]; /* The GIT hash name for this object */ |
|
40
|
}; |
|
41
|
#endif |
|
42
|
|
|
43
|
/* |
|
44
|
** Output a "committer" record for the given user. |
|
45
|
** NOTE: the given user name may be an email itself. |
|
46
|
*/ |
|
47
|
static void print_person(const char *zUser){ |
|
48
|
static Stmt q; |
|
49
|
const char *zContact; |
|
50
|
char *zName; |
|
51
|
char *zEmail; |
|
52
|
int i, j; |
|
53
|
int isBracketed, atEmailFirst, atEmailLast; |
|
54
|
|
|
55
|
if( zUser==0 ){ |
|
56
|
printf(" <unknown>"); |
|
57
|
return; |
|
58
|
} |
|
59
|
db_static_prepare(&q, "SELECT info FROM user WHERE login=:user"); |
|
60
|
db_bind_text(&q, ":user", zUser); |
|
61
|
if( db_step(&q)!=SQLITE_ROW ){ |
|
62
|
db_reset(&q); |
|
63
|
zName = fossil_strdup(zUser); |
|
64
|
for(i=j=0; zName[i]; i++){ |
|
65
|
if( zName[i]!='<' && zName[i]!='>' && zName[i]!='"' ){ |
|
66
|
zName[j++] = zName[i]; |
|
67
|
} |
|
68
|
} |
|
69
|
zName[j] = 0; |
|
70
|
printf(" %s <%s>", zName, zName); |
|
71
|
free(zName); |
|
72
|
return; |
|
73
|
} |
|
74
|
|
|
75
|
/* |
|
76
|
** We have contact information. |
|
77
|
** It may or may not contain an email address. |
|
78
|
** |
|
79
|
** ASSUME: |
|
80
|
** - General case:"Name Unicoded" <[email protected]> other info |
|
81
|
** - If contact information contains more than an email address, |
|
82
|
** then the email address is enclosed between <> |
|
83
|
** - When only email address is specified, then it's stored verbatim |
|
84
|
** - When name part is absent or all-blanks, use zUser instead |
|
85
|
*/ |
|
86
|
zName = NULL; |
|
87
|
zEmail = NULL; |
|
88
|
zContact = db_column_text(&q, 0); |
|
89
|
atEmailFirst = -1; |
|
90
|
atEmailLast = -1; |
|
91
|
isBracketed = 0; |
|
92
|
for(i=0; zContact[i] && zContact[i]!='@'; i++){ |
|
93
|
if( zContact[i]=='<' ){ |
|
94
|
isBracketed = 1; |
|
95
|
atEmailFirst = i+1; |
|
96
|
} |
|
97
|
else if( zContact[i]=='>' ){ |
|
98
|
isBracketed = 0; |
|
99
|
atEmailFirst = i+1; |
|
100
|
} |
|
101
|
else if( zContact[i]==' ' && !isBracketed ){ |
|
102
|
atEmailFirst = i+1; |
|
103
|
} |
|
104
|
} |
|
105
|
if( zContact[i]==0 ){ |
|
106
|
/* No email address found. Take as user info if not empty */ |
|
107
|
zName = fossil_strdup(zContact[0] ? zContact : zUser); |
|
108
|
for(i=j=0; zName[i]; i++){ |
|
109
|
if( zName[i]!='<' && zName[i]!='>' && zName[i]!='"' ){ |
|
110
|
zName[j++] = zName[i]; |
|
111
|
} |
|
112
|
} |
|
113
|
zName[j] = 0; |
|
114
|
|
|
115
|
printf(" %s <%s>", zName, zName); |
|
116
|
free(zName); |
|
117
|
db_reset(&q); |
|
118
|
return; |
|
119
|
} |
|
120
|
for(j=i+1; zContact[j] && zContact[j]!=' '; j++){ |
|
121
|
if( zContact[j]=='>' ) |
|
122
|
atEmailLast = j-1; |
|
123
|
} |
|
124
|
if ( atEmailLast==-1 ) atEmailLast = j-1; |
|
125
|
if ( atEmailFirst==-1 ) atEmailFirst = 0; /* Found only email */ |
|
126
|
|
|
127
|
/* |
|
128
|
** Found beginning and end of email address. |
|
129
|
** Extract the address (trimmed and sanitized). |
|
130
|
*/ |
|
131
|
for(j=atEmailFirst; zContact[j] && zContact[j]==' '; j++){} |
|
132
|
zEmail = mprintf("%.*s", atEmailLast-j+1, &zContact[j]); |
|
133
|
|
|
134
|
for(i=j=0; zEmail[i]; i++){ |
|
135
|
if( zEmail[i]!='<' && zEmail[i]!='>' ){ |
|
136
|
zEmail[j++] = zEmail[i]; |
|
137
|
} |
|
138
|
} |
|
139
|
zEmail[j] = 0; |
|
140
|
|
|
141
|
/* |
|
142
|
** When bracketed email, extract the string _before_ |
|
143
|
** email as user name (may be enquoted). |
|
144
|
** If missing or all-blank name, use zUser. |
|
145
|
*/ |
|
146
|
if( isBracketed && (atEmailFirst-1) > 0){ |
|
147
|
for(i=atEmailFirst-2; i>=0 && zContact[i] && zContact[i]==' '; i--){} |
|
148
|
if( i>=0 ){ |
|
149
|
for(j=0; j<i && zContact[j] && zContact[j]==' '; j++){} |
|
150
|
zName = mprintf("%.*s", i-j+1, &zContact[j]); |
|
151
|
} |
|
152
|
} |
|
153
|
|
|
154
|
if( zName==NULL ) zName = fossil_strdup(zUser); |
|
155
|
for(i=j=0; zName[i]; i++){ |
|
156
|
if( zName[i]!='<' && zName[i]!='>' && zName[i]!='"' ){ |
|
157
|
zName[j++] = zName[i]; |
|
158
|
} |
|
159
|
} |
|
160
|
zName[j] = 0; |
|
161
|
|
|
162
|
printf(" %s <%s>", zName, zEmail); |
|
163
|
free(zName); |
|
164
|
free(zEmail); |
|
165
|
db_reset(&q); |
|
166
|
} |
|
167
|
|
|
168
|
#define REFREPLACEMENT '_' |
|
169
|
|
|
170
|
/* |
|
171
|
** Output a sanitized git named reference. |
|
172
|
** https://git-scm.com/docs/git-check-ref-format |
|
173
|
** This implementation assumes we are only printing |
|
174
|
** the branch or tag part of the reference. |
|
175
|
*/ |
|
176
|
static void print_ref(const char *zRef){ |
|
177
|
char *zEncoded = fossil_strdup(zRef); |
|
178
|
int i, w; |
|
179
|
if (zEncoded[0]=='@' && zEncoded[1]=='\0'){ |
|
180
|
putchar(REFREPLACEMENT); |
|
181
|
return; |
|
182
|
} |
|
183
|
for(i=0, w=0; zEncoded[i]; i++, w++){ |
|
184
|
if( i!=0 ){ /* Two letter tests */ |
|
185
|
if( (zEncoded[i-1]=='.' && zEncoded[i]=='.') || |
|
186
|
(zEncoded[i-1]=='@' && zEncoded[i]=='{') ){ |
|
187
|
zEncoded[w]=zEncoded[w-1]=REFREPLACEMENT; |
|
188
|
continue; |
|
189
|
} |
|
190
|
if( zEncoded[i-1]=='/' && zEncoded[i]=='/' ){ |
|
191
|
w--; /* Normalise to a single / by rolling back w */ |
|
192
|
continue; |
|
193
|
} |
|
194
|
} |
|
195
|
/* No control characters */ |
|
196
|
if( (unsigned)zEncoded[i]<0x20 || zEncoded[i]==0x7f ){ |
|
197
|
zEncoded[w]=REFREPLACEMENT; |
|
198
|
continue; |
|
199
|
} |
|
200
|
switch( zEncoded[i] ){ |
|
201
|
case ' ': |
|
202
|
case '^': |
|
203
|
case ':': |
|
204
|
case '?': |
|
205
|
case '*': |
|
206
|
case '[': |
|
207
|
case '\\': |
|
208
|
zEncoded[w]=REFREPLACEMENT; |
|
209
|
break; |
|
210
|
} |
|
211
|
} |
|
212
|
/* Cannot begin with a . or / */ |
|
213
|
if( zEncoded[0]=='.' || zEncoded[0] == '/' ) zEncoded[0]=REFREPLACEMENT; |
|
214
|
if( i>0 ){ |
|
215
|
i--; w--; |
|
216
|
/* Or end with a . or / */ |
|
217
|
if( zEncoded[i]=='.' || zEncoded[i] == '/' ) zEncoded[w]=REFREPLACEMENT; |
|
218
|
/* Cannot end with .lock */ |
|
219
|
if ( i>4 && strcmp((zEncoded+i)-5, ".lock")==0 ) |
|
220
|
memset((zEncoded+w)-5, REFREPLACEMENT, 5); |
|
221
|
} |
|
222
|
printf("%s", zEncoded); |
|
223
|
free(zEncoded); |
|
224
|
} |
|
225
|
|
|
226
|
#define BLOBMARK(rid) ((rid) * 2) |
|
227
|
#define COMMITMARK(rid) ((rid) * 2 + 1) |
|
228
|
|
|
229
|
/* |
|
230
|
** insert_commit_xref() |
|
231
|
** Insert a new (mark,rid,uuid) entry into the 'xmark' table. |
|
232
|
** zName and zUuid must be non-null and must point to NULL-terminated strings. |
|
233
|
*/ |
|
234
|
void insert_commit_xref(int rid, const char *zName, const char *zUuid){ |
|
235
|
db_multi_exec( |
|
236
|
"INSERT OR IGNORE INTO xmark(tname, trid, tuuid)" |
|
237
|
"VALUES(%Q,%d,%Q)", |
|
238
|
zName, rid, zUuid |
|
239
|
); |
|
240
|
} |
|
241
|
|
|
242
|
/* |
|
243
|
** create_mark() |
|
244
|
** Create a new (mark,rid,uuid) entry for the given rid in the 'xmark' table, |
|
245
|
** and return that information as a struct mark_t in *mark. |
|
246
|
** *unused_mark is a value representing a mark that is free for use--that is, |
|
247
|
** it does not appear in the marks file, and has not been used during this |
|
248
|
** export run. Specifically, it is the supremum of the set of used marks |
|
249
|
** plus one. |
|
250
|
** This function returns -1 in the case where 'rid' does not exist, otherwise |
|
251
|
** it returns 0. |
|
252
|
** mark->name is dynamically allocated and is owned by the caller upon return. |
|
253
|
*/ |
|
254
|
int create_mark(int rid, struct mark_t *mark, unsigned int *unused_mark){ |
|
255
|
char sid[13]; |
|
256
|
char *zUuid = rid_to_uuid(rid); |
|
257
|
if( !zUuid ){ |
|
258
|
fossil_trace("Undefined rid=%d\n", rid); |
|
259
|
return -1; |
|
260
|
} |
|
261
|
mark->rid = rid; |
|
262
|
sqlite3_snprintf(sizeof(sid), sid, ":%d", *unused_mark); |
|
263
|
*unused_mark += 1; |
|
264
|
mark->name = fossil_strdup(sid); |
|
265
|
sqlite3_snprintf(sizeof(mark->uuid), mark->uuid, "%s", zUuid); |
|
266
|
free(zUuid); |
|
267
|
insert_commit_xref(mark->rid, mark->name, mark->uuid); |
|
268
|
return 0; |
|
269
|
} |
|
270
|
|
|
271
|
/* |
|
272
|
** mark_name_from_rid() |
|
273
|
** Find the mark associated with the given rid. Mark names always start |
|
274
|
** with ':', and are pulled from the 'xmark' temporary table. |
|
275
|
** If the given rid doesn't have a mark associated with it yet, one is |
|
276
|
** created with a value of *unused_mark. |
|
277
|
** *unused_mark functions exactly as in create_mark(). |
|
278
|
** This function returns NULL if the rid does not have an associated UUID, |
|
279
|
** (i.e. is not valid). Otherwise, it returns the name of the mark, which is |
|
280
|
** dynamically allocated and is owned by the caller of this function. |
|
281
|
*/ |
|
282
|
char * mark_name_from_rid(int rid, unsigned int *unused_mark){ |
|
283
|
char *zMark = db_text(0, "SELECT tname FROM xmark WHERE trid=%d", rid); |
|
284
|
if( zMark==NULL ){ |
|
285
|
struct mark_t mark; |
|
286
|
if( create_mark(rid, &mark, unused_mark)==0 ){ |
|
287
|
zMark = mark.name; |
|
288
|
}else{ |
|
289
|
return NULL; |
|
290
|
} |
|
291
|
} |
|
292
|
return zMark; |
|
293
|
} |
|
294
|
|
|
295
|
/* |
|
296
|
** Parse a single line of the mark file. Store the result in the mark object. |
|
297
|
** |
|
298
|
** "line" is a single line of input. |
|
299
|
** This function returns -1 in the case that the line is blank, malformed, or |
|
300
|
** the rid/uuid named in 'line' does not match what is in the repository |
|
301
|
** database. Otherwise, 0 is returned. |
|
302
|
** |
|
303
|
** mark->name is dynamically allocated, and owned by the caller. |
|
304
|
*/ |
|
305
|
int parse_mark(char *line, struct mark_t *mark){ |
|
306
|
char *cur_tok; |
|
307
|
char type_; |
|
308
|
cur_tok = strtok(line, " \t"); |
|
309
|
if( !cur_tok || strlen(cur_tok)<2 ){ |
|
310
|
return -1; |
|
311
|
} |
|
312
|
mark->rid = atoi(&cur_tok[1]); |
|
313
|
type_ = cur_tok[0]; |
|
314
|
if( type_!='c' && type_!='b' ){ |
|
315
|
/* This is probably a blob mark */ |
|
316
|
mark->name = NULL; |
|
317
|
return 0; |
|
318
|
} |
|
319
|
|
|
320
|
cur_tok = strtok(NULL, " \t"); |
|
321
|
if( !cur_tok ){ |
|
322
|
/* This mark was generated by an older version of Fossil and doesn't |
|
323
|
** include the mark name and uuid. create_mark() will name the new mark |
|
324
|
** exactly as it was when exported to git, so that we should have a |
|
325
|
** valid mapping from git hash<->mark name<->fossil hash. */ |
|
326
|
unsigned int mid; |
|
327
|
if( type_=='c' ){ |
|
328
|
mid = COMMITMARK(mark->rid); |
|
329
|
} |
|
330
|
else{ |
|
331
|
mid = BLOBMARK(mark->rid); |
|
332
|
} |
|
333
|
return create_mark(mark->rid, mark, &mid); |
|
334
|
}else{ |
|
335
|
mark->name = fossil_strdup(cur_tok); |
|
336
|
} |
|
337
|
|
|
338
|
cur_tok = strtok(NULL, "\n"); |
|
339
|
if( !cur_tok || (strlen(cur_tok)!=40 && strlen(cur_tok)!=64) ){ |
|
340
|
free(mark->name); |
|
341
|
fossil_trace("Invalid SHA-1/SHA-3 in marks file: %s\n", cur_tok); |
|
342
|
return -1; |
|
343
|
}else{ |
|
344
|
sqlite3_snprintf(sizeof(mark->uuid), mark->uuid, "%s", cur_tok); |
|
345
|
} |
|
346
|
|
|
347
|
/* make sure that rid corresponds to UUID */ |
|
348
|
if( fast_uuid_to_rid(mark->uuid)!=mark->rid ){ |
|
349
|
free(mark->name); |
|
350
|
fossil_trace("Non-existent SHA-1/SHA-3 in marks file: %s\n", mark->uuid); |
|
351
|
return -1; |
|
352
|
} |
|
353
|
|
|
354
|
/* insert a cross-ref into the 'xmark' table */ |
|
355
|
insert_commit_xref(mark->rid, mark->name, mark->uuid); |
|
356
|
return 0; |
|
357
|
} |
|
358
|
|
|
359
|
/* |
|
360
|
** Import the marks specified in file 'f'; |
|
361
|
** If 'blobs' is non-null, insert all blob marks into it. |
|
362
|
** If 'vers' is non-null, insert all commit marks into it. |
|
363
|
** If 'unused_marks' is non-null, upon return of this function, all values |
|
364
|
** x >= *unused_marks are free to use as marks, i.e. they do not clash with |
|
365
|
** any marks appearing in the marks file. |
|
366
|
** |
|
367
|
** Each line in the file must be at most 100 characters in length. This |
|
368
|
** seems like a reasonable maximum for a 40-character uuid, and 1-13 |
|
369
|
** character rid. |
|
370
|
** |
|
371
|
** The function returns -1 if any of the lines in file 'f' are malformed, |
|
372
|
** or the rid/uuid information doesn't match what is in the repository |
|
373
|
** database. Otherwise, 0 is returned. |
|
374
|
*/ |
|
375
|
int import_marks(FILE* f, Bag *blobs, Bag *vers, unsigned int *unused_mark){ |
|
376
|
char line[101]; |
|
377
|
while(fgets(line, sizeof(line), f)){ |
|
378
|
struct mark_t mark; |
|
379
|
if( strlen(line)==100 && line[99]!='\n' ){ |
|
380
|
/* line too long */ |
|
381
|
return -1; |
|
382
|
} |
|
383
|
if( parse_mark(line, &mark)<0 ){ |
|
384
|
return -1; |
|
385
|
}else if( line[0]=='b' ){ |
|
386
|
if( blobs!=NULL ){ |
|
387
|
bag_insert(blobs, mark.rid); |
|
388
|
} |
|
389
|
}else{ |
|
390
|
if( vers!=NULL ){ |
|
391
|
bag_insert(vers, mark.rid); |
|
392
|
} |
|
393
|
} |
|
394
|
if( unused_mark!=NULL ){ |
|
395
|
unsigned int mid = atoi(mark.name + 1); |
|
396
|
if( mid>=*unused_mark ){ |
|
397
|
*unused_mark = mid + 1; |
|
398
|
} |
|
399
|
} |
|
400
|
free(mark.name); |
|
401
|
} |
|
402
|
return 0; |
|
403
|
} |
|
404
|
|
|
405
|
void export_mark(FILE* f, int rid, char obj_type) |
|
406
|
{ |
|
407
|
unsigned int z = 0; |
|
408
|
char *zUuid = rid_to_uuid(rid); |
|
409
|
char *zMark; |
|
410
|
if( zUuid==NULL ){ |
|
411
|
fossil_trace("No uuid matching rid=%d when exporting marks\n", rid); |
|
412
|
return; |
|
413
|
} |
|
414
|
/* Since rid is already in the 'xmark' table, the value of z won't be |
|
415
|
** used, but pass in a valid pointer just to be safe. */ |
|
416
|
zMark = mark_name_from_rid(rid, &z); |
|
417
|
fprintf(f, "%c%d %s %s\n", obj_type, rid, zMark, zUuid); |
|
418
|
free(zMark); |
|
419
|
free(zUuid); |
|
420
|
} |
|
421
|
|
|
422
|
/* |
|
423
|
** If 'blobs' is non-null, it must point to a Bag of blob rids to be |
|
424
|
** written to disk. Blob rids are written as 'b<rid>'. |
|
425
|
** If 'vers' is non-null, it must point to a Bag of commit rids to be |
|
426
|
** written to disk. Commit rids are written as 'c<rid> :<mark> <uuid>'. |
|
427
|
** All commit (mark,rid,uuid) tuples are stored in 'xmark' table. |
|
428
|
** This function does not fail, but may produce errors if a uuid cannot |
|
429
|
** be found for an rid in 'vers'. |
|
430
|
*/ |
|
431
|
void export_marks(FILE* f, Bag *blobs, Bag *vers){ |
|
432
|
int rid; |
|
433
|
|
|
434
|
if( blobs!=NULL ){ |
|
435
|
rid = bag_first(blobs); |
|
436
|
if( rid!=0 ){ |
|
437
|
do{ |
|
438
|
export_mark(f, rid, 'b'); |
|
439
|
}while( (rid = bag_next(blobs, rid))!=0 ); |
|
440
|
} |
|
441
|
} |
|
442
|
if( vers!=NULL ){ |
|
443
|
rid = bag_first(vers); |
|
444
|
if( rid!=0 ){ |
|
445
|
do{ |
|
446
|
export_mark(f, rid, 'c'); |
|
447
|
}while( (rid = bag_next(vers, rid))!=0 ); |
|
448
|
} |
|
449
|
} |
|
450
|
} |
|
451
|
|
|
452
|
/* This is the original header command (and hence documentation) for |
|
453
|
** the "fossil export" command: |
|
454
|
** |
|
455
|
** Usage: %fossil export --git ?OPTIONS? ?REPOSITORY? |
|
456
|
** |
|
457
|
** Write an export of all check-ins to standard output. The export is |
|
458
|
** written in the git-fast-export file format assuming the --git option is |
|
459
|
** provided. The git-fast-export format is currently the only VCS |
|
460
|
** interchange format supported, though other formats may be added in |
|
461
|
** the future. |
|
462
|
** |
|
463
|
** Run this command within a check-out. Or use the -R or --repository |
|
464
|
** option to specify a Fossil repository to be exported. |
|
465
|
** |
|
466
|
** Only check-ins are exported using --git. Git does not support tickets |
|
467
|
** or wiki or tech notes or attachments, so none of those are exported. |
|
468
|
** |
|
469
|
** If the "--import-marks FILE" option is used, it contains a list of |
|
470
|
** rids to skip. |
|
471
|
** |
|
472
|
** If the "--export-marks FILE" option is used, the rid of all commits and |
|
473
|
** blobs written on exit for use with "--import-marks" on the next run. |
|
474
|
** |
|
475
|
** Options: |
|
476
|
** --export-marks FILE Export rids of exported data to FILE |
|
477
|
** --import-marks FILE Read rids of data to ignore from FILE |
|
478
|
** --rename-trunk NAME Use NAME as name of exported trunk branch |
|
479
|
** -R|--repository REPO Export the given REPOSITORY |
|
480
|
** |
|
481
|
** See also: import |
|
482
|
*/ |
|
483
|
/* |
|
484
|
** COMMAND: export* |
|
485
|
** |
|
486
|
** Usage: %fossil export --git [REPOSITORY] |
|
487
|
** |
|
488
|
** This command is deprecated. Use "fossil git export" instead. |
|
489
|
*/ |
|
490
|
void export_cmd(void){ |
|
491
|
Stmt q, q2, q3; |
|
492
|
Bag blobs, vers; |
|
493
|
unsigned int unused_mark = 1; |
|
494
|
const char *markfile_in; |
|
495
|
const char *markfile_out; |
|
496
|
const char *zMainBranch = db_main_branch(); |
|
497
|
|
|
498
|
bag_init(&blobs); |
|
499
|
bag_init(&vers); |
|
500
|
|
|
501
|
find_option("git", 0, 0); /* Ignore the --git option for now */ |
|
502
|
markfile_in = find_option("import-marks", 0, 1); |
|
503
|
markfile_out = find_option("export-marks", 0, 1); |
|
504
|
|
|
505
|
if( !(gexport.zTrunkName = find_option("rename-trunk", 0, 1)) ){ |
|
506
|
gexport.zTrunkName = fossil_strdup(zMainBranch); |
|
507
|
} |
|
508
|
|
|
509
|
db_find_and_open_repository(0, 2); |
|
510
|
verify_all_options(); |
|
511
|
if( g.argc!=2 && g.argc!=3 ){ usage("--git ?REPOSITORY?"); } |
|
512
|
|
|
513
|
db_multi_exec("CREATE TEMPORARY TABLE oldblob(rid INTEGER PRIMARY KEY)"); |
|
514
|
db_multi_exec("CREATE TEMPORARY TABLE oldcommit(rid INTEGER PRIMARY KEY)"); |
|
515
|
db_multi_exec("CREATE TEMP TABLE xmark(tname TEXT UNIQUE, trid INT," |
|
516
|
" tuuid TEXT)"); |
|
517
|
db_multi_exec("CREATE INDEX xmark_trid ON xmark(trid)"); |
|
518
|
if( markfile_in!=0 ){ |
|
519
|
Stmt qb,qc; |
|
520
|
FILE *f; |
|
521
|
int rid; |
|
522
|
|
|
523
|
f = fossil_fopen(markfile_in, "r"); |
|
524
|
if( f==0 ){ |
|
525
|
fossil_fatal("cannot open %s for reading", markfile_in); |
|
526
|
} |
|
527
|
if( import_marks(f, &blobs, &vers, &unused_mark)<0 ){ |
|
528
|
fossil_fatal("error importing marks from file: %s", markfile_in); |
|
529
|
} |
|
530
|
db_prepare(&qb, "INSERT OR IGNORE INTO oldblob VALUES (:rid)"); |
|
531
|
db_prepare(&qc, "INSERT OR IGNORE INTO oldcommit VALUES (:rid)"); |
|
532
|
rid = bag_first(&blobs); |
|
533
|
if( rid!=0 ){ |
|
534
|
do{ |
|
535
|
db_bind_int(&qb, ":rid", rid); |
|
536
|
db_step(&qb); |
|
537
|
db_reset(&qb); |
|
538
|
}while((rid = bag_next(&blobs, rid))!=0); |
|
539
|
} |
|
540
|
rid = bag_first(&vers); |
|
541
|
if( rid!=0 ){ |
|
542
|
do{ |
|
543
|
db_bind_int(&qc, ":rid", rid); |
|
544
|
db_step(&qc); |
|
545
|
db_reset(&qc); |
|
546
|
}while((rid = bag_next(&vers, rid))!=0); |
|
547
|
} |
|
548
|
db_finalize(&qb); |
|
549
|
db_finalize(&qc); |
|
550
|
fclose(f); |
|
551
|
} |
|
552
|
|
|
553
|
/* Step 1: Generate "blob" records for every artifact that is part |
|
554
|
** of a check-in |
|
555
|
*/ |
|
556
|
fossil_binary_mode(stdout); |
|
557
|
db_multi_exec("CREATE TEMP TABLE newblob(rid INTEGER KEY, srcid INTEGER)"); |
|
558
|
db_multi_exec("CREATE INDEX newblob_src ON newblob(srcid)"); |
|
559
|
db_multi_exec( |
|
560
|
"INSERT INTO newblob" |
|
561
|
" SELECT DISTINCT fid," |
|
562
|
" CASE WHEN EXISTS(SELECT 1 FROM delta" |
|
563
|
" WHERE rid=fid" |
|
564
|
" AND NOT EXISTS(SELECT 1 FROM oldblob" |
|
565
|
" WHERE srcid=fid))" |
|
566
|
" THEN (SELECT srcid FROM delta WHERE rid=fid)" |
|
567
|
" ELSE 0" |
|
568
|
" END" |
|
569
|
" FROM mlink" |
|
570
|
" WHERE fid>0 AND NOT EXISTS(SELECT 1 FROM oldblob WHERE rid=fid)"); |
|
571
|
db_prepare(&q, |
|
572
|
"SELECT DISTINCT fid FROM mlink" |
|
573
|
" WHERE fid>0 AND NOT EXISTS(SELECT 1 FROM oldblob WHERE rid=fid)"); |
|
574
|
db_prepare(&q2, "INSERT INTO oldblob VALUES (:rid)"); |
|
575
|
db_prepare(&q3, "SELECT rid FROM newblob WHERE srcid= (:srcid)"); |
|
576
|
while( db_step(&q)==SQLITE_ROW ){ |
|
577
|
int rid = db_column_int(&q, 0); |
|
578
|
Blob content; |
|
579
|
|
|
580
|
while( !bag_find(&blobs, rid) ){ |
|
581
|
char *zMark; |
|
582
|
content_get(rid, &content); |
|
583
|
db_bind_int(&q2, ":rid", rid); |
|
584
|
db_step(&q2); |
|
585
|
db_reset(&q2); |
|
586
|
zMark = mark_name_from_rid(rid, &unused_mark); |
|
587
|
printf("blob\nmark %s\ndata %d\n", zMark, blob_size(&content)); |
|
588
|
free(zMark); |
|
589
|
bag_insert(&blobs, rid); |
|
590
|
fwrite(blob_buffer(&content), 1, blob_size(&content), stdout); |
|
591
|
printf("\n"); |
|
592
|
blob_reset(&content); |
|
593
|
|
|
594
|
db_bind_int(&q3, ":srcid", rid); |
|
595
|
if( db_step(&q3) != SQLITE_ROW ){ |
|
596
|
db_reset(&q3); |
|
597
|
break; |
|
598
|
} |
|
599
|
rid = db_column_int(&q3, 0); |
|
600
|
db_reset(&q3); |
|
601
|
} |
|
602
|
} |
|
603
|
db_finalize(&q); |
|
604
|
db_finalize(&q2); |
|
605
|
db_finalize(&q3); |
|
606
|
|
|
607
|
/* Output the commit records. |
|
608
|
*/ |
|
609
|
topological_sort_checkins(0); |
|
610
|
db_prepare(&q, |
|
611
|
"SELECT strftime('%%s',mtime), objid, coalesce(ecomment,comment)," |
|
612
|
" coalesce(euser,user)," |
|
613
|
" (SELECT value FROM tagxref WHERE rid=objid AND tagid=%d)" |
|
614
|
" FROM toponode, event" |
|
615
|
" WHERE toponode.tid=event.objid" |
|
616
|
" AND event.type='ci'" |
|
617
|
" AND NOT EXISTS (SELECT 1 FROM oldcommit WHERE toponode.tid=rid)" |
|
618
|
" ORDER BY toponode.tseq ASC", |
|
619
|
TAG_BRANCH |
|
620
|
); |
|
621
|
db_prepare(&q2, "INSERT INTO oldcommit VALUES (:rid)"); |
|
622
|
while( db_step(&q)==SQLITE_ROW ){ |
|
623
|
Stmt q4; |
|
624
|
const char *zSecondsSince1970 = db_column_text(&q, 0); |
|
625
|
int ckinId = db_column_int(&q, 1); |
|
626
|
const char *zComment = db_column_text(&q, 2); |
|
627
|
const char *zUser = db_column_text(&q, 3); |
|
628
|
const char *zBranch = db_column_text(&q, 4); |
|
629
|
char *zMark; |
|
630
|
|
|
631
|
bag_insert(&vers, ckinId); |
|
632
|
db_bind_int(&q2, ":rid", ckinId); |
|
633
|
db_step(&q2); |
|
634
|
db_reset(&q2); |
|
635
|
if( zBranch==0 || fossil_strcmp(zBranch, zMainBranch)==0 ){ |
|
636
|
zBranch = gexport.zTrunkName; |
|
637
|
} |
|
638
|
zMark = mark_name_from_rid(ckinId, &unused_mark); |
|
639
|
printf("commit refs/heads/"); |
|
640
|
print_ref(zBranch); |
|
641
|
printf("\nmark %s\n", zMark); |
|
642
|
free(zMark); |
|
643
|
printf("committer"); |
|
644
|
print_person(zUser); |
|
645
|
printf(" %s +0000\n", zSecondsSince1970); |
|
646
|
if( zComment==0 ) zComment = "null comment"; |
|
647
|
printf("data %d\n%s\n", (int)strlen(zComment), zComment); |
|
648
|
db_prepare(&q3, |
|
649
|
"SELECT pid FROM plink" |
|
650
|
" WHERE cid=%d AND isprim" |
|
651
|
" AND pid IN (SELECT objid FROM event)", |
|
652
|
ckinId |
|
653
|
); |
|
654
|
if( db_step(&q3) == SQLITE_ROW ){ |
|
655
|
int pid = db_column_int(&q3, 0); |
|
656
|
zMark = mark_name_from_rid(pid, &unused_mark); |
|
657
|
printf("from %s\n", zMark); |
|
658
|
free(zMark); |
|
659
|
db_prepare(&q4, |
|
660
|
"SELECT pid FROM plink" |
|
661
|
" WHERE cid=%d AND NOT isprim" |
|
662
|
" AND NOT EXISTS(SELECT 1 FROM phantom WHERE rid=pid)" |
|
663
|
" ORDER BY pid", |
|
664
|
ckinId); |
|
665
|
while( db_step(&q4)==SQLITE_ROW ){ |
|
666
|
zMark = mark_name_from_rid(db_column_int(&q4, 0), &unused_mark); |
|
667
|
printf("merge %s\n", zMark); |
|
668
|
free(zMark); |
|
669
|
} |
|
670
|
db_finalize(&q4); |
|
671
|
}else{ |
|
672
|
printf("deleteall\n"); |
|
673
|
} |
|
674
|
|
|
675
|
db_prepare(&q4, |
|
676
|
"SELECT filename.name, mlink.fid, mlink.mperm FROM mlink" |
|
677
|
" JOIN filename ON filename.fnid=mlink.fnid" |
|
678
|
" WHERE mlink.mid=%d", |
|
679
|
ckinId |
|
680
|
); |
|
681
|
while( db_step(&q4)==SQLITE_ROW ){ |
|
682
|
const char *zName = db_column_text(&q4,0); |
|
683
|
int zNew = db_column_int(&q4,1); |
|
684
|
int mPerm = db_column_int(&q4,2); |
|
685
|
if( zNew==0 ){ |
|
686
|
printf("D %s\n", zName); |
|
687
|
}else if( bag_find(&blobs, zNew) ){ |
|
688
|
const char *zPerm; |
|
689
|
zMark = mark_name_from_rid(zNew, &unused_mark); |
|
690
|
switch( mPerm ){ |
|
691
|
case PERM_LNK: zPerm = "120000"; break; |
|
692
|
case PERM_EXE: zPerm = "100755"; break; |
|
693
|
default: zPerm = "100644"; break; |
|
694
|
} |
|
695
|
printf("M %s %s %s\n", zPerm, zMark, zName); |
|
696
|
free(zMark); |
|
697
|
} |
|
698
|
} |
|
699
|
db_finalize(&q4); |
|
700
|
db_finalize(&q3); |
|
701
|
printf("\n"); |
|
702
|
} |
|
703
|
db_finalize(&q2); |
|
704
|
db_finalize(&q); |
|
705
|
manifest_cache_clear(); |
|
706
|
|
|
707
|
|
|
708
|
/* Output tags */ |
|
709
|
db_prepare(&q, |
|
710
|
"SELECT tagname, rid, strftime('%%s',mtime)," |
|
711
|
" (SELECT coalesce(euser, user) FROM event WHERE objid=rid)," |
|
712
|
" value" |
|
713
|
" FROM tagxref JOIN tag USING(tagid)" |
|
714
|
" WHERE tagtype=1 AND tagname GLOB 'sym-*'" |
|
715
|
); |
|
716
|
while( db_step(&q)==SQLITE_ROW ){ |
|
717
|
const char *zTagname = db_column_text(&q, 0); |
|
718
|
int rid = db_column_int(&q, 1); |
|
719
|
char *zMark = mark_name_from_rid(rid, &unused_mark); |
|
720
|
const char *zSecSince1970 = db_column_text(&q, 2); |
|
721
|
const char *zUser = db_column_text(&q, 3); |
|
722
|
const char *zValue = db_column_text(&q, 4); |
|
723
|
if( rid==0 || !bag_find(&vers, rid) ) continue; |
|
724
|
zTagname += 4; |
|
725
|
printf("tag "); |
|
726
|
print_ref(zTagname); |
|
727
|
printf("\nfrom %s\n", zMark); |
|
728
|
free(zMark); |
|
729
|
printf("tagger"); |
|
730
|
print_person(zUser); |
|
731
|
printf(" %s +0000\n", zSecSince1970); |
|
732
|
printf("data %d\n", zValue==NULL?0:(int)strlen(zValue)+1); |
|
733
|
if( zValue!=NULL ) printf("%s\n",zValue); |
|
734
|
} |
|
735
|
db_finalize(&q); |
|
736
|
|
|
737
|
if( markfile_out!=0 ){ |
|
738
|
FILE *f; |
|
739
|
f = fossil_fopen(markfile_out, "w"); |
|
740
|
if( f == 0 ){ |
|
741
|
fossil_fatal("cannot open %s for writing", markfile_out); |
|
742
|
} |
|
743
|
export_marks(f, &blobs, &vers); |
|
744
|
if( ferror(f)!=0 || fclose(f)!=0 ){ |
|
745
|
fossil_fatal("error while writing %s", markfile_out); |
|
746
|
} |
|
747
|
} |
|
748
|
bag_clear(&blobs); |
|
749
|
bag_clear(&vers); |
|
750
|
} |
|
751
|
|
|
752
|
/* |
|
753
|
** Construct the temporary table toposort as follows: |
|
754
|
** |
|
755
|
** CREATE TEMP TABLE toponode( |
|
756
|
** tid INTEGER PRIMARY KEY, -- Check-in id |
|
757
|
** tseq INT -- integer total order on check-ins. |
|
758
|
** ); |
|
759
|
** |
|
760
|
** This table contains all check-ins of the repository in topological |
|
761
|
** order. "Topological order" means that every parent check-in comes |
|
762
|
** before all of its children. Topological order is *almost* the same |
|
763
|
** thing as "ORDER BY event.mtime". Differences only arise when there |
|
764
|
** are timewarps. Inasmuch as Git hates timewarps, we have to compute |
|
765
|
** a correct topological order when doing an export. |
|
766
|
** |
|
767
|
** Since mtime is a usually already nearly in topological order, the |
|
768
|
** algorithm is to start with mtime, then make adjustments as necessary |
|
769
|
** for timewarps. This is not a great algorithm for the general case, |
|
770
|
** but it is very fast for the overwhelmingly common case where there |
|
771
|
** are few timewarps. |
|
772
|
*/ |
|
773
|
int topological_sort_checkins(int bVerbose){ |
|
774
|
int nChange = 0; |
|
775
|
Stmt q1; |
|
776
|
Stmt chng; |
|
777
|
db_multi_exec( |
|
778
|
"CREATE TEMP TABLE toponode(\n" |
|
779
|
" tid INTEGER PRIMARY KEY,\n" |
|
780
|
" tseq INT\n" |
|
781
|
");\n" |
|
782
|
"INSERT INTO toponode(tid,tseq) " |
|
783
|
" SELECT objid, CAST(mtime*8640000 AS int) FROM event WHERE type='ci';\n" |
|
784
|
"CREATE TEMP TABLE topolink(\n" |
|
785
|
" tparent INT,\n" |
|
786
|
" tchild INT,\n" |
|
787
|
" PRIMARY KEY(tparent,tchild)\n" |
|
788
|
") WITHOUT ROWID;" |
|
789
|
"INSERT INTO topolink(tparent,tchild)" |
|
790
|
" SELECT pid, cid FROM plink;\n" |
|
791
|
"CREATE INDEX topolink_child ON topolink(tchild);\n" |
|
792
|
); |
|
793
|
|
|
794
|
/* Find a timewarp instance */ |
|
795
|
db_prepare(&q1, |
|
796
|
"SELECT P.tseq, C.tid, C.tseq\n" |
|
797
|
" FROM toponode P, toponode C, topolink X\n" |
|
798
|
" WHERE X.tparent=P.tid\n" |
|
799
|
" AND X.tchild=C.tid\n" |
|
800
|
" AND P.tseq>=C.tseq;" |
|
801
|
); |
|
802
|
|
|
803
|
/* Update the timestamp on :tid to have value :tseq */ |
|
804
|
db_prepare(&chng, |
|
805
|
"UPDATE toponode SET tseq=:tseq WHERE tid=:tid" |
|
806
|
); |
|
807
|
|
|
808
|
while( db_step(&q1)==SQLITE_ROW ){ |
|
809
|
i64 iParentTime = db_column_int64(&q1, 0); |
|
810
|
int iChild = db_column_int(&q1, 1); |
|
811
|
i64 iChildTime = db_column_int64(&q1, 2); |
|
812
|
nChange++; |
|
813
|
if( nChange>10000 ){ |
|
814
|
fossil_fatal("failed to fix all timewarps after 100000 attempts"); |
|
815
|
} |
|
816
|
db_reset(&q1); |
|
817
|
db_bind_int64(&chng, ":tid", iChild); |
|
818
|
db_bind_int64(&chng, ":tseq", iParentTime+1); |
|
819
|
db_step(&chng); |
|
820
|
db_reset(&chng); |
|
821
|
if( bVerbose ){ |
|
822
|
fossil_print("moving %d from %lld to %lld\n", |
|
823
|
iChild, iChildTime, iParentTime+1); |
|
824
|
} |
|
825
|
} |
|
826
|
|
|
827
|
db_finalize(&q1); |
|
828
|
db_finalize(&chng); |
|
829
|
return nChange; |
|
830
|
} |
|
831
|
|
|
832
|
/* |
|
833
|
** COMMAND: test-topological-sort |
|
834
|
** |
|
835
|
** Invoke the topological_sort_checkins() interface for testing |
|
836
|
** purposes. |
|
837
|
*/ |
|
838
|
void test_topological_sort(void){ |
|
839
|
int n; |
|
840
|
db_find_and_open_repository(0, 0); |
|
841
|
n = topological_sort_checkins(1); |
|
842
|
fossil_print("%d reorderings required\n", n); |
|
843
|
} |
|
844
|
|
|
845
|
/*************************************************************************** |
|
846
|
** Implementation of the "fossil git" command follows. We hope that the |
|
847
|
** new code that follows will largely replace the legacy "fossil export" |
|
848
|
** and "fossil import" code above. |
|
849
|
*/ |
|
850
|
|
|
851
|
/* Verbosity level. Higher means more output. |
|
852
|
** |
|
853
|
** 0 print nothing at all |
|
854
|
** 1 Errors only |
|
855
|
** 2 Progress information (This is the default) |
|
856
|
** 3 Extra details |
|
857
|
*/ |
|
858
|
#define VERB_ERROR 1 |
|
859
|
#define VERB_NORMAL 2 |
|
860
|
#define VERB_EXTRA 3 |
|
861
|
static int gitmirror_verbosity = VERB_NORMAL; |
|
862
|
|
|
863
|
/* The main branch in the Git repository. The main branch of the |
|
864
|
** Fossil repository (usually "trunk") is renamed to be this branch name. |
|
865
|
*/ |
|
866
|
static const char *gitmirror_mainbranch = 0; |
|
867
|
|
|
868
|
/* |
|
869
|
** Output routine that depends on verbosity |
|
870
|
*/ |
|
871
|
static void gitmirror_message(int iLevel, const char *zFormat, ...){ |
|
872
|
va_list ap; |
|
873
|
if( iLevel>gitmirror_verbosity ) return; |
|
874
|
va_start(ap, zFormat); |
|
875
|
fossil_vprint(zFormat, ap); |
|
876
|
va_end(ap); |
|
877
|
} |
|
878
|
|
|
879
|
/* |
|
880
|
** Convert characters of z[] that are not allowed to be in branch or |
|
881
|
** tag names into "_". |
|
882
|
*/ |
|
883
|
static void gitmirror_sanitize_name(char *z){ |
|
884
|
static unsigned char aSafe[] = { |
|
885
|
/* x0 x1 x2 x3 x4 x5 x6 x7 x8 x9 xA xB xC xD xE xF */ |
|
886
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 0x */ |
|
887
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 1x */ |
|
888
|
0, 1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, /* 2x */ |
|
889
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, /* 3x */ |
|
890
|
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* 4x */ |
|
891
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, /* 5x */ |
|
892
|
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* 6x */ |
|
893
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, /* 7x */ |
|
894
|
}; |
|
895
|
unsigned char *zu = (unsigned char*)z; |
|
896
|
int i; |
|
897
|
for(i=0; zu[i]; i++){ |
|
898
|
if( zu[i]>0x7f || !aSafe[zu[i]] ){ |
|
899
|
zu[i] = '_'; |
|
900
|
}else if( zu[i]=='/' && (i==0 || zu[i+1]==0 || zu[i+1]=='/') ){ |
|
901
|
zu[i] = '_'; |
|
902
|
}else if( zu[i]=='.' && (zu[i+1]==0 || zu[i+1]=='.' |
|
903
|
|| (i>0 && zu[i-1]=='.')) ){ |
|
904
|
zu[i] = '_'; |
|
905
|
} |
|
906
|
} |
|
907
|
} |
|
908
|
|
|
909
|
/* |
|
910
|
** COMMAND: test-sanitize-name |
|
911
|
** |
|
912
|
** Usage: %fossil ARG... |
|
913
|
** |
|
914
|
** This sanitizes each argument and make it part of an "echo" command |
|
915
|
** run by the shell. |
|
916
|
*/ |
|
917
|
void test_sanitize_name_cmd(void){ |
|
918
|
sqlite3_str *pStr; |
|
919
|
int i; |
|
920
|
char *zCmd; |
|
921
|
pStr = sqlite3_str_new(0); |
|
922
|
sqlite3_str_appendall(pStr, "echo"); |
|
923
|
for(i=2; i<g.argc; i++){ |
|
924
|
char *z = fossil_strdup(g.argv[i]); |
|
925
|
gitmirror_sanitize_name(z); |
|
926
|
sqlite3_str_appendf(pStr, " \"%s\"", z); |
|
927
|
fossil_free(z); |
|
928
|
} |
|
929
|
zCmd = sqlite3_str_finish(pStr); |
|
930
|
fossil_print("Command: %s\n", zCmd); |
|
931
|
fossil_system(zCmd); |
|
932
|
sqlite3_free(zCmd); |
|
933
|
} |
|
934
|
|
|
935
|
/* |
|
936
|
** Quote a filename as a C-style string using \\ and \" if necessary. |
|
937
|
** If quoting is not necessary, just return a copy of the input string. |
|
938
|
** |
|
939
|
** The return value is a held in memory obtained from fossil_malloc() |
|
940
|
** and must be freed by the caller. |
|
941
|
*/ |
|
942
|
static char *gitmirror_quote_filename_if_needed(const char *zIn){ |
|
943
|
int i, j; |
|
944
|
char c; |
|
945
|
int nSpecial = 0; |
|
946
|
char *zOut; |
|
947
|
for(i=0; (c = zIn[i])!=0; i++){ |
|
948
|
if( c=='\\' || c=='"' || c=='\n' ){ |
|
949
|
nSpecial++; |
|
950
|
} |
|
951
|
} |
|
952
|
if( nSpecial==0 ){ |
|
953
|
return fossil_strdup(zIn); |
|
954
|
} |
|
955
|
zOut = fossil_malloc( i+nSpecial+3 ); |
|
956
|
zOut[0] = '"'; |
|
957
|
for(i=0, j=1; (c = zIn[i])!=0; i++){ |
|
958
|
if( c=='\\' || c=='"' || c=='\n' ){ |
|
959
|
zOut[j++] = '\\'; |
|
960
|
if( c=='\n' ){ |
|
961
|
zOut[j++] = 'n'; |
|
962
|
}else{ |
|
963
|
zOut[j++] = c; |
|
964
|
} |
|
965
|
}else{ |
|
966
|
zOut[j++] = c; |
|
967
|
} |
|
968
|
} |
|
969
|
zOut[j++] = '"'; |
|
970
|
zOut[j] = 0; |
|
971
|
return zOut; |
|
972
|
} |
|
973
|
|
|
974
|
/* |
|
975
|
** Find the Git-name corresponding to the Fossil-name zUuid. |
|
976
|
** |
|
977
|
** If the mark does not exist and if the bCreate flag is false, then |
|
978
|
** return NULL. If the mark does not exist and the bCreate flag is true, |
|
979
|
** then create the mark. |
|
980
|
** |
|
981
|
** The string returned is obtained from fossil_malloc() and should |
|
982
|
** be freed by the caller. |
|
983
|
*/ |
|
984
|
static char *gitmirror_find_mark(const char *zUuid, int isFile, int bCreate){ |
|
985
|
static Stmt sFind, sIns; |
|
986
|
db_static_prepare(&sFind, |
|
987
|
"SELECT coalesce(githash,printf(':%%d',id))" |
|
988
|
" FROM mirror.mmark WHERE uuid=:uuid AND isfile=:isfile" |
|
989
|
); |
|
990
|
db_bind_text(&sFind, ":uuid", zUuid); |
|
991
|
db_bind_int(&sFind, ":isfile", isFile!=0); |
|
992
|
if( db_step(&sFind)==SQLITE_ROW ){ |
|
993
|
char *zMark = fossil_strdup(db_column_text(&sFind, 0)); |
|
994
|
db_reset(&sFind); |
|
995
|
return zMark; |
|
996
|
} |
|
997
|
db_reset(&sFind); |
|
998
|
if( !bCreate ){ |
|
999
|
return 0; |
|
1000
|
} |
|
1001
|
db_static_prepare(&sIns, |
|
1002
|
"INSERT INTO mirror.mmark(uuid,isfile) VALUES(:uuid,:isfile)" |
|
1003
|
); |
|
1004
|
db_bind_text(&sIns, ":uuid", zUuid); |
|
1005
|
db_bind_int(&sIns, ":isfile", isFile!=0); |
|
1006
|
db_step(&sIns); |
|
1007
|
db_reset(&sIns); |
|
1008
|
return mprintf(":%d", db_last_insert_rowid()); |
|
1009
|
} |
|
1010
|
|
|
1011
|
/* This is the SHA3-256 hash of an empty file */ |
|
1012
|
static const char zEmptySha3[] = |
|
1013
|
"a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a"; |
|
1014
|
|
|
1015
|
/* |
|
1016
|
** Export a single file named by zUuid. |
|
1017
|
** |
|
1018
|
** Return 0 on success and non-zero on any failure. |
|
1019
|
** |
|
1020
|
** If zUuid is a shunned file, then treat it as if it were any empty file. |
|
1021
|
** But files that are missing from the repository but have not been officially |
|
1022
|
** shunned cause an error return. Except, if bPhantomOk is true, then missing |
|
1023
|
** files are replaced by an empty file. |
|
1024
|
*/ |
|
1025
|
static int gitmirror_send_file(FILE *xCmd, const char *zUuid, int bPhantomOk){ |
|
1026
|
char *zMark; |
|
1027
|
int rid; |
|
1028
|
int rc; |
|
1029
|
Blob data; |
|
1030
|
rid = fast_uuid_to_rid(zUuid); |
|
1031
|
if( rid<0 ){ |
|
1032
|
if( bPhantomOk || uuid_is_shunned(zUuid) ){ |
|
1033
|
gitmirror_message(VERB_EXTRA, "missing file: %s\n", zUuid); |
|
1034
|
zUuid = zEmptySha3; |
|
1035
|
}else{ |
|
1036
|
return 1; |
|
1037
|
} |
|
1038
|
}else{ |
|
1039
|
rc = content_get(rid, &data); |
|
1040
|
if( rc==0 ){ |
|
1041
|
if( bPhantomOk ){ |
|
1042
|
blob_init(&data, 0, 0); |
|
1043
|
gitmirror_message(VERB_EXTRA, "missing file: %s\n", zUuid); |
|
1044
|
zUuid = zEmptySha3; |
|
1045
|
}else{ |
|
1046
|
return 1; |
|
1047
|
} |
|
1048
|
} |
|
1049
|
} |
|
1050
|
zMark = gitmirror_find_mark(zUuid, 1, 1); |
|
1051
|
if( zMark[0]==':' ){ |
|
1052
|
fprintf(xCmd, "blob\nmark %s\ndata %d\n", zMark, blob_size(&data)); |
|
1053
|
fwrite(blob_buffer(&data), 1, blob_size(&data), xCmd); |
|
1054
|
fprintf(xCmd, "\n"); |
|
1055
|
} |
|
1056
|
fossil_free(zMark); |
|
1057
|
blob_reset(&data); |
|
1058
|
return 0; |
|
1059
|
} |
|
1060
|
|
|
1061
|
/* |
|
1062
|
** Transfer a check-in over to the mirror. "rid" is the BLOB.RID for |
|
1063
|
** the check-in to export. |
|
1064
|
** |
|
1065
|
** If any ancestor of the check-in has not yet been exported, then |
|
1066
|
** invoke this routine recursively to export the ancestor first. |
|
1067
|
** This can only happen on a timewarp, so deep nesting is unlikely. |
|
1068
|
** |
|
1069
|
** Before sending the check-in, first make sure all associated files |
|
1070
|
** have already been exported, and send "blob" records for any that |
|
1071
|
** have not been. Update the MIRROR.MMARK table so that it holds the |
|
1072
|
** marks for the exported files. |
|
1073
|
** |
|
1074
|
** Return zero on success and non-zero if the export should be stopped. |
|
1075
|
*/ |
|
1076
|
static int gitmirror_send_checkin( |
|
1077
|
FILE *xCmd, /* Write fast-import text on this pipe */ |
|
1078
|
int rid, /* BLOB.RID for the check-in to export */ |
|
1079
|
const char *zUuid, /* BLOB.UUID for the check-in to export */ |
|
1080
|
int *pnLimit /* Stop when the counter reaches zero */ |
|
1081
|
){ |
|
1082
|
Manifest *pMan; /* The check-in to be output */ |
|
1083
|
int i; /* Loop counter */ |
|
1084
|
int iParent; /* Which immediate ancestor is primary. -1 for none */ |
|
1085
|
Stmt q; /* An SQL query */ |
|
1086
|
char *zBranch; /* The branch of the check-in */ |
|
1087
|
char *zMark; /* The Git-name of the check-in */ |
|
1088
|
Blob sql; /* String of SQL for part of the query */ |
|
1089
|
Blob comment; /* The comment text for the check-in */ |
|
1090
|
int nErr = 0; /* Number of errors */ |
|
1091
|
int bPhantomOk; /* True if phantom files should be ignored */ |
|
1092
|
char buf[24]; |
|
1093
|
char *zEmail; /* Contact info for Git committer field */ |
|
1094
|
int fManifest; /* Should the manifest files be included? */ |
|
1095
|
int fPManifest = 0; /* OR of the manifest files for all parents */ |
|
1096
|
const char *zMainBranch; |
|
1097
|
|
|
1098
|
pMan = manifest_get(rid, CFTYPE_MANIFEST, 0); |
|
1099
|
if( pMan==0 ){ |
|
1100
|
/* Must be a phantom. Return without doing anything, and in particular |
|
1101
|
** without creating a mark for this check-in. */ |
|
1102
|
gitmirror_message(VERB_NORMAL, "missing check-in: %s\n", zUuid); |
|
1103
|
return 0; |
|
1104
|
} |
|
1105
|
|
|
1106
|
/* Check to see if any parent logins have not yet been processed, and |
|
1107
|
** if so, create them */ |
|
1108
|
for(i=0; i<pMan->nParent; i++){ |
|
1109
|
char *zPMark = gitmirror_find_mark(pMan->azParent[i], 0, 0); |
|
1110
|
if( zPMark==0 ){ |
|
1111
|
int prid = db_int(0, "SELECT rid FROM blob WHERE uuid=%Q", |
|
1112
|
pMan->azParent[i]); |
|
1113
|
int rc = gitmirror_send_checkin(xCmd, prid, pMan->azParent[i], |
|
1114
|
pnLimit); |
|
1115
|
if( rc || *pnLimit<=0 ){ |
|
1116
|
manifest_destroy(pMan); |
|
1117
|
return 1; |
|
1118
|
} |
|
1119
|
} |
|
1120
|
fossil_free(zPMark); |
|
1121
|
} |
|
1122
|
|
|
1123
|
/* Ignore phantom files on check-ins that are over one year old */ |
|
1124
|
bPhantomOk = db_int(0, "SELECT %.6f<julianday('now','-1 year')", |
|
1125
|
pMan->rDate); |
|
1126
|
|
|
1127
|
/* Make sure all necessary files have been exported */ |
|
1128
|
db_prepare(&q, |
|
1129
|
"SELECT uuid FROM files_of_checkin(%Q)" |
|
1130
|
" WHERE uuid NOT IN (SELECT uuid FROM mirror.mmark)", |
|
1131
|
zUuid |
|
1132
|
); |
|
1133
|
while( db_step(&q)==SQLITE_ROW ){ |
|
1134
|
const char *zFUuid = db_column_text(&q, 0); |
|
1135
|
int n = gitmirror_send_file(xCmd, zFUuid, bPhantomOk); |
|
1136
|
nErr += n; |
|
1137
|
if( n ) gitmirror_message(VERB_ERROR, "missing file: %s\n", zFUuid); |
|
1138
|
} |
|
1139
|
db_finalize(&q); |
|
1140
|
|
|
1141
|
/* If some required files could not be exported, abandon the check-in |
|
1142
|
** export */ |
|
1143
|
if( nErr ){ |
|
1144
|
gitmirror_message(VERB_ERROR, |
|
1145
|
"export of %s abandoned due to missing files\n", zUuid); |
|
1146
|
*pnLimit = 0; |
|
1147
|
manifest_destroy(pMan); |
|
1148
|
return 1; |
|
1149
|
} |
|
1150
|
|
|
1151
|
/* Figure out which branch this check-in is a member of */ |
|
1152
|
zBranch = db_text(0, |
|
1153
|
"SELECT value FROM tagxref WHERE tagid=%d AND tagtype>0 AND rid=%d", |
|
1154
|
TAG_BRANCH, rid |
|
1155
|
); |
|
1156
|
zMainBranch = db_main_branch(); |
|
1157
|
if( fossil_strcmp(zBranch, zMainBranch)==0 ){ |
|
1158
|
assert( gitmirror_mainbranch!=0 ); |
|
1159
|
fossil_free(zBranch); |
|
1160
|
zBranch = fossil_strdup(gitmirror_mainbranch); |
|
1161
|
}else if( zBranch==0 ){ |
|
1162
|
zBranch = mprintf("unknown"); |
|
1163
|
}else{ |
|
1164
|
gitmirror_sanitize_name(zBranch); |
|
1165
|
} |
|
1166
|
|
|
1167
|
/* Export the check-in */ |
|
1168
|
fprintf(xCmd, "commit refs/heads/%s\n", zBranch); |
|
1169
|
fossil_free(zBranch); |
|
1170
|
zMark = gitmirror_find_mark(zUuid,0,1); |
|
1171
|
fprintf(xCmd, "mark %s\n", zMark); |
|
1172
|
fossil_free(zMark); |
|
1173
|
sqlite3_snprintf(sizeof(buf), buf, "%lld", |
|
1174
|
(sqlite3_int64)((pMan->rDate-2440587.5)*86400.0) |
|
1175
|
); |
|
1176
|
|
|
1177
|
/* |
|
1178
|
** Check for 'fx_' table from previous Git import, otherwise take contact info |
|
1179
|
** from user table for <emailaddr> in committer field. If no emailaddr, check |
|
1180
|
** if username is in email form, otherwise use generic '[email protected]'. |
|
1181
|
*/ |
|
1182
|
if (db_table_exists("repository", "fx_git")) { |
|
1183
|
zEmail = db_text(0, "SELECT email FROM fx_git WHERE user=%Q", pMan->zUser); |
|
1184
|
} else { |
|
1185
|
zEmail = db_text(0, "SELECT info FROM user WHERE login=%Q", pMan->zUser); |
|
1186
|
} |
|
1187
|
|
|
1188
|
/* Some repo 'info' fields return an empty string hence the second check */ |
|
1189
|
if( zEmail==0 ){ |
|
1190
|
/* If username is in emailaddr form, don't append '@noemail.net' */ |
|
1191
|
if( pMan->zUser==0 || strchr(pMan->zUser, '@')==0 ){ |
|
1192
|
zEmail = mprintf("%[email protected]", pMan->zUser); |
|
1193
|
} else { |
|
1194
|
zEmail = fossil_strdup(pMan->zUser); |
|
1195
|
} |
|
1196
|
}else{ |
|
1197
|
char *zTmp = strchr(zEmail, '<'); |
|
1198
|
if( zTmp ){ |
|
1199
|
char *zTmpEnd = strchr(zTmp+1, '>'); |
|
1200
|
char *zNew; |
|
1201
|
int i; |
|
1202
|
if( zTmpEnd ) *(zTmpEnd) = 0; |
|
1203
|
zNew = fossil_strdup(zTmp+1); |
|
1204
|
fossil_free(zEmail); |
|
1205
|
zEmail = zNew; |
|
1206
|
for(i=0; zEmail[i] && !fossil_isspace(zEmail[i]); i++){} |
|
1207
|
zEmail[i] = 0; |
|
1208
|
} |
|
1209
|
} |
|
1210
|
fprintf(xCmd, "# rid=%d\n", rid); |
|
1211
|
fprintf(xCmd, "committer %s <%s> %s +0000\n", pMan->zUser, zEmail, buf); |
|
1212
|
fossil_free(zEmail); |
|
1213
|
blob_init(&comment, pMan->zComment, -1); |
|
1214
|
if( blob_size(&comment)==0 ){ |
|
1215
|
blob_append(&comment, "(no comment)", -1); |
|
1216
|
} |
|
1217
|
blob_appendf(&comment, "\n\nFossilOrigin-Name: %s", zUuid); |
|
1218
|
fprintf(xCmd, "data %d\n%s\n", blob_strlen(&comment), blob_str(&comment)); |
|
1219
|
blob_reset(&comment); |
|
1220
|
iParent = -1; /* Which ancestor is the primary parent */ |
|
1221
|
for(i=0; i<pMan->nParent; i++){ |
|
1222
|
char *zOther = gitmirror_find_mark(pMan->azParent[i],0,0); |
|
1223
|
if( zOther==0 ) continue; |
|
1224
|
fPManifest |= db_get_manifest_setting(pMan->azParent[i]); |
|
1225
|
if( iParent<0 ){ |
|
1226
|
iParent = i; |
|
1227
|
fprintf(xCmd, "from %s\n", zOther); |
|
1228
|
}else{ |
|
1229
|
fprintf(xCmd, "merge %s\n", zOther); |
|
1230
|
} |
|
1231
|
fossil_free(zOther); |
|
1232
|
} |
|
1233
|
if( iParent>=0 ){ |
|
1234
|
db_prepare(&q, |
|
1235
|
"SELECT filename FROM files_of_checkin(%Q)" |
|
1236
|
" EXCEPT SELECT filename FROM files_of_checkin(%Q)", |
|
1237
|
pMan->azParent[iParent], zUuid |
|
1238
|
); |
|
1239
|
while( db_step(&q)==SQLITE_ROW ){ |
|
1240
|
fprintf(xCmd, "D %s\n", db_column_text(&q,0)); |
|
1241
|
} |
|
1242
|
db_finalize(&q); |
|
1243
|
} |
|
1244
|
blob_init(&sql, 0, 0); |
|
1245
|
blob_append_sql(&sql, |
|
1246
|
"SELECT filename, uuid, perm FROM files_of_checkin(%Q)", |
|
1247
|
zUuid |
|
1248
|
); |
|
1249
|
if( pMan->nParent ){ |
|
1250
|
blob_append_sql(&sql, |
|
1251
|
" EXCEPT SELECT filename, uuid, perm FROM files_of_checkin(%Q)", |
|
1252
|
pMan->azParent[0]); |
|
1253
|
} |
|
1254
|
db_prepare(&q, |
|
1255
|
"SELECT x.filename, x.perm," |
|
1256
|
" coalesce(mmark.githash,printf(':%%d',mmark.id))" |
|
1257
|
" FROM (%s) AS x, mirror.mmark" |
|
1258
|
" WHERE mmark.uuid=x.uuid AND isfile", |
|
1259
|
blob_sql_text(&sql) |
|
1260
|
); |
|
1261
|
blob_reset(&sql); |
|
1262
|
while( db_step(&q)==SQLITE_ROW ){ |
|
1263
|
const char *zFilename = db_column_text(&q,0); |
|
1264
|
const char *zMode = db_column_text(&q,1); |
|
1265
|
const char *zMark = db_column_text(&q,2); |
|
1266
|
const char *zGitMode = "100644"; |
|
1267
|
char *zFNQuoted = 0; |
|
1268
|
if( zMode ){ |
|
1269
|
if( strchr(zMode,'x') ) zGitMode = "100755"; |
|
1270
|
if( strchr(zMode,'l') ) zGitMode = "120000"; |
|
1271
|
} |
|
1272
|
zFNQuoted = gitmirror_quote_filename_if_needed(zFilename); |
|
1273
|
fprintf(xCmd,"M %s %s %s\n", zGitMode, zMark, zFNQuoted); |
|
1274
|
fossil_free(zFNQuoted); |
|
1275
|
} |
|
1276
|
db_finalize(&q); |
|
1277
|
manifest_destroy(pMan); |
|
1278
|
pMan = 0; |
|
1279
|
|
|
1280
|
/* Include Fossil-generated auxiliary files in the check-in */ |
|
1281
|
fManifest = db_get_manifest_setting(zUuid); |
|
1282
|
if( fManifest & MFESTFLG_RAW ){ |
|
1283
|
Blob manifest; |
|
1284
|
content_get(rid, &manifest); |
|
1285
|
sterilize_manifest(&manifest, CFTYPE_MANIFEST); |
|
1286
|
fprintf(xCmd,"M 100644 inline manifest\ndata %d\n%s\n", |
|
1287
|
blob_strlen(&manifest), blob_str(&manifest)); |
|
1288
|
blob_reset(&manifest); |
|
1289
|
}else if( fPManifest & MFESTFLG_RAW ){ |
|
1290
|
fprintf(xCmd, "D manifest\n"); |
|
1291
|
} |
|
1292
|
if( fManifest & MFESTFLG_UUID ){ |
|
1293
|
int n = (int)strlen(zUuid); |
|
1294
|
fprintf(xCmd,"M 100644 inline manifest.uuid\ndata %d\n%s\n\n", n+1, zUuid); |
|
1295
|
}else if( fPManifest & MFESTFLG_UUID ){ |
|
1296
|
fprintf(xCmd, "D manifest.uuid\n"); |
|
1297
|
} |
|
1298
|
if( fManifest & MFESTFLG_TAGS ){ |
|
1299
|
Blob tagslist; |
|
1300
|
blob_init(&tagslist, 0, 0); |
|
1301
|
get_checkin_taglist(rid, &tagslist); |
|
1302
|
fprintf(xCmd,"M 100644 inline manifest.tags\ndata %d\n%s\n", |
|
1303
|
blob_strlen(&tagslist), blob_str(&tagslist)); |
|
1304
|
blob_reset(&tagslist); |
|
1305
|
}else if( fPManifest & MFESTFLG_TAGS ){ |
|
1306
|
fprintf(xCmd, "D manifest.tags\n"); |
|
1307
|
} |
|
1308
|
|
|
1309
|
/* The check-in is finished, so decrement the counter */ |
|
1310
|
(*pnLimit)--; |
|
1311
|
return 0; |
|
1312
|
} |
|
1313
|
|
|
1314
|
/* |
|
1315
|
** Create a new Git repository at zMirror to use as the mirror. |
|
1316
|
** Try to make zMainBr be the main branch for the new repository. |
|
1317
|
** |
|
1318
|
** A side-effect of this routine is that current-working directory |
|
1319
|
** is changed to zMirror. |
|
1320
|
** |
|
1321
|
** If zMainBr is initially NULL, then the return value will be the |
|
1322
|
** name of the default branch to be used by Git. If zMainBr is |
|
1323
|
** initially non-NULL, then the return value will be a copy of zMainBr. |
|
1324
|
*/ |
|
1325
|
static char *gitmirror_init( |
|
1326
|
const char *zMirror, |
|
1327
|
char *zMainBr |
|
1328
|
){ |
|
1329
|
char *zCmd; |
|
1330
|
int rc; |
|
1331
|
|
|
1332
|
/* Create a new Git repository at zMirror */ |
|
1333
|
zCmd = mprintf("git init %$", zMirror); |
|
1334
|
gitmirror_message(VERB_NORMAL, "%s\n", zCmd); |
|
1335
|
rc = fossil_system(zCmd); |
|
1336
|
if( rc ){ |
|
1337
|
fossil_fatal("cannot initialize git repository using: %s", zCmd); |
|
1338
|
} |
|
1339
|
fossil_free(zCmd); |
|
1340
|
|
|
1341
|
/* Must be in the new Git repository directory for subsequent commands */ |
|
1342
|
rc = file_chdir(zMirror, 0); |
|
1343
|
if( rc ){ |
|
1344
|
fossil_fatal("cannot change to directory \"%s\"", zMirror); |
|
1345
|
} |
|
1346
|
|
|
1347
|
if( zMainBr ){ |
|
1348
|
/* Set the current branch to zMainBr */ |
|
1349
|
zCmd = mprintf("git symbolic-ref HEAD refs/heads/%s", zMainBr); |
|
1350
|
gitmirror_message(VERB_NORMAL, "%s\n", zCmd); |
|
1351
|
rc = fossil_system(zCmd); |
|
1352
|
if( rc ){ |
|
1353
|
fossil_fatal("git command failed: %s", zCmd); |
|
1354
|
} |
|
1355
|
fossil_free(zCmd); |
|
1356
|
}else{ |
|
1357
|
/* If zMainBr is not specified, then check to see what branch |
|
1358
|
** name Git chose for itself */ |
|
1359
|
char *z; |
|
1360
|
char zLine[1000]; |
|
1361
|
FILE *xCmd; |
|
1362
|
int i; |
|
1363
|
zCmd = "git symbolic-ref --short HEAD"; |
|
1364
|
gitmirror_message(VERB_NORMAL, "%s\n", zCmd); |
|
1365
|
xCmd = popen(zCmd, "r"); |
|
1366
|
if( xCmd==0 ){ |
|
1367
|
fossil_fatal("git command failed: %s", zCmd); |
|
1368
|
} |
|
1369
|
|
|
1370
|
z = fgets(zLine, sizeof(zLine), xCmd); |
|
1371
|
pclose(xCmd); |
|
1372
|
if( z==0 ){ |
|
1373
|
fossil_fatal("no output from \"%s\"", zCmd); |
|
1374
|
} |
|
1375
|
for(i=0; z[i] && !fossil_isspace(z[i]); i++){} |
|
1376
|
z[i] = 0; |
|
1377
|
zMainBr = fossil_strdup(z); |
|
1378
|
} |
|
1379
|
return zMainBr; |
|
1380
|
} |
|
1381
|
|
|
1382
|
/* |
|
1383
|
** Implementation of the "fossil git export" command. |
|
1384
|
*/ |
|
1385
|
void gitmirror_export_command(void){ |
|
1386
|
const char *zLimit; /* Text of the --limit flag */ |
|
1387
|
int nLimit = 0x7fffffff; /* Numeric value of the --limit flag */ |
|
1388
|
int nTotal = 0; /* Total number of check-ins to export */ |
|
1389
|
char *zMirror; /* Name of the mirror */ |
|
1390
|
char *z; /* Generic string */ |
|
1391
|
char *zCmd; /* git command to run as a subprocess */ |
|
1392
|
const char *zDebug = 0; /* Value of the --debug flag */ |
|
1393
|
const char *zAutoPush = 0; /* Value of the --autopush flag */ |
|
1394
|
char *zMainBr = 0; /* Value of the --mainbranch flag */ |
|
1395
|
char *zPushUrl; /* URL to sync the mirror to */ |
|
1396
|
double rEnd; /* time of most recent export */ |
|
1397
|
int rc; /* Result code */ |
|
1398
|
int bForce; /* Do the export and sync even if no changes*/ |
|
1399
|
int bNeedRepack = 0; /* True if we should run repack at the end */ |
|
1400
|
int bIfExists; /* The --if-mirrored flag */ |
|
1401
|
FILE *xCmd; /* Pipe to the "git fast-import" command */ |
|
1402
|
FILE *pMarks; /* Git mark files */ |
|
1403
|
Stmt q; /* Queries */ |
|
1404
|
char zLine[200]; /* One line of a mark file */ |
|
1405
|
|
|
1406
|
zDebug = find_option("debug",0,1); |
|
1407
|
db_find_and_open_repository(0, 0); |
|
1408
|
zLimit = find_option("limit", 0, 1); |
|
1409
|
if( zLimit ){ |
|
1410
|
nLimit = (unsigned int)atoi(zLimit); |
|
1411
|
if( nLimit<=0 ) fossil_fatal("--limit must be positive"); |
|
1412
|
} |
|
1413
|
zAutoPush = find_option("autopush",0,1); |
|
1414
|
zMainBr = (char*)find_option("mainbranch",0,1); |
|
1415
|
bForce = find_option("force","f",0)!=0; |
|
1416
|
bIfExists = find_option("if-mirrored",0,0)!=0; |
|
1417
|
gitmirror_verbosity = VERB_NORMAL; |
|
1418
|
if( g.fQuiet ){ gitmirror_verbosity--; } /* Global option not repeatable. */ |
|
1419
|
while( find_option("quiet","q",0)!=0 ){ gitmirror_verbosity--; } |
|
1420
|
while( find_option("verbose","v",0)!=0 ){ gitmirror_verbosity++; } |
|
1421
|
verify_all_options(); |
|
1422
|
if( g.argc!=4 && g.argc!=3 ){ usage("export ?MIRROR?"); } |
|
1423
|
if( g.argc==4 ){ |
|
1424
|
Blob mirror; |
|
1425
|
file_canonical_name(g.argv[3], &mirror, 0); |
|
1426
|
db_set("last-git-export-repo", blob_str(&mirror), 0); |
|
1427
|
blob_reset(&mirror); |
|
1428
|
} |
|
1429
|
zMirror = db_get("last-git-export-repo", 0); |
|
1430
|
if( zMirror==0 ){ |
|
1431
|
if( bIfExists ) return; |
|
1432
|
fossil_fatal("no Git repository specified"); |
|
1433
|
} |
|
1434
|
|
|
1435
|
if( zMainBr ){ |
|
1436
|
z = fossil_strdup(zMainBr); |
|
1437
|
gitmirror_sanitize_name(z); |
|
1438
|
if( strcmp(z, zMainBr) ){ |
|
1439
|
fossil_fatal("\"%s\" is not a legal branch name for Git", zMainBr); |
|
1440
|
} |
|
1441
|
fossil_free(z); |
|
1442
|
} |
|
1443
|
|
|
1444
|
/* Make sure the GIT repository directory exists */ |
|
1445
|
rc = file_mkdir(zMirror, ExtFILE, 0); |
|
1446
|
if( rc ) fossil_fatal("cannot create directory \"%s\"", zMirror); |
|
1447
|
|
|
1448
|
/* Make sure GIT has been initialized */ |
|
1449
|
z = mprintf("%s/.git", zMirror); |
|
1450
|
if( !file_isdir(z, ExtFILE) ){ |
|
1451
|
zMainBr = gitmirror_init(zMirror, zMainBr); |
|
1452
|
bNeedRepack = 1; |
|
1453
|
} |
|
1454
|
fossil_free(z); |
|
1455
|
|
|
1456
|
/* Make sure the .mirror_state subdirectory exists */ |
|
1457
|
z = mprintf("%s/.mirror_state", zMirror); |
|
1458
|
rc = file_mkdir(z, ExtFILE, 0); |
|
1459
|
if( rc ) fossil_fatal("cannot create directory \"%s\"", z); |
|
1460
|
fossil_free(z); |
|
1461
|
|
|
1462
|
/* Attach the .mirror_state/db database */ |
|
1463
|
db_multi_exec("ATTACH '%q/.mirror_state/db' AS mirror;", zMirror); |
|
1464
|
db_begin_write(); |
|
1465
|
db_multi_exec( |
|
1466
|
"CREATE TABLE IF NOT EXISTS mirror.mconfig(\n" |
|
1467
|
" key TEXT PRIMARY KEY,\n" |
|
1468
|
" Value ANY\n" |
|
1469
|
") WITHOUT ROWID;\n" |
|
1470
|
"CREATE TABLE IF NOT EXISTS mirror.mmark(\n" |
|
1471
|
" id INTEGER PRIMARY KEY,\n" |
|
1472
|
" uuid TEXT,\n" |
|
1473
|
" isfile BOOLEAN,\n" |
|
1474
|
" githash TEXT,\n" |
|
1475
|
" UNIQUE(uuid,isfile)\n" |
|
1476
|
");" |
|
1477
|
); |
|
1478
|
if( !db_table_has_column("mirror","mmark","isfile") ){ |
|
1479
|
db_multi_exec( |
|
1480
|
"ALTER TABLE mirror.mmark RENAME TO mmark_old;" |
|
1481
|
"CREATE TABLE IF NOT EXISTS mirror.mmark(\n" |
|
1482
|
" id INTEGER PRIMARY KEY,\n" |
|
1483
|
" uuid TEXT,\n" |
|
1484
|
" isfile BOOLEAN,\n" |
|
1485
|
" githash TEXT,\n" |
|
1486
|
" UNIQUE(uuid,isfile)\n" |
|
1487
|
");" |
|
1488
|
"INSERT OR IGNORE INTO mirror.mmark(id,uuid,githash,isfile)" |
|
1489
|
" SELECT id,uuid,githash," |
|
1490
|
" NOT EXISTS(SELECT 1 FROM repository.event, repository.blob" |
|
1491
|
" WHERE event.objid=blob.rid" |
|
1492
|
" AND blob.uuid=mmark_old.uuid)" |
|
1493
|
" FROM mirror.mmark_old;\n" |
|
1494
|
"DROP TABLE mirror.mmark_old;\n" |
|
1495
|
); |
|
1496
|
} |
|
1497
|
|
|
1498
|
/* Change the autopush setting if the --autopush flag is present */ |
|
1499
|
if( zAutoPush ){ |
|
1500
|
if( is_false(zAutoPush) ){ |
|
1501
|
db_multi_exec("DELETE FROM mirror.mconfig WHERE key='autopush'"); |
|
1502
|
}else{ |
|
1503
|
db_multi_exec( |
|
1504
|
"REPLACE INTO mirror.mconfig(key,value)" |
|
1505
|
"VALUES('autopush',%Q)", |
|
1506
|
zAutoPush |
|
1507
|
); |
|
1508
|
} |
|
1509
|
} |
|
1510
|
|
|
1511
|
/* Change the mainbranch setting if the --mainbranch flag is present */ |
|
1512
|
if( zMainBr && zMainBr[0] ){ |
|
1513
|
db_multi_exec( |
|
1514
|
"REPLACE INTO mirror.mconfig(key,value)" |
|
1515
|
"VALUES('mainbranch',%Q)", |
|
1516
|
zMainBr |
|
1517
|
); |
|
1518
|
gitmirror_mainbranch = fossil_strdup(zMainBr); |
|
1519
|
}else{ |
|
1520
|
/* Recover the saved name of the main branch */ |
|
1521
|
gitmirror_mainbranch = db_text("master", |
|
1522
|
"SELECT value FROM mconfig WHERE key='mainbranch'"); |
|
1523
|
} |
|
1524
|
|
|
1525
|
|
|
1526
|
/* See if there is any work to be done. Exit early if not, before starting |
|
1527
|
** the "git fast-import" command. */ |
|
1528
|
if( !bForce |
|
1529
|
&& !db_exists("SELECT 1 FROM event WHERE type IN ('ci','t')" |
|
1530
|
" AND mtime>coalesce((SELECT value FROM mconfig" |
|
1531
|
" WHERE key='start'),0.0)") |
|
1532
|
){ |
|
1533
|
gitmirror_message(VERB_NORMAL, "no changes\n"); |
|
1534
|
db_commit_transaction(); |
|
1535
|
return; |
|
1536
|
} |
|
1537
|
|
|
1538
|
/* Change to the MIRROR directory so that the Git commands will work */ |
|
1539
|
rc = file_chdir(zMirror, 0); |
|
1540
|
if( rc ) fossil_fatal("cannot change the working directory to \"%s\"", |
|
1541
|
zMirror); |
|
1542
|
|
|
1543
|
/* Start up the git fast-import command */ |
|
1544
|
if( zDebug ){ |
|
1545
|
if( fossil_strcmp(zDebug,"stdout")==0 ){ |
|
1546
|
xCmd = stdout; |
|
1547
|
}else{ |
|
1548
|
xCmd = fopen(zDebug, "wb"); |
|
1549
|
if( xCmd==0 ) fossil_fatal("cannot open file \"%s\" for writing", zDebug); |
|
1550
|
} |
|
1551
|
}else{ |
|
1552
|
zCmd = mprintf("git fast-import" |
|
1553
|
" --export-marks=.mirror_state/marks.txt" |
|
1554
|
" --quiet --done"); |
|
1555
|
gitmirror_message(VERB_NORMAL, "%s\n", zCmd); |
|
1556
|
#ifdef _WIN32 |
|
1557
|
xCmd = popen(zCmd, "wb"); |
|
1558
|
#else |
|
1559
|
xCmd = popen(zCmd, "w"); |
|
1560
|
#endif |
|
1561
|
if( xCmd==0 ){ |
|
1562
|
fossil_fatal("cannot start the \"git fast-import\" command"); |
|
1563
|
} |
|
1564
|
fossil_free(zCmd); |
|
1565
|
} |
|
1566
|
|
|
1567
|
/* Run the export */ |
|
1568
|
rEnd = 0.0; |
|
1569
|
db_multi_exec( |
|
1570
|
"CREATE TEMP TABLE tomirror(objid,mtime,uuid);\n" |
|
1571
|
"INSERT INTO tomirror " |
|
1572
|
"SELECT objid, mtime, blob.uuid FROM event, blob\n" |
|
1573
|
" WHERE type='ci'" |
|
1574
|
" AND mtime>coalesce((SELECT value FROM mconfig WHERE key='start'),0.0)" |
|
1575
|
" AND blob.rid=event.objid" |
|
1576
|
" AND blob.uuid NOT IN (SELECT uuid FROM mirror.mmark WHERE NOT isfile)" |
|
1577
|
" AND NOT EXISTS (SELECT 1 FROM private WHERE rid=blob.rid);" |
|
1578
|
); |
|
1579
|
nTotal = db_int(0, "SELECT count(*) FROM tomirror"); |
|
1580
|
if( nLimit<nTotal ){ |
|
1581
|
nTotal = nLimit; |
|
1582
|
}else if( nLimit>nTotal ){ |
|
1583
|
nLimit = nTotal; |
|
1584
|
} |
|
1585
|
db_prepare(&q, |
|
1586
|
"SELECT objid, mtime, uuid FROM tomirror ORDER BY mtime" |
|
1587
|
); |
|
1588
|
while( nLimit && db_step(&q)==SQLITE_ROW ){ |
|
1589
|
int rid = db_column_int(&q, 0); |
|
1590
|
double rMTime = db_column_double(&q, 1); |
|
1591
|
const char *zUuid = db_column_text(&q, 2); |
|
1592
|
if( rMTime>rEnd ) rEnd = rMTime; |
|
1593
|
rc = gitmirror_send_checkin(xCmd, rid, zUuid, &nLimit); |
|
1594
|
if( rc ) break; |
|
1595
|
gitmirror_message(VERB_NORMAL,"%d/%d \r", nTotal-nLimit, nTotal); |
|
1596
|
fflush(stdout); |
|
1597
|
} |
|
1598
|
db_finalize(&q); |
|
1599
|
fprintf(xCmd, "done\n"); |
|
1600
|
if( zDebug ){ |
|
1601
|
if( xCmd!=stdout ) fclose(xCmd); |
|
1602
|
}else{ |
|
1603
|
pclose(xCmd); |
|
1604
|
} |
|
1605
|
gitmirror_message(VERB_NORMAL, "%d check-ins added to the %s\n", |
|
1606
|
nTotal-nLimit, zMirror); |
|
1607
|
|
|
1608
|
/* Read the export-marks file. Transfer the new marks over into |
|
1609
|
** the import-marks file. |
|
1610
|
*/ |
|
1611
|
pMarks = fopen(".mirror_state/marks.txt", "rb"); |
|
1612
|
if( pMarks ){ |
|
1613
|
db_prepare(&q, "UPDATE mirror.mmark SET githash=:githash WHERE id=:id"); |
|
1614
|
while( fgets(zLine, sizeof(zLine), pMarks) ){ |
|
1615
|
int j, k; |
|
1616
|
if( zLine[0]!=':' ) continue; |
|
1617
|
db_bind_int(&q, ":id", atoi(zLine+1)); |
|
1618
|
for(j=1; zLine[j] && zLine[j]!=' '; j++){} |
|
1619
|
if( zLine[j]!=' ' ) continue; |
|
1620
|
j++; |
|
1621
|
if( zLine[j]==0 ) continue; |
|
1622
|
for(k=j; fossil_isalnum(zLine[k]); k++){} |
|
1623
|
zLine[k] = 0; |
|
1624
|
db_bind_text(&q, ":githash", &zLine[j]); |
|
1625
|
db_step(&q); |
|
1626
|
db_reset(&q); |
|
1627
|
} |
|
1628
|
db_finalize(&q); |
|
1629
|
fclose(pMarks); |
|
1630
|
file_delete(".mirror_state/marks.txt"); |
|
1631
|
}else{ |
|
1632
|
fossil_fatal("git fast-import didn't generate a marks file!"); |
|
1633
|
} |
|
1634
|
db_multi_exec( |
|
1635
|
"CREATE INDEX IF NOT EXISTS mirror.mmarkx1 ON mmark(githash);" |
|
1636
|
); |
|
1637
|
|
|
1638
|
/* Do any tags that have been created since the start time */ |
|
1639
|
db_prepare(&q, |
|
1640
|
"SELECT substr(tagname,5), githash" |
|
1641
|
" FROM (SELECT tagxref.tagid AS xtagid, tagname, rid, max(mtime) AS mtime" |
|
1642
|
" FROM tagxref JOIN tag ON tag.tagid=tagxref.tagid" |
|
1643
|
" WHERE tag.tagname GLOB 'sym-*'" |
|
1644
|
" AND tagxref.tagtype=1" |
|
1645
|
" AND tagxref.mtime > coalesce((SELECT value FROM mconfig" |
|
1646
|
" WHERE key='start'),0.0)" |
|
1647
|
" GROUP BY tagxref.tagid) AS tx" |
|
1648
|
" JOIN blob ON tx.rid=blob.rid" |
|
1649
|
" JOIN mmark ON mmark.uuid=blob.uuid;" |
|
1650
|
); |
|
1651
|
while( db_step(&q)==SQLITE_ROW ){ |
|
1652
|
char *zTagname = fossil_strdup(db_column_text(&q,0)); |
|
1653
|
const char *zObj = db_column_text(&q,1); |
|
1654
|
char *zTagCmd; |
|
1655
|
gitmirror_sanitize_name(zTagname); |
|
1656
|
zTagCmd = mprintf("git tag -f %$ %$", zTagname, zObj); |
|
1657
|
fossil_free(zTagname); |
|
1658
|
gitmirror_message(VERB_NORMAL, "%s\n", zTagCmd); |
|
1659
|
fossil_system(zTagCmd); |
|
1660
|
fossil_free(zTagCmd); |
|
1661
|
} |
|
1662
|
db_finalize(&q); |
|
1663
|
|
|
1664
|
/* Update all references that might have changed since the start time */ |
|
1665
|
db_prepare(&q, |
|
1666
|
"SELECT" |
|
1667
|
" tagxref.value AS name," |
|
1668
|
" max(event.mtime) AS mtime," |
|
1669
|
" mmark.githash AS gitckin" |
|
1670
|
" FROM tagxref, tag, event, blob, mmark" |
|
1671
|
" WHERE tagxref.tagid=tag.tagid" |
|
1672
|
" AND tagxref.tagtype>0" |
|
1673
|
" AND tag.tagname='branch'" |
|
1674
|
" AND event.objid=tagxref.rid" |
|
1675
|
" AND event.mtime > coalesce((SELECT value FROM mconfig" |
|
1676
|
" WHERE key='start'),0.0)" |
|
1677
|
" AND blob.rid=tagxref.rid" |
|
1678
|
" AND mmark.uuid=blob.uuid" |
|
1679
|
" GROUP BY 1" |
|
1680
|
); |
|
1681
|
while( db_step(&q)==SQLITE_ROW ){ |
|
1682
|
char *zBrname = fossil_strdup(db_column_text(&q,0)); |
|
1683
|
const char *zObj = db_column_text(&q,2); |
|
1684
|
char *zRefCmd; |
|
1685
|
if( fossil_strcmp(zBrname,"trunk")==0 ){ |
|
1686
|
fossil_free(zBrname); |
|
1687
|
zBrname = fossil_strdup(gitmirror_mainbranch); |
|
1688
|
}else{ |
|
1689
|
gitmirror_sanitize_name(zBrname); |
|
1690
|
} |
|
1691
|
zRefCmd = mprintf("git update-ref \"refs/heads/%s\" %$", zBrname, zObj); |
|
1692
|
fossil_free(zBrname); |
|
1693
|
gitmirror_message(VERB_NORMAL, "%s\n", zRefCmd); |
|
1694
|
fossil_system(zRefCmd); |
|
1695
|
fossil_free(zRefCmd); |
|
1696
|
} |
|
1697
|
db_finalize(&q); |
|
1698
|
|
|
1699
|
/* Update the start time */ |
|
1700
|
if( rEnd>0.0 ){ |
|
1701
|
db_prepare(&q, "REPLACE INTO mirror.mconfig(key,value) VALUES('start',:x)"); |
|
1702
|
db_bind_double(&q, ":x", rEnd); |
|
1703
|
db_step(&q); |
|
1704
|
db_finalize(&q); |
|
1705
|
} |
|
1706
|
db_commit_transaction(); |
|
1707
|
|
|
1708
|
/* Maybe run a git repack */ |
|
1709
|
if( bNeedRepack ){ |
|
1710
|
const char *zRepack = "git repack -adf"; |
|
1711
|
gitmirror_message(VERB_NORMAL, "%s\n", zRepack); |
|
1712
|
fossil_system(zRepack); |
|
1713
|
} |
|
1714
|
|
|
1715
|
/* Optionally do a "git push" */ |
|
1716
|
zPushUrl = db_text(0, "SELECT value FROM mconfig WHERE key='autopush'"); |
|
1717
|
if( zPushUrl ){ |
|
1718
|
char *zPushCmd; |
|
1719
|
UrlData url; |
|
1720
|
if( sqlite3_strglob("http*", zPushUrl)==0 ){ |
|
1721
|
url_parse_local(zPushUrl, 0, &url); |
|
1722
|
zPushCmd = mprintf("git push --mirror %s", url.canonical); |
|
1723
|
}else{ |
|
1724
|
zPushCmd = mprintf("git push --mirror %s", zPushUrl); |
|
1725
|
} |
|
1726
|
gitmirror_message(VERB_NORMAL, "%s\n", zPushCmd); |
|
1727
|
fossil_free(zPushCmd); |
|
1728
|
zPushCmd = mprintf("git push --mirror %$", zPushUrl); |
|
1729
|
rc = fossil_system(zPushCmd); |
|
1730
|
if( rc ){ |
|
1731
|
fossil_fatal("cannot push content using: %s", zPushCmd); |
|
1732
|
}else if( db_is_writeable("repository") ){ |
|
1733
|
db_unprotect(PROTECT_CONFIG); |
|
1734
|
db_multi_exec("REPLACE INTO config(name,value,mtime)" |
|
1735
|
"VALUES('gitpush:%q','{}',now())", zPushUrl); |
|
1736
|
db_protect_pop(); |
|
1737
|
} |
|
1738
|
fossil_free(zPushCmd); |
|
1739
|
} |
|
1740
|
} |
|
1741
|
|
|
1742
|
/* |
|
1743
|
** Implementation of the "fossil git status" command. |
|
1744
|
** |
|
1745
|
** Show the status of a "git export". |
|
1746
|
*/ |
|
1747
|
void gitmirror_status_command(void){ |
|
1748
|
char *zMirror; |
|
1749
|
char *z; |
|
1750
|
int n, k; |
|
1751
|
int rc; |
|
1752
|
char *zSql; |
|
1753
|
int bQuiet = 0; |
|
1754
|
int bByAll = 0; /* Undocumented option meaning this command was invoked |
|
1755
|
** from "fossil all" and should modify output accordingly */ |
|
1756
|
|
|
1757
|
db_find_and_open_repository(0, 0); |
|
1758
|
bQuiet = g.fQuiet; |
|
1759
|
bByAll = find_option("by-all",0,0)!=0; |
|
1760
|
verify_all_options(); |
|
1761
|
zMirror = db_get("last-git-export-repo", 0); |
|
1762
|
if( zMirror==0 ){ |
|
1763
|
if( bQuiet ) return; |
|
1764
|
if( bByAll ) return; |
|
1765
|
fossil_print("Git mirror: none\n"); |
|
1766
|
return; |
|
1767
|
} |
|
1768
|
zSql = sqlite3_mprintf("ATTACH '%q/.mirror_state/db' AS mirror", zMirror); |
|
1769
|
if( zSql==0 ) fossil_fatal("out of memory"); |
|
1770
|
g.dbIgnoreErrors++; |
|
1771
|
rc = sqlite3_exec(g.db, zSql, 0, 0, 0); |
|
1772
|
g.dbIgnoreErrors--; |
|
1773
|
sqlite3_free(zSql); |
|
1774
|
if( rc ){ |
|
1775
|
if( bQuiet ) return; |
|
1776
|
if( bByAll ) return; |
|
1777
|
fossil_print("Git mirror: %s (Inactive)\n", zMirror); |
|
1778
|
return; |
|
1779
|
} |
|
1780
|
if( bByAll ){ |
|
1781
|
size_t len = strlen(g.zRepositoryName); |
|
1782
|
int n; |
|
1783
|
if( len>60 ) len = 60; |
|
1784
|
n = (int)(65 - len); |
|
1785
|
fossil_print("%.12c %s %.*c\n", '*', g.zRepositoryName, n, '*'); |
|
1786
|
} |
|
1787
|
fossil_print("Git mirror: %s\n", zMirror); |
|
1788
|
z = db_text(0, "SELECT datetime(value) FROM mconfig WHERE key='start'"); |
|
1789
|
if( z ){ |
|
1790
|
double rAge = db_double(0.0, "SELECT julianday('now') - value" |
|
1791
|
" FROM mconfig WHERE key='start'"); |
|
1792
|
if( rAge>1.0/86400.0 ){ |
|
1793
|
fossil_print("Last export: %s (%z ago)\n", z, human_readable_age(rAge)); |
|
1794
|
}else{ |
|
1795
|
fossil_print("Last export: %s (moments ago)\n", z); |
|
1796
|
} |
|
1797
|
} |
|
1798
|
z = db_text(0, "SELECT value FROM mconfig WHERE key='autopush'"); |
|
1799
|
if( z==0 ){ |
|
1800
|
fossil_print("Autopush: off\n"); |
|
1801
|
}else{ |
|
1802
|
UrlData url; |
|
1803
|
if( sqlite3_strglob("http*", z)==0 ){ |
|
1804
|
url_parse_local(z, 0, &url); |
|
1805
|
fossil_print("Autopush: %s\n", url.canonical); |
|
1806
|
}else{ |
|
1807
|
fossil_print("Autopush: %s\n", z); |
|
1808
|
} |
|
1809
|
fossil_free(z); |
|
1810
|
} |
|
1811
|
n = db_int(0, |
|
1812
|
"SELECT count(*) FROM event" |
|
1813
|
" WHERE type='ci'" |
|
1814
|
" AND mtime>coalesce((SELECT value FROM mconfig" |
|
1815
|
" WHERE key='start'),0.0)" |
|
1816
|
); |
|
1817
|
z = db_text("master", "SELECT value FROM mconfig WHERE key='mainbranch'"); |
|
1818
|
fossil_print("Main-Branch: %s\n",z); |
|
1819
|
if( n==0 ){ |
|
1820
|
fossil_print("Status: up-to-date\n"); |
|
1821
|
}else{ |
|
1822
|
fossil_print("Status: %d check-in%s awaiting export\n", |
|
1823
|
n, n==1 ? "" : "s"); |
|
1824
|
} |
|
1825
|
n = db_int(0, "SELECT count(*) FROM mmark WHERE isfile"); |
|
1826
|
k = db_int(0, "SELECT count(*) FROM mmark WHERE NOT isfile"); |
|
1827
|
fossil_print("Exported: %d check-ins and %d file blobs\n", k, n); |
|
1828
|
} |
|
1829
|
|
|
1830
|
/* |
|
1831
|
** COMMAND: git* |
|
1832
|
** |
|
1833
|
** Usage: %fossil git SUBCOMMAND |
|
1834
|
** |
|
1835
|
** Do incremental import or export operations between Fossil and Git. |
|
1836
|
** Subcommands: |
|
1837
|
** |
|
1838
|
** > fossil git export [MIRROR] [OPTIONS] |
|
1839
|
** |
|
1840
|
** Write content from the Fossil repository into the Git repository |
|
1841
|
** in directory MIRROR. The Git repository is created if it does not |
|
1842
|
** already exist. If the Git repository does already exist, then |
|
1843
|
** new content added to fossil since the previous export is appended. |
|
1844
|
** |
|
1845
|
** Repeat this command whenever new check-ins are added to the Fossil |
|
1846
|
** repository in order to reflect those changes into the mirror. If |
|
1847
|
** the MIRROR option is omitted, the repository from the previous |
|
1848
|
** invocation is used. |
|
1849
|
** |
|
1850
|
** The MIRROR directory will contain a subdirectory named |
|
1851
|
** ".mirror_state" that contains information that Fossil needs to |
|
1852
|
** do incremental exports. Do not attempt to manage or edit the files |
|
1853
|
** in that directory since doing so can disrupt future incremental |
|
1854
|
** exports. |
|
1855
|
** |
|
1856
|
** Options: |
|
1857
|
** --autopush URL Automatically do a 'git push' to URL. The |
|
1858
|
** URL is remembered and used on subsequent exports |
|
1859
|
** to the same repository. Or if URL is "off" the |
|
1860
|
** auto-push mechanism is disabled |
|
1861
|
** --debug FILE Write fast-export text to FILE rather than |
|
1862
|
** piping it into "git fast-import" |
|
1863
|
** -f|--force Do the export even if nothing has changed |
|
1864
|
** --if-mirrored No-op if the mirror does not already exist |
|
1865
|
** --limit N Add no more than N new check-ins to MIRROR. |
|
1866
|
** Useful for debugging |
|
1867
|
** --mainbranch NAME Use NAME as the name of the main branch in Git. |
|
1868
|
** The "trunk" branch of the Fossil repository is |
|
1869
|
** mapped into this name. "master" is used if |
|
1870
|
** this option is omitted. |
|
1871
|
** -q|--quiet Reduce output. Repeat for even less output. |
|
1872
|
** -v|--verbose More output |
|
1873
|
** |
|
1874
|
** > fossil git import MIRROR |
|
1875
|
** |
|
1876
|
** TBD... |
|
1877
|
** |
|
1878
|
** > fossil git status |
|
1879
|
** |
|
1880
|
** Show the status of the current Git mirror, if there is one. |
|
1881
|
** |
|
1882
|
** -q|--quiet No output if there is nothing to report |
|
1883
|
*/ |
|
1884
|
void gitmirror_command(void){ |
|
1885
|
char *zCmd; |
|
1886
|
int nCmd; |
|
1887
|
if( g.argc<3 ){ |
|
1888
|
usage("SUBCOMMAND ..."); |
|
1889
|
} |
|
1890
|
zCmd = g.argv[2]; |
|
1891
|
nCmd = (int)strlen(zCmd); |
|
1892
|
if( nCmd>2 && strncmp(zCmd,"export",nCmd)==0 ){ |
|
1893
|
gitmirror_export_command(); |
|
1894
|
}else |
|
1895
|
if( nCmd>2 && strncmp(zCmd,"import",nCmd)==0 ){ |
|
1896
|
fossil_fatal("not yet implemented - check back later"); |
|
1897
|
}else |
|
1898
|
if( nCmd>2 && strncmp(zCmd,"status",nCmd)==0 ){ |
|
1899
|
gitmirror_status_command(); |
|
1900
|
}else |
|
1901
|
{ |
|
1902
|
fossil_fatal("unknown subcommand \"%s\": should be one of " |
|
1903
|
"\"export\", \"import\", \"status\"", |
|
1904
|
zCmd); |
|
1905
|
} |
|
1906
|
} |
|
1907
|
|