summaryrefslogtreecommitdiff
path: root/src/vfile.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/vfile.c')
-rw-r--r--src/vfile.c1114
1 files changed, 1114 insertions, 0 deletions
diff --git a/src/vfile.c b/src/vfile.c
new file mode 100644
index 0000000..be8a501
--- /dev/null
+++ b/src/vfile.c
@@ -0,0 +1,1114 @@
+/*
+** Copyright (c) 2007 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@hwaci.com
+** http://www.hwaci.com/drh/
+**
+*******************************************************************************
+**
+** Procedures for managing the VFILE table.
+*/
+#include "config.h"
+#include "vfile.h"
+#include <assert.h>
+#include <sys/types.h>
+
+/*
+** The input is guaranteed to be a 40- or 64-character well-formed
+** artifact hash. Find its rid.
+*/
+int fast_uuid_to_rid(const char *zUuid){
+ static Stmt q;
+ int rid;
+ db_static_prepare(&q, "SELECT rid FROM blob WHERE uuid=:uuid");
+ db_bind_text(&q, ":uuid", zUuid);
+ if( db_step(&q)==SQLITE_ROW ){
+ rid = db_column_int(&q, 0);
+ }else{
+ rid = 0;
+ }
+ db_reset(&q);
+ return rid;
+}
+
+/*
+** Given a UUID, return the corresponding record ID. If the UUID
+** does not exist, then return 0.
+**
+** For this routine, the UUID must be exact. For a match against
+** user input with mixed case, use resolve_uuid().
+**
+** If the UUID is not found and phantomize is 1 or 2, then attempt to
+** create a phantom record. A private phantom is created for 2 and
+** a public phantom is created for 1.
+*/
+int uuid_to_rid(const char *zUuid, int phantomize){
+ int rid, sz;
+ char z[HNAME_MAX+1];
+
+ sz = strlen(zUuid);
+ if( !hname_validate(zUuid, sz) ){
+ return 0; /* Not a valid hash */
+ }
+ memcpy(z, zUuid, sz+1);
+ canonical16(z, sz);
+ rid = fast_uuid_to_rid(z);
+ if( rid==0 && phantomize ){
+ rid = content_new(zUuid, phantomize-1);
+ }
+ return rid;
+}
+
+
+/*
+** Load a vfile from a record ID. Return the number of files with
+** missing content.
+*/
+int load_vfile_from_rid(int vid){
+ int rid, size, nMissing;
+ Stmt ins, ridq;
+ Manifest *p;
+ ManifestFile *pFile;
+
+ if( db_exists("SELECT 1 FROM vfile WHERE vid=%d", vid) ){
+ return 0;
+ }
+
+ db_begin_transaction();
+ p = manifest_get(vid, CFTYPE_MANIFEST, 0);
+ if( p==0 ) {
+ db_end_transaction(1);
+ return 0;
+ }
+ db_prepare(&ins,
+ "INSERT INTO vfile(vid,isexe,islink,rid,mrid,pathname,mhash) "
+ " VALUES(:vid,:isexe,:islink,:id,:id,:name,NULL)");
+ db_prepare(&ridq, "SELECT rid,size FROM blob WHERE uuid=:uuid");
+ db_bind_int(&ins, ":vid", vid);
+ manifest_file_rewind(p);
+ nMissing = 0;
+ while( (pFile = manifest_file_next(p,0))!=0 ){
+ if( pFile->zUuid==0 || uuid_is_shunned(pFile->zUuid) ) continue;
+ db_bind_text(&ridq, ":uuid", pFile->zUuid);
+ if( db_step(&ridq)==SQLITE_ROW ){
+ rid = db_column_int(&ridq, 0);
+ size = db_column_int(&ridq, 1);
+ }else{
+ rid = 0;
+ size = 0;
+ }
+ db_reset(&ridq);
+ if( rid==0 || size<0 ){
+ fossil_warning("content missing for %s", pFile->zName);
+ nMissing++;
+ continue;
+ }
+ db_bind_int(&ins, ":isexe", ( manifest_file_mperm(pFile)==PERM_EXE ));
+ db_bind_int(&ins, ":id", rid);
+ db_bind_text(&ins, ":name", pFile->zName);
+ db_bind_int(&ins, ":islink", ( manifest_file_mperm(pFile)==PERM_LNK ));
+ db_step(&ins);
+ db_reset(&ins);
+ }
+ db_finalize(&ridq);
+ db_finalize(&ins);
+ manifest_destroy(p);
+ db_end_transaction(0);
+ return nMissing;
+}
+
+#if INTERFACE
+/*
+** The cksigFlags parameter to vfile_check_signature() is an OR-ed
+** combination of the following bits:
+*/
+#define CKSIG_ENOTFILE 0x001 /* non-file FS objects throw an error */
+#define CKSIG_HASH 0x002 /* Verify file content using hashing */
+#define CKSIG_SETMTIME 0x004 /* Set mtime to last check-out time */
+
+#endif /* INTERFACE */
+
+/*
+** Look at every VFILE entry with the given vid and update VFILE.CHNGED field
+** according to whether or not the file has changed.
+** - 0 means no change.
+** - 1 means edited.
+** - 2 means changed due to a merge.
+** - 3 means added by a merge.
+** - 4 means changed due to an integrate merge.
+** - 5 means added by an integrate merge.
+** - 6 means became executable but has unmodified contents.
+** - 7 means became a symlink whose target equals its old contents.
+** - 8 means lost executable status but has unmodified contents.
+** - 9 means lost symlink status and has contents equal to its old target.
+**
+** If VFILE.DELETED is true or if VFILE.RID is zero, then the file was either
+** removed from configuration management via "fossil rm" or added via
+** "fossil add", respectively, and in both cases we always know that
+** the file has changed without having the check the size, mtime,
+** or on-disk content.
+**
+** If the size of the file has changed, then we always know that the file
+** changed without having to look at the mtime or on-disk content.
+**
+** The mtime of the file is only a factor if the mtime-changes setting
+** is true (or undefined - it defaults to true) and the CKSIG_HASH
+** flag is false. If the mtime-changes setting is false or if
+** CKSIG_HASH is true, then we do not trust the mtime and will examine
+** the on-disk content to determine if a file really is the same.
+**
+** If the mtime is used, it is used only to determine if files are the same.
+** If the mtime of a file has changed, we still examine the on-disk content
+** to see whether or not the edit was a null-edit.
+*/
+void vfile_check_signature(int vid, unsigned int cksigFlags){
+ int nErr = 0;
+ Stmt q;
+ int useMtime = (cksigFlags & CKSIG_HASH)==0
+ && db_get_boolean("mtime-changes", 1);
+
+ db_begin_transaction();
+ db_prepare(&q, "SELECT id, %Q || pathname,"
+ " vfile.mrid, deleted, chnged, uuid, size, mtime,"
+ " CASE WHEN isexe THEN %d WHEN islink THEN %d ELSE %d END"
+ " FROM vfile LEFT JOIN blob ON vfile.mrid=blob.rid"
+ " WHERE vid=%d ", g.zLocalRoot, PERM_EXE, PERM_LNK, PERM_REG,
+ vid);
+ while( db_step(&q)==SQLITE_ROW ){
+ int id, rid, isDeleted;
+ const char *zName;
+ int chnged = 0;
+ int oldChnged;
+#ifndef _WIN32
+ int origPerm;
+ int currentPerm;
+#endif
+ i64 oldMtime;
+ i64 currentMtime;
+ i64 origSize;
+ i64 currentSize;
+
+ id = db_column_int(&q, 0);
+ zName = db_column_text(&q, 1);
+ rid = db_column_int(&q, 2);
+ isDeleted = db_column_int(&q, 3);
+ oldChnged = chnged = db_column_int(&q, 4);
+ oldMtime = db_column_int64(&q, 7);
+ origSize = db_column_int64(&q, 6);
+ currentSize = file_size(zName, RepoFILE);
+ currentMtime = file_mtime(0, 0);
+#ifndef _WIN32
+ origPerm = db_column_int(&q, 8);
+ currentPerm = file_perm(zName, RepoFILE);
+#endif
+ if( chnged==0 && (isDeleted || rid==0) ){
+ /* "fossil rm" or "fossil add" always change the file */
+ chnged = 1;
+ }else if( !file_isfile_or_link(0) && currentSize>=0 ){
+ if( cksigFlags & CKSIG_ENOTFILE ){
+ fossil_warning("not an ordinary file: %s", zName);
+ nErr++;
+ }
+ chnged = 1;
+ }
+ if( origSize!=currentSize ){
+ if( chnged!=1 ){
+ /* A file size change is definitive - the file has changed. No
+ ** need to check the mtime or hash */
+ chnged = 1;
+ }
+ }else if( chnged==1 && rid!=0 && !isDeleted ){
+ /* File is believed to have changed but it is the same size.
+ ** Double check that it really has changed by looking at content. */
+ const char *zUuid = db_column_text(&q, 5);
+ int nUuid = db_column_bytes(&q, 5);
+ assert( origSize==currentSize );
+ if( hname_verify_file_hash(zName, zUuid, nUuid) ) chnged = 0;
+ }else if( (chnged==0 || chnged==2 || chnged==4)
+ && (useMtime==0 || currentMtime!=oldMtime) ){
+ /* For files that were formerly believed to be unchanged or that were
+ ** changed by merging, if their mtime changes, or unconditionally
+ ** if --hash is used, check to see if they have been edited by
+ ** looking at their artifact hashes */
+ const char *zUuid = db_column_text(&q, 5);
+ int nUuid = db_column_bytes(&q, 5);
+ assert( origSize==currentSize );
+ if( !hname_verify_file_hash(zName, zUuid, nUuid) ) chnged = 1;
+ }
+ if( (cksigFlags & CKSIG_SETMTIME) && (chnged==0 || chnged==2 || chnged==4)){
+ i64 desiredMtime;
+ if( mtime_of_manifest_file(vid,rid,&desiredMtime)==0 ){
+ if( currentMtime!=desiredMtime ){
+ file_set_mtime(zName, desiredMtime);
+ currentMtime = file_mtime(zName, RepoFILE);
+ }
+ }
+ }
+#ifndef _WIN32
+ if( origPerm!=PERM_LNK && currentPerm==PERM_LNK ){
+ /* Changing to a symlink takes priority over all other change types. */
+ chnged = 7;
+ }else if( origPerm==PERM_LNK && currentPerm!=PERM_LNK ){
+ /* Ditto, other direction */
+ chnged = 9;
+ }else if( chnged==0 || chnged==6 || chnged==7 || chnged==8 || chnged==9 ){
+ /* Confirm metadata change types. */
+ if( origPerm==currentPerm ){
+ chnged = 0;
+ }else if( currentPerm==PERM_EXE ){
+ chnged = 6;
+ }else if( origPerm==PERM_EXE ){
+ chnged = 8;
+ }else if( origPerm==PERM_LNK ){
+ chnged = 9;
+ }
+ }
+#endif
+ if( currentMtime!=oldMtime || chnged!=oldChnged ){
+ db_multi_exec("UPDATE vfile SET mtime=%lld, chnged=%d WHERE id=%d",
+ currentMtime, chnged, id);
+ }
+ }
+ db_finalize(&q);
+ if( nErr ) fossil_fatal("abort due to prior errors");
+ db_end_transaction(0);
+}
+
+/*
+** Write all files from vid to the disk. Or if vid==0 and id!=0
+** write just the specific file where VFILE.ID=id.
+*/
+void vfile_to_disk(
+ int vid, /* vid to write to disk */
+ int id, /* Write this one file, if not zero */
+ int verbose, /* Output progress information */
+ int promptFlag /* Prompt user to confirm overwrites */
+){
+ Stmt q;
+ Blob content;
+ int nRepos = strlen(g.zLocalRoot);
+
+ if( vid>0 && id==0 ){
+ db_prepare(&q, "SELECT id, %Q || pathname, mrid, isexe, islink"
+ " FROM vfile"
+ " WHERE vid=%d AND mrid>0",
+ g.zLocalRoot, vid);
+ }else{
+ assert( vid==0 && id>0 );
+ db_prepare(&q, "SELECT id, %Q || pathname, mrid, isexe, islink"
+ " FROM vfile"
+ " WHERE id=%d AND mrid>0",
+ g.zLocalRoot, id);
+ }
+ while( db_step(&q)==SQLITE_ROW ){
+ int id, rid, isExe, isLink;
+ const char *zName;
+
+ id = db_column_int(&q, 0);
+ zName = db_column_text(&q, 1);
+ rid = db_column_int(&q, 2);
+ isExe = db_column_int(&q, 3);
+ isLink = db_column_int(&q, 4);
+ if( file_unsafe_in_tree_path(zName) ){
+ continue;
+ }
+ content_get(rid, &content);
+ if( file_is_the_same(&content, zName) ){
+ blob_reset(&content);
+ if( file_setexe(zName, isExe) ){
+ db_multi_exec("UPDATE vfile SET mtime=%lld WHERE id=%d",
+ file_mtime(zName, RepoFILE), id);
+ }
+ continue;
+ }
+ if( promptFlag && file_size(zName, RepoFILE)>=0 ){
+ Blob ans;
+ char *zMsg;
+ char cReply;
+ zMsg = mprintf("overwrite %s (a=always/y/N)? ", zName);
+ prompt_user(zMsg, &ans);
+ free(zMsg);
+ cReply = blob_str(&ans)[0];
+ blob_reset(&ans);
+ if( cReply=='a' || cReply=='A' ){
+ promptFlag = 0;
+ } else if( cReply!='y' && cReply!='Y' ){
+ blob_reset(&content);
+ continue;
+ }
+ }
+ if( verbose ) fossil_print("%s\n", &zName[nRepos]);
+ if( file_isdir(zName, RepoFILE)==1 ){
+ /*TODO(dchest): remove directories? */
+ fossil_fatal("%s is directory, cannot overwrite", zName);
+ }
+ if( file_size(zName, RepoFILE)>=0 && (isLink || file_islink(0)) ){
+ file_delete(zName);
+ }
+ if( isLink ){
+ symlink_create(blob_str(&content), zName);
+ }else{
+ blob_write_to_file(&content, zName);
+ }
+ file_setexe(zName, isExe);
+ blob_reset(&content);
+ db_multi_exec("UPDATE vfile SET mtime=%lld WHERE id=%d",
+ file_mtime(zName, RepoFILE), id);
+ }
+ db_finalize(&q);
+}
+
+/*
+** Check to see if the directory named in zPath is the top of a check-out.
+** In other words, check to see if directory pPath contains a file named
+** "_FOSSIL_" or ".fslckout". Return true or false.
+*/
+int vfile_top_of_checkout(const char *zPath){
+ char *zFile;
+ int fileFound = 0;
+
+ zFile = mprintf("%s/_FOSSIL_", zPath);
+ fileFound = file_size(zFile, ExtFILE)>=1024;
+ fossil_free(zFile);
+ if( !fileFound ){
+ zFile = mprintf("%s/.fslckout", zPath);
+ fileFound = file_size(zFile, ExtFILE)>=1024;
+ fossil_free(zFile);
+ }
+
+ /* Check for ".fos" for legacy support. But the use of ".fos" as the
+ ** per-check-out database name is deprecated. At some point, all support
+ ** for ".fos" will end and this code should be removed. This comment
+ ** added on 2012-02-04.
+ */
+ if( !fileFound ){
+ zFile = mprintf("%s/.fos", zPath);
+ fileFound = file_size(zFile, ExtFILE)>=1024;
+ fossil_free(zFile);
+ }
+ return fileFound;
+}
+
+/*
+** Return TRUE if zFile is a temporary file. Return FALSE if not.
+*/
+static int is_temporary_file(const char *zName){
+ static const char *const azTemp[] = {
+ "baseline",
+ "merge",
+ "original",
+ "output",
+ };
+ int i, j, n;
+
+ if( sqlite3_strglob("ci-comment-????????????.txt", zName)==0 ) return 1;
+ for(; zName[0]!=0; zName++){
+ if( zName[0]=='/'
+ && sqlite3_strglob("/ci-comment-????????????.txt", zName)==0 ){
+ return 1;
+ }
+ if( zName[0]!='-' ) continue;
+ for(i=0; i<count(azTemp); i++){
+ n = (int)strlen(azTemp[i]);
+ if( memcmp(azTemp[i], zName+1, n) ) continue;
+ if( zName[n+1]==0 ) return 1;
+ if( zName[n+1]=='-' ){
+ for(j=n+2; zName[j] && fossil_isdigit(zName[j]); j++){}
+ if( zName[j]==0 ) return 1;
+ }
+ }
+ }
+ return 0;
+}
+
+#if INTERFACE
+/*
+** Values for the scanFlags parameter to vfile_scan().
+*/
+#define SCAN_ALL 0x001 /* Includes files that begin with "." */
+#define SCAN_TEMP 0x002 /* Only Fossil-generated files like *-baseline */
+#define SCAN_NESTED 0x004 /* Scan for empty dirs in nested checkouts */
+#define SCAN_MTIME 0x008 /* Populate mtime column */
+#define SCAN_SIZE 0x010 /* Populate size column */
+#define SCAN_ISEXE 0x020 /* Populate isexe column */
+#endif /* INTERFACE */
+
+/*
+** Load into table SFILE the name of every ordinary file in
+** the directory pPath. Omit the first nPrefix characters of
+** of pPath when inserting into the SFILE table.
+** Subdirectories are scanned recursively.
+**
+** Omit files named in VFILE if eFType==RepoFILE. Include all files
+** if eFType==ExtFILE.
+**
+** Files whose names begin with "." are omitted unless the SCAN_ALL
+** flag is set.
+**
+** Any files or directories that match the glob patterns pIgnore*
+** are excluded from the scan. Name matching occurs after the
+** first nPrefix characters are elided from the filename.
+*/
+void vfile_scan(
+ Blob *pPath, /* Directory to be scanned */
+ int nPrefix, /* Number of bytes in directory name */
+ unsigned scanFlags, /* Zero or more SCAN_xxx flags */
+ Glob *pIgnore1, /* Do not add files that match this GLOB */
+ Glob *pIgnore2, /* Omit files matching this GLOB too */
+ int eFType /* ExtFILE or RepoFILE */
+){
+ DIR *d;
+ int origSize;
+ struct dirent *pEntry;
+ int skipAll = 0;
+ static Stmt ins;
+ static int depth = 0;
+ void *zNative;
+
+ origSize = blob_size(pPath);
+ if( pIgnore1 || pIgnore2 ){
+ blob_appendf(pPath, "/");
+ if( glob_match(pIgnore1, &blob_str(pPath)[nPrefix+1]) ) skipAll = 1;
+ if( glob_match(pIgnore2, &blob_str(pPath)[nPrefix+1]) ) skipAll = 1;
+ blob_resize(pPath, origSize);
+ }
+ if( skipAll ) return;
+
+ if( depth==0 ){
+ if( eFType==ExtFILE ){
+ db_prepare(&ins,
+ "INSERT OR IGNORE INTO sfile(pathname%s%s%s) VALUES(:file%s%s%s)",
+ scanFlags & SCAN_MTIME ? ",mtime" : "",
+ scanFlags & SCAN_SIZE ? ",size" : "",
+ scanFlags & SCAN_ISEXE ? ",isexe" : "",
+ scanFlags & SCAN_MTIME ? ",:mtime" : "",
+ scanFlags & SCAN_SIZE ? ",:size" : "",
+ scanFlags & SCAN_ISEXE ? ",:isexe" : ""
+ );
+ }else{
+ db_prepare(&ins,
+ "INSERT OR IGNORE INTO sfile(pathname%s%s%s) SELECT :file%s%s%s"
+ " WHERE NOT EXISTS(SELECT 1 FROM vfile WHERE"
+ " pathname=:file %s)",
+ scanFlags & SCAN_MTIME ? ",mtime" : "",
+ scanFlags & SCAN_SIZE ? ",size" : "",
+ scanFlags & SCAN_ISEXE ? ",isexe" : "",
+ scanFlags & SCAN_MTIME ? ",:mtime" : "",
+ scanFlags & SCAN_SIZE ? ",:size" : "",
+ scanFlags & SCAN_ISEXE ? ",:isexe" : "",
+ filename_collation()
+ );
+ }
+ }
+ depth++;
+
+ zNative = fossil_utf8_to_path(blob_str(pPath), 1);
+ d = opendir(zNative);
+ if( d ){
+ while( (pEntry=readdir(d))!=0 ){
+ char *zPath;
+ char *zUtf8;
+ if( pEntry->d_name[0]=='.' ){
+ if( (scanFlags & SCAN_ALL)==0 ) continue;
+ if( pEntry->d_name[1]==0 ) continue;
+ if( pEntry->d_name[1]=='.' && pEntry->d_name[2]==0 ) continue;
+ }
+ zUtf8 = fossil_path_to_utf8(pEntry->d_name);
+ blob_appendf(pPath, "/%s", zUtf8);
+ zPath = blob_str(pPath);
+ if( glob_match(pIgnore1, &zPath[nPrefix+1]) ||
+ glob_match(pIgnore2, &zPath[nPrefix+1]) ){
+ /* do nothing */
+#ifdef _DIRENT_HAVE_D_TYPE
+ }else if( (pEntry->d_type==DT_UNKNOWN || pEntry->d_type==DT_LNK)
+ ? (file_isdir(zPath, eFType)==1) : (pEntry->d_type==DT_DIR) ){
+#else
+ }else if( file_isdir(zPath, eFType)==1 ){
+#endif
+ if( !vfile_top_of_checkout(zPath) ){
+ vfile_scan(pPath, nPrefix, scanFlags, pIgnore1, pIgnore2, eFType);
+ }
+#ifdef _DIRENT_HAVE_D_TYPE
+ }else if( (pEntry->d_type==DT_UNKNOWN || pEntry->d_type==DT_LNK)
+ ? (file_isfile_or_link(zPath)) : (pEntry->d_type==DT_REG) ){
+#else
+ }else if( file_isfile_or_link(zPath) ){
+#endif
+ if( (scanFlags & SCAN_TEMP)==0 || is_temporary_file(zUtf8) ){
+ db_bind_text(&ins, ":file", &zPath[nPrefix+1]);
+ if( scanFlags & SCAN_MTIME ){
+ db_bind_int(&ins, ":mtime", file_mtime(zPath, eFType));
+ }
+ if( scanFlags & SCAN_SIZE ){
+ db_bind_int(&ins, ":size", file_size(zPath, eFType));
+ }
+ if( scanFlags & SCAN_ISEXE ){
+ db_bind_int(&ins, ":isexe", file_isexe(zPath, eFType));
+ }
+ db_step(&ins);
+ db_reset(&ins);
+ }
+ }
+ fossil_path_free(zUtf8);
+ blob_resize(pPath, origSize);
+ }
+ closedir(d);
+ }
+ fossil_path_free(zNative);
+
+ depth--;
+ if( depth==0 ){
+ db_finalize(&ins);
+ }
+}
+
+/*
+** Scans the specified base directory for any directories within it, while
+** keeping a count of how many files they each contain, either directly or
+** indirectly.
+**
+** Subdirectories are scanned recursively.
+** Omit files named in VFILE.
+**
+** Directories whose names begin with "." are omitted unless the SCAN_ALL
+** flag is set.
+**
+** Any directories that match the glob patterns pIgnore* are excluded from
+** the scan. Name matching occurs after the first nPrefix characters are
+** elided from the filename.
+**
+** Returns the total number of files found.
+*/
+int vfile_dir_scan(
+ Blob *pPath, /* Base directory to be scanned */
+ int nPrefix, /* Number of bytes in base directory name */
+ unsigned scanFlags, /* Zero or more SCAN_xxx flags */
+ Glob *pIgnore1, /* Do not add directories that match this GLOB */
+ Glob *pIgnore2, /* Omit directories matching this GLOB too */
+ int eFType /* ExtFILE or RepoFILE */
+){
+ int result = 0;
+ DIR *d;
+ int origSize;
+ struct dirent *pEntry;
+ int skipAll = 0;
+ static Stmt ins;
+ static Stmt upd;
+ static int depth = 0;
+ void *zNative;
+
+ origSize = blob_size(pPath);
+ if( pIgnore1 || pIgnore2 ){
+ blob_appendf(pPath, "/");
+ if( glob_match(pIgnore1, &blob_str(pPath)[nPrefix+1]) ) skipAll = 1;
+ if( glob_match(pIgnore2, &blob_str(pPath)[nPrefix+1]) ) skipAll = 1;
+ blob_resize(pPath, origSize);
+ }
+ if( skipAll ) return result;
+
+ if( depth==0 ){
+ db_multi_exec("DROP TABLE IF EXISTS dscan_temp;"
+ "CREATE TEMP TABLE dscan_temp("
+ " x TEXT PRIMARY KEY %s, y INTEGER)",
+ filename_collation());
+ db_prepare(&ins,
+ "INSERT OR IGNORE INTO dscan_temp(x, y) SELECT :file, :count"
+ " WHERE NOT EXISTS(SELECT 1 FROM vfile WHERE"
+ " pathname GLOB :file || '/*' %s)", filename_collation()
+ );
+ db_prepare(&upd,
+ "UPDATE OR IGNORE dscan_temp SET y = coalesce(y, 0) + 1"
+ " WHERE x=:file %s",
+ filename_collation()
+ );
+ }
+ depth++;
+
+ zNative = fossil_utf8_to_path(blob_str(pPath), 1);
+ d = opendir(zNative);
+ if( d ){
+ while( (pEntry=readdir(d))!=0 ){
+ char *zOrigPath;
+ char *zPath;
+ char *zUtf8;
+ if( pEntry->d_name[0]=='.' ){
+ if( (scanFlags & SCAN_ALL)==0 ) continue;
+ if( pEntry->d_name[1]==0 ) continue;
+ if( pEntry->d_name[1]=='.' && pEntry->d_name[2]==0 ) continue;
+ }
+ zOrigPath = fossil_strdup(blob_str(pPath));
+ zUtf8 = fossil_path_to_utf8(pEntry->d_name);
+ blob_appendf(pPath, "/%s", zUtf8);
+ zPath = blob_str(pPath);
+ if( glob_match(pIgnore1, &zPath[nPrefix+1]) ||
+ glob_match(pIgnore2, &zPath[nPrefix+1]) ){
+ /* do nothing */
+#ifdef _DIRENT_HAVE_D_TYPE
+ }else if( (pEntry->d_type==DT_UNKNOWN || pEntry->d_type==DT_LNK)
+ ? (file_isdir(zPath, eFType)==1) : (pEntry->d_type==DT_DIR) ){
+#else
+ }else if( file_isdir(zPath, eFType)==1 ){
+#endif
+ if( (scanFlags & SCAN_NESTED) || !vfile_top_of_checkout(zPath) ){
+ char *zSavePath = fossil_strdup(zPath);
+ int count = vfile_dir_scan(pPath, nPrefix, scanFlags, pIgnore1,
+ pIgnore2, eFType);
+ db_bind_text(&ins, ":file", &zSavePath[nPrefix+1]);
+ db_bind_int(&ins, ":count", count);
+ db_step(&ins);
+ db_reset(&ins);
+ fossil_free(zSavePath);
+ result += count; /* found X normal files? */
+ }
+#ifdef _DIRENT_HAVE_D_TYPE
+ }else if( (pEntry->d_type==DT_UNKNOWN || pEntry->d_type==DT_LNK)
+ ? (file_isfile_or_link(zPath)) : (pEntry->d_type==DT_REG) ){
+#else
+ }else if( file_isfile_or_link(zPath) ){
+#endif
+ db_bind_text(&upd, ":file", zOrigPath);
+ db_step(&upd);
+ db_reset(&upd);
+ result++; /* found 1 normal file */
+ }
+ fossil_path_free(zUtf8);
+ blob_resize(pPath, origSize);
+ fossil_free(zOrigPath);
+ }
+ closedir(d);
+ }
+ fossil_path_free(zNative);
+
+ depth--;
+ if( depth==0 ){
+ db_finalize(&upd);
+ db_finalize(&ins);
+ }
+ return result;
+}
+
+/*
+** Compute an aggregate MD5 checksum over the disk image of every
+** file in vid. The file names are part of the checksum. The resulting
+** checksum is the same as is expected on the R-card of a manifest.
+**
+** This function operates differently if the Global.aCommitFile
+** variable is not NULL. In that case, the disk image is used for
+** each file in aCommitFile[] and the repository image
+** is used for all others).
+**
+** Newly added files that are not contained in the repository are
+** omitted from the checksum if they are not in Global.aCommitFile[].
+**
+** Newly deleted files are included in the checksum if they are not
+** part of Global.aCommitFile[]
+**
+** Renamed files use their new name if they are in Global.aCommitFile[]
+** and their original name if they are not in Global.aCommitFile[]
+**
+** Return the resulting checksum in blob pOut.
+*/
+void vfile_aggregate_checksum_disk(int vid, Blob *pOut){
+ FILE *in;
+ Stmt q;
+ char zBuf[4096];
+
+ db_must_be_within_tree();
+ db_prepare(&q,
+ "SELECT %Q || pathname, pathname, origname, is_selected(id), rid"
+ " FROM vfile"
+ " WHERE (NOT deleted OR NOT is_selected(id)) AND vid=%d"
+ " ORDER BY if_selected(id, pathname, origname) /*scan*/",
+ g.zLocalRoot, vid
+ );
+ md5sum_init();
+ while( db_step(&q)==SQLITE_ROW ){
+ const char *zFullpath = db_column_text(&q, 0);
+ const char *zName = db_column_text(&q, 1);
+ int isSelected = db_column_int(&q, 3);
+
+ if( isSelected ){
+ md5sum_step_text(zName, -1);
+ if( file_islink(zFullpath) ){
+ /* Instead of file content, use link destination path */
+ Blob pathBuf;
+
+ sqlite3_snprintf(sizeof(zBuf), zBuf, " %ld\n",
+ blob_read_link(&pathBuf, zFullpath));
+ md5sum_step_text(zBuf, -1);
+ md5sum_step_text(blob_str(&pathBuf), -1);
+ blob_reset(&pathBuf);
+ }else{
+ in = fossil_fopen(zFullpath,"rb");
+ if( in==0 ){
+ md5sum_step_text(" 0\n", -1);
+ continue;
+ }
+ fseek(in, 0L, SEEK_END);
+ sqlite3_snprintf(sizeof(zBuf), zBuf, " %ld\n", ftell(in));
+ fseek(in, 0L, SEEK_SET);
+ md5sum_step_text(zBuf, -1);
+ /*printf("%s %s %s",md5sum_current_state(),zName,zBuf);fflush(stdout);*/
+ for(;;){
+ int n;
+ n = fread(zBuf, 1, sizeof(zBuf), in);
+ if( n<=0 ) break;
+ md5sum_step_text(zBuf, n);
+ }
+ fclose(in);
+ }
+ }else{
+ int rid = db_column_int(&q, 4);
+ const char *zOrigName = db_column_text(&q, 2);
+ char zBuf[100];
+ Blob file;
+
+ if( zOrigName ) zName = zOrigName;
+ if( rid>0 ){
+ md5sum_step_text(zName, -1);
+ blob_zero(&file);
+ content_get(rid, &file);
+ sqlite3_snprintf(sizeof(zBuf), zBuf, " %d\n", blob_size(&file));
+ md5sum_step_text(zBuf, -1);
+ md5sum_step_blob(&file);
+ blob_reset(&file);
+ }
+ }
+ }
+ db_finalize(&q);
+ md5sum_finish(pOut);
+}
+
+/*
+** Write a BLOB into a random filename. Return the name of the file.
+*/
+char *write_blob_to_temp_file(Blob *pBlob){
+ sqlite3_uint64 r;
+ char *zOut = 0;
+ do{
+ sqlite3_free(zOut);
+ sqlite3_randomness(8, &r);
+ zOut = sqlite3_mprintf("file-%08llx", r);
+ }while( file_size(zOut, ExtFILE)>=0 );
+ blob_write_to_file(pBlob, zOut);
+ return zOut;
+}
+
+/*
+** Do a file-by-file comparison of the content of the repository and
+** the working check-out on disk. Report any errors.
+*/
+void vfile_compare_repository_to_disk(int vid){
+ sqlite3_int64 rc;
+ Stmt q;
+ Blob disk, repo;
+ char *zOut;
+
+ db_must_be_within_tree();
+ db_prepare(&q,
+ "SELECT %Q || pathname, pathname, rid FROM vfile"
+ " WHERE NOT deleted AND vid=%d AND is_selected(id)"
+ " ORDER BY if_selected(id, pathname, origname) /*scan*/",
+ g.zLocalRoot, vid
+ );
+ md5sum_init();
+ while( db_step(&q)==SQLITE_ROW ){
+ const char *zFullpath = db_column_text(&q, 0);
+ const char *zName = db_column_text(&q, 1);
+ int rid = db_column_int(&q, 2);
+
+ blob_zero(&disk);
+ rc = blob_read_from_file(&disk, zFullpath, RepoFILE);
+ if( rc<0 ){
+ fossil_print("ERROR: cannot read file [%s]\n", zFullpath);
+ blob_reset(&disk);
+ continue;
+ }
+ blob_zero(&repo);
+ content_get(rid, &repo);
+ if( blob_size(&repo)!=blob_size(&disk) ){
+ fossil_print("ERROR: [%s] is %d bytes on disk but %d in the repository\n",
+ zName, blob_size(&disk), blob_size(&repo));
+ zOut = write_blob_to_temp_file(&repo);
+ fossil_print("NOTICE: Repository version of [%s] stored in [%s]\n",
+ zName, zOut);
+ sqlite3_free(zOut);
+ blob_reset(&disk);
+ blob_reset(&repo);
+ continue;
+ }
+ if( blob_compare(&repo, &disk) ){
+ fossil_print(
+ "ERROR: [%s] is different on disk compared to the repository\n",
+ zName);
+ zOut = write_blob_to_temp_file(&repo);
+ fossil_print("NOTICE: Repository version of [%s] stored in [%s]\n",
+ zName, zOut);
+ sqlite3_free(zOut);
+ }
+ blob_reset(&disk);
+ blob_reset(&repo);
+ }
+ db_finalize(&q);
+}
+
+/*
+** Compute an aggregate MD5 checksum over the repository image of every
+** file in vid. The file names are part of the checksum. The resulting
+** checksum is suitable for the R-card of a manifest.
+**
+** Return the resulting checksum in blob pOut.
+*/
+void vfile_aggregate_checksum_repository(int vid, Blob *pOut){
+ Blob file;
+ Stmt q;
+ char zBuf[100];
+
+ db_must_be_within_tree();
+
+ db_prepare(&q, "SELECT pathname, origname, rid, is_selected(id)"
+ " FROM vfile"
+ " WHERE (NOT deleted OR NOT is_selected(id))"
+ " AND rid>0 AND vid=%d"
+ " ORDER BY if_selected(id,pathname,origname) /*scan*/",
+ vid);
+ blob_zero(&file);
+ md5sum_init();
+ while( db_step(&q)==SQLITE_ROW ){
+ const char *zName = db_column_text(&q, 0);
+ const char *zOrigName = db_column_text(&q, 1);
+ int rid = db_column_int(&q, 2);
+ int isSelected = db_column_int(&q, 3);
+ if( zOrigName && !isSelected ) zName = zOrigName;
+ md5sum_step_text(zName, -1);
+ content_get(rid, &file);
+ sqlite3_snprintf(sizeof(zBuf), zBuf, " %d\n", blob_size(&file));
+ md5sum_step_text(zBuf, -1);
+ /*printf("%s %s %s",md5sum_current_state(),zName,zBuf); fflush(stdout);*/
+ md5sum_step_blob(&file);
+ blob_reset(&file);
+ }
+ db_finalize(&q);
+ md5sum_finish(pOut);
+}
+
+/*
+** Compute an aggregate MD5 checksum over the repository image of every
+** file in manifest vid. The file names are part of the checksum. The
+** resulting checksum is suitable for use as the R-card of a manifest.
+**
+** Return the resulting checksum in blob pOut.
+**
+** If pManOut is not NULL then fill it with the checksum found in the
+** "R" card near the end of the manifest.
+**
+** In a well-formed manifest, the two checksums computed here, pOut and
+** pManOut, should be identical.
+*/
+void vfile_aggregate_checksum_manifest(int vid, Blob *pOut, Blob *pManOut){
+ int fid;
+ Blob file;
+ Blob err;
+ Manifest *pManifest;
+ ManifestFile *pFile;
+ char zBuf[100];
+
+ blob_zero(pOut);
+ blob_zero(&err);
+ if( pManOut ){
+ blob_zero(pManOut);
+ }
+ db_must_be_within_tree();
+ pManifest = manifest_get(vid, CFTYPE_MANIFEST, &err);
+ if( pManifest==0 ){
+ fossil_fatal("manifest file (%d) is malformed:\n%s",
+ vid, blob_str(&err));
+ }
+ manifest_file_rewind(pManifest);
+ while( (pFile = manifest_file_next(pManifest,0))!=0 ){
+ if( pFile->zUuid==0 ) continue;
+ fid = uuid_to_rid(pFile->zUuid, 0);
+ md5sum_step_text(pFile->zName, -1);
+ content_get(fid, &file);
+ sqlite3_snprintf(sizeof(zBuf), zBuf, " %d\n", blob_size(&file));
+ md5sum_step_text(zBuf, -1);
+ md5sum_step_blob(&file);
+ blob_reset(&file);
+ }
+ if( pManOut ){
+ if( pManifest->zRepoCksum ){
+ blob_append(pManOut, pManifest->zRepoCksum, -1);
+ }else{
+ blob_zero(pManOut);
+ }
+ }
+ manifest_destroy(pManifest);
+ md5sum_finish(pOut);
+}
+
+/*
+** COMMAND: test-agg-cksum
+**
+** Display the aggregate checksum for content computed in several
+** different ways. The aggregate checksum is used during "fossil commit"
+** to double-check that the information about to be committed to the
+** repository exactly matches the information currently in the check-out.
+*/
+void test_agg_cksum_cmd(void){
+ int vid;
+ Blob hash, hash2;
+ db_must_be_within_tree();
+ vid = db_lget_int("checkout", 0);
+ vfile_aggregate_checksum_disk(vid, &hash);
+ printf("disk: %s\n", blob_str(&hash));
+ blob_reset(&hash);
+ vfile_aggregate_checksum_repository(vid, &hash);
+ printf("archive: %s\n", blob_str(&hash));
+ blob_reset(&hash);
+ vfile_aggregate_checksum_manifest(vid, &hash, &hash2);
+ printf("manifest: %s\n", blob_str(&hash));
+ printf("recorded: %s\n", blob_str(&hash2));
+}
+
+/*
+** This routine recomputes certain columns of the vfile and vmerge tables
+** when the associated repository is swapped out for a clone of the same
+** project, and the blob.rid value change. The following columns are
+** updated:
+**
+** vmerge.merge
+** vfile.vid
+** vfile.rid
+** vfile.mrid
+**
+** Also:
+**
+** vvar.value WHERE name='checkout'
+*/
+void vfile_rid_renumbering_event(int dryRun){
+ int oldVid;
+ int newVid;
+ char *zUnresolved;
+
+ oldVid = db_lget_int("checkout", 0);
+ newVid = db_int(0, "SELECT blob.rid FROM blob, vvar"
+ " WHERE blob.uuid=vvar.value"
+ " AND vvar.name='checkout-hash'");
+
+ /* The idMap table will make old RID values into new ones */
+ db_multi_exec(
+ "CREATE TEMP TABLE idMap(oldrid INTEGER PRIMARY KEY, newrid INT);\n"
+ );
+
+ /* Add the RID value for the current check-out */
+ db_multi_exec(
+ "INSERT INTO idMap(oldrid, newrid) VALUES(%d,%d)",
+ oldVid, newVid
+ );
+
+ /* Add the RID values for any other check-ins that have been merged into
+ ** the current check-out. */
+ db_multi_exec(
+ "INSERT OR IGNORE INTO idMap(oldrid, newrid)"
+ " SELECT vmerge.merge, blob.rid FROM vmerge, blob"
+ " WHERE blob.uuid=vmerge.mhash;"
+ );
+
+ /* Add RID values for files in the current check-out */
+ db_multi_exec(
+ "CREATE TEMP TABLE hashoffile(name TEXT PRIMARY KEY, hash TEXT)"
+ "WITHOUT ROWID;"
+
+ "INSERT INTO hashoffile(name,hash)"
+ " SELECT filename, uuid FROM vvar, files_of_checkin(vvar.value)"
+ " WHERE vvar.name='checkout-hash';"
+
+ "INSERT OR IGNORE INTO idMap(oldrid, newrid)"
+ " SELECT vfile.rid, blob.rid FROM vfile, hashoffile, blob"
+ " WHERE hashoffile.name=coalesce(vfile.origname,vfile.pathname)"
+ " AND blob.uuid=hashoffile.hash;"
+ );
+
+ /* Add RID values for merged-in files */
+ db_multi_exec(
+ "INSERT OR IGNORE INTO idMap(oldrid, newrid)"
+ " SELECT vfile.mrid, blob.rid FROM vfile, blob"
+ " WHERE blob.uuid=vfile.mhash;"
+ );
+
+ if( dryRun ){
+ Stmt q;
+ db_prepare(&q, "SELECT oldrid, newrid, blob.uuid"
+ " FROM idMap, blob WHERE blob.rid=idMap.newrid");
+ while( db_step(&q)==SQLITE_ROW ){
+ fossil_print("%8d -> %8d %.25s\n",
+ db_column_int(&q,0),
+ db_column_int(&q,1),
+ db_column_text(&q,2));
+ }
+ db_finalize(&q);
+ }
+
+ /* Verify that all RID values in the VFILE table and VMERGE table have
+ ** been resolved. */
+ zUnresolved = db_text("",
+ "WITH allrid(x) AS ("
+ " SELECT rid FROM vfile"
+ " UNION SELECT mrid FROM vfile"
+ " UNION SELECT merge FROM vmerge"
+ " UNION SELECT %d"
+ ")"
+ "SELECT group_concat(x,' ') FROM allrid"
+ " WHERE x<>0 AND x NOT IN (SELECT oldrid FROM idMap);",
+ oldVid
+ );
+ if( zUnresolved[0] ){
+ fossil_fatal("Unresolved RID values: %s\n"
+ "\n"
+ "Local check-out database is out of sync with repository file:\n"
+ "\n"
+ " %s\n"
+ "\n"
+ "Has the repository file been replaced?\n",
+ zUnresolved, db_repository_filename());
+ }
+
+ /* Make the changes to the VFILE and VMERGE tables */
+ if( !dryRun ){
+ db_multi_exec(
+ "UPDATE vfile"
+ " SET rid=(SELECT newrid FROM idMap WHERE oldrid=vfile.rid)"
+ " WHERE vid=%d AND rid>0;", oldVid);
+
+ db_multi_exec(
+ "UPDATE vfile"
+ " SET mrid=(SELECT newrid FROM idMap WHERE oldrid=vfile.mrid)"
+ " WHERE vid=%d AND mrid>0;", oldVid);
+
+ db_multi_exec(
+ "UPDATE vfile"
+ " SET vid=%d"
+ " WHERE vid=%d", newVid, oldVid);
+
+ db_multi_exec(
+ "UPDATE vmerge"
+ " SET merge=(SELECT newrid FROM idMap WHERE oldrid=vmerge.merge);");
+
+ db_lset_int("checkout",newVid);
+ }
+
+ /* Clear out the TEMP tables we constructed */
+ db_multi_exec(
+ "DROP TABLE idMap;"
+ "DROP TABLE hashoffile;"
+ );
+}