/* ** 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/ ** ******************************************************************************* ** ** This file contains code used to implement the "diff" command */ #include "config.h" #include "diffcmd.h" #include <assert.h> /* includes needed to catch interrupts */ #ifdef _WIN32 # include <windows.h> #else # include <signal.h> #endif /* ** Use the right null device for the platform. */ #if defined(_WIN32) # define NULL_DEVICE "NUL" #else # define NULL_DEVICE "/dev/null" #endif /* ** Used when the name for the diff is unknown. */ #define DIFF_NO_NAME "(unknown)" /* ** Use the "exec-rel-paths" setting and the --exec-abs-paths and ** --exec-rel-paths command line options to determine whether ** certain external commands are executed using relative paths. */ static int determine_exec_relative_option(int force){ static int relativePaths = -1; if( force || relativePaths==-1 ){ int relPathOption = find_option("exec-rel-paths", 0, 0)!=0; int absPathOption = find_option("exec-abs-paths", 0, 0)!=0; #if defined(FOSSIL_ENABLE_EXEC_REL_PATHS) relativePaths = db_get_boolean("exec-rel-paths", 1); #else relativePaths = db_get_boolean("exec-rel-paths", 0); #endif if( relPathOption ){ relativePaths = 1; } if( absPathOption ){ relativePaths = 0; } } return relativePaths; } #if INTERFACE /* ** An array of FileDirList objects describe the files and directories listed ** on the command line of a "diff" command. Only those objects listed are ** actually diffed. */ struct FileDirList { int nUsed; /* Number of times each entry is used */ int nName; /* Length of the entry */ char *zName; /* Text of the entry */ }; #endif /* ** Return true if zFile is a file named on the azInclude[] list or is ** a file in a directory named on the azInclude[] list. ** ** if azInclude is NULL, then always include zFile. */ static int file_dir_match(FileDirList *p, const char *zFile){ if( p==0 || strcmp(p->zName,".")==0 ) return 1; if( filenames_are_case_sensitive() ){ while( p->zName ){ if( strcmp(zFile, p->zName)==0 || (strncmp(zFile, p->zName, p->nName)==0 && zFile[p->nName]=='/') ){ break; } p++; } }else{ while( p->zName ){ if( fossil_stricmp(zFile, p->zName)==0 || (fossil_strnicmp(zFile, p->zName, p->nName)==0 && zFile[p->nName]=='/') ){ break; } p++; } } if( p->zName ){ p->nUsed++; return 1; } return 0; } /* ** Print the "Index:" message that patches wants to see at the top of a diff. */ void diff_print_index(const char *zFile, DiffConfig *pCfg, Blob *pOut){ if( (pCfg->diffFlags & (DIFF_SIDEBYSIDE|DIFF_BRIEF|DIFF_NUMSTAT|DIFF_JSON| DIFF_WEBPAGE|DIFF_TCL))==0 ){ blob_appendf(pOut, "Index: %s\n%.66c\n", zFile, '='); } } /* ** Print the +++/--- filename lines or whatever filename information ** is appropriate for the output format. */ void diff_print_filenames( const char *zLeft, /* Name of the left file */ const char *zRight, /* Name of the right file */ DiffConfig *pCfg, /* Diff configuration */ Blob *pOut /* Write to this blob, or stdout of this is NULL */ ){ u64 diffFlags = pCfg->diffFlags; if( diffFlags & (DIFF_BRIEF|DIFF_RAW) ){ /* no-op */ }else if( diffFlags & DIFF_DEBUG ){ blob_appendf(pOut, "FILE-LEFT %s\nFILE-RIGHT %s\n", zLeft, zRight); }else if( diffFlags & DIFF_WEBPAGE ){ if( fossil_strcmp(zLeft,zRight)==0 ){ blob_appendf(pOut,"<h1>%h</h1>\n", zLeft); }else{ blob_appendf(pOut,"<h1>%h ⇆ %h</h1>\n", zLeft, zRight); } }else if( diffFlags & (DIFF_TCL|DIFF_JSON) ){ if( diffFlags & DIFF_TCL ){ blob_append(pOut, "FILE ", 5); blob_append_tcl_literal(pOut, zLeft, (int)strlen(zLeft)); blob_append_char(pOut, ' '); blob_append_tcl_literal(pOut, zRight, (int)strlen(zRight)); blob_append_char(pOut, '\n'); }else{ if( pOut ) blob_trim(pOut); blob_append(pOut, (pCfg->nFile==0 ? "[{" : ",\n{"), -1); pCfg->nFile++; blob_append(pOut, "\n \"leftname\":", -1); blob_append_json_literal(pOut, zLeft, (int)strlen(zLeft)); blob_append(pOut, ",\n \"rightname\":", -1); blob_append_json_literal(pOut, zRight, (int)strlen(zRight)); blob_append(pOut, ",\n \"diff\":\n", -1); } }else if( diffFlags & DIFF_SIDEBYSIDE ){ int w = diff_width(pCfg); int n1 = strlen(zLeft); int n2 = strlen(zRight); int x; if( n1==n2 && fossil_strcmp(zLeft,zRight)==0 ){ if( n1>w*2 ) n1 = w*2; x = w*2+17 - (n1+2); blob_appendf(pOut, "%.*c %.*s %.*c\n", x/2, '=', n1, zLeft, (x+1)/2, '='); }else{ if( w<20 ) w = 20; if( n1>w-10 ) n1 = w - 10; if( n2>w-10 ) n2 = w - 10; blob_appendf(pOut, "%.*c %.*s %.*c versus %.*c %.*s %.*c\n", (w-n1+10)/2, '=', n1, zLeft, (w-n1+1)/2, '=', (w-n2)/2, '=', n2, zRight, (w-n2+1)/2, '='); } }else{ blob_appendf(pOut, "--- %s\n+++ %s\n", zLeft, zRight); } } /* ** Default header text for diff with --webpage */ static const char zWebpageHdr[] = @ <!DOCTYPE html> @ <html> @ <head> @ <meta charset="UTF-8"> @ <style> @ h1 { @ font-size: 150%; @ } @ @ table.diff { @ width: 100%; @ border-spacing: 0; @ border: 1px solid black; @ } @ table.diff td { @ vertical-align: top; @ } @ table.diff pre { @ margin: 0 0 0 0; @ } @ td.diffln { @ width: 1px; @ text-align: right; @ padding: 0 1em 0 0; @ } @ td.difflne { @ padding-bottom: 0.4em; @ } @ td.diffsep { @ width: 1px; @ padding: 0 0.3em 0 1em; @ } @ td.difftxt pre { @ overflow-x: auto; @ } @ td.diffln ins { @ background-color: #a0e4b2; @ text-decoration: none; @ } @ td.diffln del { @ background-color: #ffc0c0; @ text-decoration: none; @ } @ td.difftxt del { @ background-color: #ffe8e8; @ text-decoration: none; @ } @ td.difftxt del > del { @ background-color: #ffc0c0; @ text-decoration: none; @ font-weight: bold; @ } @ td.difftxt del > del.edit { @ background-color: #c0c0ff; @ text-decoration: none; @ font-weight: bold; @ } @ td.difftxt ins { @ background-color: #dafbe1; @ text-decoration: none; @ } @ td.difftxt ins > ins { @ background-color: #a0e4b2; @ text-decoration: none; @ font-weight: bold; @ } @ td.difftxt ins > ins.edit { @ background-color: #c0c0ff; @ text-decoration: none; @ font-weight: bold; @ } @ @ </style> @ </head> @ <body> ; const char zWebpageEnd[] = @ </body> @ </html> ; /* ** State variables used by the --browser option for diff. These must ** be static variables, not elements of DiffConfig, since they are ** used by the interrupt handler. */ static char *tempDiffFilename; /* File holding the diff HTML */ static FILE *diffOut; /* Open to write into tempDiffFilename */ /* Amount of delay (in milliseconds) between launching the ** web browser and deleting the temporary file used by --browser */ #ifndef FOSSIL_BROWSER_DIFF_DELAY # define FOSSIL_BROWSER_DIFF_DELAY 5000 /* 5 seconds by default */ #endif /* ** If we catch a single while writing the temporary file for the --browser ** diff output, then delete the temporary file and exit. */ static void diff_www_interrupt(int NotUsed){ (void)NotUsed; if( diffOut ) fclose(diffOut); if( tempDiffFilename ) file_delete(tempDiffFilename); exit(1); } #ifdef _WIN32 static BOOL WINAPI diff_console_ctrl_handler(DWORD dwCtrlType){ if( dwCtrlType==CTRL_C_EVENT ) diff_www_interrupt(0); return FALSE; } #endif /* ** Do preliminary setup and output before computing a diff. ** ** For --browser, redirect stdout to a temporary file that will ** hold the result. Make arrangements to delete that temporary ** file if the diff is interrupted. ** ** For --browser and --webpage, output the HTML header. */ void diff_begin(DiffConfig *pCfg){ if( (pCfg->diffFlags & DIFF_BROWSER)!=0 ){ tempDiffFilename = fossil_temp_filename(); tempDiffFilename = sqlite3_mprintf("%z.html", tempDiffFilename); diffOut = fossil_freopen(tempDiffFilename,"wb",stdout); if( diffOut==0 ){ fossil_fatal("unable to create temporary file \"%s\"", tempDiffFilename); } #ifndef _WIN32 signal(SIGINT, diff_www_interrupt); #else SetConsoleCtrlHandler(diff_console_ctrl_handler, TRUE); #endif } if( (pCfg->diffFlags & DIFF_WEBPAGE)!=0 ){ fossil_print("%s",zWebpageHdr); fflush(stdout); } } /* Do any final output required by a diff and complete the diff ** process. ** ** For --browser and --webpage, output any javascript required by ** the diff. (Currently JS is only needed for side-by-side diffs). ** ** For --browser, close the connection to the temporary file, then ** launch a web browser to view the file. After a delay ** of FOSSIL_BROWSER_DIFF_DELAY milliseconds, delete the temp file. */ void diff_end(DiffConfig *pCfg, int nErr){ if( (pCfg->diffFlags & DIFF_WEBPAGE)!=0 ){ if( pCfg->diffFlags & DIFF_SIDEBYSIDE ){ const unsigned char *zJs = builtin_file("diff.js", 0); fossil_print("<script>\n%s</script>\n", zJs); } fossil_print("%s", zWebpageEnd); } if( (pCfg->diffFlags & DIFF_BROWSER)!=0 && nErr==0 ){ char *zCmd = mprintf("%s %$", fossil_web_browser(), tempDiffFilename); fclose(diffOut); diffOut = fossil_freopen(NULL_DEVICE, "wb", stdout); fossil_system(zCmd); fossil_free(zCmd); diffOut = 0; sqlite3_sleep(FOSSIL_BROWSER_DIFF_DELAY); file_delete(tempDiffFilename); sqlite3_free(tempDiffFilename); tempDiffFilename = 0; } if( (pCfg->diffFlags & DIFF_JSON)!=0 && pCfg->nFile>0 ){ fossil_print("]\n"); } } /* ** Show the difference between two files, one in memory and one on disk. ** ** The difference is the set of edits needed to transform pFile1 into ** zFile2. The content of pFile1 is in memory. zFile2 exists on disk. */ void diff_file( Blob *pFile1, /* In memory content to compare from */ const char *zFile2, /* On disk content to compare to */ const char *zName, /* Display name of the file */ DiffConfig *pCfg, /* Flags to control the diff */ Blob *pOut /* Blob to store diff output */ ){ if( pCfg->zDiffCmd==0 ){ Blob out; /* Diff output text */ Blob file2; /* Content of zFile2 */ const char *zName2; /* Name of zFile2 for display */ /* Read content of zFile2 into memory */ blob_zero(&file2); if( file_size(zFile2, ExtFILE)<0 ){ zName2 = NULL_DEVICE; }else{ blob_read_from_file(&file2, zFile2, ExtFILE); zName2 = zName; } /* Compute and output the differences */ if( pCfg->diffFlags & DIFF_BRIEF ){ if( blob_compare(pFile1, &file2) ){ fossil_print("CHANGED %s\n", zName); } }else{ blob_zero(&out); text_diff(pFile1, &file2, &out, pCfg); if( blob_size(&out) ){ if( pCfg->diffFlags & DIFF_NUMSTAT ){ blob_appendf(pOut, "%s %s\n", blob_str(&out), zName); }else{ diff_print_filenames(zName, zName2, pCfg, pOut); blob_appendf(pOut, "%s\n", blob_str(&out)); } } blob_reset(&out); } /* Release memory resources */ blob_reset(&file2); }else{ Blob nameFile1; /* Name of temporary file to old pFile1 content */ Blob cmd; /* Text of command to run */ if( (pCfg->diffFlags & DIFF_INCBINARY)==0 ){ Blob file2; if( looks_like_binary(pFile1) ){ fossil_print("%s",DIFF_CANNOT_COMPUTE_BINARY); return; } if( pCfg->zBinGlob ){ Glob *pBinary = glob_create(pCfg->zBinGlob); if( glob_match(pBinary, zName) ){ fossil_print("%s",DIFF_CANNOT_COMPUTE_BINARY); glob_free(pBinary); return; } glob_free(pBinary); } blob_zero(&file2); if( file_size(zFile2, ExtFILE)>=0 ){ blob_read_from_file(&file2, zFile2, ExtFILE); } if( looks_like_binary(&file2) ){ fossil_print("%s",DIFF_CANNOT_COMPUTE_BINARY); blob_reset(&file2); return; } blob_reset(&file2); } /* Construct a temporary file to hold pFile1 based on the name of ** zFile2 */ file_tempname(&nameFile1, zFile2, "orig"); blob_write_to_file(pFile1, blob_str(&nameFile1)); /* Construct the external diff command */ blob_zero(&cmd); blob_append(&cmd, pCfg->zDiffCmd, -1); if( pCfg->diffFlags & DIFF_INVERT ){ blob_append_escaped_arg(&cmd, zFile2, 1); blob_append_escaped_arg(&cmd, blob_str(&nameFile1), 1); }else{ blob_append_escaped_arg(&cmd, blob_str(&nameFile1), 1); blob_append_escaped_arg(&cmd, zFile2, 1); } /* Run the external diff command */ fossil_system(blob_str(&cmd)); /* Delete the temporary file and clean up memory used */ file_delete(blob_str(&nameFile1)); blob_reset(&nameFile1); blob_reset(&cmd); } } /* ** Show the difference between two files, both in memory. ** ** The difference is the set of edits needed to transform pFile1 into ** pFile2. ** ** Use the internal diff logic if zDiffCmd is NULL. Otherwise call the ** command zDiffCmd to do the diffing. ** ** When using an external diff program, zBinGlob contains the GLOB patterns ** for file names to treat as binary. If fIncludeBinary is zero, these files ** will be skipped in addition to files that may contain binary content. */ void diff_file_mem( Blob *pFile1, /* In memory content to compare from */ Blob *pFile2, /* In memory content to compare to */ const char *zName, /* Display name of the file */ DiffConfig *pCfg /* Diff flags */ ){ if( pCfg->diffFlags & DIFF_BRIEF ) return; if( pCfg->zDiffCmd==0 ){ Blob out; /* Diff output text */ blob_zero(&out); text_diff(pFile1, pFile2, &out, pCfg); if( pCfg->diffFlags & DIFF_NUMSTAT ){ fossil_print("%s %s\n", blob_str(&out), zName); }else{ diff_print_filenames(zName, zName, pCfg, 0); fossil_print("%s\n", blob_str(&out)); } /* Release memory resources */ blob_reset(&out); }else{ Blob cmd; Blob temp1; Blob temp2; if( (pCfg->diffFlags & DIFF_INCBINARY)==0 ){ if( looks_like_binary(pFile1) || looks_like_binary(pFile2) ){ fossil_print("%s",DIFF_CANNOT_COMPUTE_BINARY); return; } if( pCfg->zBinGlob ){ Glob *pBinary = glob_create(pCfg->zBinGlob); if( glob_match(pBinary, zName) ){ fossil_print("%s",DIFF_CANNOT_COMPUTE_BINARY); glob_free(pBinary); return; } glob_free(pBinary); } } /* Construct a temporary file names */ file_tempname(&temp1, zName, "before"); file_tempname(&temp2, zName, "after"); blob_write_to_file(pFile1, blob_str(&temp1)); blob_write_to_file(pFile2, blob_str(&temp2)); /* Construct the external diff command */ blob_zero(&cmd); blob_append(&cmd, pCfg->zDiffCmd, -1); blob_append_escaped_arg(&cmd, blob_str(&temp1), 1); blob_append_escaped_arg(&cmd, blob_str(&temp2), 1); /* Run the external diff command */ fossil_system(blob_str(&cmd)); /* Delete the temporary file and clean up memory used */ file_delete(blob_str(&temp1)); file_delete(blob_str(&temp2)); blob_reset(&temp1); blob_reset(&temp2); blob_reset(&cmd); } } /* ** Return true the disk file is identical to the Blob. Return zero ** if the files differ in any way. */ static int file_same_as_blob(Blob *blob, const char *zDiskFile){ Blob file; int rc = 0; if( blob_size(blob)!=file_size(zDiskFile, ExtFILE) ) return 0; blob_zero(&file); blob_read_from_file(&file, zDiskFile, ExtFILE); if( blob_size(&file)!=blob_size(blob) ){ rc = 0; }else{ rc = memcmp(blob_buffer(&file), blob_buffer(blob), blob_size(&file))==0; } blob_reset(&file); return rc; } /* ** Run a diff between the version zFrom and files on disk. zFrom might ** be NULL which means to simply show the difference between the edited ** files on disk and the check-out on which they are based. ** ** Use the internal diff logic if zDiffCmd is NULL. Otherwise call the ** command zDiffCmd to do the diffing. ** ** When using an external diff program, zBinGlob contains the GLOB patterns ** for file names to treat as binary. If fIncludeBinary is zero, these files ** will be skipped in addition to files that may contain binary content. */ void diff_against_disk( const char *zFrom, /* Version to difference from */ DiffConfig *pCfg, /* Flags controlling diff output */ FileDirList *pFileDir, /* Which files to diff */ Blob *pOut /* Blob to output diff instead of stdout */ ){ int vid; Blob sql; Stmt q; int asNewFile; /* Treat non-existant files as empty files */ int isNumStat; /* True for --numstat */ asNewFile = (pCfg->diffFlags & (DIFF_VERBOSE|DIFF_NUMSTAT|DIFF_HTML))!=0; isNumStat = (pCfg->diffFlags & (DIFF_NUMSTAT|DIFF_TCL|DIFF_HTML))!=0; vid = db_lget_int("checkout", 0); vfile_check_signature(vid, CKSIG_ENOTFILE); blob_zero(&sql); db_begin_transaction(); if( zFrom ){ int rid = name_to_typed_rid(zFrom, "ci"); if( !is_a_version(rid) ){ fossil_fatal("no such check-in: %s", zFrom); } load_vfile_from_rid(rid); blob_append_sql(&sql, "SELECT v2.pathname, v2.deleted, v2.chnged, v2.rid==0, v1.rid, v1.islink" " FROM vfile v1, vfile v2 " " WHERE v1.pathname=v2.pathname AND v1.vid=%d AND v2.vid=%d" " AND (v2.deleted OR v2.chnged OR v1.mrid!=v2.rid)" "UNION " "SELECT pathname, 1, 0, 0, 0, islink" " FROM vfile v1" " WHERE v1.vid=%d" " AND NOT EXISTS(SELECT 1 FROM vfile v2" " WHERE v2.vid=%d AND v2.pathname=v1.pathname)" "UNION " "SELECT pathname, 0, 0, 1, 0, islink" " FROM vfile v2" " WHERE v2.vid=%d" " AND NOT EXISTS(SELECT 1 FROM vfile v1" " WHERE v1.vid=%d AND v1.pathname=v2.pathname)" " ORDER BY 1 /*scan*/", rid, vid, rid, vid, vid, rid ); }else{ blob_append_sql(&sql, "SELECT pathname, deleted, chnged , rid==0, rid, islink" " FROM vfile" " WHERE vid=%d" " AND (deleted OR chnged OR rid==0)" " ORDER BY pathname /*scan*/", vid ); } db_prepare(&q, "%s", blob_sql_text(&sql)); blob_reset(&sql); while( db_step(&q)==SQLITE_ROW ){ const char *zPathname = db_column_text(&q,0); int isDeleted = db_column_int(&q, 1); int isChnged = db_column_int(&q,2); int isNew = db_column_int(&q,3); int srcid = db_column_int(&q, 4); int isLink = db_column_int(&q, 5); const char *zFullName; int showDiff = 1; Blob fname; if( !file_dir_match(pFileDir, zPathname) ) continue; if( determine_exec_relative_option(0) ){ blob_zero(&fname); file_relative_name(zPathname, &fname, 1); }else{ blob_set(&fname, g.zLocalRoot); blob_append(&fname, zPathname, -1); } zFullName = blob_str(&fname); if( isDeleted ){ if( !isNumStat ){ fossil_print("DELETED %s\n", zPathname); } if( !asNewFile ){ showDiff = 0; zFullName = NULL_DEVICE; } }else if( file_access(zFullName, F_OK) ){ if( !isNumStat ){ fossil_print("MISSING %s\n", zPathname); } if( !asNewFile ){ showDiff = 0; } }else if( isNew ){ if( !isNumStat ){ fossil_print("ADDED %s\n", zPathname); } srcid = 0; if( !asNewFile ){ showDiff = 0; } }else if( isChnged==3 ){ if( !isNumStat ){ fossil_print("ADDED_BY_MERGE %s\n", zPathname); } srcid = 0; if( !asNewFile ){ showDiff = 0; } }else if( isChnged==5 ){ if( !isNumStat ){ fossil_print("ADDED_BY_INTEGRATE %s\n", zPathname); } srcid = 0; if( !asNewFile ){ showDiff = 0; } } if( showDiff ){ Blob content; if( !isLink != !file_islink(zFullName) ){ diff_print_index(zPathname, pCfg, 0); diff_print_filenames(zPathname, zPathname, pCfg, 0); fossil_print("%s",DIFF_CANNOT_COMPUTE_SYMLINK); continue; } if( srcid>0 ){ content_get(srcid, &content); }else{ blob_zero(&content); } if( isChnged==0 || !file_same_as_blob(&content, zFullName) ){ diff_print_index(zPathname, pCfg, pOut); diff_file(&content, zFullName, zPathname, pCfg, pOut); } blob_reset(&content); } blob_reset(&fname); } db_finalize(&q); db_end_transaction(1); /* ROLLBACK */ } /* ** Run a diff between the undo buffer and files on disk. ** ** Use the internal diff logic if zDiffCmd is NULL. Otherwise call the ** command zDiffCmd to do the diffing. ** ** When using an external diff program, zBinGlob contains the GLOB patterns ** for file names to treat as binary. If fIncludeBinary is zero, these files ** will be skipped in addition to files that may contain binary content. */ static void diff_against_undo( DiffConfig *pCfg, /* Flags controlling diff output */ FileDirList *pFileDir /* List of files and directories to diff */ ){ Stmt q; Blob content; db_prepare(&q, "SELECT pathname, content FROM undo"); blob_init(&content, 0, 0); while( db_step(&q)==SQLITE_ROW ){ char *zFullName; const char *zFile = (const char*)db_column_text(&q, 0); if( !file_dir_match(pFileDir, zFile) ) continue; zFullName = mprintf("%s%s", g.zLocalRoot, zFile); db_column_blob(&q, 1, &content); diff_file(&content, zFullName, zFile, pCfg, 0); fossil_free(zFullName); blob_reset(&content); } db_finalize(&q); } /* ** Show the difference between two files identified by ManifestFile ** entries. ** ** Use the internal diff logic if zDiffCmd is NULL. Otherwise call the ** command zDiffCmd to do the diffing. ** ** When using an external diff program, zBinGlob contains the GLOB patterns ** for file names to treat as binary. If fIncludeBinary is zero, these files ** will be skipped in addition to files that may contain binary content. */ static void diff_manifest_entry( struct ManifestFile *pFrom, struct ManifestFile *pTo, DiffConfig *pCfg ){ Blob f1, f2; int rid; const char *zName; if( pFrom ){ zName = pFrom->zName; }else if( pTo ){ zName = pTo->zName; }else{ zName = DIFF_NO_NAME; } if( pCfg->diffFlags & DIFF_BRIEF ) return; diff_print_index(zName, pCfg, 0); if( pFrom ){ rid = uuid_to_rid(pFrom->zUuid, 0); content_get(rid, &f1); }else{ blob_zero(&f1); } if( pTo ){ rid = uuid_to_rid(pTo->zUuid, 0); content_get(rid, &f2); }else{ blob_zero(&f2); } diff_file_mem(&f1, &f2, zName, pCfg); blob_reset(&f1); blob_reset(&f2); } /* ** Output the differences between two check-ins. ** ** Use the internal diff logic if zDiffCmd is NULL. Otherwise call the ** command zDiffCmd to do the diffing. ** ** When using an external diff program, zBinGlob contains the GLOB patterns ** for file names to treat as binary. If fIncludeBinary is zero, these files ** will be skipped in addition to files that may contain binary content. */ static void diff_two_versions( const char *zFrom, const char *zTo, DiffConfig *pCfg, FileDirList *pFileDir ){ Manifest *pFrom, *pTo; ManifestFile *pFromFile, *pToFile; int asNewFlag = (pCfg->diffFlags & (DIFF_VERBOSE|DIFF_NUMSTAT))!=0 ? 1 : 0; pFrom = manifest_get_by_name(zFrom, 0); manifest_file_rewind(pFrom); pFromFile = manifest_file_next(pFrom,0); pTo = manifest_get_by_name(zTo, 0); manifest_file_rewind(pTo); pToFile = manifest_file_next(pTo,0); while( pFromFile || pToFile ){ int cmp; if( pFromFile==0 ){ cmp = +1; }else if( pToFile==0 ){ cmp = -1; }else{ cmp = fossil_strcmp(pFromFile->zName, pToFile->zName); } if( cmp<0 ){ if( file_dir_match(pFileDir, pFromFile->zName) ){ if( (pCfg->diffFlags & (DIFF_NUMSTAT|DIFF_HTML))==0 ){ fossil_print("DELETED %s\n", pFromFile->zName); } if( asNewFlag ){ diff_manifest_entry(pFromFile, 0, pCfg); } } pFromFile = manifest_file_next(pFrom,0); }else if( cmp>0 ){ if( file_dir_match(pFileDir, pToFile->zName) ){ if( (pCfg->diffFlags & (DIFF_NUMSTAT|DIFF_HTML|DIFF_TCL|DIFF_JSON))==0 ){ fossil_print("ADDED %s\n", pToFile->zName); } if( asNewFlag ){ diff_manifest_entry(0, pToFile, pCfg); } } pToFile = manifest_file_next(pTo,0); }else if( fossil_strcmp(pFromFile->zUuid, pToFile->zUuid)==0 ){ /* No changes */ (void)file_dir_match(pFileDir, pFromFile->zName); /* Record name usage */ pFromFile = manifest_file_next(pFrom,0); pToFile = manifest_file_next(pTo,0); }else{ if( file_dir_match(pFileDir, pToFile->zName) ){ if( pCfg->diffFlags & DIFF_BRIEF ){ fossil_print("CHANGED %s\n", pFromFile->zName); }else{ diff_manifest_entry(pFromFile, pToFile, pCfg); } } pFromFile = manifest_file_next(pFrom,0); pToFile = manifest_file_next(pTo,0); } } manifest_destroy(pFrom); manifest_destroy(pTo); } /* ** Return the name of the external diff command, or return NULL if ** no external diff command is defined. */ const char *diff_command_external(int guiDiff){ const char *zDefault; const char *zName; if( guiDiff ){ #if defined(_WIN32) zDefault = "WinDiff.exe"; #else zDefault = 0; #endif zName = "gdiff-command"; }else{ zDefault = 0; zName = "diff-command"; } return db_get(zName, zDefault); } /* ** Show diff output in a Tcl/Tk window, in response to the --tk option ** to the diff command. ** ** If fossil has direct access to a Tcl interpreter (either loaded ** dynamically through stubs or linked in statically), we can use it ** directly. Otherwise: ** (1) Write the Tcl/Tk script used for rendering into a temp file. ** (2) Invoke "tclsh" on the temp file using fossil_system(). ** (3) Delete the temp file. */ void diff_tk(const char *zSubCmd, int firstArg){ int i; Blob script; const char *zTempFile = 0; char *zCmd; const char *zTclsh; blob_zero(&script); blob_appendf(&script, "set fossilcmd {| \"%/\" %s -tcl -i -v", g.nameOfExe, zSubCmd); find_option("tcl",0,0); find_option("html",0,0); find_option("side-by-side","y",0); find_option("internal","i",0); find_option("verbose","v",0); zTclsh = find_option("tclsh",0,1); if( zTclsh==0 ){ zTclsh = db_get("tclsh",0); } /* The undocumented --script FILENAME option causes the Tk script to ** be written into the FILENAME instead of being run. This is used ** for testing and debugging. */ zTempFile = find_option("script",0,1); for(i=firstArg; i<g.argc; i++){ const char *z = g.argv[i]; if( sqlite3_strglob("*}*",z) ){ blob_appendf(&script, " {%/}", z); }else{ int j; blob_append(&script, " ", 1); for(j=0; z[j]; j++) blob_appendf(&script, "\\%03o", (unsigned char)z[j]); } } blob_appendf(&script, "}\n%s", builtin_file("diff.tcl", 0)); if( zTempFile ){ blob_write_to_file(&script, zTempFile); fossil_print("To see diff, run: %s \"%s\"\n", zTclsh, zTempFile); }else{ #if defined(FOSSIL_ENABLE_TCL) Th_FossilInit(TH_INIT_DEFAULT); if( evaluateTclWithEvents(g.interp, &g.tcl, blob_str(&script), blob_size(&script), 1, 1, 0)==TCL_OK ){ blob_reset(&script); return; } /* * If evaluation of the Tcl script fails, the reason may be that Tk * could not be found by the loaded Tcl, or that Tcl cannot be loaded * dynamically (e.g. x64 Tcl with x86 Fossil). Therefore, fallback * to using the external "tclsh", if available. */ #endif zTempFile = write_blob_to_temp_file(&script); zCmd = mprintf("%$ %$", zTclsh, zTempFile); fossil_system(zCmd); file_delete(zTempFile); fossil_free(zCmd); } blob_reset(&script); } /* ** Returns the GLOB pattern for file names that should be treated as binary ** by the diff subsystem, if any. */ const char *diff_get_binary_glob(void){ const char *zBinGlob = find_option("binary", 0, 1); if( zBinGlob==0 ) zBinGlob = db_get("binary-glob",0); return zBinGlob; } /* ** COMMAND: diff ** COMMAND: gdiff ** ** Usage: %fossil diff|gdiff ?OPTIONS? ?FILE1? ?FILE2 ...? ** ** Show the difference between the current version of each of the FILEs ** specified (as they exist on disk) and that same file as it was checked ** out. Or if the FILE arguments are omitted, show all unsaved changes ** currently in the working check-out. ** ** The default output format is a "unified patch" (the same as the ** output of "diff -u" on most unix systems). Many alternative formats ** are available. A few of the more useful alternatives: ** ** --tk Pop up a TCL/TK-based GUI to show the diff ** --by Show a side-by-side diff in the default web browser ** -b Show a linear diff in the default web browser ** -y Show a text side-by-side diff ** --webpage Format output as HTML ** --webpage -y HTML output in the side-by-side format ** ** The "--from VERSION" option is used it specifies the source check-in ** for the diff operation. If not specified, the source check-in is the ** base check-in for the current check-out. Similarly, the "--to VERSION" ** option specifies the check-in from which the second version of the file ** or files is taken. If there is no "--to" option then the (possibly edited) ** files in the current check-out are used. The "--checkin VERSION" option ** shows the changes made by check-in VERSION relative to its primary parent. ** The "--branch BRANCHNAME" shows all the changes on the branch BRANCHNAME. ** ** The "-i" command-line option forces the use of Fossils own the internal ** diff logic rather than any external diff program that might be configured ** using the "setting" command. If no external diff program is configured, ** then the "-i" option is a no-op. The "-i" option converts "gdiff" into ** "diff". ** ** The "--diff-binary" option enables or disables the inclusion of binary files ** when using an external diff program. ** ** The "--binary" option causes files matching the glob PATTERN to be treated ** as binary when considering if they should be used with external diff program. ** This option overrides the "binary-glob" setting. ** ** These command show differences between managed files. Use the "fossil xdiff" ** command to see differences in unmanaged files. ** ** Options: ** --binary PATTERN Treat files that match the glob PATTERN ** as binary ** --branch BRANCH Show diff of all changes on BRANCH ** --brief Show filenames only ** -b|--browser Show the diff output in a web-browser ** --by Shorthand for "--browser -y" ** --checkin VERSION Show diff of all changes in VERSION ** --command PROG External diff program. Overrides "diff-command" ** -c|--context N Show N lines of context around each change ** --diff-binary BOOL Include binary files with external commands ** --exec-abs-paths Force absolute path names on external commands ** --exec-rel-paths Force relative path names on external commands ** -r|--from VERSION Select VERSION as source for the diff ** -w|--ignore-all-space Ignore white space when comparing lines ** -i|--internal Use internal diff logic ** --json Output formatted as JSON ** -N|--new-file Alias for --verbose ** --numstat Show only the number of lines delete and added ** -y|--side-by-side Side-by-side diff ** --strip-trailing-cr Strip trailing CR ** --tcl TCL-formated output used internally by --tk ** --tclsh PATH TCL/TK used for --tk (default: "tclsh") ** --tk Launch a Tcl/Tk GUI for display ** --to VERSION Select VERSION as target for the diff ** --undo Diff against the "undo" buffer ** --unified Unified diff ** -v|--verbose Output complete text of added or deleted files ** --webpage Format output as a stand-alone HTML webpage ** -W|--width N Width of lines in side-by-side diff ** -Z|--ignore-trailing-space Ignore changes to end-of-line whitespace */ void diff_cmd(void){ int isGDiff; /* True for gdiff. False for normal diff */ const char *zFrom; /* Source version number */ const char *zTo; /* Target version number */ const char *zCheckin; /* Check-in version number */ const char *zBranch; /* Branch to diff */ int againstUndo = 0; /* Diff against files in the undo buffer */ FileDirList *pFileDir = 0; /* Restrict the diff to these files */ DiffConfig DCfg; /* Diff configuration object */ if( find_option("tk",0,0)!=0 || has_option("tclsh") ){ diff_tk("diff", 2); return; } isGDiff = g.argv[1][0]=='g'; zFrom = find_option("from", "r", 1); zTo = find_option("to", 0, 1); zCheckin = find_option("checkin", 0, 1); zBranch = find_option("branch", 0, 1); againstUndo = find_option("undo",0,0)!=0; if( againstUndo && ( zFrom!=0 || zTo!=0 || zCheckin!=0 || zBranch!=0) ){ fossil_fatal("cannot use --undo together with --from, --to, --checkin," " or --branch"); } if( zBranch ){ if( zTo || zFrom || zCheckin ){ fossil_fatal("cannot use --from, --to, or --checkin with --branch"); } zTo = zBranch; zFrom = mprintf("root:%s", zBranch); } if( zCheckin!=0 && ( zFrom!=0 || zTo!=0 ) ){ fossil_fatal("cannot use --checkin together with --from or --to"); } g.diffCnt[0] = g.diffCnt[1] = g.diffCnt[2] = 0; if( zTo==0 || againstUndo ){ db_must_be_within_tree(); }else if( zFrom==0 ){ fossil_fatal("must use --from if --to is present"); }else{ db_find_and_open_repository(0, 0); } diff_options(&DCfg, isGDiff, 0); determine_exec_relative_option(1); verify_all_options(); if( g.argc>=3 ){ int i; Blob fname; pFileDir = fossil_malloc( sizeof(*pFileDir) * (g.argc-1) ); memset(pFileDir, 0, sizeof(*pFileDir) * (g.argc-1)); for(i=2; i<g.argc; i++){ file_tree_name(g.argv[i], &fname, 0, 1); pFileDir[i-2].zName = fossil_strdup(blob_str(&fname)); if( strcmp(pFileDir[i-2].zName,".")==0 ){ pFileDir[0].zName[0] = '.'; pFileDir[0].zName[1] = 0; break; } pFileDir[i-2].nName = blob_size(&fname); pFileDir[i-2].nUsed = 0; blob_reset(&fname); } } if ( zCheckin!=0 ){ int ridTo = name_to_typed_rid(zCheckin, "ci"); zTo = zCheckin; zFrom = db_text(0, "SELECT uuid FROM blob, plink" " WHERE plink.cid=%d AND plink.isprim AND plink.pid=blob.rid", ridTo); if( zFrom==0 ){ fossil_fatal("check-in %s has no parent", zTo); } } diff_begin(&DCfg); if( againstUndo ){ if( db_lget_int("undo_available",0)==0 ){ fossil_print("No undo or redo is available\n"); return; } diff_against_undo(&DCfg, pFileDir); }else if( zTo==0 ){ diff_against_disk(zFrom, &DCfg, pFileDir, 0); }else{ diff_two_versions(zFrom, zTo, &DCfg, pFileDir); } if( pFileDir ){ int i; for(i=0; pFileDir[i].zName; i++){ if( pFileDir[i].nUsed==0 && strcmp(pFileDir[0].zName,".")!=0 && !file_isdir(g.argv[i+2], ExtFILE) ){ fossil_fatal("not found: '%s'", g.argv[i+2]); } fossil_free(pFileDir[i].zName); } fossil_free(pFileDir); } diff_end(&DCfg, 0); if ( DCfg.diffFlags & DIFF_NUMSTAT ){ fossil_print("%10d %10d TOTAL over %d changed files\n", g.diffCnt[1], g.diffCnt[2], g.diffCnt[0]); } } /* ** WEBPAGE: vpatch ** URL: /vpatch?from=FROM&to=TO ** ** Show a patch that goes from check-in FROM to check-in TO. */ void vpatch_page(void){ const char *zFrom = P("from"); const char *zTo = P("to"); DiffConfig DCfg; login_check_credentials(); if( !g.perm.Read ){ login_needed(g.anon.Read); return; } if( zFrom==0 || zTo==0 ) fossil_redirect_home(); cgi_set_content_type("text/plain"); diff_config_init(&DCfg, DIFF_VERBOSE); diff_two_versions(zFrom, zTo, &DCfg, 0); }