/* ** Copyright (c) 2010 D. Richard Hipp ** ** This program is free software; you can redistribute it and/or ** modify it under the terms of the Simplified BSD License (also ** known as the "2-Clause License" or "FreeBSD License".) ** This program is distributed in the hope that it will be useful, ** but without any warranty; without even the implied warranty of ** merchantability or fitness for a particular purpose. ** ** Author contact information: ** drh@sqlite.org ** ******************************************************************************* ** ** This file contains code used to import the content of a Git/SVN ** repository in the git-fast-import/svn-dump formats as a new Fossil ** repository. */ #include "config.h" #include "import.h" #include #if INTERFACE /* ** A single file change record. */ struct ImportFile { char *zName; /* Name of a file */ char *zUuid; /* UUID of the file */ char *zPrior; /* Prior name if the name was changed */ char isFrom; /* True if obtained from the parent */ char isExe; /* True if executable */ char isLink; /* True if symlink */ }; #endif /* ** State information about an on-going fast-import parse. */ static struct { void (*xFinish)(void); /* Function to finish a prior record */ int nData; /* Bytes of data */ char *zTag; /* Name of a tag */ char *zBranch; /* Name of a branch for a commit */ char *zPrevBranch; /* The branch of the previous check-in */ char *aData; /* Data content */ char *zMark; /* The current mark */ char *zDate; /* Date/time stamp */ char *zUser; /* User name */ char *zComment; /* Comment of a commit */ char *zFrom; /* from value as a UUID */ char *zPrevCheckin; /* Name of the previous check-in */ char *zFromMark; /* The mark of the "from" field */ int nMerge; /* Number of merge values */ int nMergeAlloc; /* Number of slots in azMerge[] */ char **azMerge; /* Merge values */ int nFile; /* Number of aFile values */ int nFileAlloc; /* Number of slots in aFile[] */ ImportFile *aFile; /* Information about files in a commit */ int fromLoaded; /* True zFrom content loaded into aFile[] */ int hasLinks; /* True if git repository contains symlinks */ int tagCommit; /* True if the commit adds a tag */ } gg; /* ** Duplicate a string. */ char *fossil_strdup(const char *zOrig){ char *z = 0; if( zOrig ){ int n = strlen(zOrig); z = fossil_malloc( n+1 ); memcpy(z, zOrig, n+1); } return z; } /* ** A no-op "xFinish" method */ static void finish_noop(void){} /* ** Deallocate the state information. ** ** The azMerge[] and aFile[] arrays are zeroed by allocated space is ** retained unless the freeAll flag is set. */ static void import_reset(int freeAll){ int i; gg.xFinish = 0; fossil_free(gg.zTag); gg.zTag = 0; fossil_free(gg.zBranch); gg.zBranch = 0; fossil_free(gg.aData); gg.aData = 0; fossil_free(gg.zMark); gg.zMark = 0; fossil_free(gg.zDate); gg.zDate = 0; fossil_free(gg.zUser); gg.zUser = 0; fossil_free(gg.zComment); gg.zComment = 0; fossil_free(gg.zFrom); gg.zFrom = 0; fossil_free(gg.zFromMark); gg.zFromMark = 0; for(i=0; izName, pB->zName); } /* ** Compare two strings for sorting. */ static int string_cmp(const void *pLeft, const void *pRight){ const char *zLeft = *(const char **)pLeft; const char *zRight = *(const char **)pRight; return fossil_strcmp(zLeft, zRight); } /* Forward reference */ static void import_prior_files(void); /* ** Use data accumulated in gg from a "commit" record to add a new ** manifest artifact to the BLOB table. */ static void finish_commit(void){ int i; char *zFromBranch; char *aTCard[4]; /* Array of T cards for manifest */ int nTCard = 0; /* Entries used in aTCard[] */ Blob record, cksum; import_prior_files(); qsort(gg.aFile, gg.nFile, sizeof(gg.aFile[0]), mfile_cmp); blob_zero(&record); blob_appendf(&record, "C %F\n", gg.zComment); blob_appendf(&record, "D %s\n", gg.zDate); for(i=0; i=gg.nFileAlloc ){ gg.nFileAlloc = gg.nFileAlloc*2 + 100; gg.aFile = fossil_realloc(gg.aFile, gg.nFileAlloc*sizeof(gg.aFile[0])); } pFile = &gg.aFile[gg.nFile++]; memset(pFile, 0, sizeof(*pFile)); return pFile; } /* ** Load all file information out of the gg.zFrom check-in */ static void import_prior_files(void){ Manifest *p; int rid; ManifestFile *pOld; ImportFile *pNew; if( gg.fromLoaded ) return; gg.fromLoaded = 1; if( gg.zFrom==0 && gg.zPrevCheckin!=0 && fossil_strcmp(gg.zBranch, gg.zPrevBranch)==0 ){ gg.zFrom = gg.zPrevCheckin; gg.zPrevCheckin = 0; } if( gg.zFrom==0 ) return; rid = fast_uuid_to_rid(gg.zFrom); if( rid==0 ) return; p = manifest_get(rid, CFTYPE_MANIFEST, 0); if( p==0 ) return; manifest_file_rewind(p); while( (pOld = manifest_file_next(p, 0))!=0 ){ pNew = import_add_file(); pNew->zName = fossil_strdup(pOld->zName); pNew->isExe = pOld->zPerm && strstr(pOld->zPerm, "x")!=0; pNew->isLink = pOld->zPerm && strstr(pOld->zPerm, "l")!=0; pNew->zUuid = fossil_strdup(pOld->zUuid); pNew->isFrom = 1; } manifest_destroy(p); } /* ** Locate a file in the gg.aFile[] array by its name. Begin the search ** with the *pI-th file. Update *pI to be one past the file found. ** Do not search past the mx-th file. */ static ImportFile *import_find_file(const char *zName, int *pI, int mx){ int i = *pI; int nName = strlen(zName); while( i=0 && z[i]!='/'; i--){} gg.tagCommit = strncmp(&z[i-4], "tags", 4)==0; /* True for pattern B */ if( z[i+1]!=0 ) z += i+1; if( fossil_strcmp(z, "master")==0 ) z = "trunk"; gg.zBranch = fossil_strdup(z); gg.fromLoaded = 0; }else if( strncmp(zLine, "tag ", 4)==0 ){ gg.xFinish(); gg.xFinish = finish_tag; trim_newline(&zLine[4]); gg.zTag = fossil_strdup(&zLine[4]); }else if( strncmp(zLine, "reset ", 4)==0 ){ gg.xFinish(); }else if( strncmp(zLine, "checkpoint", 10)==0 ){ gg.xFinish(); }else if( strncmp(zLine, "feature", 7)==0 ){ gg.xFinish(); }else if( strncmp(zLine, "option", 6)==0 ){ gg.xFinish(); }else if( strncmp(zLine, "progress ", 9)==0 ){ gg.xFinish(); trim_newline(&zLine[9]); fossil_print("%s\n", &zLine[9]); fflush(stdout); }else if( strncmp(zLine, "data ", 5)==0 ){ fossil_free(gg.aData); gg.aData = 0; gg.nData = atoi(&zLine[5]); if( gg.nData ){ int got; gg.aData = fossil_malloc( gg.nData+1 ); got = fread(gg.aData, 1, gg.nData, pIn); if( got!=gg.nData ){ fossil_fatal("short read: got %d of %d bytes", got, gg.nData); } gg.aData[got] = 0; if( gg.zComment==0 && gg.xFinish==finish_commit ){ gg.zComment = gg.aData; gg.aData = 0; gg.nData = 0; } } }else if( strncmp(zLine, "author ", 7)==0 ){ /* No-op */ }else if( strncmp(zLine, "mark ", 5)==0 ){ trim_newline(&zLine[5]); fossil_free(gg.zMark); gg.zMark = fossil_strdup(&zLine[5]); }else if( strncmp(zLine, "tagger ", 7)==0 || strncmp(zLine, "committer ",10)==0 ){ sqlite3_int64 secSince1970; for(i=0; zLine[i] && zLine[i]!='<'; i++){} if( zLine[i]==0 ) goto malformed_line; z = &zLine[i+1]; for(i=i+1; zLine[i] && zLine[i]!='>'; i++){} if( zLine[i]==0 ) goto malformed_line; zLine[i] = 0; fossil_free(gg.zUser); gg.zUser = fossil_strdup(z); secSince1970 = 0; for(i=i+2; fossil_isdigit(zLine[i]); i++){ secSince1970 = secSince1970*10 + zLine[i] - '0'; } fossil_free(gg.zDate); gg.zDate = db_text(0, "SELECT datetime(%lld, 'unixepoch')", secSince1970); gg.zDate[10] = 'T'; }else if( strncmp(zLine, "from ", 5)==0 ){ trim_newline(&zLine[5]); fossil_free(gg.zFromMark); gg.zFromMark = fossil_strdup(&zLine[5]); fossil_free(gg.zFrom); gg.zFrom = resolve_committish(&zLine[5]); }else if( strncmp(zLine, "merge ", 6)==0 ){ trim_newline(&zLine[6]); if( gg.nMerge>=gg.nMergeAlloc ){ gg.nMergeAlloc = gg.nMergeAlloc*2 + 10; gg.azMerge = fossil_realloc(gg.azMerge, gg.nMergeAlloc*sizeof(char*)); } gg.azMerge[gg.nMerge] = resolve_committish(&zLine[6]); if( gg.azMerge[gg.nMerge] ) gg.nMerge++; }else if( strncmp(zLine, "M ", 2)==0 ){ import_prior_files(); z = &zLine[2]; zPerm = next_token(&z); zUuid = next_token(&z); zName = rest_of_line(&z); dequote_git_filename(zName); i = 0; pFile = import_find_file(zName, &i, gg.nFile); if( pFile==0 ){ pFile = import_add_file(); pFile->zName = fossil_strdup(zName); } pFile->isExe = (fossil_strcmp(zPerm, "100755")==0); pFile->isLink = (fossil_strcmp(zPerm, "120000")==0); fossil_free(pFile->zUuid); pFile->zUuid = resolve_committish(zUuid); pFile->isFrom = 0; }else if( strncmp(zLine, "D ", 2)==0 ){ import_prior_files(); z = &zLine[2]; zName = rest_of_line(&z); dequote_git_filename(zName); i = 0; while( (pFile = import_find_file(zName, &i, gg.nFile))!=0 ){ if( pFile->isFrom==0 ) continue; fossil_free(pFile->zName); fossil_free(pFile->zPrior); fossil_free(pFile->zUuid); *pFile = gg.aFile[--gg.nFile]; i--; } }else if( strncmp(zLine, "C ", 2)==0 ){ int nFrom; import_prior_files(); z = &zLine[2]; zFrom = next_token(&z); zTo = rest_of_line(&z); i = 0; mx = gg.nFile; nFrom = strlen(zFrom); while( (pFile = import_find_file(zFrom, &i, mx))!=0 ){ if( pFile->isFrom==0 ) continue; pNew = import_add_file(); pFile = &gg.aFile[i-1]; if( strlen(pFile->zName)>nFrom ){ pNew->zName = mprintf("%s%s", zTo, pFile->zName[nFrom]); }else{ pNew->zName = fossil_strdup(pFile->zName); } pNew->isExe = pFile->isExe; pNew->isLink = pFile->isLink; pNew->zUuid = fossil_strdup(pFile->zUuid); pNew->isFrom = 0; } }else if( strncmp(zLine, "R ", 2)==0 ){ int nFrom; import_prior_files(); z = &zLine[2]; zFrom = next_token(&z); zTo = rest_of_line(&z); i = 0; nFrom = strlen(zFrom); while( (pFile = import_find_file(zFrom, &i, gg.nFile))!=0 ){ if( pFile->isFrom==0 ) continue; pNew = import_add_file(); pFile = &gg.aFile[i-1]; if( strlen(pFile->zName)>nFrom ){ pNew->zName = mprintf("%s%s", zTo, pFile->zName[nFrom]); }else{ pNew->zName = fossil_strdup(pFile->zName); } pNew->zPrior = pFile->zName; pNew->isExe = pFile->isExe; pNew->isLink = pFile->isLink; pNew->zUuid = pFile->zUuid; pNew->isFrom = 0; gg.nFile--; *pFile = *pNew; memset(pNew, 0, sizeof(*pNew)); } fossil_fatal("cannot handle R records, use --full-tree"); }else if( strncmp(zLine, "deleteall", 9)==0 ){ gg.fromLoaded = 1; }else if( strncmp(zLine, "N ", 2)==0 ){ /* No-op */ }else { goto malformed_line; } } gg.xFinish(); if( gg.hasLinks ){ db_set_int("allow-symlinks", 1, 0); } import_reset(1); return; malformed_line: trim_newline(zLine); fossil_fatal("bad fast-import line: [%s]", zLine); return; } static struct{ char *zBranch; /* Name of a branch for a commit */ char *zDate; /* Date/time stamp */ char *zUser; /* User name */ char *zComment; /* Comment of a commit */ char *zFrom; /* from value as a UUID */ int nMerge; /* Number of merge values */ int nMergeAlloc; /* Number of slots in azMerge[] */ char **azMerge; /* Merge values */ int nFile; /* Number of aFile values */ int nFileAlloc; /* Number of slots in aFile[] */ ImportFile *aFile; /* Information about files in a commit */ int fromLoaded; /* True zFrom content loaded into aFile[] */ } gsvn; /* ** Create a new entry in the gsvn.aFile[] array */ static ImportFile *svn_import_add_file(){ ImportFile *pFile; if( gsvn.nFile>=gsvn.nFileAlloc ){ gsvn.nFileAlloc = gsvn.nFileAlloc*2 + 100; gsvn.aFile = fossil_realloc(gsvn.aFile, gsvn.nFileAlloc*sizeof(gsvn.aFile[0])); } pFile = &gsvn.aFile[gsvn.nFile++]; memset(pFile, 0, sizeof(*pFile)); return pFile; } /* ** Load all file information out of the gsvn.zFrom check-in */ static void svn_import_prior_files(void){ Manifest *p; int rid; ManifestFile *pOld; ImportFile *pNew; if( gsvn.fromLoaded ) return; gsvn.fromLoaded = 1; if( gsvn.zFrom==0 && gsvn.zPrevCheckin!=0 && fossil_strcmp(gsvn.zBranch, gsvn.zPrevBranch)==0 ){ gsvn.zFrom = gsvn.zPrevCheckin; gsvn.zPrevCheckin = 0; } if( gsvn.zFrom==0 ) return; rid = fast_uuid_to_rid(gsvn.zFrom); if( rid==0 ) return; p = manifest_get(rid, CFTYPE_MANIFEST, 0); if( p==0 ) return; manifest_file_rewind(p); while( (pOld = manifest_file_next(p, 0))!=0 ){ pNew = import_add_file(); pNew->zName = fossil_strdup(pOld->zName); pNew->isExe = pOld->zPerm && strstr(pOld->zPerm, "x")!=0; pNew->isLink = pOld->zPerm && strstr(pOld->zPerm, "l")!=0; pNew->zUuid = fossil_strdup(pOld->zUuid); pNew->isFrom = 1; } manifest_destroy(p); } /* ** Deallocate the state information. ** ** The azMerge[] and aFile[] arrays are zeroed but allocated space is ** retained unless the freeAll flag is set. */ static void svn_import_reset(int freeAll){ int i; // gsvn.xFinish = 0; // fossil_free(gsvn.zTag); gsvn.zTag = 0; fossil_free(gsvn.zBranch); gsvn.zBranch = 0; // fossil_free(gsvn.aData); gsvn.aData = 0; // fossil_free(gsvn.zMark); gsvn.zMark = 0; fossil_free(gsvn.zDate); gsvn.zDate = 0; fossil_free(gsvn.zUser); gsvn.zUser = 0; fossil_free(gsvn.zComment); gsvn.zComment = 0; fossil_free(gsvn.zFrom); gsvn.zFrom = 0; // fossil_free(gsvn.zFromMark); gsvn.zFromMark = 0; for(i=0; inHeaders; i++){ fossil_free(rec->aHeaders[i].zKey); } fossil_free(rec->aHeaders); fossil_free(rec->aProps); fossil_free(rec->pRawProps); blob_reset(&rec->content); } static int svn_read_headers(FILE *pIn, SvnRecord *rec){ char zLine[1000]; rec->aHeaders = 0; rec->nHeaders = 0; while( fgets(zLine, sizeof(zLine), pIn) ){ if( zLine[0]!='\n' ) break; } if( feof(pIn) ) return 0; do{ char *sep; if( zLine[0]=='\n' ) break; rec->nHeaders += 1; rec->aHeaders = fossil_realloc(rec->aHeaders, sizeof(rec->aHeaders[0])*rec->nHeaders); rec->aHeaders[rec->nHeaders-1].zKey = mprintf("%s", zLine); sep = strchr(rec->aHeaders[rec->nHeaders-1].zKey, ':'); if( !sep ){ trim_newline(zLine); fossil_fatal("bad header line: [%s]", zLine); } *sep = 0; rec->aHeaders[rec->nHeaders-1].zVal = sep+1; sep = strchr(rec->aHeaders[rec->nHeaders-1].zVal, '\n'); *sep = 0; while(rec->aHeaders[rec->nHeaders-1].zVal && fossil_isspace(*(rec->aHeaders[rec->nHeaders-1].zVal)) ) { rec->aHeaders[rec->nHeaders-1].zVal++; } }while( fgets(zLine, sizeof(zLine), pIn) ); if( zLine[0]!='\n' ){ trim_newline(zLine); fossil_fatal("svn-dump data ended unexpectedly"); } return 1; } static void svn_read_props(FILE *pIn, SvnRecord *rec){ int nRawProps = 0; char *pRawProps; const char *zLen; rec->pRawProps = 0; rec->aProps = 0; rec->nProps = 0; zLen = svn_find_header(*rec, "Prop-content-length"); if( zLen ){ nRawProps = atoi(zLen); } if( nRawProps ){ int got; char *zLine; rec->pRawProps = pRawProps = fossil_malloc( nRawProps ); got = fread(rec->pRawProps, 1, nRawProps, pIn); if( got!=nRawProps ){ fossil_fatal("short read: got %d of %d bytes", got, nRawProps); } if( memcmp(&pRawProps[got-10], "PROPS-END\n", 10)!=0 ){ fossil_fatal("svn-dump data ended unexpectedly"); } zLine = pRawProps; while( zLine<(pRawProps+nRawProps-10) ){ char *eol; int propLen; if( zLine[0]=='D' ){ propLen = atoi(&zLine[2]); eol = strchr(zLine, '\n'); zLine = eol+1+propLen+1; }else{ if( zLine[0]!='K' ){ fossil_fatal("svn-dump data format broken"); } propLen = atoi(&zLine[2]); eol = strchr(zLine, '\n'); zLine = eol+1; eol = zLine+propLen; if( *eol!='\n' ){ fossil_fatal("svn-dump data format broken"); } *eol = 0; rec->nProps += 1; rec->aProps = fossil_realloc(rec->aProps, sizeof(rec->aProps[0])*rec->nProps); rec->aProps[rec->nProps-1].zKey = zLine; zLine = eol+1; if( zLine[0]!='V' ){ fossil_fatal("svn-dump data format broken"); } propLen = atoi(&zLine[2]); eol = strchr(zLine, '\n'); zLine = eol+1; eol = zLine+propLen; if( *eol!='\n' ){ fossil_fatal("svn-dump data format broken"); } *eol = 0; rec->aProps[rec->nProps-1].zVal = zLine; zLine = eol+1; } } } } static int svn_read_rec(FILE *pIn, SvnRecord *rec){ const char *zLen; int nLen = 0; if( svn_read_headers(pIn, rec)==0 ) return 0; svn_read_props(pIn, rec); blob_zero(&rec->content); zLen = svn_find_header(*rec, "Text-content-length"); if( zLen ){ rec->contentFlag = 1; nLen = atoi(zLen); blob_read_from_channel(&rec->content, pIn, nLen); if( blob_size(&rec->content)!=nLen ){ fossil_fatal("short read: got %d of %d bytes", blob_size(&rec->content), nLen ); } }else{ rec->contentFlag = 0; } return 1; } static void svn_finish_revision( ){ Blob manifest; static Stmt insRev; static Stmt qParent; static Stmt qParent2; static Stmt qFiles; static Stmt qTags; int nBaseFilter; int nFilter; int rid; char *zParentBranch = 0; Blob mcksum; blob_zero(&manifest); nBaseFilter = blob_size(&gsvn.filter); if( !gsvn.flatFlag ){ if( gsvn.zBranch==0 ){ return; } if( strncmp(gsvn.zBranch, gsvn.zTrunk, gsvn.lenTrunk-1)==0 ){ blob_appendf(&gsvn.filter, "%s*", gsvn.zTrunk); }else{ blob_appendf(&gsvn.filter, "%s%s/*", gsvn.zBranches, gsvn.zBranch); } }else{ blob_append(&gsvn.filter, "*", 1); } if( db_int(0, "SELECT 1 FROM xhist WHERE trev=%d AND tpath GLOB %Q LIMIT 1", gsvn.rev, blob_str(&gsvn.filter))==0 ){ goto skip_revision; } db_static_prepare(&insRev, "REPLACE INTO xrevisions (trev, tbranch, tuuid) " "VALUES(:rev, :branch, " " (SELECT uuid FROM blob WHERE rid=:rid))"); db_static_prepare(&qParent, "SELECT tuuid, max(trev) FROM xrevisions " "WHERE trev<=:rev AND tbranch=:branch"); db_static_prepare(&qParent2, "SELECT tuuid, min(trev) " "FROM xrevisions WHERE tbranch=:branch"); db_static_prepare(&qFiles, "SELECT tpath, trid, tperm FROM xfiles " "WHERE tpath GLOB :filter ORDER BY tpath"); db_static_prepare(&qTags, "SELECT ttag FROM xtags WHERE trev=:rev"); if( !gsvn.flatFlag ){ if( gsvn.parentRev<0 ){ gsvn.parentRev = db_int(-1, "SELECT ifnull(max(trev),-1) FROM xrevisions " "WHERE tbranch=%Q", gsvn.zBranch); if( gsvn.parentRev<0 ){ gsvn.parentRev = db_int(-1, "SELECT ifnull(max(trev),-1) FROM xrevisions"); } gsvn.zParentBranch = gsvn.zBranch; }else if( gsvn.zParentBranch==0 ){ gsvn.zParentBranch = mprintf("trunk"); } /*db_bind_int(&insRev, ":rev", gsvn.rev); db_bind_text(&insRev, ":branch", gsvn.zBranch); db_bind_int(&insRev, ":rid", 0); db_step(&insRev); db_reset(&insRev);*/ }else{ static int prevRev = -1; gsvn.parentRev = prevRev; gsvn.zParentBranch = gsvn.zBranch = ""; prevRev = gsvn.rev; } if( gsvn.zComment ){ blob_appendf(&manifest, "C %F\n", gsvn.zComment); }else{ blob_append(&manifest, "C (no\\scomment)\n", 16); } if( gsvn.zDate ){ blob_appendf(&manifest, "D %s\n", gsvn.zDate); }else{ goto skip_revision; } nFilter = blob_size(&gsvn.filter)-1; db_bind_text(&qFiles, ":filter", blob_str(&gsvn.filter)); while( db_step(&qFiles)==SQLITE_ROW ){ const char *zFile = db_column_text(&qFiles, 0); int rid = db_column_int(&qFiles, 1); const char *zPerm = db_column_text(&qFiles, 2); char *zUuid; zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d", rid); blob_appendf(&manifest, "F %F %s %s\n", zFile+nFilter, zUuid, zPerm); fossil_free(zUuid); } db_reset(&qFiles); if( gsvn.parentRev>=0 ){ const char *zParentUuid; db_bind_int(&qParent, ":rev", gsvn.parentRev); db_bind_text(&qParent, ":branch", gsvn.zParentBranch); db_step(&qParent); zParentUuid = db_column_text(&qParent, 0); if( zParentUuid==0 ){ db_bind_text(&qParent2, ":branch", gsvn.zParentBranch); db_step(&qParent2); zParentUuid = db_column_text(&qParent2, 0); } blob_appendf(&manifest, "P %s\n", zParentUuid); if( !gsvn.flatFlag ){ if( strcmp(gsvn.zBranch, gsvn.zParentBranch)!=0 ){ blob_appendf(&manifest, "T *branch * %F\n", gsvn.zBranch); blob_appendf(&manifest, "T *sym-%F *\n", gsvn.zBranch); zParentBranch = mprintf("%F", gsvn.zParentBranch); }else{ zParentBranch = 0; } } db_reset(&qParent); db_reset(&qParent2); }else{ blob_appendf(&manifest, "T *branch * trunk\n"); blob_appendf(&manifest, "T *sym-trunk *\n"); } db_bind_int(&qTags, ":rev", gsvn.rev); while( db_step(&qTags)==SQLITE_ROW ){ const char *zTag = db_column_text(&qTags, 0); blob_appendf(&manifest, "T +sym-%s *\n", zTag); } db_reset(&qTags); blob_appendf(&manifest, "T +sym-svn-rev-%d *\n", gsvn.rev); if( zParentBranch ) { blob_appendf(&manifest, "T -sym-%s *\n", zParentBranch); fossil_free(zParentBranch); } if( gsvn.zUser ){ blob_appendf(&manifest, "U %F\n", gsvn.zUser); }else{ const char *zUserOvrd = find_option("user-override",0,1); blob_appendf(&manifest, "U %F\n", zUserOvrd ? zUserOvrd : login_name()); } md5sum_blob(&manifest, &mcksum); blob_appendf(&manifest, "Z %b\n", &mcksum); blob_reset(&mcksum); rid = content_put(&manifest); db_bind_int(&insRev, ":rev", gsvn.rev); db_bind_text(&insRev, ":branch", gsvn.zBranch); db_bind_int(&insRev, ":rid", rid); db_step(&insRev); db_reset(&insRev); skip_revision: blob_resize(&gsvn.filter, nBaseFilter); if( gsvn.zParentBranch == gsvn.zBranch ){ gsvn.zParentBranch = 0; } if( gsvn.flatFlag ){ gsvn.zBranch = 0; } blob_reset(&manifest); } static u64 svn_get_varint(const char **pz){ unsigned int v = 0; do{ v = (v<<7) | ((*pz)[0]&0x7f); }while( (*pz)++[0]&0x80 ); return v; } static void svn_apply_svndiff(Blob *pDiff, Blob *pSrc, Blob *pOut){ const char *zDiff = blob_buffer(pDiff); char *zOut; if( blob_size(pDiff)<4 || memcmp(zDiff, "SVN", 4)!=0 ){ fossil_fatal("Invalid svndiff0 format"); } zDiff += 4; blob_zero(pOut); while( zDiff<(blob_buffer(pDiff)+blob_size(pDiff)) ){ u64 offSrc = svn_get_varint(&zDiff); /*u64 lenSrc =*/ svn_get_varint(&zDiff); u64 lenOut = svn_get_varint(&zDiff); u64 lenInst = svn_get_varint(&zDiff); u64 lenData = svn_get_varint(&zDiff); const char *zInst = zDiff; const char *zData = zInst+lenInst; u64 lenOld = blob_size(pOut); blob_resize(pOut, lenOut+lenOld); zOut = blob_buffer(pOut)+lenOld; while( zDiff 0 ){ *zOut++ = *zCpy++; } } zDiff += lenData; } } /* ** Extract the name of the branch or tag that the given path is on. ** Returns: 1 - It is on the trunk ** 2 - It is on a branch ** 3 - It is a tag ** 0 - It is none of the above */ static int *svn_parse_path(char *zPath, char **zBranch, char **zFile){ if( strncmp(zPath, gsvn.zTrunk, gsvn.lenTrunk)==0 ){ *zBranch = "trunk"; *zFile = zPath+gsvn.lenTrunk; return 1; }else if( strncmp(zPath, gsvn.zBranches, gsvn.lenBranches)==0 ){ *zFile = *zBranch = zPath+gsvn.lenBranches; while( **zFile && **zFile!='/' ){ (*zFile)++; } if( *zFile ){ **zFile = '\0'; (*zFile)++; }else{ *zFile = 0; } return 2; }else if( strncmp(zPath, gsvn.zTags, gsvn.lenTags)==0 ){ *zFile = *zBranch = zPath+gsvn.lenTags; while( **zFile && **zFile!='/' ){ (*zFile)++; } if( *zFile ){ **zFile = '\0'; (*zFile)++; }else{ *zFile = 0; } return 3; } *zFile = *zBranch = 0; return 0; } /* ** Read the svn-dump format from pIn and insert the corresponding ** content into the database. */ static void svn_dump_import(FILE *pIn){ SvnRecord rec; int ver; char *zTemp; const char *zUuid; Stmt addFile; Stmt delPath; Stmt addSrc; Stmt addBranch; Stmt cpyPath; /* version */ if( svn_read_rec(pIn, &rec) && (zTemp = svn_find_header(rec, "SVN-fs-dump-format-version")) ){ ver = atoi(zTemp); if( ver!=2 && ver!=3 ){ fossil_fatal("Unknown svn-dump format version: %d", ver); } }else{ fossil_fatal("Input is not an svn-dump!"); } svn_free_rec(&rec); /* UUID */ if( !svn_read_rec(pIn, &rec) || !(zUuid = svn_find_header(rec, "UUID")) ){ fossil_fatal("Missing UUID!"); } svn_free_rec(&rec); /* content */ db_prepare(&addFile, "INSERT INTO xfiles (tpath, tbranch, tuuid, tperm)" " VALUES(:path, :branch, :uuid, :perm)" ); db_prepare(&delPath, "DELETE FROM xfiles" " WHERE (tpath=:path OR (tpath>:path||'/' AND tpath<:path||'0'))" " AND tbranch=:branch" ); db_prepare(&addSrc, "INSERT INTO xsrc (tpath, tbranch, tsrc, tsrcbranch, tsrcrev)" " VALUES(:path, :branch, :srcpath, :srcbranch, :srcrev)" ); db_prepare(&addBranch, "INSERT INTO xchanges (tbranch, ttype) VALUES(:branch, :type)" ); db_prepare(&cpyPath, "INSERT INTO xfiles (tpath, tbranch, tuuid, tperm)" " SELECT :path||substr(filename, length(:srcpath)+1), :branch, uuid, perm" " FROM xfoci" " WHERE chekinID=:rid AND filename>:srcpath||'/' AND filename<:srcpath||'0'" ); gsvn.rev = -1; while( svn_read_rec(pIn, &rec) ){ if( (zTemp = svn_find_header(rec, "Revision-number")) ){ /* revision node */ /* finish previous revision */ const char *zDate = NULL; if( gsvn.rev>=0 ){ svn_finish_revision(); fossil_free(gsvn.zUser); fossil_free(gsvn.zComment); fossil_free(gsvn.zDate); } /* start new revision */ gsvn.rev = atoi(zTemp); gsvn.zUser = mprintf("%s", svn_find_prop(rec, "svn:author")); gsvn.zComment = mprintf("%s", svn_find_prop(rec, "svn:log")); zDate = svn_find_prop(rec, "svn:date"); if( zDate ){ zDate = date_in_standard_format(zDate); } gsvn.zDate = zDate; fossil_print("\rImporting SVN revision: %d", gsvn.rev); }else if( (zTemp = svn_find_header(rec, "Node-path")) ){ /* file/dir node */ const char *zAction = svn_find_header(rec, "Node-action"); const char *zKind = svn_find_header(rec, "Node-kind"); const char *zSrcPath = svn_find_header(rec, "Node-copyfrom-path"); const char *zPerm = svn_find_prop(rec, "svn:executable") ? "x" : 0; char *zBranch; char *zFile; char *zSrcBranch; char *zSrcFile; int deltaFlag = 0; int srcRev = 0; int branchType = svn_parse_path(zTemp, &zBranch, &zFile); if( branchType==0 ){ svn_free_rec(&rec); continue; } db_bind_text(&addBranch, ":branch", zBranch); db_bind_int(&addBranch, ":type", branchType); db_step(&addBranch); db_reset(&addBranch); if( (zTemp = svn_find_header(rec, "Text-delta")) ){ deltaFlag = strncmp(zTemp, "true", 4)==0; } if( zSrcPath ){ zTemp = svn_find_header(rec, "Node-copyfrom-rev"); if( zTemp ){ srcRev = atoi(zTemp); }else{ fossil_fatal("Missing copyfrom-rev"); } if( svn_extract_branch(zSrcPath, &zSrcBranch, &zSrcFile)==0 ){ fossil_fatal("Copy from path outside the import paths"); } db_bind_text(&addSrc, ":path", zFile); db_bind_text(&addSrc, ":branch", zBranch); db_bind_text(&addSrc, ":srcpath", zSrcFile); db_bind_text(&addSrc, ":srcbranch", zSrcBranch); db_bind_int(&addSrc, ":srcrev", srcRev); db_step(&addSrc); db_reset(&addSrc); } if( strncmp(zAction, "delete", 6)==0 || strncmp(zAction, "replace", 7)==0 ) { db_bind_text(&delPath, ":path", zFile); db_bind_text(&delPath, ":branch", zBranch); db_step(&delPath); db_reset(&delPath); } /* no 'else' here since 'replace' does both a 'delete' and an 'add' */ if( strncmp(zAction, "add", 3)==0 || strncmp(zAction, "replace", 7)==0 ) { if( zKind==0 ){ fossil_fatal("Missing Node-kind"); }else if( strncmp(zKind, "dir", 3)==0 ){ if( zSrcPath ){ int srcRid = db_int(0, "SELECT trid, max(trev) FROM xrevisions" " WHERE trev<=%d AND tbranch=%Q", srcRev, zSrcBranch); db_bind_text(&cpyPath, ":path", zFile); db_bind_text(&cpyPath, ":branch", zBranch); db_bind_text(&cpyPath, ":srcpath", zSrcFile); db_bind_int(&cpyPath, ":rid", srcRid); db_step(&cpyPath); db_reset(&cpyPath); } }else{ int rid = 0; if( zSrcPath ){ int srcRid = db_int(0, "SELECT trid, max(trev) FROM xrevisions" " WHERE trev<=%d AND tbranch=%Q", srcRev, zSrcBranch); rid = db_int(0, "SELECT rid FROM xfoci" " WHERE chekinID=%d AND filename=%Q", srcRid, zSrcFile) } if( deltaFlag ){ Blob deltaSrc; Blob target; if( rid!=0 ){ content_get(rid, &deltaSrc); }else{ blob_zero(&deltaSrc); } svn_apply_svndiff(&rec.content, &deltaSrc, &target); rid = content_put(&target); }else if( rec.contentFlag ){ rid = content_put(&rec.content); } db_bind_int(&addHist, ":rid", rid); db_bind_text(&addHist, ":path", zPath); db_bind_text(&addHist, ":perm", zPerm); db_step(&addHist); db_reset(&addHist); bHasFiles = 1; } }else if( strncmp(zAction, "change", 6)==0 ){ int rid = 0; if( zKind==0 ){ fossil_fatal("Missing Node-kind"); } if( strncmp(zKind, "dir", 3)!=0 ){ if( deltaFlag ){ Blob deltaSrc; Blob target; rid = db_int(0, "SELECT trid, max(trev) FROM xhist" " WHERE trev<=%d AND tpath=%Q", gsvn.rev, zPath); content_get(rid, &deltaSrc); svn_apply_svndiff(&rec.content, &deltaSrc, &target); rid = content_put(&target); }else{ rid = content_put(&rec.content); } db_bind_int(&addHist, ":rid", rid); db_bind_text(&addHist, ":path", zPath); db_bind_text(&addHist, ":perm", zPerm); db_step(&addHist); db_reset(&addHist); bHasFiles = 1; } }else if( strncmp(zAction, "delete", 6)!=0 ){ /* already did this one above */ fossil_fatal("Unknown Node-action"); } }else{ fossil_fatal("Unknown record type"); } svn_free_rec(&rec); } if( bHasFiles ){ svn_create_manifest(); } fossil_free(gsvn.zUser); fossil_free(gsvn.zComment); fossil_free(gsvn.zDate); db_finalize(&addHist); db_finalize(&insTag); db_finalize(&cpyPath); db_finalize(&delPath); fossil_print(" Done!\n"); } /* ** COMMAND: import ** ** Usage: %fossil import FORMAT ?OPTIONS? NEW-REPOSITORY ?INPUT-FILE? ** ** Read interchange format generated by another VCS and use it to ** construct a new Fossil repository named by the NEW-REPOSITORY ** argument. If no input file is supplied the interchange format ** data is read from standard input. ** ** The following formats are currently understood by this command ** ** git Import from the git-fast-export file format ** ** svn Import from the svnadmin-dump file format. The default ** behaviour (unless overridden by --flat) is to treat 3 folders ** in the SVN root as special, following the common layout of ** SVN repositories. These are (by default) trunk/, branches/ ** and tags/ ** Options: ** --trunk FOLDER Name of trunk folder ** --branches FOLDER Name of branches folder ** --tags FOLDER Name of tags folder ** --base PATH Path to project root in repository ** --flat The whole dump is a single branch ** ** The --incremental option allows an existing repository to be extended ** with new content. ** ** Options: ** --incremental allow importing into an existing repository ** ** See also: export */ void import_cmd(void){ char *zPassword; FILE *pIn; Stmt q; const char *zBase = find_option("base", 0, 1); int lenFilter; int forceFlag = find_option("force", "f", 0)!=0; int incrFlag = find_option("incremental", "i", 0)!=0; gsvn.zTrunk = find_option("trunk", 0, 1); gsvn.zBranches = find_option("branches", 0, 1); gsvn.zTags = find_option("tags", 0, 1); int flatFlag = find_option("flat", 0, 0)!=0; verify_all_options(); if( g.argc!=4 && g.argc!=5 ){ usage("FORMAT REPOSITORY-NAME"); } if( g.argc==5 ){ pIn = fossil_fopen(g.argv[4], "rb"); }else{ pIn = stdin; fossil_binary_mode(pIn); } if( !incrFlag ){ if( forceFlag ) file_delete(g.argv[3]); db_create_repository(g.argv[3]); } db_open_repository(g.argv[3]); db_open_config(0); db_begin_transaction(); if( !incrFlag ) db_initial_setup(0, 0, 0, 1); if( strncmp(g.argv[2], "git", 3)==0 ){ /* The following temp-tables are used to hold information needed for ** the import. ** ** The XMARK table provides a mapping from fast-import "marks" and symbols ** into artifact ids (UUIDs - the 40-byte hex SHA1 hash of artifacts). ** Given any valid fast-import symbol, the corresponding fossil rid and ** uuid can found by searching against the xmark.tname field. ** ** The XBRANCH table maps commit marks and symbols into the branch those ** commits belong to. If xbranch.tname is a fast-import symbol for a ** checkin then xbranch.brnm is the branch that checkin is part of. ** ** The XTAG table records information about tags that need to be applied ** to various branches after the import finishes. The xtag.tcontent field ** contains the text of an artifact that will add a tag to a check-in. ** The git-fast-export file format might specify the same tag multiple ** times but only the last tag should be used. And we do not know which ** occurrence of the tag is the last until the import finishes. */ db_multi_exec( "CREATE TEMP TABLE xmark(tname TEXT UNIQUE, trid INT, tuuid TEXT);" "CREATE TEMP TABLE xbranch(tname TEXT UNIQUE, brnm TEXT);" "CREATE TEMP TABLE xtag(tname TEXT UNIQUE, tcontent TEXT);" ); git_fast_import(pIn); db_prepare(&q, "SELECT tcontent FROM xtag"); while( db_step(&q)==SQLITE_ROW ){ Blob record; db_ephemeral_blob(&q, 0, &record); fast_insert_content(&record, 0, 0); import_reset(0); } db_finalize(&q); }else if( strncmp(g.argv[2], "svn", 3)==0 ){ db_multi_exec( "CREATE TEMP TABLE xrevisions(" " trev INTEGER, tbranch TEXT, trid INT, PRIMARY KEY(tbranch, trev)" ");" "CREATE TEMP TABLE xfiles(" " tpath TEXT, tbranch TEXT, tuuid TEXT, tperm TEXT," " UNIQUE (tbranch, tpath) ON CONFLICT REPLACE" ");" "CREATE TEMP TABLE xsrc(" " tpath TEXT, tbranch TEXT, tsrc TEXT, tsrcbranch TEXT, tsrcrev INT" ");" "CREATE TEMP TABLE xchanged(" " tbranch TEXT, ttype INT," " UNIQUE (tbranch) ON CONFLICT REPLACE" ");" "CREATE VIRTUAL TABLE temp.xfoci USING files_of_checkin;" ); if( zBase==0 ){ zBase = ""; } if( strlen(zBase)>0 ){ if( zBase[strlen(zBase)-1]!='/' ){ zBase = mprintf("%s/", zBase); } if( flatFlag ){ gsvn.zTrunk = zBase; gsvn.zBranches = 0; gsvn.zTags = 0; gsvn.lenTrunk = strlen(zBase); gsvn.lenBranches = 0; gsvn.lenTags = 0; }else{ if( gsvn.zTrunk==0 ){ gsvn.zTrunk = "trunk/"; } if( gsvn.zBranches==0 ){ gsvn.zBranches = "branches/"; } if( gsvn.zTags==0 ){ gsvn.zTags = "tags/"; } gsvn.zTrunk = mprintf("%s%s", zBase, gsvn.zTrunk); gsvn.zBranches = mprintf("%s%s", zBase, gsvn.zBranches); gsvn.zTags = mprintf("%s%s", zBase, gsvn.zTags); gsvn.lenTrunk = strlen(gsvn.zTrunk); gsvn.lenBranches = strlen(gsvn.zBranches); gsvn.lenTags = strlen(gsvn.zTags); if( gsvn.zTrunk[gsvn.lenTrunk-1]!='/' ){ gsvn.zTrunk = mprintf("%s/", gsvn.zTrunk); gsvn.lenTrunk++; } if( gsvn.zBranches[gsvn.lenBranches-1]!='/' ){ gsvn.zBranches = mprintf("%s/", gsvn.zBranches); gsvn.lenBranches++; } if( gsvn.zTags[gsvn.lenTags-1]!='/' ){ gsvn.zTags = mprintf("%s/", gsvn.zTags); gsvn.lenTags++; } } svn_dump_import(pIn); } verify_cancel(); db_end_transaction(0); db_begin_transaction(); fossil_print("Rebuilding repository meta-data...\n"); rebuild_db(0, 1, !incrFlag); verify_cancel(); db_end_transaction(0); fossil_print("Vacuuming..."); fflush(stdout); db_multi_exec("VACUUM"); fossil_print(" ok\n"); if( !incrFlag ){ fossil_print("project-id: %s\n", db_get("project-code", 0)); fossil_print("server-id: %s\n", db_get("server-code", 0)); zPassword = db_text(0, "SELECT pw FROM user WHERE login=%Q", g.zLogin); fossil_print("admin-user: %s (password is \"%s\")\n", g.zLogin, zPassword); } }