Fossil SCM
Begin work on the "fossil mirror" command.
Commit
dbc1c62a995ae7616015ca2e40252bfed1df914443ec357f732a530fde204287
Parent
60af514d13969c7…
1 file changed
+439
-35
+439
-35
| --- src/export.c | ||
| +++ src/export.c | ||
| @@ -28,23 +28,17 @@ | ||
| 28 | 28 | const char *zTrunkName; /* Name of trunk branch */ |
| 29 | 29 | } gexport; |
| 30 | 30 | |
| 31 | 31 | #if INTERFACE |
| 32 | 32 | /* |
| 33 | -** struct mark_t | |
| 34 | -** holds information for translating between git commits | |
| 35 | -** and fossil commits. | |
| 36 | -** -git_name: This is the mark name that identifies the commit to git. | |
| 37 | -** It will always begin with a ':'. | |
| 38 | -** -rid: The unique object ID that identifies this commit within the | |
| 39 | -** repository database. | |
| 40 | -** -uuid: The SHA-1/SHA-3 of artifact corresponding to rid. | |
| 33 | +** Each line in a git-fast-export "marK" file is an instance of | |
| 34 | +** this object. | |
| 41 | 35 | */ |
| 42 | -struct mark_t{ | |
| 43 | - char *name; | |
| 44 | - int rid; | |
| 45 | - char uuid[65]; | |
| 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 */ | |
| 46 | 40 | }; |
| 47 | 41 | #endif |
| 48 | 42 | |
| 49 | 43 | /* |
| 50 | 44 | ** Output a "committer" record for the given user. |
| @@ -297,18 +291,18 @@ | ||
| 297 | 291 | } |
| 298 | 292 | return zMark; |
| 299 | 293 | } |
| 300 | 294 | |
| 301 | 295 | /* |
| 302 | -** parse_mark() | |
| 303 | -** Create a new (mark,rid,uuid) entry in the 'xmark' table given a line | |
| 304 | -** from a marks file. Return the cross-ref information as a struct mark_t | |
| 305 | -** in *mark. | |
| 306 | -** This function returns -1 in the case that the line is blank, malformed, or | |
| 307 | -** the rid/uuid named in 'line' does not match what is in the repository | |
| 308 | -** database. Otherwise, 0 is returned. | |
| 309 | -** mark->name is dynamically allocated, and owned by the caller. | |
| 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. | |
| 310 | 304 | */ |
| 311 | 305 | int parse_mark(char *line, struct mark_t *mark){ |
| 312 | 306 | char *cur_tok; |
| 313 | 307 | char type_; |
| 314 | 308 | cur_tok = strtok(line, " \t"); |
| @@ -361,23 +355,24 @@ | ||
| 361 | 355 | insert_commit_xref(mark->rid, mark->name, mark->uuid); |
| 362 | 356 | return 0; |
| 363 | 357 | } |
| 364 | 358 | |
| 365 | 359 | /* |
| 366 | -** import_marks() | |
| 367 | -** Import the marks specified in file 'f' into the 'xmark' table. | |
| 368 | -** If 'blobs' is non-null, insert all blob marks into it. | |
| 369 | -** If 'vers' is non-null, insert all commit marks into it. | |
| 370 | -** If 'unused_marks' is non-null, upon return of this function, all values | |
| 371 | -** x >= *unused_marks are free to use as marks, i.e. they do not clash with | |
| 372 | -** any marks appearing in the marks file. | |
| 373 | -** Each line in the file must be at most 100 characters in length. This | |
| 374 | -** seems like a reasonable maximum for a 40-character uuid, and 1-13 | |
| 375 | -** character rid. | |
| 376 | -** The function returns -1 if any of the lines in file 'f' are malformed, | |
| 377 | -** or the rid/uuid information doesn't match what is in the repository | |
| 378 | -** database. Otherwise, 0 is returned. | |
| 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. | |
| 379 | 374 | */ |
| 380 | 375 | int import_marks(FILE* f, Bag *blobs, Bag *vers, unsigned int *unused_mark){ |
| 381 | 376 | char line[101]; |
| 382 | 377 | while(fgets(line, sizeof(line), f)){ |
| 383 | 378 | struct mark_t mark; |
| @@ -507,11 +502,12 @@ | ||
| 507 | 502 | verify_all_options(); |
| 508 | 503 | if( g.argc!=2 && g.argc!=3 ){ usage("--git ?REPOSITORY?"); } |
| 509 | 504 | |
| 510 | 505 | db_multi_exec("CREATE TEMPORARY TABLE oldblob(rid INTEGER PRIMARY KEY)"); |
| 511 | 506 | db_multi_exec("CREATE TEMPORARY TABLE oldcommit(rid INTEGER PRIMARY KEY)"); |
| 512 | - db_multi_exec("CREATE TEMP TABLE xmark(tname TEXT UNIQUE, trid INT, tuuid TEXT)"); | |
| 507 | + db_multi_exec("CREATE TEMP TABLE xmark(tname TEXT UNIQUE, trid INT," | |
| 508 | + " tuuid TEXT)"); | |
| 513 | 509 | db_multi_exec("CREATE INDEX xmark_trid ON xmark(trid)"); |
| 514 | 510 | if( markfile_in!=0 ){ |
| 515 | 511 | Stmt qb,qc; |
| 516 | 512 | FILE *f; |
| 517 | 513 | int rid; |
| @@ -754,11 +750,11 @@ | ||
| 754 | 750 | ** ); |
| 755 | 751 | ** |
| 756 | 752 | ** This table contains all check-ins of the repository in topological |
| 757 | 753 | ** order. "Topological order" means that every parent check-in comes |
| 758 | 754 | ** before all of its children. Topological order is *almost* the same |
| 759 | -** thing as "ORDER BY event.mtime". Differences only arrise when there | |
| 755 | +** thing as "ORDER BY event.mtime". Differences only arise when there | |
| 760 | 756 | ** are timewarps. In as much as Git hates timewarps, we have to compute |
| 761 | 757 | ** a correct topological order when doing an export. |
| 762 | 758 | ** |
| 763 | 759 | ** Since mtime is a usually already nearly in topological order, the |
| 764 | 760 | ** algorithm is to start with mtime, then make adjustments as necessary |
| @@ -835,5 +831,413 @@ | ||
| 835 | 831 | int n; |
| 836 | 832 | db_find_and_open_repository(0, 0); |
| 837 | 833 | n = topological_sort_checkins(1); |
| 838 | 834 | fossil_print("%d reorderings required\n", n); |
| 839 | 835 | } |
| 836 | + | |
| 837 | +/* | |
| 838 | +** Transfer a tag over to the mirror. "rid" is the BLOB.RID value for | |
| 839 | +** the record that describes the tag. | |
| 840 | +** | |
| 841 | +** The Git tag mechanism is very limited compared to Fossil. Many Fossil | |
| 842 | +** tags cannot be exported to Git. If this tag cannot be exported, then | |
| 843 | +** silently ignore it. | |
| 844 | +*/ | |
| 845 | +static void mirror_send_tag(FILE *xCmd, int rid){ | |
| 846 | + return; | |
| 847 | +} | |
| 848 | + | |
| 849 | +/* | |
| 850 | +** Locate the mark for a UUID. | |
| 851 | +** | |
| 852 | +** If the mark does not exist and if the bCreate flag is false, then | |
| 853 | +** return 0. If the mark does not exist and the bCreate flag is true, | |
| 854 | +** then create the mark. | |
| 855 | +*/ | |
| 856 | +static int mirror_find_mark(const char *zUuid, int bCreate){ | |
| 857 | + int iMark; | |
| 858 | + static Stmt sFind, sIns; | |
| 859 | + db_static_prepare(&sFind, | |
| 860 | + "SELECT id FROM mirror.mmark WHERE uuid=:uuid" | |
| 861 | + ); | |
| 862 | + db_bind_text(&sFind, ":uuid", zUuid); | |
| 863 | + if( db_step(&sFind)==SQLITE_ROW ){ | |
| 864 | + iMark = db_column_int(&sFind, 0); | |
| 865 | + db_reset(&sFind); | |
| 866 | + return iMark; | |
| 867 | + } | |
| 868 | + db_reset(&sFind); | |
| 869 | + if( !bCreate ) return 0; | |
| 870 | + db_static_prepare(&sIns, | |
| 871 | + "INSERT INTO mirror.mmark(uuid) VALUES(:uuid)" | |
| 872 | + ); | |
| 873 | + db_bind_text(&sIns, ":uuid", zUuid); | |
| 874 | + db_step(&sIns); | |
| 875 | + db_reset(&sIns); | |
| 876 | + return db_last_insert_rowid(); | |
| 877 | +} | |
| 878 | + | |
| 879 | +/* | |
| 880 | +** Export a single file named by zUuid. | |
| 881 | +*/ | |
| 882 | +static void mirror_send_file(FILE *xCmd, const char *zUuid){ | |
| 883 | + int iMark; | |
| 884 | + int rid; | |
| 885 | + int rc; | |
| 886 | + Blob data; | |
| 887 | + rid = fast_uuid_to_rid(zUuid); | |
| 888 | + if( rid<0 ) fossil_fatal("no rid for %s", zUuid); | |
| 889 | + rc = content_get(rid, &data); | |
| 890 | + if( rc==0 ) fossil_fatal("%s is a phantom", zUuid); | |
| 891 | + iMark = mirror_find_mark(zUuid, 1); | |
| 892 | + fprintf(xCmd, "blob\nmark :%d\ndata %d\n", iMark, blob_size(&data)); | |
| 893 | + fwrite(blob_buffer(&data), 1, blob_size(&data), xCmd); | |
| 894 | + fprintf(xCmd, "\n"); | |
| 895 | + blob_reset(&data); | |
| 896 | +} | |
| 897 | + | |
| 898 | +/* | |
| 899 | +** Transfer a check-in over to the mirror. "rid" is the BLOB.RID for | |
| 900 | +** the check-in to export. | |
| 901 | +** | |
| 902 | +** If any ancestor of the check-in has not yet been exported, then | |
| 903 | +** invoke this routine recursively to export the ancestor first. | |
| 904 | +** This can only happen on a timewarp, so deep nesting is unlikely. | |
| 905 | +** | |
| 906 | +** Before sending the check-in, first make sure all associated files | |
| 907 | +** have already been exported, and send "blob" records for any that | |
| 908 | +** have not been. Update the MIRROR.MMARK table so that it holds the | |
| 909 | +** marks for the exported files. | |
| 910 | +*/ | |
| 911 | +static void mirror_send_checkin( | |
| 912 | + FILE *xCmd, /* Write fast-import text on this pipe */ | |
| 913 | + int rid, /* BLOB.RID for the check-in to export */ | |
| 914 | + const char *zUuid, /* BLOB.UUID for the check-in to export */ | |
| 915 | + int *pnLimit /* Stop when the counter reaches zero */ | |
| 916 | +){ | |
| 917 | + Manifest *pMan; | |
| 918 | + int i; | |
| 919 | + Blob err; | |
| 920 | + Stmt q; | |
| 921 | + char *zBranch; | |
| 922 | + int iMark; | |
| 923 | + Blob sql; | |
| 924 | + | |
| 925 | + blob_init(&err, 0, 0); | |
| 926 | + pMan = manifest_get(rid, CFTYPE_MANIFEST, &err); | |
| 927 | + if( pMan==0 ){ | |
| 928 | + fossil_fatal("cannot fetch manifest for check-in %d: %s", rid, | |
| 929 | + blob_str(&err)); | |
| 930 | + } | |
| 931 | + | |
| 932 | + /* Check to see if any parent logins have not yet been processed, and | |
| 933 | + ** if so, create them */ | |
| 934 | + for(i=0; i<pMan->nParent; i++){ | |
| 935 | + int iMark = mirror_find_mark(pMan->azParent[i], 0); | |
| 936 | + if( iMark<=0 ){ | |
| 937 | + int prid = db_int(0, "SELECT rid FROM blob WHERE uuid=%Q", | |
| 938 | + pMan->azParent[i]); | |
| 939 | + mirror_send_checkin(xCmd, prid, pMan->azParent[i], pnLimit); | |
| 940 | + if( *pnLimit<=0 ){ | |
| 941 | + manifest_destroy(pMan); | |
| 942 | + return; | |
| 943 | + } | |
| 944 | + } | |
| 945 | + } | |
| 946 | + | |
| 947 | + /* Make sure all necessary files have been exported */ | |
| 948 | + db_prepare(&q, | |
| 949 | + "SELECT uuid FROM files_of_checkin(%Q)" | |
| 950 | + " WHERE uuid NOT IN (SELECT uuid FROM mirror.mmark)", | |
| 951 | + zUuid | |
| 952 | + ); | |
| 953 | + while( db_step(&q)==SQLITE_ROW ){ | |
| 954 | + const char *zFUuid = db_column_text(&q, 0); | |
| 955 | + mirror_send_file(xCmd, zFUuid); | |
| 956 | + } | |
| 957 | + db_finalize(&q); | |
| 958 | + | |
| 959 | + /* Figure out which branch this check-in is a member of */ | |
| 960 | + zBranch = db_text(0, | |
| 961 | + "SELECT value FROM tagxref WHERE tagid=%d AND tagtype>0 AND rid=%d", | |
| 962 | + TAG_BRANCH, rid | |
| 963 | + ); | |
| 964 | + if( fossil_strcmp(zBranch,"trunk")==0 ){ | |
| 965 | + fossil_free(zBranch); | |
| 966 | + zBranch = mprintf("master"); | |
| 967 | + } | |
| 968 | + | |
| 969 | + /* Export the check-in */ | |
| 970 | + fprintf(xCmd, "commit refs/head/%s\n", zBranch); | |
| 971 | + fossil_free(zBranch); | |
| 972 | + iMark = mirror_find_mark(zUuid, 1); | |
| 973 | + fprintf(xCmd, "mark :%d\n", iMark); | |
| 974 | + fprintf(xCmd, "committer %s <%[email protected]> %lld +0000\n", | |
| 975 | + pMan->zUser, pMan->zUser, | |
| 976 | + (sqlite3_int64)(pMan->rDate-2440587.5)*86400 | |
| 977 | + ); | |
| 978 | + fprintf(xCmd, "data %d\n", (int)strlen(pMan->zComment)); | |
| 979 | + fprintf(xCmd, "%s\n", pMan->zComment); | |
| 980 | + for(i=0; i<pMan->nParent; i++){ | |
| 981 | + int iOther = mirror_find_mark(pMan->azParent[i], 0); | |
| 982 | + if( i==0 ){ | |
| 983 | + fprintf(xCmd, "from :%d\n", iOther); | |
| 984 | + }else{ | |
| 985 | + fprintf(xCmd, "merge :%d\n", iOther); | |
| 986 | + } | |
| 987 | + } | |
| 988 | + if( pMan->nParent ){ | |
| 989 | + db_prepare(&q, | |
| 990 | + "SELECT filename FROM files_of_checkin(%Q)" | |
| 991 | + " EXCEPT SELECT filename FROM files_of_checkin(%Q)", | |
| 992 | + pMan->azParent[0], zUuid | |
| 993 | + ); | |
| 994 | + while( db_step(&q)==SQLITE_ROW ){ | |
| 995 | + fprintf(xCmd, "D %s\n", db_column_text(&q,0)); | |
| 996 | + } | |
| 997 | + db_finalize(&q); | |
| 998 | + } | |
| 999 | + blob_init(&sql, 0, 0); | |
| 1000 | + blob_append_sql(&sql, | |
| 1001 | + "SELECT filename, uuid, perm FROM files_of_checkin(%Q)", | |
| 1002 | + zUuid | |
| 1003 | + ); | |
| 1004 | + if( pMan->nParent ){ | |
| 1005 | + blob_append_sql(&sql, | |
| 1006 | + " EXCEPT SELECT filename, uuid, perm FROM files_of_checkin(%Q)", | |
| 1007 | + pMan->azParent[0]); | |
| 1008 | + } | |
| 1009 | + db_prepare(&q, | |
| 1010 | + "SELECT x.filename, x.perm, mmark.id FROM (%s) AS x, mirror.mmark" | |
| 1011 | + " WHERE mmark.uuid=x.uuid", | |
| 1012 | + blob_sql_text(&sql) | |
| 1013 | + ); | |
| 1014 | + blob_reset(&sql); | |
| 1015 | + while( db_step(&q)==SQLITE_ROW ){ | |
| 1016 | + const char *zFilename = db_column_text(&q,0); | |
| 1017 | + const char *zMode = db_column_text(&q,1); | |
| 1018 | + int iMark = db_column_int(&q,2); | |
| 1019 | + const char *zGitMode = "100644"; | |
| 1020 | + if( zMode ){ | |
| 1021 | + if( strchr(zMode,'x') ) zGitMode = "100755"; | |
| 1022 | + if( strchr(zMode,'l') ) zGitMode = "120000"; | |
| 1023 | + } | |
| 1024 | + fprintf(xCmd,"M %s :%d %s\n", zGitMode, iMark, zFilename); | |
| 1025 | + } | |
| 1026 | + db_finalize(&q); | |
| 1027 | + | |
| 1028 | + /* The check-in is finished, so decrement the counter */ | |
| 1029 | + (*pnLimit)--; | |
| 1030 | +} | |
| 1031 | + | |
| 1032 | +/* | |
| 1033 | +** COMMAND: mirror | |
| 1034 | +** | |
| 1035 | +** Usage: %fossil mirror [--git] MIRROR [-R FOSSIL-REPO] | |
| 1036 | +** | |
| 1037 | +** Create or update another type of repository that is is mirror of | |
| 1038 | +** a Fossil repository. | |
| 1039 | +** | |
| 1040 | +** The current implementation only supports mirrors to Git, and so | |
| 1041 | +** the --git option is optional. The ability to mirror to other version | |
| 1042 | +** control systems may be added in the future, in which case an argument | |
| 1043 | +** to specify the target version control system will become required. | |
| 1044 | +** | |
| 1045 | +** The MIRROR argument is the name of the secondary repository. In the | |
| 1046 | +** case of Git, it is the directory that houses the Git repository. | |
| 1047 | +** If MIRROR does not previously exist, it is created and initialized to | |
| 1048 | +** a copy of the Fossil repository. If MIRROR does already exist, it is | |
| 1049 | +** updated with new check-ins that have been added to the Fossil repository | |
| 1050 | +** since the last "fossil mirror" command to that particular repository. | |
| 1051 | +** | |
| 1052 | +** Implementation notes: | |
| 1053 | +** | |
| 1054 | +** * The git version control system must be installed in order for | |
| 1055 | +** this command to work. Fossil will invoke various git commands | |
| 1056 | +** to run as subprocesses. | |
| 1057 | +** | |
| 1058 | +** * Fossil creates a directory named ".mirror_state" in the top level of | |
| 1059 | +** the created git repository and stores state information in that | |
| 1060 | +** directory. Do not attempt to manage any files in that directory. | |
| 1061 | +** Do not change or delete any files in that directory. Doing so | |
| 1062 | +** may disrupt future calls to the "fossil mirror" command for the | |
| 1063 | +** mirror repository. | |
| 1064 | +** | |
| 1065 | +** Options: | |
| 1066 | +** | |
| 1067 | +** --debug FILE Write fast-export text to FILE rather than | |
| 1068 | +** piping it into "git fast-import". | |
| 1069 | +** | |
| 1070 | +** --limit N Add no more than N new check-ins to MIRROR. | |
| 1071 | +** Useful for debugging | |
| 1072 | +*/ | |
| 1073 | +void mirror_command(void){ | |
| 1074 | + const char *zLimit; | |
| 1075 | + int nLimit = 0x7fffffff; | |
| 1076 | + int nTotal = 0; | |
| 1077 | + char *zMirror; | |
| 1078 | + char *z; | |
| 1079 | + char *zInFile; | |
| 1080 | + char *zOutFile; | |
| 1081 | + char *zCmd; | |
| 1082 | + const char *zDebug = 0; | |
| 1083 | + double rEnd; | |
| 1084 | + int rc; | |
| 1085 | + FILE *xCmd; | |
| 1086 | + FILE *pIn, *pOut; | |
| 1087 | + Stmt q; | |
| 1088 | + char zLine[200]; | |
| 1089 | + | |
| 1090 | + find_option("git", 0, 0); /* Ignore the --git option for now */ | |
| 1091 | + zDebug = find_option("debug",0,1); | |
| 1092 | + db_find_and_open_repository(0, 2); | |
| 1093 | + zLimit = find_option("limit", 0, 1); | |
| 1094 | + if( zLimit ){ | |
| 1095 | + nLimit = (unsigned int)atoi(zLimit); | |
| 1096 | + if( nLimit<=0 ) fossil_fatal("--limit must be positive"); | |
| 1097 | + } | |
| 1098 | + verify_all_options(); | |
| 1099 | + if( g.argc!=3 ){ usage("--git MIRROR"); } | |
| 1100 | + zMirror = g.argv[2]; | |
| 1101 | + | |
| 1102 | + /* Make sure the GIT repository directory exists */ | |
| 1103 | + rc = file_mkdir(zMirror, ExtFILE, 0); | |
| 1104 | + if( rc ) fossil_fatal("cannot create directory \"%s\"", zMirror); | |
| 1105 | + | |
| 1106 | + /* Make sure GIT has been initialized */ | |
| 1107 | + z = mprintf("%s/.git", zMirror); | |
| 1108 | + if( !file_isdir(z, ExtFILE) ){ | |
| 1109 | + zCmd = mprintf("git init '%s'",zMirror); | |
| 1110 | + fossil_print("%s\n", zCmd); | |
| 1111 | + rc = fossil_system(zCmd); | |
| 1112 | + if( rc ){ | |
| 1113 | + fossil_fatal("command failed: \"%s\"", zCmd); | |
| 1114 | + } | |
| 1115 | + fossil_free(zCmd); | |
| 1116 | + } | |
| 1117 | + fossil_free(z); | |
| 1118 | + | |
| 1119 | + /* Make sure the .mirror_state subdirectory exists */ | |
| 1120 | + z = mprintf("%s/.mirror_state", zMirror); | |
| 1121 | + rc = file_mkdir(z, ExtFILE, 0); | |
| 1122 | + if( rc ) fossil_fatal("cannot create directory \"%s\"", z); | |
| 1123 | + fossil_free(z); | |
| 1124 | + | |
| 1125 | + /* Attach the .mirror_state/db database */ | |
| 1126 | + db_multi_exec("ATTACH '%q/.mirror_state/db' AS mirror;", zMirror); | |
| 1127 | + db_multi_exec( | |
| 1128 | + "CREATE TABLE IF NOT EXISTS mirror.mconfig(\n" | |
| 1129 | + " key TEXT PRIMARY KEY,\n" | |
| 1130 | + " Value ANY\n" | |
| 1131 | + ") WITHOUT ROWID;\n" | |
| 1132 | + "CREATE TABLE IF NOT EXISTS mirror.mmark(\n" | |
| 1133 | + " id INTEGER PRIMARY KEY,\n" | |
| 1134 | + " uuid TEXT UNIQUE\n" | |
| 1135 | + ");" | |
| 1136 | + "CREATE TABLE IF NOT EXISTS mirror.mtag(\n" | |
| 1137 | + " tagname TEXT PRIMARY KEY,\n" | |
| 1138 | + " cnt INTEGER DEFAULT 1\n" | |
| 1139 | + ") WITHOUT ROWID;" | |
| 1140 | + ); | |
| 1141 | + | |
| 1142 | + /* See if there is any work to be done. Exit early if not, before starting | |
| 1143 | + ** the "git fast-import" command. */ | |
| 1144 | + if( !db_exists("SELECT 1 FROM event WHERE type IN ('t','ci')" | |
| 1145 | + " AND mtime>coalesce((SELECT value FROM mconfig" | |
| 1146 | + " WHERE key='start'),0.0)") | |
| 1147 | + ){ | |
| 1148 | + fossil_print("no changes\n"); | |
| 1149 | + return; | |
| 1150 | + } | |
| 1151 | + | |
| 1152 | + /* Change to the MIRROR directory so that the Git commands will work */ | |
| 1153 | + rc = file_chdir(zMirror, 0); | |
| 1154 | + if( rc ) fossil_fatal("cannot change the working directory to \"%s\"", | |
| 1155 | + zMirror); | |
| 1156 | + | |
| 1157 | + /* Start up the git fast-import command */ | |
| 1158 | + if( zDebug ){ | |
| 1159 | + if( fossil_strcmp(zDebug,"stdout")==0 ){ | |
| 1160 | + xCmd = stdout; | |
| 1161 | + }else{ | |
| 1162 | + xCmd = fopen(zDebug, "wb"); | |
| 1163 | + if( xCmd==0 ) fossil_fatal("cannot open file \"%s\" for writing", zDebug); | |
| 1164 | + } | |
| 1165 | + }else{ | |
| 1166 | + zCmd = mprintf("git fast-import" | |
| 1167 | + " --import-marks-if-exists=.mirror_state/in" | |
| 1168 | + " --export-marks=.mirror_state/out" | |
| 1169 | + " --quiet --done"); | |
| 1170 | + fossil_print("%s\n", zCmd); | |
| 1171 | + xCmd = popen(zCmd, "w"); | |
| 1172 | + if( zCmd==0 ){ | |
| 1173 | + fossil_fatal("cannot start the \"git fast-import\" command"); | |
| 1174 | + } | |
| 1175 | + fossil_free(zCmd); | |
| 1176 | + } | |
| 1177 | + | |
| 1178 | + /* Run the export */ | |
| 1179 | + rEnd = 0.0; | |
| 1180 | + db_multi_exec( | |
| 1181 | + "CREATE TEMP TABLE tomirror(objid INTEGER PRIMARY KEY,type,mtime,uuid);\n" | |
| 1182 | + "INSERT INTO tomirror " | |
| 1183 | + "SELECT objid, type, mtime, blob.uuid FROM event, blob\n" | |
| 1184 | + " WHERE type IN ('ci','t')" | |
| 1185 | + " AND mtime>coalesce((SELECT value FROM mconfig WHERE key='start'),0.0)" | |
| 1186 | + " AND blob.rid=event.objid" | |
| 1187 | + " AND blob.uuid NOT IN (SELECT uuid FROM mirror.mmark);" | |
| 1188 | + ); | |
| 1189 | + nTotal = db_int(0, "SELECT count(*) FROM tomirror"); | |
| 1190 | + if( nLimit<nTotal ) nTotal = nLimit; | |
| 1191 | + db_prepare(&q, | |
| 1192 | + "SELECT objid, type, mtime, uuid FROM tomirror ORDER BY mtime" | |
| 1193 | + ); | |
| 1194 | + while( nLimit && db_step(&q)==SQLITE_ROW ){ | |
| 1195 | + double rMTime = db_column_double(&q, 2); | |
| 1196 | + const char *zType = db_column_text(&q, 1); | |
| 1197 | + int rid = db_column_int(&q, 0); | |
| 1198 | + const char *zUuid = db_column_text(&q, 3); | |
| 1199 | + if( rMTime>rEnd ) rEnd = rMTime; | |
| 1200 | + if( zType[0]=='t' ){ | |
| 1201 | + mirror_send_tag(xCmd, rid); | |
| 1202 | + }else{ | |
| 1203 | + mirror_send_checkin(xCmd, rid, zUuid, &nLimit); | |
| 1204 | + printf("\r%d/%d ", nTotal-nLimit, nTotal); | |
| 1205 | + fflush(stdout); | |
| 1206 | + } | |
| 1207 | + } | |
| 1208 | + db_finalize(&q); | |
| 1209 | + db_prepare(&q, "REPLACE INTO mirror.mconfig(key,value) VALUES('start',:x)"); | |
| 1210 | + db_bind_double(&q, ":x", rEnd); | |
| 1211 | + db_step(&q); | |
| 1212 | + db_finalize(&q); | |
| 1213 | + fprintf(xCmd, "done\n"); | |
| 1214 | + if( zDebug ){ | |
| 1215 | + if( xCmd!=stdout ) fclose(xCmd); | |
| 1216 | + }else{ | |
| 1217 | + pclose(xCmd); | |
| 1218 | + } | |
| 1219 | + fossil_print("%d check-ins added to the mirror\n", nTotal-nLimit); | |
| 1220 | + | |
| 1221 | + /* Read the export-marks file. Transfer the new marks over into | |
| 1222 | + ** the import-marks file. | |
| 1223 | + */ | |
| 1224 | + zInFile = mprintf("%s/.mirror_state/in", zMirror); | |
| 1225 | + zOutFile = mprintf("%s/.mirror_state/out", zMirror); | |
| 1226 | + pOut = fopen(zOutFile, "rb"); | |
| 1227 | + if( pOut ){ | |
| 1228 | + pIn = fopen(zInFile, "ab"); | |
| 1229 | + if( pIn==0 ){ | |
| 1230 | + fossil_fatal("cannot open %s for appending", zInFile); | |
| 1231 | + } | |
| 1232 | + while( fgets(zLine, sizeof(zLine), pIn) ){ | |
| 1233 | + fputs(zLine, pOut); | |
| 1234 | + } | |
| 1235 | + fclose(pOut); | |
| 1236 | + fclose(pIn); | |
| 1237 | + file_delete(zOutFile); | |
| 1238 | + } | |
| 1239 | + fossil_free(zInFile); | |
| 1240 | + fossil_free(zOutFile); | |
| 1241 | + | |
| 1242 | + /* Optionally do a "git push" */ | |
| 1243 | +} | |
| 840 | 1244 |
| --- src/export.c | |
| +++ src/export.c | |
| @@ -28,23 +28,17 @@ | |
| 28 | const char *zTrunkName; /* Name of trunk branch */ |
| 29 | } gexport; |
| 30 | |
| 31 | #if INTERFACE |
| 32 | /* |
| 33 | ** struct mark_t |
| 34 | ** holds information for translating between git commits |
| 35 | ** and fossil commits. |
| 36 | ** -git_name: This is the mark name that identifies the commit to git. |
| 37 | ** It will always begin with a ':'. |
| 38 | ** -rid: The unique object ID that identifies this commit within the |
| 39 | ** repository database. |
| 40 | ** -uuid: The SHA-1/SHA-3 of artifact corresponding to rid. |
| 41 | */ |
| 42 | struct mark_t{ |
| 43 | char *name; |
| 44 | int rid; |
| 45 | char uuid[65]; |
| 46 | }; |
| 47 | #endif |
| 48 | |
| 49 | /* |
| 50 | ** Output a "committer" record for the given user. |
| @@ -297,18 +291,18 @@ | |
| 297 | } |
| 298 | return zMark; |
| 299 | } |
| 300 | |
| 301 | /* |
| 302 | ** parse_mark() |
| 303 | ** Create a new (mark,rid,uuid) entry in the 'xmark' table given a line |
| 304 | ** from a marks file. Return the cross-ref information as a struct mark_t |
| 305 | ** in *mark. |
| 306 | ** This function returns -1 in the case that the line is blank, malformed, or |
| 307 | ** the rid/uuid named in 'line' does not match what is in the repository |
| 308 | ** database. Otherwise, 0 is returned. |
| 309 | ** mark->name is dynamically allocated, and owned by the caller. |
| 310 | */ |
| 311 | int parse_mark(char *line, struct mark_t *mark){ |
| 312 | char *cur_tok; |
| 313 | char type_; |
| 314 | cur_tok = strtok(line, " \t"); |
| @@ -361,23 +355,24 @@ | |
| 361 | insert_commit_xref(mark->rid, mark->name, mark->uuid); |
| 362 | return 0; |
| 363 | } |
| 364 | |
| 365 | /* |
| 366 | ** import_marks() |
| 367 | ** Import the marks specified in file 'f' into the 'xmark' table. |
| 368 | ** If 'blobs' is non-null, insert all blob marks into it. |
| 369 | ** If 'vers' is non-null, insert all commit marks into it. |
| 370 | ** If 'unused_marks' is non-null, upon return of this function, all values |
| 371 | ** x >= *unused_marks are free to use as marks, i.e. they do not clash with |
| 372 | ** any marks appearing in the marks file. |
| 373 | ** Each line in the file must be at most 100 characters in length. This |
| 374 | ** seems like a reasonable maximum for a 40-character uuid, and 1-13 |
| 375 | ** character rid. |
| 376 | ** The function returns -1 if any of the lines in file 'f' are malformed, |
| 377 | ** or the rid/uuid information doesn't match what is in the repository |
| 378 | ** database. Otherwise, 0 is returned. |
| 379 | */ |
| 380 | int import_marks(FILE* f, Bag *blobs, Bag *vers, unsigned int *unused_mark){ |
| 381 | char line[101]; |
| 382 | while(fgets(line, sizeof(line), f)){ |
| 383 | struct mark_t mark; |
| @@ -507,11 +502,12 @@ | |
| 507 | verify_all_options(); |
| 508 | if( g.argc!=2 && g.argc!=3 ){ usage("--git ?REPOSITORY?"); } |
| 509 | |
| 510 | db_multi_exec("CREATE TEMPORARY TABLE oldblob(rid INTEGER PRIMARY KEY)"); |
| 511 | db_multi_exec("CREATE TEMPORARY TABLE oldcommit(rid INTEGER PRIMARY KEY)"); |
| 512 | db_multi_exec("CREATE TEMP TABLE xmark(tname TEXT UNIQUE, trid INT, tuuid TEXT)"); |
| 513 | db_multi_exec("CREATE INDEX xmark_trid ON xmark(trid)"); |
| 514 | if( markfile_in!=0 ){ |
| 515 | Stmt qb,qc; |
| 516 | FILE *f; |
| 517 | int rid; |
| @@ -754,11 +750,11 @@ | |
| 754 | ** ); |
| 755 | ** |
| 756 | ** This table contains all check-ins of the repository in topological |
| 757 | ** order. "Topological order" means that every parent check-in comes |
| 758 | ** before all of its children. Topological order is *almost* the same |
| 759 | ** thing as "ORDER BY event.mtime". Differences only arrise when there |
| 760 | ** are timewarps. In as much as Git hates timewarps, we have to compute |
| 761 | ** a correct topological order when doing an export. |
| 762 | ** |
| 763 | ** Since mtime is a usually already nearly in topological order, the |
| 764 | ** algorithm is to start with mtime, then make adjustments as necessary |
| @@ -835,5 +831,413 @@ | |
| 835 | int n; |
| 836 | db_find_and_open_repository(0, 0); |
| 837 | n = topological_sort_checkins(1); |
| 838 | fossil_print("%d reorderings required\n", n); |
| 839 | } |
| 840 |
| --- src/export.c | |
| +++ src/export.c | |
| @@ -28,23 +28,17 @@ | |
| 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. |
| @@ -297,18 +291,18 @@ | |
| 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"); |
| @@ -361,23 +355,24 @@ | |
| 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; |
| @@ -507,11 +502,12 @@ | |
| 502 | verify_all_options(); |
| 503 | if( g.argc!=2 && g.argc!=3 ){ usage("--git ?REPOSITORY?"); } |
| 504 | |
| 505 | db_multi_exec("CREATE TEMPORARY TABLE oldblob(rid INTEGER PRIMARY KEY)"); |
| 506 | db_multi_exec("CREATE TEMPORARY TABLE oldcommit(rid INTEGER PRIMARY KEY)"); |
| 507 | db_multi_exec("CREATE TEMP TABLE xmark(tname TEXT UNIQUE, trid INT," |
| 508 | " tuuid TEXT)"); |
| 509 | db_multi_exec("CREATE INDEX xmark_trid ON xmark(trid)"); |
| 510 | if( markfile_in!=0 ){ |
| 511 | Stmt qb,qc; |
| 512 | FILE *f; |
| 513 | int rid; |
| @@ -754,11 +750,11 @@ | |
| 750 | ** ); |
| 751 | ** |
| 752 | ** This table contains all check-ins of the repository in topological |
| 753 | ** order. "Topological order" means that every parent check-in comes |
| 754 | ** before all of its children. Topological order is *almost* the same |
| 755 | ** thing as "ORDER BY event.mtime". Differences only arise when there |
| 756 | ** are timewarps. In as much as Git hates timewarps, we have to compute |
| 757 | ** a correct topological order when doing an export. |
| 758 | ** |
| 759 | ** Since mtime is a usually already nearly in topological order, the |
| 760 | ** algorithm is to start with mtime, then make adjustments as necessary |
| @@ -835,5 +831,413 @@ | |
| 831 | int n; |
| 832 | db_find_and_open_repository(0, 0); |
| 833 | n = topological_sort_checkins(1); |
| 834 | fossil_print("%d reorderings required\n", n); |
| 835 | } |
| 836 | |
| 837 | /* |
| 838 | ** Transfer a tag over to the mirror. "rid" is the BLOB.RID value for |
| 839 | ** the record that describes the tag. |
| 840 | ** |
| 841 | ** The Git tag mechanism is very limited compared to Fossil. Many Fossil |
| 842 | ** tags cannot be exported to Git. If this tag cannot be exported, then |
| 843 | ** silently ignore it. |
| 844 | */ |
| 845 | static void mirror_send_tag(FILE *xCmd, int rid){ |
| 846 | return; |
| 847 | } |
| 848 | |
| 849 | /* |
| 850 | ** Locate the mark for a UUID. |
| 851 | ** |
| 852 | ** If the mark does not exist and if the bCreate flag is false, then |
| 853 | ** return 0. If the mark does not exist and the bCreate flag is true, |
| 854 | ** then create the mark. |
| 855 | */ |
| 856 | static int mirror_find_mark(const char *zUuid, int bCreate){ |
| 857 | int iMark; |
| 858 | static Stmt sFind, sIns; |
| 859 | db_static_prepare(&sFind, |
| 860 | "SELECT id FROM mirror.mmark WHERE uuid=:uuid" |
| 861 | ); |
| 862 | db_bind_text(&sFind, ":uuid", zUuid); |
| 863 | if( db_step(&sFind)==SQLITE_ROW ){ |
| 864 | iMark = db_column_int(&sFind, 0); |
| 865 | db_reset(&sFind); |
| 866 | return iMark; |
| 867 | } |
| 868 | db_reset(&sFind); |
| 869 | if( !bCreate ) return 0; |
| 870 | db_static_prepare(&sIns, |
| 871 | "INSERT INTO mirror.mmark(uuid) VALUES(:uuid)" |
| 872 | ); |
| 873 | db_bind_text(&sIns, ":uuid", zUuid); |
| 874 | db_step(&sIns); |
| 875 | db_reset(&sIns); |
| 876 | return db_last_insert_rowid(); |
| 877 | } |
| 878 | |
| 879 | /* |
| 880 | ** Export a single file named by zUuid. |
| 881 | */ |
| 882 | static void mirror_send_file(FILE *xCmd, const char *zUuid){ |
| 883 | int iMark; |
| 884 | int rid; |
| 885 | int rc; |
| 886 | Blob data; |
| 887 | rid = fast_uuid_to_rid(zUuid); |
| 888 | if( rid<0 ) fossil_fatal("no rid for %s", zUuid); |
| 889 | rc = content_get(rid, &data); |
| 890 | if( rc==0 ) fossil_fatal("%s is a phantom", zUuid); |
| 891 | iMark = mirror_find_mark(zUuid, 1); |
| 892 | fprintf(xCmd, "blob\nmark :%d\ndata %d\n", iMark, blob_size(&data)); |
| 893 | fwrite(blob_buffer(&data), 1, blob_size(&data), xCmd); |
| 894 | fprintf(xCmd, "\n"); |
| 895 | blob_reset(&data); |
| 896 | } |
| 897 | |
| 898 | /* |
| 899 | ** Transfer a check-in over to the mirror. "rid" is the BLOB.RID for |
| 900 | ** the check-in to export. |
| 901 | ** |
| 902 | ** If any ancestor of the check-in has not yet been exported, then |
| 903 | ** invoke this routine recursively to export the ancestor first. |
| 904 | ** This can only happen on a timewarp, so deep nesting is unlikely. |
| 905 | ** |
| 906 | ** Before sending the check-in, first make sure all associated files |
| 907 | ** have already been exported, and send "blob" records for any that |
| 908 | ** have not been. Update the MIRROR.MMARK table so that it holds the |
| 909 | ** marks for the exported files. |
| 910 | */ |
| 911 | static void mirror_send_checkin( |
| 912 | FILE *xCmd, /* Write fast-import text on this pipe */ |
| 913 | int rid, /* BLOB.RID for the check-in to export */ |
| 914 | const char *zUuid, /* BLOB.UUID for the check-in to export */ |
| 915 | int *pnLimit /* Stop when the counter reaches zero */ |
| 916 | ){ |
| 917 | Manifest *pMan; |
| 918 | int i; |
| 919 | Blob err; |
| 920 | Stmt q; |
| 921 | char *zBranch; |
| 922 | int iMark; |
| 923 | Blob sql; |
| 924 | |
| 925 | blob_init(&err, 0, 0); |
| 926 | pMan = manifest_get(rid, CFTYPE_MANIFEST, &err); |
| 927 | if( pMan==0 ){ |
| 928 | fossil_fatal("cannot fetch manifest for check-in %d: %s", rid, |
| 929 | blob_str(&err)); |
| 930 | } |
| 931 | |
| 932 | /* Check to see if any parent logins have not yet been processed, and |
| 933 | ** if so, create them */ |
| 934 | for(i=0; i<pMan->nParent; i++){ |
| 935 | int iMark = mirror_find_mark(pMan->azParent[i], 0); |
| 936 | if( iMark<=0 ){ |
| 937 | int prid = db_int(0, "SELECT rid FROM blob WHERE uuid=%Q", |
| 938 | pMan->azParent[i]); |
| 939 | mirror_send_checkin(xCmd, prid, pMan->azParent[i], pnLimit); |
| 940 | if( *pnLimit<=0 ){ |
| 941 | manifest_destroy(pMan); |
| 942 | return; |
| 943 | } |
| 944 | } |
| 945 | } |
| 946 | |
| 947 | /* Make sure all necessary files have been exported */ |
| 948 | db_prepare(&q, |
| 949 | "SELECT uuid FROM files_of_checkin(%Q)" |
| 950 | " WHERE uuid NOT IN (SELECT uuid FROM mirror.mmark)", |
| 951 | zUuid |
| 952 | ); |
| 953 | while( db_step(&q)==SQLITE_ROW ){ |
| 954 | const char *zFUuid = db_column_text(&q, 0); |
| 955 | mirror_send_file(xCmd, zFUuid); |
| 956 | } |
| 957 | db_finalize(&q); |
| 958 | |
| 959 | /* Figure out which branch this check-in is a member of */ |
| 960 | zBranch = db_text(0, |
| 961 | "SELECT value FROM tagxref WHERE tagid=%d AND tagtype>0 AND rid=%d", |
| 962 | TAG_BRANCH, rid |
| 963 | ); |
| 964 | if( fossil_strcmp(zBranch,"trunk")==0 ){ |
| 965 | fossil_free(zBranch); |
| 966 | zBranch = mprintf("master"); |
| 967 | } |
| 968 | |
| 969 | /* Export the check-in */ |
| 970 | fprintf(xCmd, "commit refs/head/%s\n", zBranch); |
| 971 | fossil_free(zBranch); |
| 972 | iMark = mirror_find_mark(zUuid, 1); |
| 973 | fprintf(xCmd, "mark :%d\n", iMark); |
| 974 | fprintf(xCmd, "committer %s <%[email protected]> %lld +0000\n", |
| 975 | pMan->zUser, pMan->zUser, |
| 976 | (sqlite3_int64)(pMan->rDate-2440587.5)*86400 |
| 977 | ); |
| 978 | fprintf(xCmd, "data %d\n", (int)strlen(pMan->zComment)); |
| 979 | fprintf(xCmd, "%s\n", pMan->zComment); |
| 980 | for(i=0; i<pMan->nParent; i++){ |
| 981 | int iOther = mirror_find_mark(pMan->azParent[i], 0); |
| 982 | if( i==0 ){ |
| 983 | fprintf(xCmd, "from :%d\n", iOther); |
| 984 | }else{ |
| 985 | fprintf(xCmd, "merge :%d\n", iOther); |
| 986 | } |
| 987 | } |
| 988 | if( pMan->nParent ){ |
| 989 | db_prepare(&q, |
| 990 | "SELECT filename FROM files_of_checkin(%Q)" |
| 991 | " EXCEPT SELECT filename FROM files_of_checkin(%Q)", |
| 992 | pMan->azParent[0], zUuid |
| 993 | ); |
| 994 | while( db_step(&q)==SQLITE_ROW ){ |
| 995 | fprintf(xCmd, "D %s\n", db_column_text(&q,0)); |
| 996 | } |
| 997 | db_finalize(&q); |
| 998 | } |
| 999 | blob_init(&sql, 0, 0); |
| 1000 | blob_append_sql(&sql, |
| 1001 | "SELECT filename, uuid, perm FROM files_of_checkin(%Q)", |
| 1002 | zUuid |
| 1003 | ); |
| 1004 | if( pMan->nParent ){ |
| 1005 | blob_append_sql(&sql, |
| 1006 | " EXCEPT SELECT filename, uuid, perm FROM files_of_checkin(%Q)", |
| 1007 | pMan->azParent[0]); |
| 1008 | } |
| 1009 | db_prepare(&q, |
| 1010 | "SELECT x.filename, x.perm, mmark.id FROM (%s) AS x, mirror.mmark" |
| 1011 | " WHERE mmark.uuid=x.uuid", |
| 1012 | blob_sql_text(&sql) |
| 1013 | ); |
| 1014 | blob_reset(&sql); |
| 1015 | while( db_step(&q)==SQLITE_ROW ){ |
| 1016 | const char *zFilename = db_column_text(&q,0); |
| 1017 | const char *zMode = db_column_text(&q,1); |
| 1018 | int iMark = db_column_int(&q,2); |
| 1019 | const char *zGitMode = "100644"; |
| 1020 | if( zMode ){ |
| 1021 | if( strchr(zMode,'x') ) zGitMode = "100755"; |
| 1022 | if( strchr(zMode,'l') ) zGitMode = "120000"; |
| 1023 | } |
| 1024 | fprintf(xCmd,"M %s :%d %s\n", zGitMode, iMark, zFilename); |
| 1025 | } |
| 1026 | db_finalize(&q); |
| 1027 | |
| 1028 | /* The check-in is finished, so decrement the counter */ |
| 1029 | (*pnLimit)--; |
| 1030 | } |
| 1031 | |
| 1032 | /* |
| 1033 | ** COMMAND: mirror |
| 1034 | ** |
| 1035 | ** Usage: %fossil mirror [--git] MIRROR [-R FOSSIL-REPO] |
| 1036 | ** |
| 1037 | ** Create or update another type of repository that is is mirror of |
| 1038 | ** a Fossil repository. |
| 1039 | ** |
| 1040 | ** The current implementation only supports mirrors to Git, and so |
| 1041 | ** the --git option is optional. The ability to mirror to other version |
| 1042 | ** control systems may be added in the future, in which case an argument |
| 1043 | ** to specify the target version control system will become required. |
| 1044 | ** |
| 1045 | ** The MIRROR argument is the name of the secondary repository. In the |
| 1046 | ** case of Git, it is the directory that houses the Git repository. |
| 1047 | ** If MIRROR does not previously exist, it is created and initialized to |
| 1048 | ** a copy of the Fossil repository. If MIRROR does already exist, it is |
| 1049 | ** updated with new check-ins that have been added to the Fossil repository |
| 1050 | ** since the last "fossil mirror" command to that particular repository. |
| 1051 | ** |
| 1052 | ** Implementation notes: |
| 1053 | ** |
| 1054 | ** * The git version control system must be installed in order for |
| 1055 | ** this command to work. Fossil will invoke various git commands |
| 1056 | ** to run as subprocesses. |
| 1057 | ** |
| 1058 | ** * Fossil creates a directory named ".mirror_state" in the top level of |
| 1059 | ** the created git repository and stores state information in that |
| 1060 | ** directory. Do not attempt to manage any files in that directory. |
| 1061 | ** Do not change or delete any files in that directory. Doing so |
| 1062 | ** may disrupt future calls to the "fossil mirror" command for the |
| 1063 | ** mirror repository. |
| 1064 | ** |
| 1065 | ** Options: |
| 1066 | ** |
| 1067 | ** --debug FILE Write fast-export text to FILE rather than |
| 1068 | ** piping it into "git fast-import". |
| 1069 | ** |
| 1070 | ** --limit N Add no more than N new check-ins to MIRROR. |
| 1071 | ** Useful for debugging |
| 1072 | */ |
| 1073 | void mirror_command(void){ |
| 1074 | const char *zLimit; |
| 1075 | int nLimit = 0x7fffffff; |
| 1076 | int nTotal = 0; |
| 1077 | char *zMirror; |
| 1078 | char *z; |
| 1079 | char *zInFile; |
| 1080 | char *zOutFile; |
| 1081 | char *zCmd; |
| 1082 | const char *zDebug = 0; |
| 1083 | double rEnd; |
| 1084 | int rc; |
| 1085 | FILE *xCmd; |
| 1086 | FILE *pIn, *pOut; |
| 1087 | Stmt q; |
| 1088 | char zLine[200]; |
| 1089 | |
| 1090 | find_option("git", 0, 0); /* Ignore the --git option for now */ |
| 1091 | zDebug = find_option("debug",0,1); |
| 1092 | db_find_and_open_repository(0, 2); |
| 1093 | zLimit = find_option("limit", 0, 1); |
| 1094 | if( zLimit ){ |
| 1095 | nLimit = (unsigned int)atoi(zLimit); |
| 1096 | if( nLimit<=0 ) fossil_fatal("--limit must be positive"); |
| 1097 | } |
| 1098 | verify_all_options(); |
| 1099 | if( g.argc!=3 ){ usage("--git MIRROR"); } |
| 1100 | zMirror = g.argv[2]; |
| 1101 | |
| 1102 | /* Make sure the GIT repository directory exists */ |
| 1103 | rc = file_mkdir(zMirror, ExtFILE, 0); |
| 1104 | if( rc ) fossil_fatal("cannot create directory \"%s\"", zMirror); |
| 1105 | |
| 1106 | /* Make sure GIT has been initialized */ |
| 1107 | z = mprintf("%s/.git", zMirror); |
| 1108 | if( !file_isdir(z, ExtFILE) ){ |
| 1109 | zCmd = mprintf("git init '%s'",zMirror); |
| 1110 | fossil_print("%s\n", zCmd); |
| 1111 | rc = fossil_system(zCmd); |
| 1112 | if( rc ){ |
| 1113 | fossil_fatal("command failed: \"%s\"", zCmd); |
| 1114 | } |
| 1115 | fossil_free(zCmd); |
| 1116 | } |
| 1117 | fossil_free(z); |
| 1118 | |
| 1119 | /* Make sure the .mirror_state subdirectory exists */ |
| 1120 | z = mprintf("%s/.mirror_state", zMirror); |
| 1121 | rc = file_mkdir(z, ExtFILE, 0); |
| 1122 | if( rc ) fossil_fatal("cannot create directory \"%s\"", z); |
| 1123 | fossil_free(z); |
| 1124 | |
| 1125 | /* Attach the .mirror_state/db database */ |
| 1126 | db_multi_exec("ATTACH '%q/.mirror_state/db' AS mirror;", zMirror); |
| 1127 | db_multi_exec( |
| 1128 | "CREATE TABLE IF NOT EXISTS mirror.mconfig(\n" |
| 1129 | " key TEXT PRIMARY KEY,\n" |
| 1130 | " Value ANY\n" |
| 1131 | ") WITHOUT ROWID;\n" |
| 1132 | "CREATE TABLE IF NOT EXISTS mirror.mmark(\n" |
| 1133 | " id INTEGER PRIMARY KEY,\n" |
| 1134 | " uuid TEXT UNIQUE\n" |
| 1135 | ");" |
| 1136 | "CREATE TABLE IF NOT EXISTS mirror.mtag(\n" |
| 1137 | " tagname TEXT PRIMARY KEY,\n" |
| 1138 | " cnt INTEGER DEFAULT 1\n" |
| 1139 | ") WITHOUT ROWID;" |
| 1140 | ); |
| 1141 | |
| 1142 | /* See if there is any work to be done. Exit early if not, before starting |
| 1143 | ** the "git fast-import" command. */ |
| 1144 | if( !db_exists("SELECT 1 FROM event WHERE type IN ('t','ci')" |
| 1145 | " AND mtime>coalesce((SELECT value FROM mconfig" |
| 1146 | " WHERE key='start'),0.0)") |
| 1147 | ){ |
| 1148 | fossil_print("no changes\n"); |
| 1149 | return; |
| 1150 | } |
| 1151 | |
| 1152 | /* Change to the MIRROR directory so that the Git commands will work */ |
| 1153 | rc = file_chdir(zMirror, 0); |
| 1154 | if( rc ) fossil_fatal("cannot change the working directory to \"%s\"", |
| 1155 | zMirror); |
| 1156 | |
| 1157 | /* Start up the git fast-import command */ |
| 1158 | if( zDebug ){ |
| 1159 | if( fossil_strcmp(zDebug,"stdout")==0 ){ |
| 1160 | xCmd = stdout; |
| 1161 | }else{ |
| 1162 | xCmd = fopen(zDebug, "wb"); |
| 1163 | if( xCmd==0 ) fossil_fatal("cannot open file \"%s\" for writing", zDebug); |
| 1164 | } |
| 1165 | }else{ |
| 1166 | zCmd = mprintf("git fast-import" |
| 1167 | " --import-marks-if-exists=.mirror_state/in" |
| 1168 | " --export-marks=.mirror_state/out" |
| 1169 | " --quiet --done"); |
| 1170 | fossil_print("%s\n", zCmd); |
| 1171 | xCmd = popen(zCmd, "w"); |
| 1172 | if( zCmd==0 ){ |
| 1173 | fossil_fatal("cannot start the \"git fast-import\" command"); |
| 1174 | } |
| 1175 | fossil_free(zCmd); |
| 1176 | } |
| 1177 | |
| 1178 | /* Run the export */ |
| 1179 | rEnd = 0.0; |
| 1180 | db_multi_exec( |
| 1181 | "CREATE TEMP TABLE tomirror(objid INTEGER PRIMARY KEY,type,mtime,uuid);\n" |
| 1182 | "INSERT INTO tomirror " |
| 1183 | "SELECT objid, type, mtime, blob.uuid FROM event, blob\n" |
| 1184 | " WHERE type IN ('ci','t')" |
| 1185 | " AND mtime>coalesce((SELECT value FROM mconfig WHERE key='start'),0.0)" |
| 1186 | " AND blob.rid=event.objid" |
| 1187 | " AND blob.uuid NOT IN (SELECT uuid FROM mirror.mmark);" |
| 1188 | ); |
| 1189 | nTotal = db_int(0, "SELECT count(*) FROM tomirror"); |
| 1190 | if( nLimit<nTotal ) nTotal = nLimit; |
| 1191 | db_prepare(&q, |
| 1192 | "SELECT objid, type, mtime, uuid FROM tomirror ORDER BY mtime" |
| 1193 | ); |
| 1194 | while( nLimit && db_step(&q)==SQLITE_ROW ){ |
| 1195 | double rMTime = db_column_double(&q, 2); |
| 1196 | const char *zType = db_column_text(&q, 1); |
| 1197 | int rid = db_column_int(&q, 0); |
| 1198 | const char *zUuid = db_column_text(&q, 3); |
| 1199 | if( rMTime>rEnd ) rEnd = rMTime; |
| 1200 | if( zType[0]=='t' ){ |
| 1201 | mirror_send_tag(xCmd, rid); |
| 1202 | }else{ |
| 1203 | mirror_send_checkin(xCmd, rid, zUuid, &nLimit); |
| 1204 | printf("\r%d/%d ", nTotal-nLimit, nTotal); |
| 1205 | fflush(stdout); |
| 1206 | } |
| 1207 | } |
| 1208 | db_finalize(&q); |
| 1209 | db_prepare(&q, "REPLACE INTO mirror.mconfig(key,value) VALUES('start',:x)"); |
| 1210 | db_bind_double(&q, ":x", rEnd); |
| 1211 | db_step(&q); |
| 1212 | db_finalize(&q); |
| 1213 | fprintf(xCmd, "done\n"); |
| 1214 | if( zDebug ){ |
| 1215 | if( xCmd!=stdout ) fclose(xCmd); |
| 1216 | }else{ |
| 1217 | pclose(xCmd); |
| 1218 | } |
| 1219 | fossil_print("%d check-ins added to the mirror\n", nTotal-nLimit); |
| 1220 | |
| 1221 | /* Read the export-marks file. Transfer the new marks over into |
| 1222 | ** the import-marks file. |
| 1223 | */ |
| 1224 | zInFile = mprintf("%s/.mirror_state/in", zMirror); |
| 1225 | zOutFile = mprintf("%s/.mirror_state/out", zMirror); |
| 1226 | pOut = fopen(zOutFile, "rb"); |
| 1227 | if( pOut ){ |
| 1228 | pIn = fopen(zInFile, "ab"); |
| 1229 | if( pIn==0 ){ |
| 1230 | fossil_fatal("cannot open %s for appending", zInFile); |
| 1231 | } |
| 1232 | while( fgets(zLine, sizeof(zLine), pIn) ){ |
| 1233 | fputs(zLine, pOut); |
| 1234 | } |
| 1235 | fclose(pOut); |
| 1236 | fclose(pIn); |
| 1237 | file_delete(zOutFile); |
| 1238 | } |
| 1239 | fossil_free(zInFile); |
| 1240 | fossil_free(zOutFile); |
| 1241 | |
| 1242 | /* Optionally do a "git push" */ |
| 1243 | } |
| 1244 |