diff options
| author | cashmere <cashmere@autistici.org> | 2026-09-04 09:05:24 +0200 |
|---|---|---|
| committer | cashmere <cashmere@autistici.org> | 2026-09-04 09:05:24 +0200 |
| commit | 4f49b16079b27d7ce633619e4cddfdf2bffbc952 (patch) | |
| tree | c296c1e7f3c3e2f6859c6d3a1b1b208779b89679 /src/diffcmd.c | |
| download | relic-4f49b16079b27d7ce633619e4cddfdf2bffbc952.tar.gz | |
Diffstat (limited to 'src/diffcmd.c')
| -rw-r--r-- | src/diffcmd.c | 1595 |
1 files changed, 1595 insertions, 0 deletions
diff --git a/src/diffcmd.c b/src/diffcmd.c new file mode 100644 index 0000000..ce295e2 --- /dev/null +++ b/src/diffcmd.c @@ -0,0 +1,1595 @@ +/* +** 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 details about the compared versions - possibly the working directory +** or the undo buffer. For check-ins, show hash and commit time. +** +** This is intended primarily to go into the "header garbage" that is ignored +** by patch(1). +** +** zFrom and zTo are interpreted as symbolic version names, unless they +** start with '(', in which case they are printed directly. +*/ +void diff_print_versions(const char *zFrom, const char *zTo, DiffConfig *pCfg){ + if( (pCfg->diffFlags & (DIFF_SIDEBYSIDE|DIFF_BRIEF|DIFF_NUMSTAT| + DIFF_HTML|DIFF_WEBPAGE|DIFF_BROWSER|DIFF_JSON|DIFF_TCL))==0 ){ + fossil_print("Fossil-Diff-From: %s\n", + zFrom[0]=='(' ? zFrom : mprintf("%S %s", + rid_to_uuid(symbolic_name_to_rid(zFrom, "ci")), + db_text("","SELECT datetime(%f)||' UTC'", + symbolic_name_to_mtime(zFrom, 0, 0)))); + fossil_print("Fossil-Diff-To: %s\n", + zTo[0]=='(' ? zTo : mprintf("%S %s", + rid_to_uuid(symbolic_name_to_rid(zTo, "ci")), + db_text("","SELECT datetime(%f)||' UTC'", + symbolic_name_to_mtime(zTo, 0, 1)))); + fossil_print("%.66c\n", '-'); + } +} + +/* +** 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; + /* Standardize on /dev/null, regardless of platform. */ + if( pCfg->diffFlags & DIFF_FILE_ADDED ) zLeft = "/dev/null"; + if( pCfg->diffFlags & DIFF_FILE_DELETED ) zRight = "/dev/null"; + if( pCfg->azLabel[0] ) zLeft = pCfg->azLabel[0]; + if( pCfg->azLabel[1] ) zRight = pCfg->azLabel[1]; + 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\t\n+++ %s\t\n", zLeft, zRight); + } +} + + +/* +** Default header texts for diff with --webpage +*/ +static const char zWebpageHdr[] = +@ <!DOCTYPE html> +@ <html> +@ <head> +@ <meta charset="UTF-8"> +@ <style> +@ body { +@ background-color: white; +@ } +@ h1 { +@ font-size: 150%; +@ } +@ +@ table.diff { +@ width: 100%; +@ border-spacing: 0; +@ border: 1px solid black; +@ line-height: inherit; +@ font-size: inherit; +@ } +@ table.diff td { +@ vertical-align: top; +@ line-height: inherit; +@ font-size: inherit; +@ } +@ table.diff pre { +@ margin: 0 0 0 0; +@ line-height: inherit; +@ font-size: inherit; +@ } +@ td.diffln { +@ width: fit-content; +@ text-align: right; +@ padding: 0 1em 0 0; +@ } +@ td.difflne { +@ padding-bottom: 0.4em; +@ } +@ td.diffsep { +@ width: fit-content; +@ padding: 0 0.3em 0 1em; +@ line-height: inherit; +@ font-size: inherit; +@ } +@ td.diffsep pre { +@ line-height: inherit; +@ font-size: inherit; +@ } +@ td.difftxt pre { +@ overflow-x: auto; +@ } +@ td.diffln ins { +@ background-color: #a0e4b2; +@ text-decoration: none; +@ line-height: inherit; +@ font-size: inherit; +@ } +@ td.diffln del { +@ background-color: #ffc0c0; +@ text-decoration: none; +@ line-height: inherit; +@ font-size: inherit; +@ } +@ td.difftxt del { +@ background-color: #ffe8e8; +@ text-decoration: none; +@ line-height: inherit; +@ font-size: inherit; +@ } +@ 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; +@ line-height: inherit; +@ font-size: inherit; +@ } +@ 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; +@ } +@ @media (prefers-color-scheme: dark) { +@ body { +@ background-color: #353535; +@ color: #ffffff; +@ } +@ td.diffln ins { +@ background-color: #559855; +@ color: #000000; +@ } +@ td.diffln del { +@ background-color: #cc5555; +@ color: #000000; +@ } +@ td.difftxt del { +@ background-color: #f9cfcf; +@ color: #000000; +@ } +@ td.difftxt del > del { +@ background-color: #cc5555; +@ color: #000000; +@ } +@ td.difftxt ins { +@ background-color: #a2dbb2; +@ color: #000000; +@ } +@ td.difftxt ins > ins { +@ background-color: #559855; +@ } +@ } +@ +@ </style> +@ </head> +@ <body> +; +static const char zWebpageHdrDark[] = +@ <!DOCTYPE html> +@ <html> +@ <head> +@ <meta charset="UTF-8"> +@ <style> +@ body { +@ background-color: #353535; +@ color: #ffffff; +@ } +@ h1 { +@ font-size: 150%; +@ } +@ +@ table.diff { +@ width: 100%; +@ border-spacing: 0; +@ border: 1px solid black; +@ line-height: inherit; +@ font-size: inherit; +@ } +@ table.diff td { +@ vertical-align: top; +@ line-height: inherit; +@ font-size: inherit; +@ } +@ table.diff pre { +@ margin: 0 0 0 0; +@ line-height: inherit; +@ font-size: inherit; +@ } +@ td.diffln { +@ width: fit-content; +@ text-align: right; +@ padding: 0 1em 0 0; +@ } +@ td.difflne { +@ padding-bottom: 0.4em; +@ } +@ td.diffsep { +@ width: fit-content; +@ padding: 0 0.3em 0 1em; +@ line-height: inherit; +@ font-size: inherit; +@ } +@ td.diffsep pre { +@ line-height: inherit; +@ font-size: inherit; +@ } +@ td.difftxt pre { +@ overflow-x: auto; +@ } +@ td.diffln ins { +@ background-color: #559855; +@ color: #000000; +@ text-decoration: none; +@ line-height: inherit; +@ font-size: inherit; +@ } +@ td.diffln del { +@ background-color: #cc5555; +@ color: #000000; +@ text-decoration: none; +@ line-height: inherit; +@ font-size: inherit; +@ } +@ td.difftxt del { +@ background-color: #f9cfcf; +@ color: #000000; +@ text-decoration: none; +@ line-height: inherit; +@ font-size: inherit; +@ } +@ td.difftxt del > del { +@ background-color: #cc5555; +@ color: #000000; +@ 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: #a2dbb2; +@ color: #000000; +@ text-decoration: none; +@ line-height: inherit; +@ font-size: inherit; +@ } +@ td.difftxt ins > ins { +@ background-color: #559855; +@ 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",(pCfg->diffFlags & DIFF_DARKMODE)!=0 ? zWebpageHdrDark : + 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( pCfg->diffFlags & DIFF_FILE_DELETED || 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) && !(pCfg->diffFlags & DIFF_NUMSTAT) ){ + 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 ){ + if( !(pCfg->diffFlags & DIFF_BRIEF) ){ + 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 */ + int useTempfile = 1; + + 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"); +#if !defined(_WIN32) + /* On Unix, use /dev/null for added or deleted files. */ + if( pCfg->diffFlags & DIFF_FILE_ADDED ){ + blob_init(&nameFile1, NULL_DEVICE, -1); + useTempfile = 0; + }else if( pCfg->diffFlags & DIFF_FILE_DELETED ){ + zFile2 = NULL_DEVICE; + } +#endif + if( useTempfile ) 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 */ + if( useTempfile ) 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) && !(pCfg->diffFlags & DIFF_NUMSTAT) ){ + return; + } + if( pCfg->zDiffCmd==0 ){ + Blob out; /* Diff output text */ + + blob_zero(&out); + text_diff(pFile1, pFile2, &out, pCfg); + if( pCfg->diffFlags & DIFF_NUMSTAT ){ + if( !(pCfg->diffFlags & DIFF_BRIEF) ){ + 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; + int useTempfile1 = 1; + int useTempfile2 = 1; + + 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 temporary file names */ + file_tempname(&temp1, zName, "before"); + file_tempname(&temp2, zName, "after"); +#if !defined(_WIN32) + /* On Unix, use /dev/null for added or deleted files. */ + if( pCfg->diffFlags & DIFF_FILE_ADDED ){ + useTempfile1 = 0; + blob_init(&temp1, NULL_DEVICE, -1); + }else if( pCfg->diffFlags & DIFF_FILE_DELETED ){ + useTempfile2 = 0; + blob_init(&temp2, NULL_DEVICE, -1); + } +#endif + if( useTempfile1 ) blob_write_to_file(pFile1, blob_str(&temp1)); + if( useTempfile2 ) 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 */ + if( useTempfile1 ) file_delete(blob_str(&temp1)); + if( useTempfile2 ) file_delete(blob_str(&temp2)); + + blob_reset(&temp1); + blob_reset(&temp2); + blob_reset(&cmd); + } +} + +/* +** Return true if the disk file is identical to the Blob. Return zero +** if the files differ in any way. +*/ +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 in the current +** working checkout. 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_version_to_checkout( + 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-existent 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 + ); + } + if( (pCfg->diffFlags & DIFF_SHOW_VERS)!=0 ){ + diff_print_versions(zFrom ? zFrom : db_lget("checkout-hash", 0), + "(workdir)", pCfg); + } + 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); + pCfg->diffFlags &= (~DIFF_FILE_MASK); + if( isDeleted ){ + if( !isNumStat ){ fossil_print("DELETED %s\n", zPathname); } + pCfg->diffFlags |= DIFF_FILE_DELETED; + 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); } + pCfg->diffFlags |= DIFF_FILE_ADDED; + srcid = 0; + if( !asNewFile ){ showDiff = 0; } + }else if( isChnged==3 ){ + if( !isNumStat ){ fossil_print("ADDED_BY_MERGE %s\n", zPathname); } + pCfg->diffFlags |= DIFF_FILE_ADDED; + srcid = 0; + if( !asNewFile ){ showDiff = 0; } + }else if( isChnged==5 ){ + if( !isNumStat ){ fossil_print("ADDED_BY_INTEGRATE %s\n", zPathname); } + pCfg->diffFlags |= DIFF_FILE_ADDED; + 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 + || pCfg->diffFlags & DIFF_FILE_DELETED + || !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 from the undo buffer to 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_undo_to_checkout( + 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); + if( (pCfg->diffFlags & DIFF_SHOW_VERS)!=0 ){ + diff_print_versions("(undo)", "(workdir)", pCfg); + } + 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) && !(pCfg->diffFlags & DIFF_NUMSTAT) ){ + 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); + if( (pCfg->diffFlags & DIFF_SHOW_VERS)!=0 ){ + diff_print_versions(zFrom, zTo, pCfg); + } + while( pFromFile || pToFile ){ + int cmp; + if( pFromFile==0 ){ + cmp = +1; + }else if( pToFile==0 ){ + cmp = -1; + }else{ + cmp = fossil_strcmp(pFromFile->zName, pToFile->zName); + } + pCfg->diffFlags &= (~DIFF_FILE_MASK); + 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); + } + pCfg->diffFlags |= DIFF_FILE_DELETED; + 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); + } + pCfg->diffFlags |= DIFF_FILE_ADDED; + 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) && !(pCfg->diffFlags & DIFF_NUMSTAT)){ + 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); +} + +/* +** Compute the difference from an external tree of files to the current +** working checkout with its edits. +** +** To put it another way: Every managed file in the current working +** checkout is compared to the file with same name under zExternBase. The +** zExternBase files are on the left and the files in the current working +** directory are on the right. +*/ +void diff_externbase_to_checkout( + const char *zExternBase, /* Remote tree to use as the baseline */ + DiffConfig *pCfg, /* Diff settings */ + FileDirList *pFileDir /* Only look at these files */ +){ + int vid; + Stmt q; + + vid = db_lget_int("checkout",0); + if( file_isdir(zExternBase, ExtFILE)!=1 ){ + fossil_fatal("\"%s\" is not a directory", zExternBase); + } + db_prepare(&q, + "SELECT pathname FROM vfile WHERE vid=%d ORDER BY pathname", + vid + ); + while( db_step(&q)==SQLITE_ROW ){ + const char *zFile; /* Name of file in the repository */ + char *zLhs; /* Full name of left-hand side file */ + char *zRhs; /* Full name of right-hand side file */ + Blob rhs; /* Full text of RHS */ + Blob lhs; /* Full text of LHS */ + + zFile = db_column_text(&q,0); + if( !file_dir_match(pFileDir, zFile) ) continue; + zLhs = mprintf("%s/%s", zExternBase, zFile); + zRhs = mprintf("%s%s", g.zLocalRoot, zFile); + if( file_size(zLhs, ExtFILE)<0 ){ + blob_zero(&lhs); + }else{ + blob_read_from_file(&lhs, zLhs, ExtFILE); + } + blob_read_from_file(&rhs, zRhs, ExtFILE); + if( blob_size(&lhs)!=blob_size(&rhs) + || memcmp(blob_buffer(&lhs), blob_buffer(&rhs), blob_size(&lhs))!=0 + ){ + diff_print_index(zFile, pCfg, 0); + diff_file_mem(&lhs, &rhs, zFile, pCfg); + } + blob_reset(&lhs); + blob_reset(&rhs); + fossil_free(zLhs); + fossil_free(zRhs); + } + db_finalize(&q); +} + + +/* +** 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 *zName; + zName = guiDiff ? "gdiff-command" : "diff-command"; + return db_get(zName, 0); +} + +/* +** Return true if it reasonable to run "diff -tk" for "gdiff". +** +** Details: Return true if all of the following are true: +** +** (1) The isGDiff flags is true +** (2) The "gdiff-command" setting is undefined +** (3) There is a "tclsh" on PATH +** (4) There is a "wish" on PATH +*/ +int gdiff_using_tk(int isGdiff){ + if( isGdiff + && db_get("gdiff-command","")[0]==0 + && fossil_app_on_path("tclsh",0) + && fossil_app_on_path("wish",0) + ){ + return 1; + } + return 0; +} + +/* +** A TCL list is being constructed in pBlob. Append a single list +** element z[] to that list. Do all necessary quoting and escaping. +** The content of z[] might be controlled by an attacker. +*/ +static void diff_tcl_lappend(Blob *pBlob, const char *z, int isFilename){ + if( strchr(z,'{')==0 && strchr(z,'}')==0 ){ + if( isFilename ){ + blob_appendf(pBlob, " {%/}", z); + }else{ + blob_appendf(pBlob, " {%s}", z); + } + }else{ + int j; + blob_append_char(pBlob, ' '); + for(j=0; z[j]; j++){ + if( fossil_isalnum(z[j]) ){ + blob_append_char(pBlob, z[j]); + }else{ + blob_appendf(pBlob, "\\%03o", (unsigned char)z[j]); + } + } + } +} + +/* +** Return TRUE (non-zero) if zArg is a valid option to "diff" or +** "gdiff" that takes an argument that might be confused for a +** HOST:PATH or USER@HOST:PATH filename. +*/ +static int isDiffOptionWithArg(const char *zArg){ + int i; + const char *az[] = { + "binary", + "branch", + "ci", "checkin", + "r", "from", + "to" + }; + if( zArg[0]!='-' ) return 0; + zArg++; + if( zArg[0]=='-' ) zArg++; + for(i=0; i<count(az); i++){ + if( fossil_strcmp(zArg, az[i])==0 ) return 1; + } + return 0; +} + +/* +** 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; + const char *zHost = 0; + const char *zDir = 0; + int bDebug = find_option("tkdebug",0,0)!=0; + int bDarkMode = find_option("dark",0,0)!=0; + (void)find_option("debug",0,0); + blob_zero(&script); + /* Caution: When this routine is called from the merge-info command, + ** the --tcl argument requires an argument. But merge-info does not + ** use -i, so we can take -i as that argument. This routine needs to + ** always have -i after --tcl. + ** CAUTION! + ** vvvvvvv */ + 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( zHost==0 + && (i==firstArg || !isDiffOptionWithArg(g.argv[i-1])) + && !file_isfile_or_link(z) + && (zDir = file_skip_userhost(z))!=0 + ){ + zHost = mprintf("%.*s", (int)(zDir - z - 1), z); + continue; + } + diff_tcl_lappend(&script, z, 1); + } + blob_appendf(&script, "}\nset darkmode %d\n", bDarkMode); + blob_appendf(&script, "set debug %d\n", bDebug); + if( zHost ){ + blob_appendf(&script, "set remotehost"); + diff_tcl_lappend(&script, zHost, 0); + blob_appendf(&script, "\nset remotedir"); + diff_tcl_lappend(&script, zDir, 1); + blob_append_char(&script, '\n'); + } + blob_appendf(&script, "%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 committed changes +** currently in the working check-out. The "gdiff" variant means to +** use a GUI diff specified by the "gdiff-command" setting. If +** gdiff-command is unset or is an empty string, then "gdiff" behaves +** like "diff --tk". +** +** 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 to specify 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. +** +** With the "--from VERSION" option, if VERSION is actually a directory name +** (not a tag or check-in hash) then the files under that directory are used +** as the baseline for the diff. +** +** The "-i" command-line option forces the use of Fossil's own 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 the external diff +** program. This option overrides the "binary-glob" setting. +** +** If the "--tk" option is used (or "gdiff" is used and no third-party +** graphical diff tool is specified) then an additional argument of +** the form "USER@HOST:DIRECTORY" or "HOST:DIRECTORY" can be added to specify +** a remote host and directory from which the diff should be taken. If this +** happens, Fossil will run the diff using SSH, capture the results, then +** display the results locally using the Tcl/Tk diff GUI. +** +** These commands 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" +** -ci|--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, +** with negative N meaning show all content +** --dark Use dark mode for the Tcl/Tk-based GUI and HTML +** --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 Use VERSION as the baseline for the diff, or +** if VERSION is a directory name, use files in +** that directory as the baseline. +** -w|--ignore-all-space Ignore white space when comparing lines +** -i|--internal Use internal diff logic +** --invert Invert the diff +** --json Output formatted as JSON +** -n|--linenum Show line numbers +** -N|--new-file Alias for --verbose +** -s|--numstat Show the number of added and deleted lines per +** file, omitting the diff. When combined +** with --brief, show only the total row. +** -y|--side-by-side Side-by-side diff +** --strip-trailing-cr Strip trailing CR +** --tcl Tcl-formatted output used internally by --tk +** --tclsh PATH Tcl/Tk shell used for --tk (default: "tclsh") +** --tk Launch a Tcl/Tk GUI for display +** --to VERSION Select VERSION as target for the diff +** --undo Use the undo buffer as the baseline +** --unified Unified diff +** -v|--verbose Output complete text of added or deleted files +** -h|--versions Show compared versions in the diff header +** --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 */ + int bFromIsDir = 0; /* True if zFrom is a directory name */ + + isGDiff = g.argv[1][0]=='g'; + if( find_option("tk",0,0)!=0 + || has_option("tclsh") + || gdiff_using_tk(isGDiff) + ){ + diff_tk("diff", 2); + return; + } + zFrom = find_option("from", "r", 1); + zTo = find_option("to", 0, 1); + zCheckin = find_option("checkin", "ci", 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("merge-in:%s", zBranch); + zBranch = 0; + } + if( zCheckin!=0 && (zFrom!=0 || zTo!=0) ){ + fossil_fatal("cannot use --checkin together with --from or --to"); + } + if( 0==zCheckin ){ + 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); + } + }else{ + db_find_and_open_repository(0, 0); + } + determine_exec_relative_option(1); + if( zFrom!=file_tail(zFrom) + && file_isdir(zFrom, ExtFILE)==1 + && !db_exists("SELECT 1 FROM tag WHERE tagname='sym-%q'", zFrom) + ){ + bFromIsDir = 1; + if( zTo ){ + fossil_fatal("cannot use --to together with \"--from PATH\""); + } + } + diff_options(&DCfg, isGDiff, 0); + verify_all_options(); + g.diffCnt[0] = g.diffCnt[1] = g.diffCnt[2] = 0; + 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( (DCfg.diffFlags & DIFF_NUMSTAT) && !(DCfg.diffFlags & DIFF_BRIEF) ){ + fossil_print("%10s %10s\n", "INSERTED", "DELETED"); + } + 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( bFromIsDir ){ + diff_externbase_to_checkout(zFrom, &DCfg, pFileDir); + }else if( againstUndo ){ + if( db_lget_int("undo_available",0)==0 ){ + fossil_print("No undo or redo is available\n"); + return; + } + diff_undo_to_checkout(&DCfg, pFileDir); + }else if( zTo==0 ){ + diff_version_to_checkout(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 file%s\n", + g.diffCnt[1], g.diffCnt[2], g.diffCnt[0], g.diffCnt[0]!=1 ? "s": ""); + } +} + +/* +** 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; + cgi_check_for_malice(); + login_check_credentials(); + if( !g.perm.Read ){ login_needed(g.anon.Read); return; } + if( zFrom==0 || zTo==0 ) fossil_redirect_home(); + if( robot_restrict("diff") ) return; + + fossil_nice_default(); + cgi_set_content_type("text/plain"); + diff_config_init(&DCfg, DIFF_VERBOSE); + diff_two_versions(zFrom, zTo, &DCfg, 0); +} |
