/* ** 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 merge two or more branches into ** a single tree. */ #include "config.h" #include "merge.h" #include /* ** Bring up a Tcl/Tk GUI to show details of the most recent merge. */ static void merge_info_tk(int bDark, int bAll, int nContext){ int i; Blob script; const char *zTempFile = 0; char *zCmd; const char *zTclsh; 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); verify_all_options(); blob_zero(&script); blob_appendf(&script, "set ncontext %d\n", nContext); blob_appendf(&script, "set fossilcmd {| \"%/\" merge-info}\n", g.nameOfExe); blob_appendf(&script, "set filelist [list"); if( g.argc==2 ){ /* No files named on the command-line. Use every file mentioned ** in the MERGESTAT table to generate the file list. */ Stmt q; int cnt = 0; db_prepare(&q, "SELECT coalesce(fnr,fn), op FROM mergestat %s ORDER BY 1", bAll ? "" : "WHERE op IN ('MERGE','CONFLICT')" /*safe-for-%s*/ ); while( db_step(&q)==SQLITE_ROW ){ blob_appendf(&script," %s ", db_column_text(&q,1)); blob_append_tcl_literal(&script, db_column_text(&q,0), db_column_bytes(&q,0)); cnt++; } db_finalize(&q); if( cnt==0 ){ fossil_print( "No interesting changes in this merge. Use --all to see everything\n" ); return; } }else{ /* Use only files named on the command-line in the file list. ** But verify each file named is actually found in the MERGESTAT ** table first. */ for(i=2; i0 ){ /* The origin file had been edited so we'll have to pull its ** original content out of the undo buffer */ Stmt q2; db_prepare(&q2, "SELECT content FROM undo" " WHERE pathname=%Q AND octet_length(content)=%d", zFN, sz ); blob_zero(&v1); if( db_step(&q2)==SQLITE_ROW ){ db_column_blob(&q, 0, &v1); }else{ mb.zV1 = "(local content missing)"; } db_finalize(&q2); }else{ /* The origin file was unchanged when the merge first occurred */ content_get(rid, &v1); } } mb.pV1 = &v1; /* Set up the output */ zFN = db_column_text(&q, 7); if( zFN==0 ){ mb.zOut = "(Merge Result)"; }else{ mb.zOut = mprintf("%s (after merge)", file_tail(zFN)); } blob_zero(&out); mb.pOut = &out; merge_three_blobs(&mb); blob_write_to_file(&out, "-"); mb.xDestroy(&mb); blob_reset(&pivot); blob_reset(&v1); blob_reset(&v2); blob_reset(&out); db_finalize(&q); } /* ** COMMAND: merge-info ** ** Usage: %fossil merge-info [OPTIONS] ** ** Display information about the most recent merge operation. ** ** Options: ** -a|--all Show all all file changes that happened because of ** the merge. Normally only MERGE, CONFLICT, and ERROR ** lines are shown ** -c|--context N Show N lines of context around each change, ** with negative N meaning show all content. Only ** meaningful in combination with --tcl or --tk. ** --dark Use dark mode for the Tcl/Tk-based GUI ** --tcl FILE Generate (to stdout) a TCL list containing ** information needed to display the changes to ** FILE caused by the most recent merge. FILE must ** be a pathname relative to the root of the check-out. ** --tk Bring up a Tcl/Tk GUI that shows the changes ** associated with the most recent merge. ** */ void merge_info_cmd(void){ const char *zCnt; const char *zTcl; int bTk; int bDark; int bAll; int nContext; Stmt q; const char *zWhere; int cnt = 0; db_must_be_within_tree(); zTcl = find_option("tcl", 0, 1); bTk = find_option("tk", 0, 0)!=0; zCnt = find_option("context", "c", 1); bDark = find_option("dark", 0, 0)!=0; bAll = find_option("all", "a", 0)!=0; if( bTk==0 ){ verify_all_options(); if( g.argc>2 ){ usage("[OPTIONS]"); } } if( zCnt ){ nContext = atoi(zCnt); if( nContext<0 ) nContext = 0xfffffff; }else{ nContext = 6; } if( !db_table_exists("localdb","mergestat") ){ if( zTcl ){ fossil_print("ERROR {no merge data available}\n"); }else{ fossil_print("No merge data is available\n"); } return; } if( bTk ){ merge_info_tk(bDark, bAll, nContext); return; } if( zTcl ){ merge_info_tcl(zTcl, nContext); return; } if( bAll ){ zWhere = ""; }else{ zWhere = "WHERE op IN ('MERGE','CONFLICT','ERROR')"; } db_prepare(&q, /* 0 1 2 */ "SELECT op, coalesce(fnr,fn), msg" " FROM mergestat" " %s" " ORDER BY coalesce(fnr,fn)", zWhere /*safe-for-%s*/ ); while( db_step(&q)==SQLITE_ROW ){ const char *zOp = db_column_text(&q, 0); const char *zName = db_column_text(&q, 1); const char *zErr = db_column_text(&q, 2); if( zErr && fossil_strcmp(zOp,"CONFLICT")!=0 ){ fossil_print("%-9s %s (%s)\n", zOp, zName, zErr); }else{ fossil_print("%-9s %s\n", zOp, zName); } cnt++; } db_finalize(&q); if( !bAll && cnt==0 ){ fossil_print( "No interesting change in this merge. Use --all to see everything.\n" ); } } /* ** Erase all information about prior merges. Do this, for example, after ** a commit. */ void merge_info_forget(void){ db_multi_exec("DROP TABLE IF EXISTS localdb.mergestat"); } /* ** Initialize the MERGESTAT table. ** ** Notes about mergestat: ** ** * ridv is a positive integer and sz is NULL if the V file contained ** no local edits prior to the merge. If the V file was modified prior ** to the merge then ridv is NULL and sz is the size of the file prior ** to merge. ** ** * fnp, ridp, fn, ridv, and sz are all NULL for a file that was ** added by merge. */ void merge_info_init(void){ db_multi_exec( "DROP TABLE IF EXISTS localdb.mergestat;\n" "CREATE TABLE localdb.mergestat(\n" " op TEXT, -- 'UPDATE', 'ADDED', 'MERGE', etc...\n" " fnp TEXT, -- Name of the pivot file (P)\n" " ridp INT, -- RID for the pivot file\n" " fn TEXT, -- Name of origin file (V)\n" " ridv INT, -- RID for origin file, or NULL if previously edited\n" " sz INT, -- Size of origin file in bytes, NULL if unedited\n" " fnm TEXT, -- Name of the file being merged in (M)\n" " ridm INT, -- RID for the merge-in file\n" " fnr TEXT, -- Name of the final output file, after all renaming\n" " nc INT DEFAULT 0, -- Number of conflicts\n" " msg TEXT -- Error message\n" ");" ); } /* ** Print information about a particular check-in. */ void print_checkin_description(int rid, int indent, const char *zLabel){ Stmt q; db_prepare(&q, "SELECT datetime(mtime,toLocal())," " coalesce(euser,user), coalesce(ecomment,comment)," " (SELECT uuid FROM blob WHERE rid=%d)," " (SELECT group_concat(substr(tagname,5), ', ') FROM tag, tagxref" " WHERE tagname GLOB 'sym-*' AND tag.tagid=tagxref.tagid" " AND tagxref.rid=%d AND tagxref.tagtype>0)" " FROM event WHERE objid=%d", rid, rid, rid); if( db_step(&q)==SQLITE_ROW ){ const char *zTagList = db_column_text(&q, 4); char *zCom; if( zTagList && zTagList[0] ){ zCom = mprintf("%s (%s)", db_column_text(&q, 2), zTagList); }else{ zCom = mprintf("%s", db_column_text(&q,2)); } fossil_print("%-*s [%S] by %s on %s\n%*s", indent-1, zLabel, db_column_text(&q, 3), db_column_text(&q, 1), db_column_text(&q, 0), indent, ""); comment_print(zCom, db_column_text(&q,2), indent, -1, get_comment_format()); fossil_free(zCom); } db_finalize(&q); } /* Pick the most recent leaf that is (1) not equal to vid and (2) ** has not already been merged into vid and (3) the leaf is not ** closed and (4) the leaf is in the same branch as vid. ** ** Set vmergeFlag to control whether the vmerge table is checked. */ int fossil_find_nearest_fork(int vid, int vmergeFlag){ Blob sql; Stmt q; int rid = 0; blob_zero(&sql); blob_append_sql(&sql, "SELECT leaf.rid" " FROM leaf, event" " WHERE leaf.rid=event.objid" " AND leaf.rid!=%d", /* Constraint (1) */ vid ); if( vmergeFlag ){ blob_append_sql(&sql, " AND leaf.rid NOT IN (SELECT merge FROM vmerge)" /* Constraint (2) */ ); } blob_append_sql(&sql, " AND NOT EXISTS(SELECT 1 FROM tagxref" /* Constraint (3) */ " WHERE rid=leaf.rid" " AND tagid=%d" " AND tagtype>0)" " AND (SELECT value FROM tagxref" /* Constraint (4) */ " WHERE tagid=%d AND rid=%d AND tagtype>0) =" " (SELECT value FROM tagxref" " WHERE tagid=%d AND rid=leaf.rid AND tagtype>0)" " ORDER BY event.mtime DESC LIMIT 1", TAG_CLOSED, TAG_BRANCH, vid, TAG_BRANCH ); db_prepare(&q, "%s", blob_sql_text(&sql)); blob_reset(&sql); if( db_step(&q)==SQLITE_ROW ){ rid = db_column_int(&q, 0); } db_finalize(&q); return rid; } /* ** Check content that was received with rcvid and return true if any ** fork was created. */ int fossil_any_has_fork(int rcvid){ static Stmt q; int fForkSeen = 0; if( rcvid==0 ) return 0; db_static_prepare(&q, " SELECT pid FROM plink WHERE pid>0 AND isprim" " AND cid IN (SELECT blob.rid FROM blob" " WHERE rcvid=:rcvid)"); db_bind_int(&q, ":rcvid", rcvid); while( !fForkSeen && db_step(&q)==SQLITE_ROW ){ int pid = db_column_int(&q, 0); if( count_nonbranch_children(pid)>1 ){ compute_leaves(pid,1); if( db_int(0, "SELECT count(*) FROM leaves")>1 ){ int rid = db_int(0, "SELECT rid FROM leaves, event" " WHERE event.objid=leaves.rid" " ORDER BY event.mtime DESC LIMIT 1"); fForkSeen = fossil_find_nearest_fork(rid, db_open_local(0))!=0; } } } db_finalize(&q); return fForkSeen; } /* ** Add an entry to the FV table for all files renamed between ** version N and the version specified by vid. */ static void add_renames( const char *zFnCol, /* The FV column for the filename in vid */ int vid, /* The desired version's- RID */ int nid, /* The check-in rid for the name pivot */ int revOK, /* OK to move backwards (child->parent) if true */ const char *zDebug /* Generate trace output if not NULL */ ){ int nChng; /* Number of file name changes */ int *aChng; /* An array of file name changes */ int i; /* Loop counter */ find_filename_changes(nid, vid, revOK, &nChng, &aChng, zDebug); if( nChng==0 ) return; for(i=0; i=3 ){ int i; /* Mid is specified as an argument on the command-line */ zVersion = g.argv[2]; mid = name_to_typed_rid(zVersion, "ci"); if( mid==0 || !is_a_version(mid) ){ fossil_fatal("not a version: %s", zVersion); } bMultiMerge = g.argc>3; if( bMultiMerge ){ for(i=3; i0", TAG_BRANCH, vid) ); } db_prepare(&q, "SELECT blob.uuid," " datetime(event.mtime,toLocal())," " coalesce(ecomment, comment)," " coalesce(euser, user)" " FROM event, blob" " WHERE event.objid=%d AND blob.rid=%d", mid, mid ); zVersion = 0; if( db_step(&q)==SQLITE_ROW ){ char *zCom = mprintf("Merging fork [%S] at %s by %s: \"%s\"", db_column_text(&q, 0), db_column_text(&q, 1), db_column_text(&q, 3), db_column_text(&q, 2)); comment_print(zCom, db_column_text(&q,2), 0, -1, get_comment_format()); fossil_free(zCom); zVersion = mprintf("%S",db_column_text(&q,0)); } db_finalize(&q); }else{ usage("?OPTIONS? ?VERSION?"); return; } if( zPivot ){ pid = name_to_typed_rid(zPivot, "ci"); if( pid==0 || !is_a_version(pid) ){ fossil_fatal("not a version: %s", zPivot); } if( pickFlag ){ fossil_fatal("incompatible options: --cherrypick and --baseline"); } } if( pickFlag || backoutFlag ){ if( integrateFlag ){ fossil_fatal("incompatible options: --integrate and --cherrypick " "with --backout"); } pid = db_int(0, "SELECT pid FROM plink WHERE cid=%d AND isprim", mid); if( pid<=0 ){ fossil_fatal("cannot find an ancestor for %s", zVersion); } }else{ if( !zPivot ){ pivot_set_primary(mid); pivot_set_secondary(vid); db_prepare(&q, "SELECT merge FROM vmerge WHERE id=0"); while( db_step(&q)==SQLITE_ROW ){ pivot_set_secondary(db_column_int(&q,0)); } db_finalize(&q); pid = pivot_find(0); if( pid<=0 ){ fossil_fatal("cannot find a common ancestor between the current " "check-out and %s", zVersion); } } pivot_set_primary(mid); pivot_set_secondary(vid); nid = pivot_find(1); if( nid!=pid ){ pivot_set_primary(nid); pivot_set_secondary(pid); nid = pivot_find(1); } } if( backoutFlag ){ int t = pid; pid = mid; mid = t; } if( nid==0 ) nid = pid; if( !is_a_version(pid) ){ fossil_fatal("not a version: record #%d", pid); } if( !forceFlag && mid==pid ){ fossil_print("Merge skipped because it is a no-op. " " Use --force to override.\n"); return; } if( integrateFlag && !is_a_leaf(mid)){ fossil_warning("ignoring --integrate: %s is not a leaf", zVersion); integrateFlag = 0; } if( integrateFlag && content_is_private(mid) ){ fossil_warning( "ignoring --integrate: %s is on a private branch" "\n Use \"fossil amend --close\" (after commit) to close the leaf.", zVersion); integrateFlag = 0; } if( verboseFlag ){ print_checkin_description(mid, 12, integrateFlag ? "integrate:" : "merge-from:"); print_checkin_description(pid, 12, "baseline:"); } vfile_check_signature(vid, CKSIG_ENOTFILE); if( nMerge==0 ) db_begin_transaction(); if( !dryRunFlag ) undo_begin(); if( load_vfile_from_rid(mid) && !forceMissingFlag ){ fossil_fatal("missing content, unable to merge"); } if( load_vfile_from_rid(pid) && !forceMissingFlag ){ fossil_fatal("missing content, unable to merge"); } if( zPivot ){ vAncestor = db_exists( "WITH RECURSIVE ancestor(id) AS (" " VALUES(%d)" " UNION" " SELECT pid FROM plink, ancestor" " WHERE cid=ancestor.id AND pid!=%d AND cid!=%d)" "SELECT 1 FROM ancestor WHERE id=%d LIMIT 1", vid, nid, pid, pid ) ? 'p' : 'n'; } if( debugFlag ){ char *z; z = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", nid); fossil_print("N=%-4d %z (file rename pivot)\n", nid, z); z = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", pid); fossil_print("P=%-4d %z (file content pivot)\n", pid, z); z = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", mid); fossil_print("M=%-4d %z (merged-in version)\n", mid, z); z = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", vid); fossil_print("V=%-4d %z (current version)\n", vid, z); fossil_print("vAncestor = '%c'\n", vAncestor); } if( showVfileFlag ) debug_show_vfile(); /* ** The vfile.pathname field is used to match files against each other. The ** FV table contains one row for each each unique filename in ** in the current check-out, the pivot, and the version being merged. */ db_multi_exec( "DROP TABLE IF EXISTS fv;" "CREATE TEMP TABLE fv(\n" " fn TEXT UNIQUE %s,\n" /* The filename */ " idv INTEGER DEFAULT 0,\n" /* VFILE entry for current version */ " idp INTEGER DEFAULT 0,\n" /* VFILE entry for the pivot */ " idm INTEGER DEFAULT 0,\n" /* VFILE entry for version merging in */ " chnged BOOLEAN,\n" /* True if current version has been edited */ " ridv INTEGER DEFAULT 0,\n" /* Record ID for current version */ " ridp INTEGER DEFAULT 0,\n" /* Record ID for pivot */ " ridm INTEGER DEFAULT 0,\n" /* Record ID for merge */ " isexe BOOLEAN,\n" /* Execute permission enabled */ " fnp TEXT UNIQUE %s,\n" /* The filename in the pivot */ " fnm TEXT UNIQUE %s,\n" /* The filename in the merged version */ " fnn TEXT UNIQUE %s,\n" /* The filename in the name pivot */ " islinkv BOOLEAN,\n" /* True if current version is a symlink */ " islinkm BOOLEAN\n" /* True if merged version in is a symlink */ ");", filename_collation(), filename_collation(), filename_collation(), filename_collation() ); /* ** Compute name changes from N to V, P, and M */ add_renames("fn", vid, nid, 0, debugFlag ? "N->V" : 0); add_renames("fnp", pid, nid, 0, debugFlag ? "N->P" : 0); add_renames("fnm", mid, nid, backoutFlag, debugFlag ? "N->M" : 0); if( debugFlag ){ fossil_print("******** FV after name change search *******\n"); debug_fv_dump(1); } if( nid!=pid ){ /* See forum thread https://fossil-scm.org/forum/forumpost/549700437b ** ** If a filename changes between nid and one of the other check-ins ** pid, vid, or mid, then it might not have changed for all of them. ** try to fill in the appropriate filename in all slots where the ** name is missing. ** ** This does not work if ** (1) The filename changes more than once in between nid and vid/mid ** (2) Two or more filenames swap places - for example if A is renamed ** to B and B is renamed to A. ** The Fossil merge algorithm breaks down in those cases. It will need ** to be completely rewritten to handle such complex cases. Such cases ** appear to be rare, and also confusing to humans. */ db_multi_exec( "UPDATE OR IGNORE fv SET fnp=vfile.pathname FROM vfile" " WHERE fnp IS NULL" " AND vfile.pathname = fv.fnn" " AND vfile.vid=%d;", pid ); db_multi_exec( "UPDATE OR IGNORE fv SET fn=vfile.pathname FROM vfile" " WHERE fn IS NULL" " AND vfile.pathname = coalesce(fv.fnp,fv.fnn)" " AND vfile.vid=%d;", vid ); db_multi_exec( "UPDATE OR IGNORE fv SET fnm=vfile.pathname FROM vfile" " WHERE fnm IS NULL" " AND vfile.pathname = coalesce(fv.fnp,fv.fnn)" " AND vfile.vid=%d;", mid ); db_multi_exec( "UPDATE OR IGNORE fv SET fnp=vfile.pathname FROM vfile" " WHERE fnp IS NULL" " AND vfile.pathname IN (fv.fnm,fv.fn)" " AND vfile.vid=%d;", pid ); db_multi_exec( "UPDATE OR IGNORE fv SET fn=vfile.pathname FROM vfile" " WHERE fn IS NULL" " AND vfile.pathname = fv.fnm" " AND vfile.vid=%d;", vid ); db_multi_exec( "UPDATE OR IGNORE fv SET fnm=vfile.pathname FROM vfile" " WHERE fnm IS NULL" " AND vfile.pathname = fv.fn" " AND vfile.vid=%d;", mid ); } if( debugFlag ){ fossil_print("******** FV after name change fill-in *******\n"); debug_fv_dump(1); } /* ** Add files found in V */ db_multi_exec( "UPDATE OR IGNORE fv SET fn=coalesce(fn%c,fnn) WHERE fn IS NULL;" "REPLACE INTO fv(fn,fnp,fnm,fnn,idv,ridv,islinkv,isexe,chnged)" " SELECT pathname, fnp, fnm, fnn, id, rid, islink, vf.isexe, vf.chnged" " FROM vfile vf" " LEFT JOIN fv ON fn=coalesce(origname,pathname)" " AND rid>0 AND vf.chnged NOT IN (3,5)" " WHERE vid=%d;", vAncestor, vid ); if( debugFlag>=2 ){ fossil_print("******** FV after adding files in current version *******\n"); debug_fv_dump(1); } /* ** Add files found in P */ db_multi_exec( "UPDATE OR IGNORE fv SET fnp=coalesce(fnn," " (SELECT coalesce(origname,pathname) FROM vfile WHERE id=idv))" " WHERE fnp IS NULL;" "INSERT OR IGNORE INTO fv(fnp)" " SELECT coalesce(origname,pathname) FROM vfile WHERE vid=%d;", pid ); if( debugFlag>=2 ){ fossil_print("******** FV after adding pivot files *******\n"); debug_fv_dump(1); } /* ** Add files found in M */ db_multi_exec( "UPDATE OR IGNORE fv SET fnm=fnp WHERE fnm IS NULL;" "INSERT OR IGNORE INTO fv(fnm)" " SELECT pathname FROM vfile WHERE vid=%d;", mid ); if( debugFlag>=2 ){ fossil_print("******** FV after adding merge-in files *******\n"); debug_fv_dump(1); } /* ** Compute the file version ids for P and M */ if( pid==vid ){ db_multi_exec( "UPDATE fv SET idp=idv, ridp=ridv WHERE ridv>0 AND chnged NOT IN (3,5)" ); }else{ db_multi_exec( "UPDATE fv SET idp=coalesce(vfile.id,0), ridp=coalesce(vfile.rid,0)" " FROM vfile" " WHERE vfile.vid=%d AND fv.fnp=vfile.pathname", pid ); } db_multi_exec( "UPDATE fv SET" " idm=coalesce(vfile.id,0)," " ridm=coalesce(vfile.rid,0)," " islinkm=coalesce(vfile.islink,0)," " isexe=coalesce(vfile.isexe,fv.isexe)" " FROM vfile" " WHERE vid=%d AND fnm=pathname", mid ); /* ** Update the execute bit on files where it's changed from P->M but not P->V */ db_prepare(&q, "SELECT idv, fn, fv.isexe FROM fv, vfile p, vfile v" " WHERE p.id=idp AND v.id=idv AND fv.isexe!=p.isexe AND v.isexe=p.isexe" ); while( db_step(&q)==SQLITE_ROW ){ int idv = db_column_int(&q, 0); const char *zName = db_column_text(&q, 1); int isExe = db_column_int(&q, 2); fossil_print("%s %s\n", isExe ? "EXECUTABLE" : "UNEXEC", zName); if( !dryRunFlag ){ char *zFullPath = mprintf("%s/%s", g.zLocalRoot, zName); file_setexe(zFullPath, isExe); free(zFullPath); db_multi_exec("UPDATE vfile SET isexe=%d WHERE id=%d", isExe, idv); } } db_finalize(&q); if( debugFlag ){ fossil_print("******** FV final *******\n"); debug_fv_dump( debugFlag>=2 ); } /************************************************************************ ** All of the information needed to do the merge is now contained in the ** FV table. Starting here, we begin to actually carry out the merge. ** ** Begin by constructing the localdb.mergestat table. */ merge_info_init(); /* ** Find files that have changed from P->M but not P->V. ** Copy the M content over into V. */ db_prepare(&q, /* 0 1 2 3 4 5 6 7 */ "SELECT idv, ridm, fn, islinkm, fnp, ridp, ridv, fnm FROM fv" " WHERE idp>0 AND idv>0 AND idm>0" " AND ridm!=ridp AND ridv=ridp AND NOT chnged" ); while( db_step(&q)==SQLITE_ROW ){ int idv = db_column_int(&q, 0); int ridm = db_column_int(&q, 1); const char *zName = db_column_text(&q, 2); int islinkm = db_column_int(&q, 3); /* Copy content from idm over into idv. Overwrite idv. */ fossil_print("UPDATE %s\n", zName); if( !dryRunFlag ){ undo_save(zName); db_multi_exec( "UPDATE vfile SET mtime=0, mrid=%d, chnged=%d, islink=%d," " mhash=CASE WHEN rid<>%d" " THEN (SELECT uuid FROM blob WHERE blob.rid=%d) END" " WHERE id=%d", ridm, integrateFlag?4:2, islinkm, ridm, ridm, idv ); vfile_to_disk(0, idv, 0, 0); } db_multi_exec( "INSERT INTO mergestat(op,fnp,ridp,fn,ridv,fnm,ridm,fnr)" "VALUES('UPDATE',%Q,%d,%Q,%d,%Q,%d,%Q)", /* fnp */ db_column_text(&q, 4), /* ridp */ db_column_int(&q,5), /* fn */ zName, /* ridv */ db_column_int(&q,6), /* fnm */ db_column_text(&q, 7), /* ridm */ ridm, /* fnr */ zName ); } db_finalize(&q); /* ** Do a three-way merge on files that have changes on both P->M and P->V. ** ** Proceed even if the file doesn't exist on P, just like the common ancestor ** of M and V is an empty file. In this case, merge conflict marks will be ** added to the file and user will be forced to take a decision. */ db_prepare(&q, /* 0 1 2 3 4 5 6 7 8 */ "SELECT ridm, idv, ridp, ridv, %s, fn, isexe, islinkv, islinkm," /* 9 10 11 */ " fnp, fnm, chnged" " FROM fv" " WHERE idv>0 AND idm>0" " AND ridm!=ridp AND (ridv!=ridp OR chnged)", glob_expr("fv.fn", zBinGlob) ); while( db_step(&q)==SQLITE_ROW ){ int ridm = db_column_int(&q, 0); int idv = db_column_int(&q, 1); int ridp = db_column_int(&q, 2); int ridv = db_column_int(&q, 3); int isBinary = db_column_int(&q, 4); const char *zName = db_column_text(&q, 5); int isExe = db_column_int(&q, 6); int islinkv = db_column_int(&q, 7); int islinkm = db_column_int(&q, 8); int chnged = db_column_int(&q, 11); int rc; char *zFullPath; const char *zType = "MERGE"; Blob m, p, r; /* Do a 3-way merge of idp->idm into idp->idv. The results go into idv. */ if( verboseFlag ){ fossil_print("MERGE %s (pivot=%d v1=%d v2=%d)\n", zName, ridp, ridm, ridv); }else{ fossil_print("MERGE %s\n", zName); } if( islinkv || islinkm ){ fossil_print("***** Cannot merge symlink %s\n", zName); nConflict++; db_multi_exec( "INSERT INTO mergestat(op,fnp,ridp,fn,ridv,fnm,ridm,fnr,nc,msg)" "VALUES('ERROR',%Q,%d,%Q,%d,%Q,%d,%Q,1,'cannot merge symlink')", /* fnp */ db_column_text(&q, 9), /* ridp */ ridp, /* fn */ zName, /* ridv */ ridv, /* fnm */ db_column_text(&q, 10), /* ridm */ ridm, /* fnr */ zName ); }else{ i64 sz; const char *zErrMsg = 0; int nc = 0; if( !dryRunFlag ) undo_save(zName); zFullPath = mprintf("%s/%s", g.zLocalRoot, zName); sz = file_size(zFullPath, ExtFILE); content_get(ridp, &p); content_get(ridm, &m); if( isBinary ){ rc = -1; blob_zero(&r); }else{ unsigned mergeFlags = dryRunFlag ? MERGE_DRYRUN : 0; if(keepMergeFlag!=0) mergeFlags |= MERGE_KEEP_FILES; rc = merge_3way(&p, zFullPath, &m, &r, mergeFlags); } if( rc>=0 ){ if( !dryRunFlag ){ blob_write_to_file(&r, zFullPath); file_setexe(zFullPath, isExe); } db_multi_exec("UPDATE vfile SET mtime=0 WHERE id=%d", idv); if( rc>0 ){ fossil_print("***** %d merge conflict%s in %s\n", rc, rc>1 ? "s" : "", zName); nConflict++; nc = rc; zErrMsg = "merge conflicts"; zType = "CONFLICT"; } }else{ fossil_print("***** Cannot merge binary file %s\n", zName); nConflict++; nc = 1; zErrMsg = "cannot merge binary file"; zType = "ERROR"; } db_multi_exec( "INSERT INTO mergestat(op,fnp,ridp,fn,ridv,sz,fnm,ridm,fnr,nc,msg)" "VALUES(%Q,%Q,%d,%Q,iif(%d,%d,NULL),iif(%d,%d,NULL),%Q,%d," "%Q,%d,%Q)", /* op */ zType, /* fnp */ db_column_text(&q, 9), /* ridp */ ridp, /* fn */ zName, /* ridv */ chnged==0, ridv, /* sz */ chnged!=0, sz, /* fnm */ db_column_text(&q, 10), /* ridm */ ridm, /* fnr */ zName, /* nc */ nc, /* msg */ zErrMsg ); fossil_free(zFullPath); blob_reset(&p); blob_reset(&m); blob_reset(&r); } vmerge_insert(idv, ridm); } db_finalize(&q); /* ** Drop files that are in P and V but not in M */ db_prepare(&q, "SELECT idv, fn, chnged, ridv FROM fv" " WHERE idp>0 AND idv>0 AND idm=0" ); while( db_step(&q)==SQLITE_ROW ){ int idv = db_column_int(&q, 0); const char *zName = db_column_text(&q, 1); int chnged = db_column_int(&q, 2); int ridv = db_column_int(&q, 3); int sz = -1; const char *zErrMsg = 0; int nc = 0; /* Delete the file idv */ fossil_print("DELETE %s\n", zName); if( chnged ){ char *zFullPath; fossil_warning("WARNING: local edits lost for %s", zName); nConflict++; ridv = 0; nc = 1; zErrMsg = "local edits lost"; zFullPath = mprintf("%s/%s", g.zLocalRoot, zName); sz = file_size(zFullPath, ExtFILE); fossil_free(zFullPath); } if( !dryRunFlag ) undo_save(zName); db_multi_exec( "UPDATE vfile SET deleted=1 WHERE id=%d", idv ); if( !dryRunFlag ){ char *zFullPath = mprintf("%s%s", g.zLocalRoot, zName); file_delete(zFullPath); free(zFullPath); } db_multi_exec( "INSERT INTO localdb.mergestat(op,fnp,ridp,fn,ridv,sz,fnm,ridm,nc,msg)" "VALUES('DELETE',NULL,NULL,%Q,iif(%d,%d,NULL),iif(%d,%d,NULL)," "NULL,NULL,%d,%Q)", /* fn */ zName, /* ridv */ chnged==0, ridv, /* sz */ chnged!=0, sz, /* nc */ nc, /* msg */ zErrMsg ); } db_finalize(&q); /* For certain sets of renames (e.g. A -> B and B -> A), a file that is ** being renamed must first be moved to a temporary location to avoid ** being overwritten by another rename operation. A row is added to the ** TMPRN table for each of these temporary renames. */ db_multi_exec( "DROP TABLE IF EXISTS tmprn;" "CREATE TEMP TABLE tmprn(fn UNIQUE, tmpfn);" ); /* ** Rename files that have taken a rename on P->M but which keep the same ** name on P->V. If a file is renamed on P->V only or on both P->V and ** P->M then we retain the V name of the file. */ db_prepare(&q, "SELECT idv, fnp, fnm, isexe FROM fv" " WHERE idv>0 AND idp>0 AND idm>0 AND fnp=fn AND fnm!=fnp" ); while( db_step(&q)==SQLITE_ROW ){ int idv = db_column_int(&q, 0); const char *zOldName = db_column_text(&q, 1); const char *zNewName = db_column_text(&q, 2); int isExe = db_column_int(&q, 3); fossil_print("RENAME %s -> %s\n", zOldName, zNewName); if( !dryRunFlag ) undo_save(zOldName); if( !dryRunFlag ) undo_save(zNewName); db_multi_exec( "UPDATE mergestat SET fnr=fnm WHERE fnp=%Q", zOldName ); db_multi_exec( "UPDATE vfile SET pathname=NULL, origname=pathname" " WHERE vid=%d AND pathname=%Q;" "UPDATE vfile SET pathname=%Q, origname=coalesce(origname,pathname)" " WHERE id=%d;", vid, zNewName, zNewName, idv ); if( !dryRunFlag ){ char *zFullOldPath, *zFullNewPath; zFullOldPath = db_text(0,"SELECT tmpfn FROM tmprn WHERE fn=%Q", zOldName); if( !zFullOldPath ){ zFullOldPath = mprintf("%s%s", g.zLocalRoot, zOldName); } zFullNewPath = mprintf("%s%s", g.zLocalRoot, zNewName); if( file_size(zFullNewPath, RepoFILE)>=0 ){ Blob tmpPath; file_tempname(&tmpPath, "", 0); db_multi_exec("INSERT INTO tmprn(fn,tmpfn) VALUES(%Q,%Q)", zNewName, blob_str(&tmpPath)); if( file_islink(zFullNewPath) ){ symlink_copy(zFullNewPath, blob_str(&tmpPath)); }else{ file_copy(zFullNewPath, blob_str(&tmpPath)); } blob_reset(&tmpPath); } if( file_islink(zFullOldPath) ){ symlink_copy(zFullOldPath, zFullNewPath); }else{ file_copy(zFullOldPath, zFullNewPath); } file_setexe(zFullNewPath, isExe); file_delete(zFullOldPath); free(zFullNewPath); free(zFullOldPath); } } db_finalize(&q); /* A file that has been deleted and replaced by a renamed file will have a ** NULL pathname. Change it to something that makes the output of "status" ** and similar commands make sense for such files and that will (most likely) ** not be an actual existing pathname. */ db_multi_exec( "UPDATE vfile SET pathname=origname || ' (overwritten by rename)'" " WHERE pathname IS NULL" ); /* ** Insert into V any files that are not in V or P but are in M. */ db_prepare(&q, "SELECT idm, fnm, ridm FROM fv" " WHERE idp=0 AND idv=0 AND idm>0" ); while( db_step(&q)==SQLITE_ROW ){ int idm = db_column_int(&q, 0); const char *zName; char *zFullName; db_multi_exec( "REPLACE INTO vfile(vid,chnged,deleted,rid,mrid," "isexe,islink,pathname,mhash)" " SELECT %d,%d,0,rid,mrid,isexe,islink,pathname," "CASE WHEN rid<>mrid" " THEN (SELECT uuid FROM blob WHERE blob.rid=vfile.mrid) END " "FROM vfile WHERE id=%d", vid, integrateFlag?5:3, idm ); zName = db_column_text(&q, 1); zFullName = mprintf("%s%s", g.zLocalRoot, zName); if( file_isfile_or_link(zFullName) && !db_exists("SELECT 1 FROM fv WHERE fn=%Q", zName) ){ /* Name of backup file with Original content */ char *zOrig = file_newname(zFullName, "original", 1); /* Backup previously unanaged file before to be overwritten */ file_copy(zFullName, zOrig); fossil_free(zOrig); fossil_print("ADDED %s (overwrites an unmanaged file)", zName); if( !dryRunFlag ) fossil_print(", original copy backed up locally"); fossil_print("\n"); nOverwrite++; }else{ fossil_print("ADDED %s\n", zName); } fossil_free(zFullName); db_multi_exec( "INSERT INTO mergestat(op,fnm,ridm,fnr)" "VALUES('ADDED',%Q,%d,%Q)", /* fnm */ zName, /* ridm */ db_column_int(&q,2), /* fnr */ zName ); if( !dryRunFlag ){ undo_save(zName); vfile_to_disk(0, idm, 0, 0); } } db_finalize(&q); /* Report on conflicts */ if( nConflict ){ fossil_warning("WARNING: %d merge conflicts", nConflict); if( bMultiMerge ){ int i; Blob msg; blob_init(&msg, 0, 0); blob_appendf(&msg, "The following %ss were not attempted due to prior conflicts:", pickFlag ? "cherrypick" : backoutFlag ? "backout" : "merge" ); for(i=2; i