/* ** Copyright (c) 2021 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/ ** ******************************************************************************* ** ** This file contains code used to implement the "patch" command */ #include "config.h" #include "patch.h" #include /* ** Try to compute the name of the computer on which this process ** is running. */ char *fossil_hostname(void){ FILE *in; char zBuf[200]; in = popen("hostname","r"); if( in ){ int n = fread(zBuf, 1, sizeof(zBuf)-1, in); while( n>0 && fossil_isspace(zBuf[n-1]) ){ n--; } if( n<0 ) n = 0; zBuf[n] = 0; pclose(in); return fossil_strdup(zBuf); } return 0; } /* ** Flags passed from the main patch_cmd() routine into subfunctions used ** to implement the various subcommands. */ #define PATCH_DRYRUN 0x0001 #define PATCH_VERBOSE 0x0002 #define PATCH_FORCE 0x0004 /* ** Implementation of the "readfile(X)" SQL function. The entire content ** of the check-out file named X is read and returned as a BLOB. */ static void readfileFunc( sqlite3_context *context, int argc, sqlite3_value **argv ){ const char *zName; Blob x; sqlite3_int64 sz; (void)(argc); /* Unused parameter */ zName = (const char*)sqlite3_value_text(argv[0]); if( zName==0 || (zName[0]=='-' && zName[1]==0) ) return; sz = blob_read_from_file(&x, zName, RepoFILE); sqlite3_result_blob64(context, x.aData, sz, SQLITE_TRANSIENT); blob_reset(&x); } /* ** mkdelta(X,Y) ** ** X is an numeric artifact id. Y is a filename. ** ** Compute a compressed delta that carries X into Y. Or return ** and zero-length blob if X is equal to Y. */ static void mkdeltaFunc( sqlite3_context *context, int argc, sqlite3_value **argv ){ const char *zFile; Blob x, y; int rid; char *aOut; int nOut; sqlite3_int64 sz; rid = sqlite3_value_int(argv[0]); if( !content_get(rid, &x) ){ sqlite3_result_error(context, "mkdelta(X,Y): no content for X", -1); return; } zFile = (const char*)sqlite3_value_text(argv[1]); if( zFile==0 ){ sqlite3_result_error(context, "mkdelta(X,Y): NULL Y argument", -1); blob_reset(&x); return; } sz = blob_read_from_file(&y, zFile, RepoFILE); if( sz<0 ){ sqlite3_result_error(context, "mkdelta(X,Y): cannot read file Y", -1); blob_reset(&x); return; } aOut = sqlite3_malloc64(sz+70); if( aOut==0 ){ sqlite3_result_error_nomem(context); blob_reset(&y); blob_reset(&x); return; } if( blob_size(&x)==blob_size(&y) && memcmp(blob_buffer(&x), blob_buffer(&y), blob_size(&x))==0 ){ blob_reset(&y); blob_reset(&x); sqlite3_result_blob64(context, "", 0, SQLITE_STATIC); return; } nOut = delta_create(blob_buffer(&x),blob_size(&x), blob_buffer(&y),blob_size(&y), aOut); blob_reset(&x); blob_reset(&y); blob_init(&x, aOut, nOut); blob_compress(&x, &x); sqlite3_result_blob64(context, blob_buffer(&x), blob_size(&x), SQLITE_TRANSIENT); blob_reset(&x); } /* ** Generate a binary patch file and store it into the file ** named zOut. */ void patch_create(unsigned mFlags, const char *zOut, FILE *out){ int vid; char *z; if( zOut && file_isdir(zOut, ExtFILE)!=0 ){ if( mFlags & PATCH_FORCE ){ file_delete(zOut); } if( file_isdir(zOut, ExtFILE)!=0 ){ fossil_fatal("patch file already exists: %s", zOut); } } add_content_sql_commands(g.db); deltafunc_init(g.db); sqlite3_create_function(g.db, "read_co_file", 1, SQLITE_UTF8, 0, readfileFunc, 0, 0); sqlite3_create_function(g.db, "mkdelta", 2, SQLITE_UTF8, 0, mkdeltaFunc, 0, 0); db_multi_exec("ATTACH %Q AS patch;", zOut ? zOut : ":memory:"); db_multi_exec( "PRAGMA patch.journal_mode=OFF;\n" "PRAGMA patch.page_size=512;\n" "CREATE TABLE patch.chng(\n" " pathname TEXT,\n" /* Filename */ " origname TEXT,\n" /* Name before rename. NULL if not renamed */ " hash TEXT,\n" /* Baseline hash. NULL for new files. */ " isexe BOOL,\n" /* True if executable */ " islink BOOL,\n" /* True if is a symbolic link */ " delta BLOB\n" /* compressed delta. NULL if deleted. ** length 0 if unchanged */ ");" "CREATE TABLE patch.cfg(\n" " key TEXT,\n" " value ANY\n" ");" ); vid = db_lget_int("checkout", 0); vfile_check_signature(vid, CKSIG_ENOTFILE); user_select(); db_multi_exec( "INSERT INTO patch.cfg(key,value)" "SELECT 'baseline',uuid FROM blob WHERE rid=%d " "UNION ALL" " SELECT 'ckout',rtrim(%Q,'/')" "UNION ALL" " SELECT 'repo',%Q " "UNION ALL" " SELECT 'user',%Q " "UNION ALL" " SELECT 'date',julianday('now')" "UNION ALL" " SELECT name,value FROM repository.config" " WHERE name IN ('project-code','project-name') " "UNION ALL" " SELECT 'fossil-date',julianday('" MANIFEST_DATE "')" ";", vid, g.zLocalRoot, g.zRepositoryName, g.zLogin); z = fossil_hostname(); if( z ){ db_multi_exec( "INSERT INTO patch.cfg(key,value)VALUES('hostname',%Q)", z); fossil_free(z); } /* New files */ db_multi_exec( "INSERT INTO patch.chng(pathname,hash,isexe,islink,delta)" " SELECT pathname, NULL, isexe, islink," " compress(read_co_file(%Q||pathname))" " FROM vfile WHERE rid==0;", g.zLocalRoot ); /* Deleted files */ db_multi_exec( "INSERT INTO patch.chng(pathname,hash,isexe,islink,delta)" " SELECT pathname, NULL, 0, 0, NULL" " FROM vfile WHERE deleted;" ); /* Changed files */ db_multi_exec( "INSERT INTO patch.chng(pathname,origname,hash,isexe,islink,delta)" " SELECT pathname, nullif(origname,pathname), blob.uuid, isexe, islink," " mkdelta(blob.rid, %Q||pathname)" " FROM vfile, blob" " WHERE blob.rid=vfile.rid" " AND NOT deleted AND (chnged OR origname<>pathname);", g.zLocalRoot ); /* Merges */ if( db_exists("SELECT 1 FROM localdb.vmerge WHERE id<=0") ){ db_multi_exec( "CREATE TABLE patch.patchmerge(type TEXT,mhash TEXT);\n" "WITH tmap(id,type) AS (VALUES(0,'merge'),(-1,'cherrypick')," "(-2,'backout'),(-4,'integrate'))" "INSERT INTO patch.patchmerge(type,mhash)" " SELECT tmap.type,vmerge.mhash FROM vmerge, tmap" " WHERE tmap.id=vmerge.id;" ); } /* Write the database to standard output if zOut==0 */ if( zOut==0 ){ sqlite3_int64 sz; unsigned char *pData; pData = sqlite3_serialize(g.db, "patch", &sz, 0); if( pData==0 ){ fossil_fatal("out of memory"); } #ifdef _WIN32 fflush(out); _setmode(_fileno(out), _O_BINARY); #endif fwrite(pData, sz, 1, out); sqlite3_free(pData); fflush(out); } } /* ** Attempt to load and validate a patchfile identified by the first ** argument. */ void patch_attach(const char *zIn, FILE *in){ Stmt q; if( g.db==0 ){ sqlite3_open(":memory:", &g.db); } if( zIn==0 ){ Blob buf; int rc; int sz; unsigned char *pData; blob_init(&buf, 0, 0); #ifdef _WIN32 _setmode(_fileno(in), _O_BINARY); #endif sz = blob_read_from_channel(&buf, in, -1); pData = (unsigned char*)blob_buffer(&buf); db_multi_exec("ATTACH ':memory:' AS patch"); if( g.fSqlTrace ){ fossil_trace("-- deserialize(\"patch\", pData, %lld);\n", sz); } rc = sqlite3_deserialize(g.db, "patch", pData, sz, sz, 0); if( rc ){ fossil_fatal("cannot open patch database: %s", sqlite3_errmsg(g.db)); } }else if( !file_isfile(zIn, ExtFILE) ){ fossil_fatal("no such file: %s", zIn); }else{ db_multi_exec("ATTACH %Q AS patch", zIn); } db_prepare(&q, "PRAGMA patch.quick_check"); while( db_step(&q)==SQLITE_ROW ){ if( fossil_strcmp(db_column_text(&q,0),"ok")!=0 ){ fossil_fatal("file %s is not a well-formed Fossil patchfile", zIn); } } db_finalize(&q); } /* ** Show a summary of the content of a patch on standard output */ void patch_view(unsigned mFlags){ Stmt q; db_prepare(&q, "WITH nmap(nkey,nm) AS (VALUES" "('baseline','BASELINE')," "('project-name','PROJECT-NAME'))" "SELECT nm, value FROM nmap, patch.cfg WHERE nkey=key;" ); while( db_step(&q)==SQLITE_ROW ){ fossil_print("%-12s %s\n", db_column_text(&q,0), db_column_text(&q,1)); } db_finalize(&q); if( mFlags & PATCH_VERBOSE ){ db_prepare(&q, "WITH nmap(nkey,nm,isDate) AS (VALUES" "('project-code','PROJECT-CODE',0)," "('date','TIMESTAMP',1)," "('user','USER',0)," "('hostname','HOSTNAME',0)," "('ckout','CHECKOUT',0)," "('repo','REPOSITORY',0))" "SELECT nm, CASE WHEN isDate THEN datetime(value) ELSE value END" " FROM nmap, patch.cfg WHERE nkey=key;" ); while( db_step(&q)==SQLITE_ROW ){ fossil_print("%-12s %s\n", db_column_text(&q,0), db_column_text(&q,1)); } db_finalize(&q); } if( db_table_exists("patch","patchmerge") ){ db_prepare(&q, "SELECT upper(type),mhash FROM patchmerge"); while( db_step(&q)==SQLITE_ROW ){ fossil_print("%-12s %s\n", db_column_text(&q,0), db_column_text(&q,1)); } db_finalize(&q); } db_prepare(&q, "SELECT pathname," /* 0: new name */ " hash IS NULL AND delta IS NOT NULL," /* 1: isNew */ " delta IS NULL," /* 2: isDeleted */ " origname" /* 3: old name or NULL */ " FROM patch.chng ORDER BY 1"); while( db_step(&q)==SQLITE_ROW ){ const char *zClass = "EDIT"; const char *zName = db_column_text(&q,0); const char *zOrigName = db_column_text(&q, 3); if( db_column_int(&q, 1) && zOrigName==0 ){ zClass = "NEW"; }else if( db_column_int(&q, 2) ){ zClass = zOrigName==0 ? "DELETE" : 0; } if( zOrigName!=0 && zOrigName[0]!=0 ){ fossil_print("%-12s %s -> %s\n", "RENAME",zOrigName,zName); } if( zClass ){ fossil_print("%-12s %s\n", zClass, zName); } } db_finalize(&q); } /* ** Apply the patch currently attached as database "patch". ** ** First update the check-out to be at "baseline". Then loop through ** and update all files. */ void patch_apply(unsigned mFlags){ Stmt q; Blob cmd; blob_init(&cmd, 0, 0); if( unsaved_changes(0) ){ if( (mFlags & PATCH_FORCE)==0 ){ fossil_fatal("there are unsaved changes in the current check-out"); }else{ blob_appendf(&cmd, "%$ revert", g.nameOfExe); if( mFlags & PATCH_DRYRUN ){ fossil_print("%s\n", blob_str(&cmd)); }else{ int rc = fossil_system(blob_str(&cmd)); if( rc ){ fossil_fatal("unable to revert preexisting changes: %s", blob_str(&cmd)); } } blob_reset(&cmd); } } file_chdir(g.zLocalRoot, 0); db_prepare(&q, "SELECT patch.cfg.value" " FROM patch.cfg, localdb.vvar" " WHERE patch.cfg.key='baseline'" " AND localdb.vvar.name='checkout-hash'" " AND patch.cfg.key<>localdb.vvar.name" ); if( db_step(&q)==SQLITE_ROW ){ blob_append_escaped_arg(&cmd, g.nameOfExe, 1); blob_appendf(&cmd, " update %s", db_column_text(&q, 0)); if( mFlags & PATCH_VERBOSE ){ fossil_print("%-10s %s\n", "BASELINE", db_column_text(&q,0)); } } db_finalize(&q); if( blob_size(&cmd)>0 ){ if( mFlags & PATCH_DRYRUN ){ fossil_print("%s\n", blob_str(&cmd)); }else{ int rc = fossil_system(blob_str(&cmd)); if( rc ){ fossil_fatal("unable to update to the baseline check-out: %s", blob_str(&cmd)); } } } blob_reset(&cmd); if( db_table_exists("patch","patchmerge") ){ db_prepare(&q, "SELECT type, mhash, upper(type) FROM patch.patchmerge" " WHERE type IN ('merge','cherrypick','backout','integrate')" " AND mhash NOT GLOB '*[^a-fA-F0-9]*';" ); while( db_step(&q)==SQLITE_ROW ){ const char *zType = db_column_text(&q,0); blob_append_escaped_arg(&cmd, g.nameOfExe, 1); if( strcmp(zType,"merge")==0 ){ blob_appendf(&cmd, " merge %s\n", db_column_text(&q,1)); }else{ blob_appendf(&cmd, " merge --%s %s\n", zType, db_column_text(&q,1)); } if( mFlags & PATCH_VERBOSE ){ fossil_print("%-10s %s\n", db_column_text(&q,2), db_column_text(&q,0)); } } db_finalize(&q); if( mFlags & PATCH_DRYRUN ){ fossil_print("%s", blob_str(&cmd)); }else{ int rc = fossil_unsafe_system(blob_str(&cmd)); if( rc ){ fossil_fatal("unable to do merges:\n%s", blob_str(&cmd)); } } blob_reset(&cmd); } /* Deletions */ db_prepare(&q, "SELECT pathname FROM patch.chng" " WHERE origname IS NULL AND delta IS NULL"); while( db_step(&q)==SQLITE_ROW ){ blob_append_escaped_arg(&cmd, g.nameOfExe, 1); blob_appendf(&cmd, " rm --hard %$\n", db_column_text(&q,0)); if( mFlags & PATCH_VERBOSE ){ fossil_print("%-10s %s\n", "DELETE", db_column_text(&q,0)); } } db_finalize(&q); if( blob_size(&cmd)>0 ){ if( mFlags & PATCH_DRYRUN ){ fossil_print("%s", blob_str(&cmd)); }else{ int rc = fossil_unsafe_system(blob_str(&cmd)); if( rc ){ fossil_fatal("unable to do merges:\n%s", blob_str(&cmd)); } } blob_reset(&cmd); } /* Renames */ db_prepare(&q, "SELECT origname, pathname FROM patch.chng" " WHERE origname IS NOT NULL" " AND origname<>pathname" ); while( db_step(&q)==SQLITE_ROW ){ blob_append_escaped_arg(&cmd, g.nameOfExe, 1); blob_appendf(&cmd, " mv --hard %$ %$\n", db_column_text(&q,0), db_column_text(&q,1)); if( mFlags & PATCH_VERBOSE ){ fossil_print("%-10s %s -> %s\n", "RENAME", db_column_text(&q,0), db_column_text(&q,1)); } } db_finalize(&q); if( blob_size(&cmd)>0 ){ if( mFlags & PATCH_DRYRUN ){ fossil_print("%s", blob_str(&cmd)); }else{ int rc = fossil_unsafe_system(blob_str(&cmd)); if( rc ){ fossil_print("%-10s unable to rename files:\n%s", "WARNING!", blob_str(&cmd)); } } blob_reset(&cmd); } /* Edits and new files */ db_prepare(&q, "SELECT pathname, hash, isexe, islink, delta FROM patch.chng" " WHERE delta IS NOT NULL" ); while( db_step(&q)==SQLITE_ROW ){ const char *zPathname = db_column_text(&q,0); const char *zHash = db_column_text(&q,1); int isExe = db_column_int(&q,2); int isLink = db_column_int(&q,3); Blob data; blob_init(&data, 0, 0); db_ephemeral_blob(&q, 4, &data); if( blob_size(&data) ){ blob_uncompress(&data, &data); } if( blob_size(&data)==0 ){ /* No changes to the file */ }else if( zHash ){ Blob basis; int rid = fast_uuid_to_rid(zHash); int outSize, sz; char *aOut; if( rid==0 ){ fossil_fatal("cannot locate basis artifact %s for %s", zHash, zPathname); } if( !content_get(rid, &basis) ){ fossil_fatal("cannot load basis artifact %d for %s", rid, zPathname); } outSize = delta_output_size(blob_buffer(&data),blob_size(&data)); if( outSize<=0 ){ fossil_fatal("malformed delta for %s", zPathname); } aOut = sqlite3_malloc64( outSize+1 ); if( aOut==0 ){ fossil_fatal("out of memory"); } sz = delta_apply(blob_buffer(&basis), blob_size(&basis), blob_buffer(&data), blob_size(&data), aOut); if( sz<0 ){ fossil_fatal("malformed delta for %s", zPathname); } blob_reset(&basis); blob_reset(&data); blob_append(&data, aOut, sz); sqlite3_free(aOut); if( mFlags & PATCH_VERBOSE ){ fossil_print("%-10s %s\n", "EDIT", zPathname); } }else{ blob_append_escaped_arg(&cmd, g.nameOfExe, 1); blob_appendf(&cmd, " add %$\n", zPathname); if( mFlags & PATCH_VERBOSE ){ fossil_print("%-10s %s\n", "NEW", zPathname); } } if( (mFlags & PATCH_DRYRUN)==0 ){ if( isLink ){ symlink_create(blob_str(&data), zPathname); }else{ blob_write_to_file(&data, zPathname); } file_setexe(zPathname, isExe); blob_reset(&data); } } db_finalize(&q); if( blob_size(&cmd)>0 ){ if( mFlags & PATCH_DRYRUN ){ fossil_print("%s", blob_str(&cmd)); }else{ int rc = fossil_unsafe_system(blob_str(&cmd)); if( rc ){ fossil_fatal("unable to add new files:\n%s", blob_str(&cmd)); } } blob_reset(&cmd); } } /* ** This routine processes the ** ** ... [--dir64 DIR64] [DIRECTORY] FILENAME ** ** part of various "fossil patch" subcommands. ** ** Find and return the filename of the patch file to be used by ** "fossil patch apply" or "fossil patch create". Space to hold ** the returned name is obtained from fossil_malloc() and should ** be freed by the caller. ** ** If the name is "-" return NULL. The caller will interpret this ** to mean the patch is coming in over stdin or going out over ** stdout. ** ** If there is a prior DIRECTORY argument, or if ** the --dir64 option is present, first chdir to the specified ** directory, and adjust the path of FILENAME as appropriate so ** that it still points to the same file. ** ** The --dir64 option is undocumented. The argument to --dir64 ** is a base64-encoded directory name. The --dir64 option is used ** to transmit the directory as part of the command argument to ** a "ssh" command without having to worry about quoting ** any special characters in the filename. ** ** The returned name is obtained from fossil_malloc() and should ** be freed by the caller. */ static char *patch_find_patch_filename(const char *zCmdName){ const char *zDir64 = find_option("dir64",0,1); const char *zDir = 0; const char *zBaseName; char *zToFree = 0; char *zPatchFile = 0; if( zDir64 ){ int n = 0; zToFree = decode64(zDir64, &n); zDir = zToFree; } verify_all_options(); if( g.argc!=4 && g.argc!=5 ){ usage(mprintf("%s [DIRECTORY] FILENAME", zCmdName)); } if( g.argc==5 ){ zDir = g.argv[3]; zBaseName = g.argv[4]; }else{ zBaseName = g.argv[3]; } if( fossil_strcmp(zBaseName, "-")==0 ){ zPatchFile = 0; }else if( zDir ){ zPatchFile = file_canonical_name_dup(g.argv[4]); }else{ zPatchFile = fossil_strdup(g.argv[3]); } if( zDir && file_chdir(zDir,0) ){ fossil_fatal("cannot change to directory \"%s\"", zDir); } fossil_free(zToFree); return zPatchFile; } /* ** Create a FILE* that will execute the remote side of a push or pull ** using ssh (probably) or fossil for local pushes and pulls. Return ** a FILE* obtained from popen() into which we write the patch, or from ** which we read the patch, depending on whether this is a push or pull. */ static FILE *patch_remote_command( unsigned mFlags, /* flags */ const char *zThisCmd, /* "push" or "pull" */ const char *zRemoteCmd, /* "apply" or "create" */ const char *zFossilCmd, /* Name of "fossil" on remote system */ const char *zRW /* "w" or "r" */ ){ char *zRemote; char *zDir; Blob cmd; FILE *f; Blob flgs; char *zForce; blob_init(&flgs, 0, 0); if( mFlags & PATCH_FORCE ) blob_appendf(&flgs, " -f"); if( mFlags & PATCH_VERBOSE ) blob_appendf(&flgs, " -v"); if( mFlags & PATCH_DRYRUN ) blob_appendf(&flgs, " -n"); zForce = blob_size(&flgs)>0 ? blob_str(&flgs) : ""; if( g.argc!=4 ){ usage(mprintf("%s [USER@]HOST:DIRECTORY", zThisCmd)); } zRemote = fossil_strdup(g.argv[3]); zDir = (char*)file_skip_userhost(zRemote); if( zDir==0 ){ zDir = zRemote; blob_init(&cmd, 0, 0); blob_append_escaped_arg(&cmd, g.nameOfExe, 1); blob_appendf(&cmd, " patch %s%s %$ -", zRemoteCmd, zForce, zDir); }else{ Blob remote; *(char*)(zDir-1) = 0; transport_ssh_command(&cmd); blob_appendf(&cmd, " -T"); blob_append_escaped_arg(&cmd, zRemote, 0); blob_init(&remote, 0, 0); if( zFossilCmd==0 ) zFossilCmd = "fossil"; blob_appendf(&remote, "%$ patch %s%s --dir64 %z -", zFossilCmd, zRemoteCmd, zForce, encode64(zDir, -1)); blob_append_escaped_arg(&cmd, blob_str(&remote), 0); blob_reset(&remote); } if( mFlags & PATCH_VERBOSE ){ fossil_print("# %s\n", blob_str(&cmd)); fflush(stdout); } f = popen(blob_str(&cmd), zRW); if( f==0 ){ fossil_fatal("cannot run command: %s", blob_str(&cmd)); } blob_reset(&cmd); blob_reset(&flgs); return f; } /* ** Show a diff for the patch currently loaded into database "patch". */ static void patch_diff( unsigned mFlags, /* Patch flags. only -f is allowed */ DiffConfig *pCfg /* Diff options */ ){ int nErr = 0; Stmt q; int bWebpage = (pCfg->diffFlags & DIFF_WEBPAGE)!=0; Blob empty; blob_zero(&empty); if( (mFlags & PATCH_FORCE)==0 ){ /* Check to ensure that the patch is against the repository that ** we have opened. ** ** To do: If there is a mismatch, should we scan all of the repositories ** listed in the global_config table looking for a match? */ if( db_exists( "SELECT 1 FROM patch.cfg" " WHERE cfg.key='baseline'" " AND NOT EXISTS(SELECT 1 FROM blob WHERE uuid=cfg.value)" )){ char *zBaseline; db_prepare(&q, "SELECT config.value, cfg.value FROM config, cfg" " WHERE config.name='project-name'" " AND cfg.key='project-name'" " AND config.value<>cfg.value" ); if( db_step(&q)==SQLITE_ROW ){ char *zRepo = fossil_strdup(db_column_text(&q,0)); char *zPatch = fossil_strdup(db_column_text(&q,1)); db_finalize(&q); fossil_fatal("the patch is against project \"%z\" but you are using " "project \"%z\"", zPatch, zRepo); } db_finalize(&q); zBaseline = db_text(0, "SELECT value FROM patch.cfg" " WHERE key='baseline'"); if( zBaseline ){ fossil_fatal("the baseline of the patch (check-in %S) is not found " "in the %s repository", zBaseline, g.zRepositoryName); } } } diff_begin(pCfg); db_prepare(&q, "SELECT" " (SELECT blob.rid FROM blob WHERE blob.uuid=chng.hash)," " pathname," /* 1: new pathname */ " origname," /* 2: original pathname. Null if not renamed */ " delta," /* 3: delta. NULL if deleted. empty is no change */ " hash" /* 4: baseline hash */ " FROM patch.chng" " ORDER BY pathname" ); while( db_step(&q)==SQLITE_ROW ){ int rid; const char *zName; Blob a, b; if( db_column_type(&q,0)!=SQLITE_INTEGER && db_column_type(&q,4)==SQLITE_TEXT ){ char *zUuid = fossil_strdup(db_column_text(&q,4)); char *zName = fossil_strdup(db_column_text(&q,1)); if( mFlags & PATCH_FORCE ){ fossil_print("ERROR cannot find base artifact %S for file \"%s\"\n", zUuid, zName); nErr++; fossil_free(zUuid); fossil_free(zName); continue; }else{ db_finalize(&q); fossil_fatal("base artifact %S for file \"%s\" not found", zUuid, zName); } } zName = db_column_text(&q, 1); rid = db_column_int(&q, 0); if( db_column_type(&q,3)==SQLITE_NULL ){ if( !bWebpage ) fossil_print("DELETE %s\n", zName); diff_print_index(zName, pCfg, 0); content_get(rid, &a); diff_file_mem(&a, &empty, zName, pCfg); }else if( rid==0 ){ db_ephemeral_blob(&q, 3, &a); blob_uncompress(&a, &a); if( !bWebpage ) fossil_print("ADDED %s\n", zName); diff_print_index(zName, pCfg, 0); diff_file_mem(&empty, &a, zName, pCfg); blob_reset(&a); }else if( db_column_bytes(&q, 3)>0 ){ Blob delta; db_ephemeral_blob(&q, 3, &delta); blob_uncompress(&delta, &delta); content_get(rid, &a); blob_delta_apply(&a, &delta, &b); diff_file_mem(&a, &b, zName, pCfg); blob_reset(&a); blob_reset(&b); blob_reset(&delta); } } db_finalize(&q); diff_end(pCfg, nErr); if( nErr ) fossil_fatal("abort due to prior errors"); } /* ** COMMAND: patch ** ** Usage: %fossil patch SUBCOMMAND ?ARGS ..? ** ** This command is used to create, view, and apply Fossil binary patches. ** A Fossil binary patch is a single (binary) file that captures all of the ** uncommitted changes of a check-out. Use Fossil binary patches to transfer ** proposed or incomplete changes between machines for testing or analysis. ** ** > fossil patch create [DIRECTORY] FILENAME ** ** Create a new binary patch in FILENAME that captures all uncommitted ** changes in the check-out at DIRECTORY, or the current directory if ** DIRECTORY is omitted. If FILENAME is "-" then the binary patch ** is written to standard output. ** ** Options: ** -f|--force Overwrite an existing patch with the same name ** ** > fossil patch apply [DIRECTORY] FILENAME ** ** Apply the changes in FILENAME to the check-out at DIRECTORY, or ** in the current directory if DIRECTORY is omitted. ** ** Options: ** -f|--force Apply the patch even though there are unsaved ** changes in the current check-out. Unsaved changes ** are reverted and permanently lost. ** -n|--dry-run Do nothing, but print what would have happened ** -v|--verbose Extra output explaining what happens ** ** > fossil patch diff [DIRECTORY] FILENAME ** ** Show a human-readable diff for the patch. All the usual ** diff flags described at "fossil help diff" apply. In addition: ** ** -f|--force Continue trying to perform the diff even if ** baseline information is missing from the current ** repository ** ** > fossil patch push REMOTE-CHECKOUT ** ** Create a patch for the current check-out, transfer that patch to ** a remote machine (using ssh) and apply the patch there. The ** REMOTE-CHECKOUT is in one of the following formats: ** ** * DIRECTORY ** * HOST:DIRECTORY ** * USER@HOST:DIRECTORY ** ** Command-line options: ** ** -f|--force Apply the patch even though there are unsaved ** changes in the current check-out. Unsaved ** changes will be reverted and then the patch is ** applied. ** --fossilcmd EXE Name of the "fossil" executable on the remote ** -n|--dry-run Do nothing, but print what would have happened ** -v|--verbose Extra output explaining what happens ** ** ** > fossil patch pull REMOTE-CHECKOUT ** ** Like "fossil patch push" except that the transfer is from remote ** to local. All the same command-line options apply. ** ** > fossil patch view FILENAME ** ** View a summary of the changes in the binary patch FILENAME. ** Use "fossil patch diff" for detailed patch content. ** ** -v|--verbose Show extra detail about the patch ** */ void patch_cmd(void){ const char *zCmd; size_t n; if( g.argc<3 ){ patch_usage: usage("apply|create|diff|pull|push|view"); } zCmd = g.argv[2]; n = strlen(zCmd); if( strncmp(zCmd, "apply", n)==0 ){ char *zIn; unsigned flags = 0; if( find_option("dry-run","n",0) ) flags |= PATCH_DRYRUN; if( find_option("verbose","v",0) ) flags |= PATCH_VERBOSE; if( find_option("force","f",0) ) flags |= PATCH_FORCE; zIn = patch_find_patch_filename("apply"); db_must_be_within_tree(); patch_attach(zIn, stdin); patch_apply(flags); fossil_free(zIn); }else if( strncmp(zCmd, "create", n)==0 ){ char *zOut; unsigned flags = 0; if( find_option("force","f",0) ) flags |= PATCH_FORCE; zOut = patch_find_patch_filename("create"); verify_all_options(); db_must_be_within_tree(); patch_create(flags, zOut, stdout); fossil_free(zOut); }else if( strncmp(zCmd, "diff", n)==0 ){ char *zIn; unsigned flags = 0; DiffConfig DCfg; if( find_option("tk",0,0)!=0 ){ db_close(0); diff_tk("patch diff", 3); return; } diff_options(&DCfg, zCmd[0]=='g', 0); db_find_and_open_repository(0, 0); if( find_option("force","f",0) ) flags |= PATCH_FORCE; verify_all_options(); zIn = patch_find_patch_filename("apply"); patch_attach(zIn, stdin); patch_diff(flags, &DCfg); fossil_free(zIn); }else if( strncmp(zCmd, "pull", n)==0 ){ FILE *pIn = 0; unsigned flags = 0; const char *zFossilCmd = find_option("fossilcmd",0,1); if( find_option("dry-run","n",0) ) flags |= PATCH_DRYRUN; if( find_option("verbose","v",0) ) flags |= PATCH_VERBOSE; if( find_option("force","f",0) ) flags |= PATCH_FORCE; db_must_be_within_tree(); verify_all_options(); pIn = patch_remote_command(flags & (~PATCH_FORCE), "pull", "create", zFossilCmd, "r"); if( pIn ){ patch_attach(0, pIn); pclose(pIn); patch_apply(flags); } }else if( strncmp(zCmd, "push", n)==0 ){ FILE *pOut = 0; unsigned flags = 0; const char *zFossilCmd = find_option("fossilcmd",0,1); if( find_option("dry-run","n",0) ) flags |= PATCH_DRYRUN; if( find_option("verbose","v",0) ) flags |= PATCH_VERBOSE; if( find_option("force","f",0) ) flags |= PATCH_FORCE; db_must_be_within_tree(); verify_all_options(); pOut = patch_remote_command(flags, "push", "apply", zFossilCmd, "w"); if( pOut ){ patch_create(0, 0, pOut); pclose(pOut); } }else if( strncmp(zCmd, "view", n)==0 ){ const char *zIn; unsigned int flags = 0; if( find_option("verbose","v",0) ) flags |= PATCH_VERBOSE; verify_all_options(); if( g.argc!=4 ){ usage("view FILENAME"); } zIn = g.argv[3]; if( fossil_strcmp(zIn, "-")==0 ) zIn = 0; patch_attach(zIn, stdin); patch_view(flags); }else { goto patch_usage; } }