/* * Copyright 1995, 1996 Perforce Software. All rights reserved. * * This file is part of Perforce - the FAST SCM System. */ # include <stdhdrs.h> # include <strbuf.h> # include <error.h> # include <handler.h> # include <filesys.h> # include <md5.h> # include <i18napi.h> # include <charcvt.h> # include <diffmerge.h> # include <msgclient.h> # include "clientuser.h" # include "clientmerge.h" # include "clientmerge3.h" static const char *const mergeHelp[] = { "Three-way merge options:", "", " Accept:", " at Keep only changes to their file.", " ay Keep only changes to your file.", " am Keep merged file.", " ae Keep merged and edited file.", " a Keep autoselected file.", "", " Diff:", " dt See their changes alone.", " dy See your changes alone.", " dm See merged changes.", " d Diff your file against merged file.", "", " Edit:", " et Edit their file (read only).", " ey Edit your file (read/write).", " e Edit merged file (read/write).", "", " Misc:", " m Run '$P4MERGE base theirs yours merged'.", " s Skip this file.", " h Print this help message.", " ^C Quit the resolve operation.", "", "The autoselected 'accept' option is shown in []'s on the prompt.", "", 0 }; ClientMerge3::ClientMerge3( ClientUser *ui, FileSysType type, FileSysType resType, FileSysType theirType, FileSysType baseType ) { // Save UI for future use this->ui = ui; // Set up files. yours = ui->File( type ); result = ui->File( resType ); theirs = ui->File( theirType ); base = ui->File( baseType ); // make base,theirs,result temp base->SetDeleteOnClose(); theirs->SetDeleteOnClose(); result->SetDeleteOnClose(); // Digests for seeing if edited result matches inputs yoursMD5 = new MD5; theirsMD5 = new MD5; resultMD5 = new MD5; showAll = 0; theirs_cvt = NULL; result_cvt = NULL; } ClientMerge3::~ClientMerge3() { delete yours; delete base; delete theirs; delete result; delete yoursMD5; delete theirsMD5; delete resultMD5; delete theirs_cvt; delete result_cvt; } void ClientMerge3::SetNames( StrPtr *b, StrPtr *t, StrPtr *y ) { int i; StrPtr n = StrRef::Null(); if( !b ) b = &n; if( !t ) t = &n; if( !y ) y = &n; for( i = 0; i < MarkerLast; i++ ) markertab[i].Clear(); markertab[ MarkerOriginal ] << ">>>> ORIGINAL " << b; markertab[ MarkerTheirs ] << "==== THEIRS " << t; markertab[ MarkerYours ] << "==== YOURS " << y; markertab[ MarkerBoth ] << "==== BOTH " << t << " " << y; markertab[ MarkerEnd ] << "<<<<"; } void ClientMerge3::Open( StrPtr *name, Error *e, CharSetCvt *cvt, int charset ) { // No names? if( !markertab[0].Length() ) SetNames( 0, 0, 0 ); // Open the various temps for writing. // Figure if the first one succeeds then the others // probably will, too. yours->Set( *name ); if( charset ) { base->SetContentCharSetPriv( charset ); theirs->SetContentCharSetPriv( charset ); yours->SetContentCharSetPriv( charset ); result->SetContentCharSetPriv( charset ); } base->MakeLocalTemp( name->Text() ); theirs->MakeLocalTemp( name->Text() ); result->MakeLocalTemp( name->Text() ); base->Open( FOM_WRITE, e ); if( e->Test() ) return; result->Perms( FPM_RW ); theirs->Open( FOM_WRITE, e ); result->Open( FOM_WRITE, e ); if( cvt ) { theirs_cvt = cvt->Clone(); result_cvt = cvt->Clone(); base->Translator( cvt ); theirs->Translator( theirs_cvt ); result->Translator( result_cvt ); } // And zonk counters for chunks chunksYours = chunksTheirs = chunksConflict = chunksBoth = 0; oldBits = 0; markersInFile = 0; needNl = 0; } void ClientMerge3::Write( StrPtr *buf, StrPtr *bits, Error *e ) { Marker3 marker = MarkerOriginal; int b = bits ? bits->Atoi() : 0; if( oldBits && oldBits != b ) { switch( b ) { case SEL_BASE|SEL_CONF: ++chunksConflict; default: case SEL_BASE: case SEL_BASE|SEL_LEG1: case SEL_BASE|SEL_LEG2: marker = MarkerOriginal; break; case SEL_LEG1|SEL_RSLT: ++chunksTheirs; case SEL_LEG1|SEL_RSLT|SEL_CONF: marker = MarkerTheirs; break; case SEL_LEG2|SEL_RSLT: ++chunksYours; case SEL_LEG2|SEL_RSLT|SEL_CONF: marker = MarkerYours; break; case SEL_LEG1|SEL_LEG2|SEL_RSLT: marker = MarkerBoth; ++chunksBoth; break; case SEL_ALL: marker = MarkerEnd; break; } if( showAll || b & SEL_CONF || b == SEL_ALL && oldBits & SEL_CONF ) { if( needNl ) result->Write( "\n", 1, e ); result->Write( &markertab[ marker ], e ); result->Write( "\n", 1, e ); ++markersInFile; } } oldBits = b; // Just a shortcut - we get back empty buffers as marker placeholders. if( !buf->Length() ) return; // Write to each file, according to the bits. // We don't need to write 'yours', because that is what is // already on the client. // We compute the md5 digest of each file at this point. // We'll use that later to provide a default for 'accept'. // The extra check in writing result is that we want to write // out the original version (the base) for any conflicts. If // a chunk is only in the base, then leg1 and leg2 must be in // conflict for that chunk. if( b & SEL_BASE ) { base->Write( buf, e ); // no base digest -- can't accept it } if( b & SEL_LEG1 ) { theirs->Write( buf, e ); theirsMD5->Update( *buf ); } if( b & SEL_LEG2 ) { // no writing yours -- already there! yoursMD5->Update( *buf ); } if( b & SEL_RSLT ) { resultMD5->Update( *buf ); } if( ( b & SEL_RSLT ) || showAll || b == ( SEL_BASE | SEL_CONF ) ) { result->Write( buf, e ); } // If this block didn't end in a linefeed, we may need to add // one before putting out the next marker. This can happen if // some yoyo has a conflict on the last line of a file, and that // line has no newline. needNl = buf->Text()[ buf->Length() - 1 ] != '\n'; } void ClientMerge3::Close( Error *e ) { base->Close( e ); theirs->Close( e ); result->Close( e ); theirsMD5->Final( theirsDigest ); yoursMD5->Final( yoursDigest ); resultMD5->Final( resultDigest ); } void ClientMerge3::Chmod( const char *perms, Error *e ) { yours->Chmod2( perms, e ); } void ClientMerge3::SetTheirModTime( StrPtr *modTime ) { theirs->ModTime( modTime ); } MergeStatus ClientMerge3::AutoResolve( MergeForce force ) { /* * Automatic logic: */ /* If showing all markers, always merge */ /* If conflict -- skip or merge (if forced) */ /* If we have no unique changes, accept theirs */ /* If they have no unique changes, take ours */ /* If we both have changes -- merge or skip (if safe). */ Error e; e.Set( MsgClient::MergeMsg3 ) << chunksYours << chunksTheirs << chunksBoth << chunksConflict; ui->Message( &e ); if( showAll && force == CMF_FORCE ) return CMS_EDIT; if( chunksConflict ) return force == CMF_FORCE ? CMS_EDIT : CMS_SKIP; /* Now we know there are no conflicts. */ if( !chunksYours ) return CMS_THEIRS; if( !chunksTheirs ) return CMS_YOURS; // There can be markers in the file without conflicts if // showAll (resolve -v) was selected. if( markersInFile ) return force == CMF_FORCE ? CMS_EDIT : CMS_SKIP; /* We both have changes. */ switch( force ) { case CMF_AUTO: return CMS_MERGED; case CMF_SAFE: return CMS_SKIP; case CMF_FORCE: return CMS_MERGED; } /* not reached */ return CMS_SKIP; } MergeStatus ClientMerge32::AutoResolve( MergeForce force ) { /* * It's just a two-way diff, so we accept theirs if forced * to or if there are no differences. */ Error e; e.Set( MsgClient::MergeMsg32 ) << chunksTheirs; ui->Message( &e ); /* Hmmm. showAll for 32? I don't think so. */ /* But just in case. */ if( showAll && force == CMF_FORCE ) return CMS_EDIT; /* No changes -- better to take theirs */ if( !chunksTheirs ) return CMS_THEIRS; /* Some changes */ switch( force ) { case CMF_AUTO: return CMS_SKIP; case CMF_SAFE: return CMS_SKIP; case CMF_FORCE: return CMS_THEIRS; } /* not reached */ return CMS_SKIP; } MergeStatus ClientMerge3::Resolve( Error *e ) { /* If the files are identical, autoaccept theirs */ /* We prefer theirs over ours, because then integration history */ /* marks it as a "copy" and we know the files are now identical. */ /* Note that this silently swallows chunksBoth... */ /* get autoresolve's suggestion */ MergeStatus autoStat = AutoResolve( CMF_FORCE ); /* Iteratively prompt the user for what to do. */ StrBuf buf; for(;;) { const char *autoSuggest; int resultEdited = 0; // default action: edit if conflict, else accept suggestion // made by autoResolve. switch( autoStat ) { default: case CMS_SKIP: autoSuggest = "s" ; break; case CMS_THEIRS: autoSuggest = "at" ; break; case CMS_YOURS: autoSuggest = "ay" ; break; case CMS_MERGED: autoSuggest = "am" ; break; case CMS_EDIT: autoSuggest = markersInFile ? "e" : "ae"; break; } // Prompt. If no response, use auto buf.Clear(); e->Clear(); e->Set( MsgClient::MergePrompt ) << autoSuggest; e->Fmt( buf, EF_PLAIN ); e->Clear(); ui->Prompt( buf, buf, 0, e ); if( e->Test() ) return CMS_QUIT; if( !buf[0] ) buf = autoSuggest; // Do the user-requested operation # define pair( a, b ) ( (a) << 8 | (b) ) switch( pair( buf[0], buf[1] ) ) { case pair( 'a', 0 ): // take suggestion // But if merged, check for conflicts. if( autoStat != CMS_EDIT ) return autoStat; // fall through case pair( 'a', 'e' ): // accept edited result if( markersInFile ) { e->Set( MsgClient::ConfirmMarkers ); if ( !Verify( e, e ) ) break; } return CMS_EDIT; case pair( 'a', 'm' ): // accept result if( autoStat == CMS_EDIT ) { e->Set( MsgClient::ConfirmEdit ); if ( !Verify( e, e ) ) break; } return CMS_MERGED; case pair( 'a', 't' ): // accept theirs if( chunksYours + chunksConflict ) { e->Set( MsgClient::Confirm ); if ( !Verify( e, e ) ) break; } return CMS_THEIRS; case pair( 'a', 'y' ): // accept yours return CMS_YOURS; case pair( 'e', 't' ): // edit theirs ui->Edit( theirs, e ); break; case pair( 'e', 'y' ): // edit yours ui->Edit( yours, e ); break; case pair( 'e', 0 ): // edit the merged result // If no markers are left in the file, zero the // conflict count. Once edited, the default becomes // to accept the merge. ui->Edit( result, e ); resultEdited = 1; break; case pair( 'd', 't' ): // diff theirs ui->Diff( base, theirs, 1, flags.Text(), e ); break; case pair( 'd', 'y' ): // diff yours ui->Diff( base, yours, 1, flags.Text(), e ); break; case pair( 'd', 'm' ): // diff merge ui->Diff( base, result, 1, flags.Text(), e ); break; case pair( 'd', 0 ): // diff result ui->Diff( yours, result, 1, flags.Text(), e ); break; case pair( 'm', 0 ): // merge ui->Merge( base, theirs, yours, result, e ); resultEdited = 1; break; case pair( 's', 0 ): // skip return CMS_SKIP; case pair( '?', 0 ): case pair( 'h', 0 ): // help ui->Help( mergeHelp ); break; default: e->Set( MsgClient::BadFlag ); break; } // If the result was edited, we're going to have to // try to pick a new result. We have the digest of // yours/theirs/result, so we just compute the digest // of the current result and see which it matches. // If none match, it's an original edit. if( !e->Test() && resultEdited ) { autoStat = DetectResolve(); // Edited result has markers if (a) it // isn't theirs/yours/merged, (b) it had markers before, // and (c) CheckForMarkers() finds some. markersInFile = markersInFile && autoStat == CMS_EDIT && CheckForMarkers( result, e ); // If the edited result equals yours, suggest edit instead. // In most cases if the user has picked out the yours diff // in an editor, they will want to reverse-integrate it. if( autoStat == CMS_YOURS ) autoStat = CMS_EDIT; } // Report Errors if( e->Test() ) { ui->Message( e ); e->Clear(); } } } void ClientMerge3::Select( MergeStatus stat, Error *e ) { // Given the automatic or manual selection, now move the proper // file into place. Usually it is already in place. switch( stat ) { case CMS_THEIRS: // accept theirs theirs->Chmod( FPM_RW, e ); theirs->Rename( yours, e ); if( e->Test() ) return; // in case the file type has changed, move the filesys objects // around to leave the yours object with a result file type theirs->Set( yours->Name() ); delete yours; yours = theirs; theirs = NULL; return; case CMS_MERGED: case CMS_EDIT: // accept result result->Rename( yours, e ); if( e->Test() ) return; // in case the file type has changed, move the filesys objects // around to leave the yours object with a result file type result->Set( yours->Name() ); delete yours; yours = result; result = NULL; return; default: return; } } int ClientMerge3::CheckForMarkers( FileSys *f, Error *e ) const { StrBuf l1; int markers = 0; f->Open( FOM_READ, e ); if( e->Test() ) return 0; while( !markers ) { if( !f->ReadLine( &l1, e ) ) break; if( !l1.Length() || !strchr( "<>==", l1.Text()[0] ) ) continue; for( int i = 0; i < MarkerLast; i++ ) if( l1 == markertab[ i ] ) ++markers; } f->Close( e ); return markers > 0; } int ClientMerge3::IsAcceptable() const { // For the GUI: no error; we'll assume a missing // result will be seen downstream! Error e; return !markersInFile || !CheckForMarkers( result, &e ); } MergeStatus ClientMerge3::DetectResolve() const { StrBuf d; Error e[1]; CharSetCvt *icvt = 0; if( result_cvt ) { icvt = result_cvt->ReverseCvt(); result->Translator( icvt ); } // Get & Compare digest of result against various legs // Blow off errors -- if we can't get to the result we'll // find out sooner or later. result->Digest( &d, e ); delete icvt; if( d == theirsDigest ) return CMS_THEIRS; else if( d == yoursDigest ) return CMS_YOURS; else if( d == resultDigest ) return CMS_MERGED; else return CMS_EDIT; } const StrPtr * ClientMerge3::GetMergeDigest() const { // If the file has conflicts, do not report merge digest if( markersInFile || chunksConflict ) return NULL; return &resultDigest; } const StrPtr * ClientMerge3::GetYourDigest() const { return &yoursDigest; } const StrPtr * ClientMerge3::GetTheirDigest() const { return &theirsDigest; }