├── README.md ├── LICENSE ├── ckeyword.cpp └── cmirror.cpp /README.md: -------------------------------------------------------------------------------- 1 | # cmirror 2 | 3 | This is a simple source code control utility. It was initially 4 | written by Casey Muratori but has since grown to include the fine 5 | work of Sean Barrett and Jeff Roberts. It is in the public domain. 6 | Anyone can use it, modify it, roll'n'smoke hardcopies of the source 7 | code, sell it to the terrorists, etc. 8 | 9 | But the authors make absolutely no warranty as to the reliability, 10 | suitability, or usability of the software. It works with files - 11 | probably files that are important to you. There might be bad bugs 12 | in here. It could delete them all. It could format your hard drive. 13 | We have no idea. If you lose all your files from using it, we will 14 | point and laugh. Cmirror is not a substitute for making backups. 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The authors of this software MAKE NO WARRANTY as to the RELIABILITY, 2 | SUITABILITY, or USABILITY of this software. USE IT AT YOUR OWN RISK. 3 | 4 | This is a simple source code control utility. It was initially 5 | written by Casey Muratori but has since grown to include the fine 6 | work of Sean Barrett and Jeff Roberts. It is in the public domain. 7 | Anyone can use it, modify it, roll'n'smoke hardcopies of the source 8 | code, sell it to the terrorists, etc. 9 | 10 | But the authors make absolutely no warranty as to the reliability, 11 | suitability, or usability of the software. It works with files - 12 | probably files that are important to you. There might be bad bugs 13 | in here. It could delete them all. It could format your hard drive. 14 | We have no idea. If you lose all your files from using it, we will 15 | point and laugh. Cmirror is not a substitute for making backups. 16 | 17 | If you're a snappy coder and you'd like to contribute bug fixes or 18 | feature additions, please see http://www.mollyrocket.com/tools 19 | -------------------------------------------------------------------------------- /ckeyword.cpp: -------------------------------------------------------------------------------- 1 | /* ======================================================================== 2 | $File: tools/ckeyword/ckeyword.cpp $ 3 | $Date: 2007/04/12 03:22:15AM $ 4 | $Revision: 7 $ 5 | $Creator: Casey Muratori $ 6 | $Notice: $ 7 | ======================================================================== */ 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #include 16 | 17 | #define CMIRROR_MAX_PATH 4096 18 | 19 | struct keyword_context 20 | { 21 | bool SuspectedBinary; 22 | bool ErrorOnRead; 23 | bool ErrorOnWrite; 24 | bool TypoSuspected; 25 | bool ReplacedSomething; 26 | 27 | bool UseFileKeyword; 28 | bool UseNoticeKeyword; 29 | bool UseDateKeyword; 30 | bool UseRevisionKeyword; 31 | 32 | FILE *SourceFile; 33 | FILE *DestFile; 34 | 35 | char *SourceFileName; 36 | char *SourceRepositoryRelativeName; 37 | char *NoticeText; 38 | int SourceRevisionIndex; 39 | struct stat SourceStat; 40 | }; 41 | 42 | static int 43 | Input(keyword_context &Context) 44 | { 45 | int Char = fgetc(Context.SourceFile); 46 | if(Char == EOF) 47 | { 48 | if(ferror(Context.SourceFile)) 49 | { 50 | Context.ErrorOnRead = true; 51 | } 52 | } 53 | else 54 | { 55 | if(((Char >= 0) && (Char <= 9)) || 56 | ((Char >= 16) && (Char <= 31))) 57 | { 58 | Context.SuspectedBinary = true; 59 | } 60 | } 61 | 62 | return(Char); 63 | } 64 | 65 | static void 66 | Output(keyword_context &Context, int Char) 67 | { 68 | if(Char != -1) 69 | { 70 | if(fputc(Char, Context.DestFile) == EOF) 71 | { 72 | Context.ErrorOnWrite = true; 73 | } 74 | } 75 | } 76 | 77 | static bool 78 | EndOfFile(keyword_context &Context) 79 | { 80 | return(feof(Context.SourceFile) != 0); 81 | } 82 | 83 | static void 84 | SkipToNextDollarsign(keyword_context &Context, bool StopAtNewline) 85 | { 86 | int Char = 0; 87 | while(!EndOfFile(Context) && (Char != '$')) 88 | { 89 | Char = Input(Context); 90 | if(StopAtNewline && ((Char == '\n') || (Char == '\r'))) 91 | { 92 | Context.TypoSuspected = true; 93 | 94 | Output(Context, Char); 95 | break; 96 | } 97 | } 98 | 99 | if(Char != '$') 100 | { 101 | Context.TypoSuspected = true; 102 | } 103 | } 104 | 105 | static void 106 | MatchAnchor(keyword_context &Context, bool StopAtNewline) 107 | { 108 | long OriginalAt = ftell(Context.SourceFile); 109 | if(OriginalAt >= 0) 110 | { 111 | char Keyword[32]; 112 | int MaxKeywordLength = (sizeof(Keyword) / sizeof(Keyword[0])); 113 | {for(int CharIndex = 0; 114 | CharIndex < MaxKeywordLength; 115 | ++CharIndex) 116 | { 117 | Keyword[CharIndex] = EndOfFile(Context) ? '\0' : (char)Input(Context); 118 | }} 119 | Keyword[MaxKeywordLength - 1] = '\0'; 120 | if(fseek(Context.SourceFile, OriginalAt, SEEK_SET) != 0) 121 | { 122 | Context.ErrorOnRead = true; 123 | } 124 | 125 | if(Context.UseFileKeyword && 126 | ((strncmp(Keyword, "File: ", 6) == 0) || 127 | (strncmp(Keyword, "RCSFile: ", 9) == 0))) 128 | { 129 | Context.ReplacedSomething = true; 130 | 131 | fprintf(Context.DestFile, "$File: %s $", Context.SourceRepositoryRelativeName); 132 | SkipToNextDollarsign(Context, StopAtNewline); 133 | } 134 | else if(Context.UseNoticeKeyword && 135 | (strncmp(Keyword, "Notice: ", 8) == 0)) 136 | { 137 | Context.ReplacedSomething = true; 138 | 139 | fprintf(Context.DestFile, "$Notice: "); 140 | {for(char *I = Context.NoticeText; 141 | *I; 142 | ++I) 143 | { 144 | if((*I == '\\') && 145 | (*(I + 1) == 'n')) 146 | { 147 | fprintf(Context.DestFile, "\r\n"); 148 | ++I; 149 | } 150 | else 151 | { 152 | fputc(*I, Context.DestFile); 153 | } 154 | }} 155 | fprintf(Context.DestFile, " $"); 156 | SkipToNextDollarsign(Context, StopAtNewline); 157 | } 158 | else if(Context.UseDateKeyword && 159 | (strncmp(Keyword, "Date: ", 6) == 0)) 160 | { 161 | Context.ReplacedSomething = true; 162 | 163 | struct tm *TM = localtime(&Context.SourceStat.st_mtime); 164 | int Hour = 12; 165 | char *AMPM = "AM"; 166 | if(TM->tm_hour >= 1) 167 | { 168 | Hour = TM->tm_hour; 169 | } 170 | if(TM->tm_hour >= 12) 171 | { 172 | AMPM = "PM"; 173 | } 174 | if(TM->tm_hour >= 13) 175 | { 176 | Hour -= 12; 177 | } 178 | 179 | fprintf(Context.DestFile, "$Date: %04d/%02d/%02d %02d:%02d:%02d%s $", 180 | TM->tm_year + 1900, 181 | TM->tm_mon + 1, 182 | TM->tm_mday, 183 | Hour, 184 | TM->tm_min, 185 | TM->tm_sec, 186 | AMPM); 187 | SkipToNextDollarsign(Context, StopAtNewline); 188 | } 189 | else if(Context.UseRevisionKeyword && 190 | (strncmp(Keyword, "Revision: ", 10) == 0)) 191 | { 192 | Context.ReplacedSomething = true; 193 | 194 | fprintf(Context.DestFile, "$Revision: %d $", Context.SourceRevisionIndex); 195 | SkipToNextDollarsign(Context, StopAtNewline); 196 | } 197 | else 198 | { 199 | Output(Context, '$'); 200 | } 201 | } 202 | else 203 | { 204 | Context.ErrorOnRead = true; 205 | } 206 | } 207 | 208 | void 209 | ProcessFile(keyword_context &Context) 210 | { 211 | int InsideCComment = 0; 212 | bool InsideCPPComment = false; 213 | 214 | while(!EndOfFile(Context)) 215 | { 216 | int Char = Input(Context); 217 | if((InsideCComment > 0) || InsideCPPComment) 218 | { 219 | if(Char == '$') 220 | { 221 | MatchAnchor(Context, InsideCComment <= 0); 222 | } 223 | else 224 | { 225 | if(Char == '*') 226 | { 227 | Output(Context, Char); 228 | Char = Input(Context); 229 | if(Char == '/') 230 | { 231 | --InsideCComment; 232 | } 233 | } 234 | else if((Char == '\n') || (Char == '\r')) 235 | { 236 | InsideCPPComment = false; 237 | } 238 | 239 | Output(Context, Char); 240 | } 241 | } 242 | else 243 | { 244 | if(Char == '/') 245 | { 246 | Output(Context, Char); 247 | Char = Input(Context); 248 | if(Char == '/') 249 | { 250 | InsideCPPComment = true; 251 | } 252 | else if(Char == '*') 253 | { 254 | ++InsideCComment; 255 | } 256 | } 257 | 258 | Output(Context, Char); 259 | } 260 | } 261 | } 262 | 263 | int 264 | Go(keyword_context &Context) 265 | { 266 | int Result = -1; 267 | 268 | char DeleteFileName[CMIRROR_MAX_PATH]; 269 | sprintf(DeleteFileName, "%s_DELETE", Context.SourceFileName); 270 | 271 | char DestFileName[CMIRROR_MAX_PATH]; 272 | sprintf(DestFileName, "%s_KEYWORDS", Context.SourceFileName); 273 | 274 | stat(Context.SourceFileName, &Context.SourceStat); 275 | Context.SourceFile = fopen(Context.SourceFileName, "rb"); 276 | Context.DestFile = fopen(DestFileName, "wb"); 277 | if(Context.SourceFile && Context.DestFile) 278 | { 279 | ProcessFile(Context); 280 | 281 | Result = 0; 282 | 283 | if(Context.SuspectedBinary) 284 | { 285 | printf("CKeyword: file appears to binary.\n"); 286 | Result = -1; 287 | } 288 | 289 | if(Context.ErrorOnRead) 290 | { 291 | printf("CKeyword: error reading file %s.\n", Context.SourceFileName); 292 | Result = -1; 293 | } 294 | 295 | if(Context.ErrorOnWrite) 296 | { 297 | printf("CKeyword: error writing file.\n"); 298 | Result = -1; 299 | } 300 | 301 | if(Context.TypoSuspected) 302 | { 303 | printf("CKeyword: typo suspected.\n"); 304 | Result = -1; 305 | } 306 | } 307 | else 308 | { 309 | printf("CKeyword: Cannot open files\n"); 310 | } 311 | fclose(Context.SourceFile); 312 | fclose(Context.DestFile); 313 | 314 | if((Result == 0) && Context.ReplacedSomething) 315 | { 316 | if(rename(Context.SourceFileName, DeleteFileName) == 0) 317 | { 318 | if(rename(DestFileName, Context.SourceFileName) == 0) 319 | { 320 | unlink(DeleteFileName); 321 | } 322 | else 323 | { 324 | printf("CKeyword: Unable to replace source file with keyword-stamped source file.\n"); 325 | rename(DeleteFileName, Context.SourceFileName); 326 | unlink(DestFileName); 327 | } 328 | } 329 | else 330 | { 331 | printf("CKeyword: Unable to move source file for keyword replacement.\n"); 332 | unlink(DestFileName); 333 | } 334 | } 335 | else 336 | { 337 | unlink(DestFileName); 338 | } 339 | 340 | return(Result); 341 | } 342 | 343 | int 344 | main(int ArgCount, char **Args) 345 | { 346 | int Result = -1; 347 | 348 | keyword_context Context = {0}; 349 | 350 | if(ArgCount == 4) 351 | { 352 | Context.SourceFileName = Args[1]; 353 | Context.SourceRepositoryRelativeName = Args[2]; 354 | Context.SourceRevisionIndex = atoi(Args[3]); 355 | 356 | Context.UseFileKeyword = true; 357 | Context.UseDateKeyword = true; 358 | Context.UseRevisionKeyword = true; 359 | 360 | Result = Go(Context); 361 | } 362 | else if(ArgCount == 3) 363 | { 364 | Context.SourceFileName = Args[1]; 365 | Context.NoticeText = Args[2]; 366 | 367 | Context.UseNoticeKeyword = true; 368 | 369 | Result = Go(Context); 370 | } 371 | else 372 | { 373 | printf("CKeyword: Needs either 2 or 3 arguments\n"); 374 | } 375 | 376 | return(Result); 377 | } -------------------------------------------------------------------------------- /cmirror.cpp: -------------------------------------------------------------------------------- 1 | /* ======================================================================== 2 | 3 | cmirror.cpp v1.3c 4 | 5 | The authors of this software MAKE NO WARRANTY as to the RELIABILITY, 6 | SUITABILITY, or USABILITY of this software. USE IT AT YOUR OWN RISK. 7 | 8 | This is a simple source code control utility. It was initially 9 | written by Casey Muratori but has since grown to include the fine 10 | work of Sean Barrett and Jeff Roberts. It is in the public domain. 11 | Anyone can use it, modify it, roll'n'smoke hardcopies of the source 12 | code, sell it to the terrorists, etc. 13 | 14 | But the authors make absolutely no warranty as to the reliability, 15 | suitability, or usability of the software. It works with files - 16 | probably files that are important to you. There might be bad bugs 17 | in here. It could delete them all. It could format your hard drive. 18 | We have no idea. If you lose all your files from using it, we will 19 | point and laugh. Cmirror is not a substitute for making backups. 20 | 21 | If you're a snappy coder and you'd like to contribute bug fixes or 22 | feature additions, please see http://www.mollyrocket.com/tools 23 | 24 | ======================================================================== */ 25 | 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #define STB_DEFINE 33 | #include "stb.h" 34 | 35 | // To try to be extra safe when people make changes, we have all the 36 | // "low level" file delete/move/copy operations at the beginning of 37 | // the file, wrapped in these "LowLevel" functions that do some sanity 38 | // checking and maybe even prompt the user if it's potentially legit 39 | // but rare. Then we scan the source and make sure all the unwrapped 40 | // calls appear before the EOLLFO tag below. So if you write some new 41 | // code and forget and call the unwrapped functions, hopefully it'll 42 | // catch this and print a warning. 43 | 44 | static bool 45 | ShouldContinue(bool &ContinueAlways, char *Message, char *Alternatives=NULL, char *ResultChar=NULL) 46 | { 47 | bool Result = ContinueAlways; 48 | 49 | if(!Result) 50 | { 51 | if (Message == NULL) 52 | Message = "Continue? (y/n/a)"; 53 | printf("%s\n", Message); 54 | 55 | bool ValidResponse = false; 56 | do 57 | { 58 | char Character; 59 | scanf("%c", &Character); 60 | switch(Character) 61 | { 62 | case 'y': 63 | { 64 | Result = true; 65 | ValidResponse = true; 66 | } break; 67 | 68 | case 'n': 69 | { 70 | Result = false; 71 | ValidResponse = true; 72 | } break; 73 | 74 | case 'a': 75 | { 76 | Result = ContinueAlways = true; 77 | ValidResponse = true; 78 | } break; 79 | 80 | default: 81 | { 82 | if (Alternatives) { 83 | char *s = stb_strichr(Alternatives, Character); 84 | if (s) { 85 | *ResultChar = *s; 86 | Result = false; 87 | ValidResponse = true; 88 | } 89 | } 90 | } 91 | } 92 | } while(!ValidResponse); 93 | } 94 | 95 | return(Result); 96 | } 97 | 98 | static BOOL DeleteFileLowLevel(char *File) 99 | { 100 | BOOL Result = DeleteFile(File); 101 | 102 | // If deletion fails, try to move the offending file to the 103 | // Windows temp directory so it will be out of the repository 104 | // and hopefully cleaned up by Windows sometime in the future 105 | // when it is no longer in use. 106 | if(!Result) 107 | { 108 | char TempPath[MAX_PATH]; 109 | char TempFile[MAX_PATH]; 110 | GetTempPath(sizeof(TempPath), TempPath); 111 | if(GetTempFileName(TempPath, "cm", 0, TempFile)) 112 | { 113 | // TODO: DeleteFile is required here because Windows actually 114 | // creates the file, and so the move would fail. 115 | DeleteFile(TempFile); 116 | printf("\nUnable to delete \"%s\", attempting rename to \"%s\".\n", 117 | File, TempFile); 118 | Result = MoveFile(File, TempFile); 119 | } 120 | } 121 | 122 | return Result; 123 | } 124 | 125 | static bool CopyFileAlways; 126 | static bool DeleteFileAlways; 127 | 128 | //#if !defined CopyFileEx 129 | #define COPY_FILE_FAIL_IF_EXISTS 0x00000001 130 | #define COPY_FILE_RESTARTABLE 0x00000002 131 | #define COPY_FILE_OPEN_SOURCE_FOR_WRITE 0x00000004 132 | #define COPY_FILE_ALLOW_DECRYPTED_DESTINATION 0x00000008 133 | 134 | typedef 135 | DWORD 136 | (WINAPI *LPPROGRESS_ROUTINE)(LARGE_INTEGER TotalFileSize, 137 | LARGE_INTEGER TotalBytesTransferred, 138 | LARGE_INTEGER StreamSize, 139 | LARGE_INTEGER StreamBytesTransferred, 140 | DWORD dwStreamNumber, 141 | DWORD dwCallbackReason, 142 | HANDLE hSourceFile, 143 | HANDLE hDestinationFile, 144 | LPVOID lpData 145 | ); 146 | typedef BOOL WINAPI copy_file_ex_a(LPCSTR lpExistingFileName, 147 | LPCSTR lpNewFileName, 148 | LPPROGRESS_ROUTINE lpProgressRoutine, 149 | LPVOID lpData, 150 | LPBOOL pbCancel, 151 | DWORD dwCopyFlags); 152 | //#endif 153 | 154 | static BOOL 155 | Win32CopyFile(char *ExistingName, char *NewName, bool FailIfExists) 156 | { 157 | BOOL Result = FALSE; 158 | 159 | static copy_file_ex_a *CopyFileExPtr; 160 | static bool InitializationAttempted; 161 | if(!InitializationAttempted) 162 | { 163 | HINSTANCE Kernel32 = LoadLibrary("kernel32.dll"); 164 | if(Kernel32) 165 | { 166 | CopyFileExPtr = (copy_file_ex_a *)GetProcAddress(Kernel32, "CopyFileExA"); 167 | } 168 | 169 | InitializationAttempted = true; 170 | } 171 | 172 | if(CopyFileExPtr) 173 | { 174 | Result = CopyFileExPtr(ExistingName, NewName, 0, 0, 0, COPY_FILE_ALLOW_DECRYPTED_DESTINATION | (FailIfExists ? COPY_FILE_FAIL_IF_EXISTS : 0)); 175 | } 176 | else 177 | { 178 | Result = CopyFile(ExistingName, NewName, FailIfExists ? TRUE : FALSE); 179 | } 180 | 181 | return(Result); 182 | } 183 | 184 | BOOL CopyFileWrapped(char *Workspace, char *ExistingName, char *NewName, bool FailIfExists) 185 | { 186 | if (FailIfExists) 187 | { 188 | BOOL res = Win32CopyFile(ExistingName, NewName, FailIfExists); 189 | if (!res) { 190 | int err = GetLastError(); 191 | } 192 | return res; 193 | 194 | } else { 195 | 196 | if (!stb_prefixi(NewName, Workspace)) 197 | { 198 | printf(" Internal error! Attempted to copy '%s' over '%s' with overwrite allowed,\n" 199 | "but the target filename was not in the workspace.\n", ExistingName, NewName); 200 | exit(1); 201 | } 202 | #if 0 203 | if (stb_fexists(NewName)) 204 | { 205 | if (!CopyFileAlways) 206 | printf("About to copy '%s' over existing '%s'.\n", ExistingName, NewName); 207 | bool OkToProceed = ShouldContinue(CopyFileAlways, "Continue? (y/n/a, choose 'a' to allow all copy-overwrites)"); 208 | if (!OkToProceed) 209 | return FALSE; 210 | } 211 | #endif 212 | return Win32CopyFile(ExistingName, NewName, FailIfExists); 213 | } 214 | } 215 | 216 | BOOL DeleteFileWrapped(char *Workspace, char *Local, char *Central, 217 | char *File) 218 | { 219 | if (stb_prefixi(File, Local) || stb_prefixi(File, Central)) 220 | { 221 | // PARANOIA: 222 | // make sure there's a cmirror version number in the filename 223 | if (!strstr(File, ",cm")) { 224 | printf("Internal error! Attempted to delete '%s', which is in the %s repository, but it\n" 225 | "does not have a cmirror version number, so we have no business deleting it!\n", 226 | File, stb_prefixi(File,Local) ? "local" : "central"); 227 | exit(1); 228 | } 229 | // ok, go ahead and delete it 230 | return DeleteFileLowLevel(File); 231 | } 232 | else if (stb_prefixi(File, Workspace)) 233 | { 234 | #if 0 235 | if (!DeleteFileAlways) 236 | printf("About to delete file '%s'.\n", File); 237 | bool OkToProceed = ShouldContinue(DeleteFileAlways, "Continue? (y/n/a, choose 'a' to allow all workspace deletes)"); 238 | if (!OkToProceed) 239 | return FALSE; 240 | #endif 241 | return DeleteFileLowLevel(File); 242 | } 243 | else 244 | { 245 | printf("Internal error! Attempted to delete '%s', which does not come from any\n" 246 | "directory that cmirror is supposed to be manipulating.\n", File); 247 | exit(1); 248 | } 249 | } 250 | 251 | // EOLLFO: end of low-level file operations (this marker is for source-checking tools; do not delete it) 252 | 253 | enum file_type 254 | { 255 | NullFileType, 256 | 257 | WorkspaceFileType, 258 | LocalFileType, 259 | CentralFileType, 260 | 261 | OnePastLastFileType 262 | }; 263 | 264 | char *FileTypeText[] = 265 | { 266 | "NullFileType", 267 | "WorkspaceFileType", 268 | "LocalFileType", 269 | "CentralFileType", 270 | }; 271 | 272 | struct file_version 273 | { 274 | int VersionNumber; 275 | bool Deleted; 276 | unsigned int dwHighDateTime; 277 | unsigned int dwLowDateTime; 278 | bool OldStyle; 279 | 280 | file_version stb_bst_fields_parent(fv); 281 | }; 282 | 283 | stb_bst_parent(file_version, // node name 284 | fv, // same name as field above; also name prefix for functions that take nodes 285 | file_versions, // name of (optional) 'tree' data type that holds the root 286 | Version, // function name prefix for functions that take the tree type 287 | VersionNumber, // name of field to compare 288 | int, // type of that field 289 | a - b) // comparison function for that field 290 | 291 | struct file_record 292 | { 293 | bool Present; 294 | bool Deleted; 295 | 296 | int MinVersion; 297 | int MaxVersion; 298 | file_versions Versions; 299 | }; 300 | 301 | struct directory_entry 302 | { 303 | char *Name; 304 | 305 | file_record File[OnePastLastFileType]; 306 | 307 | directory_entry stb_bst_fields_parent(dire); 308 | }; 309 | 310 | stb_bst_parent(directory_entry, dire, directory_contents, Dir, Name, char *, stricmp(a,b)) 311 | 312 | enum conflict_behavior 313 | { 314 | HaltOnConflicts = 0, 315 | 316 | IgnoreConflicts, 317 | }; 318 | 319 | enum ignore_behavior 320 | { 321 | NoIgnoreEffect, 322 | DoIgnore, 323 | DoNotIgnore 324 | }; 325 | 326 | struct file_rule 327 | { 328 | // NOTE: When you add something to this structure, please make sure 329 | // to fill in a default value in the UpdateRuleFor function immediately 330 | // following. 331 | 332 | char *WildCard; 333 | int WildCardLength; // for reverse scanning 334 | 335 | conflict_behavior Behavior; 336 | 337 | // When Ignore is set to true, files matching the wildcard behave 338 | // literally as if they did not exist. They will not be sync'd, 339 | // versioned, or copied by cmirror. 340 | ignore_behavior Ignore; 341 | 342 | // When CapVersionCount is set to true, files matching the wildcard 343 | // will have their last MaxVersionCount versions stored instead of 344 | // an unbounded number of versions. If the file is deleted from 345 | // the repository, on the next sync, cmirror will delete all versions 346 | // stored that are later than MaxVersionCountIfDeleted. 347 | bool CapVersionCount; 348 | int MaxVersionCount; 349 | int MaxVersionCountIfDeleted; 350 | 351 | // When PreCheckinCommand is non-zero, it is treated as an 352 | // executable name that will be run with the workspace file and 353 | // version information as arguments when cmirror detects that it 354 | // has changed from the repository version (this is designed to 355 | // faciliate keyword substitution and such) 356 | char *PreCheckinCommand; 357 | char *PreCheckinCommandParameters; 358 | }; 359 | 360 | typedef file_rule* file_rule_list; 361 | 362 | static file_rule * 363 | UpdateRuleFor(file_rule_list *FileRuleList, char *Wildcard) 364 | { 365 | // NOTE: These used to be kept sorted, but now they are left 366 | // as a linked list to facilitate processing them in the order 367 | // in which they appeared. 368 | // file_rule *Rule = Find(FileRuleList, Wildcard); 369 | file_rule *Rule = stb_arr_add(*FileRuleList); 370 | 371 | Rule->WildCard = Wildcard; 372 | Rule->WildCardLength = strlen(Wildcard); 373 | Rule->Behavior = HaltOnConflicts; 374 | Rule->Ignore = NoIgnoreEffect; 375 | Rule->CapVersionCount = false; 376 | Rule->MaxVersionCount = INT_MAX; 377 | Rule->MaxVersionCountIfDeleted = INT_MAX; 378 | Rule->PreCheckinCommand = 0; 379 | Rule->PreCheckinCommandParameters = 0; 380 | 381 | return(Rule); 382 | } 383 | 384 | enum mirror_mode 385 | { 386 | FullSyncMode, 387 | LocalSyncMode, 388 | SidewaysSyncMode, 389 | CheckpointMode, 390 | }; 391 | 392 | enum diff_method 393 | { 394 | ByteByByteDiff, 395 | ModificationStampDiff, 396 | TimestampRepresentationTolerantStampDiff, 397 | TRTSWithByteFallbackDiff, 398 | }; 399 | 400 | struct mirror_config 401 | { 402 | file_rule_list FileRuleList; 403 | 404 | mirror_mode MirrorMode; 405 | diff_method DiffMethod[OnePastLastFileType][OnePastLastFileType]; 406 | char *CopyCommand; 407 | char *DeleteCommand; 408 | 409 | bool WritableWorkspace; 410 | 411 | bool ContinueAlways; 412 | 413 | bool SyncWorkspace; 414 | bool SyncCentral; 415 | char *CentralCache; 416 | 417 | bool SuppressIgnores; 418 | bool SummarizeIgnores; 419 | int SyncPrintThreshold; 420 | int LineLength; 421 | bool PrintReason; 422 | bool SummarizeSync; 423 | bool SummarizeSyncIfNotPrinted; 424 | bool SummarizeDirs; 425 | bool SummarizeDirsIfNotPrinted; 426 | 427 | int RetryTimes[OnePastLastFileType]; 428 | 429 | char *Directory[OnePastLastFileType]; 430 | }; 431 | 432 | struct string_table 433 | { 434 | char *StoreCurrent; 435 | char *StoreBase; 436 | }; 437 | 438 | // 439 | // --- === --- 440 | // 441 | 442 | static void 443 | InitializeStringTable(string_table &Table) 444 | { 445 | Table.StoreCurrent = Table.StoreBase = (char *)malloc(1 << 24); 446 | } 447 | 448 | static char * 449 | PushTableState(string_table &Table) 450 | { 451 | return(Table.StoreCurrent); 452 | } 453 | 454 | static void 455 | PopTableState(string_table &Table, char *Mark) 456 | { 457 | Table.StoreCurrent = Mark; 458 | } 459 | 460 | static void 461 | Cat(string_table &Table, char *String) 462 | { 463 | while(*String) {*Table.StoreCurrent++ = *String++;} 464 | } 465 | 466 | static void 467 | CatSlash(string_table &Table, char *String) 468 | { 469 | while(*String) 470 | { 471 | if(*String == '\\') 472 | { 473 | *Table.StoreCurrent++ = '/'; 474 | } 475 | else 476 | { 477 | *Table.StoreCurrent++ = *String++; 478 | } 479 | } 480 | } 481 | 482 | static void 483 | Cat(string_table &Table, char Character) 484 | { 485 | *Table.StoreCurrent++ = Character; 486 | } 487 | 488 | static char * 489 | StoreDirectoryFile(string_table &Table, char *Directory, char *File) 490 | { 491 | char *Result = Table.StoreCurrent; 492 | 493 | if(Directory && *Directory) 494 | { 495 | CatSlash(Table, Directory); 496 | Cat(Table, '/'); 497 | } 498 | CatSlash(Table, File); 499 | Cat(Table, '\0'); 500 | 501 | return(Result); 502 | } 503 | 504 | static void 505 | Unixify(char *String) 506 | { 507 | stb_fixpath(String); 508 | } 509 | 510 | static void 511 | FreeStringTable(string_table &Table) 512 | { 513 | free(Table.StoreBase); 514 | Table.StoreBase = 0; 515 | Table.StoreCurrent = 0; 516 | } 517 | 518 | static char * 519 | FindLastChar(char *String, int ch) 520 | { 521 | return strrchr(String,ch); 522 | } 523 | 524 | static char * 525 | FindLastSlash(char *Path) 526 | { 527 | char *p = stb_strrchr2((char *) Path, '/', '\\'); 528 | return p ? p : Path; 529 | } 530 | 531 | static void 532 | AddFileVersion(file_record &Record, int VersionIndex, unsigned int dwLowDateTime, unsigned int dwHighDateTime, bool Deleted, bool OldStyle) 533 | { 534 | Record.Present = true; 535 | if(VersionIndex != -1) 536 | { 537 | if((Record.MinVersion == -1) || (Record.MinVersion > VersionIndex)) 538 | { 539 | Record.MinVersion = VersionIndex; 540 | } 541 | 542 | if((Record.MaxVersion == -1) || (Record.MaxVersion < VersionIndex)) 543 | { 544 | Record.Deleted = Deleted; 545 | Record.MaxVersion = VersionIndex; 546 | } 547 | } 548 | 549 | file_version *Version = (file_version *) malloc(sizeof(*Version)); 550 | if(!Version) 551 | { 552 | fprintf(stderr, "Fatal error we're toast\n"); 553 | return; 554 | } 555 | Version->VersionNumber = VersionIndex; 556 | Version->dwLowDateTime = dwLowDateTime; 557 | Version->dwHighDateTime = dwHighDateTime; 558 | Version->Deleted = Deleted; 559 | Version->OldStyle = OldStyle; 560 | VersionInsert(&Record.Versions, Version); 561 | } 562 | 563 | static void 564 | RemoveFileVersion(file_record &Record, int VersionIndex) 565 | { 566 | file_versions *VersionList = &Record.Versions; 567 | file_version *Version = VersionFind(VersionList, VersionIndex); 568 | if (!Version) 569 | { 570 | fprintf(stderr, "Tried to remove a version that wasn't present.\n"); 571 | return; 572 | } 573 | VersionRemove(VersionList, Version); 574 | if (VersionFind(VersionList, VersionIndex)) 575 | fprintf(stderr, "Internal error: attempt to remove version failed -- problem with stb_bst\n"); 576 | } 577 | 578 | static bool 579 | Win32FindFile(char *FileName, WIN32_FIND_DATA &Data) 580 | { 581 | HANDLE Handle = FindFirstFile(FileName, &Data); 582 | if(Handle != INVALID_HANDLE_VALUE) 583 | { 584 | FindClose(Handle); 585 | return(true); 586 | } 587 | 588 | return(false); 589 | } 590 | 591 | static bool 592 | WildCardMatch(char* name, char* wild) 593 | { 594 | return stb_wildmatchi(wild, name) != 0; 595 | } 596 | 597 | static bool 598 | Accept(file_rule_list &IgnoreList, char *Name) 599 | { 600 | int NameLen = strlen(Name); 601 | bool result = true; 602 | 603 | {for(file_rule *Ignore = IgnoreList; 604 | !stb_arr_end(IgnoreList,Ignore); 605 | ++Ignore) 606 | { 607 | if (Ignore->Ignore == NoIgnoreEffect) 608 | continue; 609 | // could put this in WildCardMatch, but the recursion there screws us up 610 | if (Ignore->WildCard[0] == '*') { 611 | // check the ending 612 | char *WildEnd = Ignore->WildCard + Ignore->WildCardLength; 613 | char *NameEnd = Name + NameLen; 614 | // scan backwards until we exhaust name characters or 615 | // until we hit a '*' in the pattern 616 | for (int i=0; i < NameLen; ++i) { 617 | --NameEnd, --WildEnd; 618 | if (*WildEnd == '*') break; 619 | if (*WildEnd == '?') continue; 620 | if (tolower(*WildEnd) != tolower(*NameEnd)) 621 | goto no_effect; // 'continue' at next level of nesting 622 | } 623 | } 624 | switch(Ignore->Ignore) { 625 | case DoIgnore: 626 | if(WildCardMatch(Name, Ignore->WildCard)) { 627 | result = false; 628 | } 629 | break; 630 | case DoNotIgnore: 631 | if(WildCardMatch(Name, Ignore->WildCard)) { 632 | result = true; 633 | } 634 | break; 635 | } 636 | no_effect: 637 | ; 638 | }} 639 | 640 | return result; 641 | } 642 | 643 | static bool 644 | GetFileVersionCap(mirror_config &Config, directory_entry *Entry, int &MaxVersionCount) 645 | { 646 | bool Result = false; 647 | MaxVersionCount = 1 << 24; 648 | 649 | if(Entry) 650 | { 651 | // This always returns the LAST version cap it finds, because that 652 | // way the user can control which wildcard takes precendence by 653 | // intelligently ordering them in the file. 654 | {for(file_rule *Cap = Config.FileRuleList; 655 | !stb_arr_end(Config.FileRuleList,Cap); 656 | ++Cap) 657 | { 658 | if(WildCardMatch(Entry->Name, Cap->WildCard) && 659 | Cap->CapVersionCount) 660 | { 661 | if(Entry->File[CentralFileType].Deleted) 662 | { 663 | MaxVersionCount = Cap->MaxVersionCountIfDeleted; 664 | } 665 | else 666 | { 667 | MaxVersionCount = Cap->MaxVersionCount; 668 | } 669 | Result = true; 670 | } 671 | }} 672 | } 673 | 674 | return(Result); 675 | } 676 | 677 | static conflict_behavior 678 | GetConflictBehaviorFor(mirror_config &Config, directory_entry *Entry) 679 | { 680 | conflict_behavior Result = HaltOnConflicts; 681 | 682 | if(Entry) 683 | { 684 | // This always returns the LAST conflict behavior it finds, because that 685 | // way the user can control which wildcard takes precendence by 686 | // intelligently ordering them in the file. 687 | {for(file_rule *Behavior = Config.FileRuleList; 688 | !stb_arr_end(Config.FileRuleList, Behavior); 689 | ++Behavior) 690 | { 691 | if(WildCardMatch(Entry->Name, Behavior->WildCard)) 692 | { 693 | Result = Behavior->Behavior; 694 | } 695 | }} 696 | } 697 | 698 | return(Result); 699 | } 700 | 701 | static char SearchString[3 * MAX_PATH]; 702 | 703 | bool 704 | ProcessFilename(string_table &Strings, directory_contents &Contents, 705 | file_rule_list &IgnoreList, 706 | file_type FileType, char *Name, FILETIME ftLastWriteTime, 707 | bool SuppressIgnores, int &IgnoredFileCount) 708 | { 709 | bool Result = false; 710 | bool OldStyle = false; 711 | 712 | bool Deleted = false; 713 | int Version = -1; 714 | 715 | if(FileType != WorkspaceFileType) 716 | { 717 | char *LastComma = FindLastChar(Name, ','); 718 | if(LastComma) 719 | { 720 | char *CommaLoc = LastComma; 721 | // check for new-style internal comma 722 | if (LastComma[1] == 'c' && LastComma[2] == 'm') { 723 | char *Dot = strchr(LastComma, '.'); 724 | LastComma += 3; 725 | if (Dot) { 726 | // double-check that it's the last dot 727 | if (strchr(Dot+1, '.')) { 728 | // ok, somehow we ended up with a file of the form: 729 | // "somefile,cm00001.foo.bar" 730 | // even though we only insert ",cm000001" before the _last_ dot! 731 | // so this code path is for future expansion, and for now is an error 732 | // what do we do on error? casey just lets it through with version -1 733 | // so do nothing 734 | } else { 735 | if ( LastComma[ 0 ] == 'd' ) { 736 | Deleted = true; 737 | ++LastComma; 738 | } 739 | *Dot = '\0'; 740 | Version = atoi(LastComma); 741 | *Dot = '.'; 742 | // splice the extension over the ",cm0000" part 743 | strcpy(CommaLoc, Dot); 744 | } 745 | } else { 746 | // no dot! 747 | *CommaLoc = '\0'; 748 | if ( LastComma[ 0 ] == 'd') { 749 | Deleted = true; 750 | ++LastComma; 751 | } 752 | Version = atoi(LastComma); 753 | } 754 | } else { 755 | OldStyle = true; 756 | *LastComma = '\0'; 757 | ++LastComma; 758 | if ( ( LastComma[ 0 ] == 'd' ) ) 759 | { 760 | Deleted = true; 761 | ++LastComma; 762 | } 763 | Version = atoi(LastComma); 764 | } 765 | } 766 | } 767 | 768 | assert(Name[0]); 769 | if(Accept(IgnoreList, Name)) 770 | { 771 | directory_entry *Entry = DirFind(&Contents, Name); 772 | if(!Entry) 773 | { 774 | Entry = (directory_entry *) malloc(sizeof(*Entry)); 775 | if(!Entry) 776 | { 777 | fprintf(stderr, "Fatal error we're toast\n"); 778 | return(false); 779 | } 780 | 781 | Entry->Name = Name; 782 | DirInsert(&Contents, Entry); 783 | 784 | {for(int FileIndex = 0; 785 | FileIndex < OnePastLastFileType; 786 | ++FileIndex) 787 | { 788 | file_record &Record = Entry->File[FileIndex]; 789 | Record.Present = false; 790 | Record.Deleted = false; 791 | Record.MinVersion = -1; 792 | Record.MaxVersion = -1; 793 | VersionInit(&Record.Versions); 794 | }} 795 | } 796 | assert(stricmp(Entry->Name, Name) == 0); 797 | 798 | file_record &Record = Entry->File[FileType]; 799 | AddFileVersion(Record, Version, 800 | ftLastWriteTime.dwLowDateTime, 801 | ftLastWriteTime.dwHighDateTime, Deleted, OldStyle); 802 | 803 | Result = true; 804 | } 805 | else 806 | { 807 | ++IgnoredFileCount; 808 | if (!SuppressIgnores) 809 | { 810 | printf("\n (ignored: %s)\n", Name); 811 | } 812 | } 813 | 814 | return(Result); 815 | } 816 | 817 | void 818 | BuildDirectoryRecursive(string_table &Strings, directory_contents &Contents, 819 | file_rule_list &IgnoreList, 820 | file_type FileType, char *Base, char *Directory, 821 | char *Description, int &EntryIndex, 822 | bool SuppressIgnores, int &IgnoredFiles, int &IgnoredDirectories) 823 | { 824 | if(Directory) 825 | { 826 | sprintf(SearchString, "%s/%s/*", Base, Directory); 827 | } 828 | else 829 | { 830 | sprintf(SearchString, "%s/*", Base); 831 | } 832 | 833 | WIN32_FIND_DATA FindData; 834 | HANDLE SearchHandle = FindFirstFile(SearchString, &FindData); 835 | if(SearchHandle != INVALID_HANDLE_VALUE) 836 | { 837 | do 838 | { 839 | if(stricmp(FindData.cFileName, ".") && stricmp(FindData.cFileName, "..")) 840 | { 841 | bool IsDirectory = ((FindData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0); 842 | 843 | // TODO: This potentially stores the string multiple times 844 | char *Name = StoreDirectoryFile(Strings, Directory, FindData.cFileName); 845 | 846 | if(IsDirectory) 847 | { 848 | if(Accept(IgnoreList, Name)) 849 | { 850 | BuildDirectoryRecursive(Strings, Contents, IgnoreList, 851 | FileType, Base, Name, 852 | Description, EntryIndex, 853 | SuppressIgnores, IgnoredFiles, IgnoredDirectories); 854 | } else { 855 | ++IgnoredDirectories; 856 | if (!SuppressIgnores) 857 | { 858 | printf("\n (ignored: %s)\n", Name); 859 | } 860 | } 861 | } 862 | else 863 | { 864 | if(ProcessFilename(Strings, Contents, IgnoreList, 865 | FileType, Name, FindData.ftLastWriteTime, 866 | SuppressIgnores, IgnoredFiles)) 867 | { 868 | ++EntryIndex; 869 | if ( ( EntryIndex & 127 ) == 0 ) 870 | { 871 | printf("\r %s: %d ", Description, EntryIndex); 872 | } 873 | } 874 | } 875 | } 876 | } while(FindNextFile(SearchHandle, &FindData)); 877 | 878 | FindClose(SearchHandle); 879 | } 880 | } 881 | 882 | bool 883 | AcceptDirectories(file_rule_list &IgnoreList, char *filename) 884 | { 885 | char path[MAX_PATH * 3], *s; 886 | 887 | strcpy(path, filename); 888 | s = path + strlen(path) - 1; 889 | while (s >= path) { 890 | if (*s == '/' || *s == '\\') { 891 | *s = 0; 892 | if (!Accept(IgnoreList, path)) 893 | return false; 894 | } 895 | --s; 896 | } 897 | return true; 898 | } 899 | 900 | int unsigned 901 | ReadUnsigned(char *From) 902 | { 903 | int unsigned Number = 0; 904 | while((*From >= '0') && (*From <= '9')) 905 | { 906 | Number *= 10; 907 | Number += (*From - '0'); 908 | ++From; 909 | } 910 | 911 | return(Number); 912 | } 913 | 914 | bool 915 | BuildDirectoryFromCache(string_table &Strings, directory_contents &Contents, 916 | file_rule_list &IgnoreList, 917 | file_type FileType, char *CacheFileName, 918 | char *Description, int &EntryIndex, 919 | bool &SuppressIgnores, int &IgnoredFiles, int &IgnoredDirectories) 920 | { 921 | bool Result = false, Compressed = true; 922 | unsigned int FileSize; 923 | size_t FileSize_t; 924 | char *CacheBuffer; 925 | if (!CacheFileName[0]) { 926 | return Result; 927 | } 928 | 929 | // try a compressed file 930 | CacheBuffer = stb_decompress_fromfile(CacheFileName, &FileSize); 931 | if (!CacheBuffer) { 932 | // try uncompressed 933 | CacheBuffer = stb_filec(CacheFileName, &FileSize_t); 934 | Compressed = false; 935 | } 936 | if (CacheBuffer) 937 | { 938 | // drawback to stb_file is this doesn't get printed until AFTER we read it! 939 | printf("Using%s directory cache (%dkb)...\n", Compressed ? " compressed": "", FileSize / 1024); 940 | 941 | Result = true; 942 | 943 | char *Parse = CacheBuffer; 944 | while(*Parse) 945 | { 946 | FILETIME ftLastWriteTime; 947 | 948 | ftLastWriteTime.dwHighDateTime = ReadUnsigned(Parse); 949 | while(*Parse && (*Parse != ' ')) {++Parse;} 950 | while(*Parse == ' ') {++Parse;} 951 | 952 | ftLastWriteTime.dwLowDateTime = ReadUnsigned(Parse); 953 | while(*Parse && (*Parse != ' ')) {++Parse;} 954 | while(*Parse == ' ') {++Parse;} 955 | 956 | char *ParsedName = Parse; 957 | while(*Parse && (*Parse != '\n' && *Parse != '\r')) {++Parse;} 958 | if(*Parse) 959 | { 960 | *Parse = '\0'; 961 | ++Parse; 962 | while((*Parse == '\n') || 963 | (*Parse == '\r')) {++Parse;} 964 | } 965 | 966 | char *Name = StoreDirectoryFile(Strings, 0, ParsedName); 967 | 968 | if(AcceptDirectories(IgnoreList, Name)) 969 | { 970 | if(ProcessFilename(Strings, Contents, IgnoreList, 971 | FileType, Name, ftLastWriteTime, 972 | SuppressIgnores, IgnoredFiles)) 973 | { 974 | ++EntryIndex; 975 | if ( ( EntryIndex & 127 ) == 0 ) 976 | { 977 | printf("\r %s: %d ", Description, EntryIndex); 978 | } 979 | } 980 | } else { 981 | ++IgnoredDirectories; 982 | if (!SuppressIgnores) 983 | { 984 | printf("\n (ignored: %s)\n", Name); 985 | } 986 | } 987 | } 988 | free(CacheBuffer); 989 | } 990 | 991 | return(Result); 992 | } 993 | 994 | static char BufferA[4096]; 995 | static char BufferB[4096]; 996 | 997 | static void 998 | GetVersionFileNameRelative(char *Directory, directory_entry *Entry, file_type FileType, 999 | int VersionIndex, bool Deleted, bool OldStyle, char *Buffer) 1000 | { 1001 | if(Entry) 1002 | { 1003 | char *Name = Entry->Name; 1004 | char *Separator = (Directory && *Directory) ? "/" : ""; 1005 | 1006 | if(FileType == WorkspaceFileType) 1007 | { 1008 | sprintf(Buffer, "%s%s%s", Directory, Separator, Name); 1009 | } 1010 | else 1011 | { 1012 | if (OldStyle) { 1013 | sprintf(Buffer, "%s%s%s,%s%08d", 1014 | Directory, Separator, Name, 1015 | Deleted ? "d" : "", VersionIndex 1016 | ); 1017 | } else { 1018 | char *Slash = FindLastSlash(Name); 1019 | char NameWithoutExt[4096], *Ext, *Format; 1020 | strcpy(NameWithoutExt, Name); 1021 | Ext = FindLastChar(Slash ? Slash : Name, '.'); 1022 | if (Ext == NULL) { 1023 | Ext = ""; 1024 | } else { 1025 | int ExtLoc = Ext - Name; 1026 | NameWithoutExt[ ExtLoc ] = 0; 1027 | } 1028 | // Shorten the length of the version numbers, because with 1029 | // inline version numbers it's harder to see that something 1030 | // is e.g. "foo.txt"... that is, "foo.txt,00000004" is easy to 1031 | // read as "foo.txt", but "foo,cm00000004.txt" is not. 1032 | if (VersionIndex < 1000) 1033 | Format = "%s%s%s,cm%s%04d%s"; 1034 | else 1035 | Format = "%s%s%s,cm%s%d%s"; 1036 | sprintf(Buffer, Format, 1037 | Directory, Separator, NameWithoutExt, 1038 | Deleted ? "d" : "", VersionIndex, Ext 1039 | ); 1040 | } 1041 | } 1042 | } 1043 | else 1044 | { 1045 | *Buffer = '\0'; 1046 | } 1047 | } 1048 | 1049 | static void 1050 | GetVersionFileName(mirror_config &Config, 1051 | directory_entry *Entry, file_type FileType, 1052 | int VersionIndex, bool Deleted, bool OldStyle, char *Buffer) 1053 | { 1054 | GetVersionFileNameRelative(Config.Directory[FileType], 1055 | Entry, FileType, VersionIndex, Deleted, OldStyle, Buffer); 1056 | } 1057 | 1058 | static bool 1059 | WriteDirectoryToCache(directory_contents &Contents, file_type FileType, 1060 | char *Description, char *CacheFileName) 1061 | { 1062 | bool Result = false; 1063 | int EntryIndex = 0; 1064 | 1065 | FILE *Cache = fopen(CacheFileName, "wb"); 1066 | if(Cache) 1067 | { 1068 | stb_compress_stream_start(Cache); 1069 | 1070 | {for(directory_entry *Entry = DirFirst(&Contents); 1071 | Entry; 1072 | Entry = DirNext(&Contents, Entry)) 1073 | { 1074 | file_record &Record = Entry->File[FileType]; 1075 | if(Record.Present) 1076 | { 1077 | {for(file_version *Version = VersionFirst(&Record.Versions); 1078 | Version; 1079 | Version = VersionNext(&Record.Versions, Version)) 1080 | { 1081 | char timebuffer[100]; 1082 | 1083 | GetVersionFileNameRelative("", Entry, FileType, Version->VersionNumber, Version->Deleted, Version->OldStyle, BufferA); 1084 | sprintf(timebuffer, "%u %u ", 1085 | Version->dwHighDateTime, 1086 | Version->dwLowDateTime); 1087 | stb_write(timebuffer, strlen(timebuffer)); 1088 | stb_write(BufferA, strlen(BufferA)); 1089 | stb_write("\r\n", 2); 1090 | 1091 | ++EntryIndex; 1092 | if ( ( EntryIndex & 127 ) == 0 ) 1093 | { 1094 | printf("\r %s: %d ", Description, EntryIndex); 1095 | } 1096 | }} 1097 | } 1098 | }} 1099 | 1100 | stb_compress_stream_end(TRUE); 1101 | Result = true; 1102 | } 1103 | 1104 | printf("\r %s: %d ", Description, EntryIndex); 1105 | 1106 | return(Result); 1107 | } 1108 | 1109 | static void 1110 | PrintContents(directory_contents &Contents) 1111 | { 1112 | {for(directory_entry *Entry = DirFirst(&Contents); 1113 | Entry; 1114 | Entry = DirNext(&Contents, Entry)) 1115 | { 1116 | printf("[%c%c%c] %s\n", 1117 | Entry->File[WorkspaceFileType].Present ? 'W' : ' ', 1118 | Entry->File[CentralFileType].Present ? 'C' : ' ', 1119 | Entry->File[LocalFileType].Present ? 'L' : ' ', 1120 | Entry->Name); 1121 | }} 1122 | } 1123 | 1124 | void 1125 | BuildDirectory(mirror_config &Config, directory_contents &RootContents, string_table &Strings) 1126 | { 1127 | int EntryIndex; 1128 | int IgnoredFiles; 1129 | int IgnoredDirectories; 1130 | 1131 | printf("Building directory structure:\n"); 1132 | 1133 | if(Config.SyncCentral) 1134 | { 1135 | EntryIndex = 0; 1136 | IgnoredFiles = 0; 1137 | IgnoredDirectories = 0; 1138 | if(!BuildDirectoryFromCache(Strings, RootContents, Config.FileRuleList, 1139 | CentralFileType, Config.CentralCache, 1140 | "Central (cached)", EntryIndex, 1141 | Config.SuppressIgnores, IgnoredFiles, IgnoredDirectories)) 1142 | { 1143 | BuildDirectoryRecursive(Strings, RootContents, Config.FileRuleList, 1144 | CentralFileType, 1145 | Config.Directory[CentralFileType], 0, 1146 | "Central", EntryIndex, 1147 | Config.SuppressIgnores, IgnoredFiles, IgnoredDirectories); 1148 | } 1149 | if(EntryIndex) 1150 | { 1151 | printf("\r Central: %d \n", EntryIndex); 1152 | } 1153 | if (Config.SummarizeIgnores && (IgnoredFiles || IgnoredDirectories)) 1154 | { 1155 | printf("\r (Central ignored %d dirs, %d files) \n", IgnoredDirectories, IgnoredFiles); 1156 | } 1157 | } 1158 | 1159 | if(Config.SyncWorkspace) 1160 | { 1161 | EntryIndex = 0; 1162 | IgnoredFiles = 0; 1163 | IgnoredDirectories = 0; 1164 | BuildDirectoryRecursive(Strings, RootContents, Config.FileRuleList, 1165 | WorkspaceFileType, 1166 | Config.Directory[WorkspaceFileType], 0, 1167 | "Workspace", EntryIndex, 1168 | Config.SuppressIgnores, IgnoredFiles, IgnoredDirectories); 1169 | if(EntryIndex) 1170 | { 1171 | printf("\r Workspace: %d \n", EntryIndex); 1172 | } 1173 | if (Config.SummarizeIgnores) // always show workspace if requested 1174 | { 1175 | printf("\r (Workspace ignored %d dirs, %d files) \n", IgnoredDirectories, IgnoredFiles); 1176 | } 1177 | } 1178 | 1179 | EntryIndex = 0; 1180 | IgnoredFiles = 0; 1181 | IgnoredDirectories = 0; 1182 | BuildDirectoryRecursive(Strings, RootContents, Config.FileRuleList, 1183 | LocalFileType, 1184 | Config.Directory[LocalFileType], 0, 1185 | "Local", EntryIndex, 1186 | Config.SuppressIgnores, IgnoredFiles, IgnoredDirectories); 1187 | if(EntryIndex) 1188 | { 1189 | printf("\r Local: %d \n", EntryIndex); 1190 | } 1191 | if (Config.SummarizeIgnores && (IgnoredFiles || IgnoredDirectories)) 1192 | { 1193 | printf("\r (Local ignored %d dirs, %d files) \n", IgnoredDirectories, IgnoredFiles); 1194 | } 1195 | 1196 | printf("\n"); 1197 | } 1198 | 1199 | enum sync_operation_type 1200 | { 1201 | VersionCopyOperation, 1202 | VersionDeleteOperation, 1203 | VersionMoveOperation, 1204 | MarkAsDeletedOperation, 1205 | PreCheckinOperation, 1206 | 1207 | OperationCount 1208 | }; 1209 | 1210 | char *OperationName[] = 1211 | { 1212 | "copy operations", 1213 | "delete operations", 1214 | "move operations", 1215 | "delete-mark operations", 1216 | "pre-checkin", 1217 | }; 1218 | 1219 | enum sync_reason 1220 | { 1221 | PreCheckinReason, 1222 | WorkspaceNewReason, 1223 | WorkspaceChangedReason, 1224 | WorkspaceDeletedReason, 1225 | LocalNonLatestReason, 1226 | LocalOnlyNonLatestReason, 1227 | LocalOnlyLatestReason, 1228 | LocalCentralConflictIgnoreReason, 1229 | VersionCapReason, 1230 | CentralNewReason, 1231 | CentralOnlyLatestReason, 1232 | CentralLatestDeletedReason, 1233 | 1234 | ReasonCount 1235 | }; 1236 | 1237 | char *ReasonName[] = 1238 | { 1239 | "pre-checkin rule", 1240 | "new file in workspace", 1241 | "changed file in workspace", 1242 | "deleted file from workspace", 1243 | "old version in local", 1244 | "old version only in local", 1245 | "newest version only in local", 1246 | "conflict between local & central", 1247 | "version cap", 1248 | "new file in central", 1249 | "newest version only in central", 1250 | "newest version deleted in central", 1251 | }; 1252 | 1253 | 1254 | struct sync_operation 1255 | { 1256 | sync_operation_type OperationType; 1257 | sync_reason Reason; 1258 | 1259 | directory_entry *EntryA; 1260 | file_type EntryTypeA; 1261 | 1262 | directory_entry *EntryB; 1263 | file_type EntryTypeB; 1264 | 1265 | int VersionIndex; 1266 | 1267 | // Set Overwrite to true if it's OK to clobber existing files 1268 | bool Overwrite; 1269 | 1270 | // Used for external-command operations 1271 | char *Command; 1272 | char *Parameters; 1273 | }; 1274 | 1275 | struct sync_context 1276 | { 1277 | mirror_config *Config; 1278 | bool ContinueAlways; 1279 | 1280 | sync_operation* Operations; 1281 | }; 1282 | 1283 | static void 1284 | AddError(sync_context &Context, char *FormatString, ...) 1285 | { 1286 | va_list Args; 1287 | va_start(Args, FormatString); 1288 | vfprintf(stderr, FormatString, Args); 1289 | fprintf(stderr, "\n"); 1290 | va_end(Args); 1291 | } 1292 | 1293 | static void 1294 | AddNotice(sync_context &Context, char *FormatString, ...) 1295 | { 1296 | va_list Args; 1297 | va_start(Args, FormatString); 1298 | vfprintf(stderr, FormatString, Args); 1299 | fprintf(stderr, "\n"); 1300 | va_end(Args); 1301 | } 1302 | 1303 | static bool 1304 | InitializeSyncContext(sync_context &Context, mirror_config &Config) 1305 | { 1306 | Context.Config = &Config; 1307 | Context.ContinueAlways = Config.ContinueAlways; 1308 | Context.Operations = NULL; 1309 | return(Context.Operations != 0); 1310 | } 1311 | 1312 | static sync_operation * 1313 | AddOperation(sync_context &Context, 1314 | sync_operation_type OperationType, sync_reason Reason, 1315 | directory_entry *EntryA, file_type TypeA, 1316 | directory_entry *EntryB, file_type TypeB, 1317 | int VersionIndex) 1318 | { 1319 | sync_operation &Operation = *stb_arr_add(Context.Operations); 1320 | Operation.OperationType = OperationType; 1321 | Operation.Reason = Reason; 1322 | Operation.EntryA = EntryA; 1323 | Operation.EntryTypeA = TypeA; 1324 | Operation.EntryB = EntryB; 1325 | Operation.EntryTypeB = TypeB; 1326 | Operation.VersionIndex = VersionIndex; 1327 | Operation.Overwrite = false; 1328 | 1329 | return &Operation; 1330 | } 1331 | 1332 | static file_version * 1333 | GetVersion(directory_entry *Entry, file_type Type, int VersionIndex) 1334 | { 1335 | file_version *Result = 0; 1336 | 1337 | if(Entry) 1338 | { 1339 | file_versions *Versions = &Entry->File[Type].Versions; 1340 | if(Type == WorkspaceFileType) 1341 | { 1342 | Result = VersionFirst(Versions); 1343 | } 1344 | else 1345 | { 1346 | Result = VersionFind(Versions, VersionIndex); 1347 | assert(!Result || (Result->VersionNumber == VersionIndex)); 1348 | } 1349 | } 1350 | 1351 | return(Result); 1352 | } 1353 | 1354 | static void 1355 | EnsureDirectoryExistsForFile(char *FileName) 1356 | { 1357 | {for(char *Scan = FileName; 1358 | *Scan; 1359 | ++Scan) 1360 | { 1361 | if((*Scan == '/') || (*Scan == '\\')) 1362 | { 1363 | char Swap = *Scan; 1364 | *Scan = '\0'; 1365 | CreateDirectory(FileName, 0); 1366 | *Scan = Swap; 1367 | } 1368 | }} 1369 | } 1370 | 1371 | char ExecuteBuffer[1 << 16]; 1372 | char CommandLineBuffer[1 << 16]; 1373 | 1374 | static bool 1375 | ExecuteSystemCommand(char *CommandPath, char *CommandLine, 1376 | char *WorkingDirectory, int &ExitCodeReturn) 1377 | { 1378 | STARTUPINFO StartupInformation = {sizeof(StartupInformation)}; 1379 | StartupInformation.lpTitle = CommandPath; 1380 | StartupInformation.dwFlags = 0;//STARTF_USESTDHANDLES; 1381 | StartupInformation.hStdInput = GetStdHandle(STD_INPUT_HANDLE); 1382 | StartupInformation.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE); 1383 | StartupInformation.hStdError = GetStdHandle(STD_ERROR_HANDLE); 1384 | 1385 | char *Scan = CommandPath; 1386 | char *OutBuffer = ExecuteBuffer; 1387 | *OutBuffer++='"'; // quote the command 1388 | while(*Scan) 1389 | { 1390 | if(*Scan == '/') 1391 | { 1392 | *OutBuffer++ = '\\'; 1393 | ++Scan; 1394 | } 1395 | else 1396 | { 1397 | *OutBuffer++ = *Scan++; 1398 | } 1399 | } 1400 | *OutBuffer++='"'; // quote the command 1401 | *OutBuffer++ = ' '; 1402 | Scan = CommandLine; 1403 | while(*Scan) 1404 | { 1405 | *OutBuffer++ = *Scan++; 1406 | } 1407 | *OutBuffer++ = '\0'; 1408 | 1409 | 1410 | PROCESS_INFORMATION ProcessInformation; 1411 | if(CreateProcess(0, ExecuteBuffer, 1412 | 0, 0, 1413 | FALSE, // Don't inherit handles 1414 | NORMAL_PRIORITY_CLASS, 1415 | 0, // TODO: Manage the environment here? 1416 | WorkingDirectory, 1417 | &StartupInformation, 1418 | &ProcessInformation)) 1419 | { 1420 | if(WaitForSingleObject(ProcessInformation.hProcess, INFINITE) == WAIT_OBJECT_0) 1421 | { 1422 | DWORD ExitCode = -1; 1423 | GetExitCodeProcess(ProcessInformation.hProcess, &ExitCode); 1424 | CloseHandle(ProcessInformation.hProcess); 1425 | 1426 | ExitCodeReturn = (int)ExitCode; 1427 | return(true); 1428 | } 1429 | } 1430 | 1431 | return(false); 1432 | } 1433 | 1434 | static bool 1435 | FileExists(char *FileName) 1436 | { 1437 | DWORD InvalidFileAttributes = 0xFFFFFFFF; 1438 | return(GetFileAttributes(FileName) != InvalidFileAttributes); 1439 | } 1440 | 1441 | static bool 1442 | CMirrorDeleteFile(mirror_config &Config, file_type FileType, char *File) 1443 | { 1444 | bool Result = false; 1445 | 1446 | if(FileExists(File)) 1447 | { 1448 | if(Config.DeleteCommand) 1449 | { 1450 | sprintf(CommandLineBuffer, "%s \"%s\"", 1451 | FileTypeText[FileType], File); 1452 | 1453 | int ExitCode; 1454 | Result = (ExecuteSystemCommand(Config.DeleteCommand, 1455 | CommandLineBuffer, 0, ExitCode) && 1456 | (ExitCode == 0)); 1457 | } 1458 | else 1459 | { 1460 | DWORD Attributes = GetFileAttributes(File); 1461 | if (Attributes & FILE_ATTRIBUTE_READONLY) 1462 | { 1463 | SetFileAttributes(File, Attributes & (~FILE_ATTRIBUTE_READONLY)); 1464 | } 1465 | 1466 | Result = (DeleteFileWrapped(Config.Directory[WorkspaceFileType], 1467 | Config.Directory[LocalFileType], 1468 | Config.Directory[CentralFileType], 1469 | File) == TRUE); 1470 | } 1471 | } 1472 | 1473 | return(Result); 1474 | } 1475 | 1476 | static bool 1477 | CMirrorCopyFile(mirror_config &Config, 1478 | file_type FromFileType, 1479 | file_type ToFileType, 1480 | char *From, 1481 | char *To, 1482 | bool Overwrite) 1483 | { 1484 | if(Config.CopyCommand) 1485 | { 1486 | sprintf(CommandLineBuffer, "%s %s \"%s\" \"%s\" \"%s\"", 1487 | FileTypeText[FromFileType], 1488 | FileTypeText[ToFileType], 1489 | From, To, 1490 | Config.Directory[ToFileType]); 1491 | 1492 | int ExitCode; 1493 | return(ExecuteSystemCommand(Config.CopyCommand, 1494 | CommandLineBuffer, 0, ExitCode) && 1495 | (ExitCode == 0)); 1496 | } 1497 | else 1498 | { 1499 | // Copy the new file in its place 1500 | if(CopyFileWrapped(Config.Directory[WorkspaceFileType], From, To, !Overwrite)) 1501 | { 1502 | return(true); 1503 | } 1504 | else 1505 | { 1506 | // if we failed, AND overwrite is enabled, try deleting it ourselves... 1507 | // important to structure it this way so it can use Config.DeleteCommand... 1508 | // alternative is to move the Config.DeleteCommand stuff down to the low level... 1509 | // which maybe is a good idea? Note old code had a bug that it didn't check Overwrite. 1510 | if (Overwrite && FileExists(To)) { 1511 | // Delete the dest file if it exists 1512 | CMirrorDeleteFile(Config, ToFileType, To); 1513 | if (!FileExists(To)) { 1514 | CopyFileWrapped(Config.Directory[WorkspaceFileType], From, To, !Overwrite); 1515 | } 1516 | } 1517 | return(false); 1518 | } 1519 | } 1520 | } 1521 | 1522 | static bool 1523 | CMirrorPreCheckin(sync_context &Context, char *Command, char *Params, 1524 | char *Filename, char *RepositoryRelativeName, int VersionNumber) 1525 | { 1526 | bool Result = false; 1527 | 1528 | sprintf(CommandLineBuffer, "%s \"%s\" \"%s\" %d", 1529 | Params, Filename, RepositoryRelativeName, VersionNumber); 1530 | 1531 | int ExitCode; 1532 | if((ExecuteSystemCommand(Command, CommandLineBuffer, 0, ExitCode) && 1533 | (ExitCode == 0))) 1534 | { 1535 | // Success 1536 | Result = true; 1537 | } 1538 | 1539 | return(Result); 1540 | } 1541 | 1542 | static bool 1543 | Execute(sync_context &Context, sync_operation &Operation) 1544 | { 1545 | bool Result = false; 1546 | 1547 | int VersionIndex = Operation.VersionIndex; 1548 | 1549 | mirror_config &Config = *Context.Config; 1550 | 1551 | directory_entry *EntryA = Operation.EntryA; 1552 | file_type TypeA = Operation.EntryTypeA; 1553 | file_version *VersionA = GetVersion(EntryA, TypeA, VersionIndex); 1554 | 1555 | directory_entry *EntryB = Operation.EntryB; 1556 | file_type TypeB = Operation.EntryTypeB; 1557 | file_version *VersionB = GetVersion(EntryB, TypeB, VersionIndex); 1558 | 1559 | switch(Operation.OperationType) 1560 | { 1561 | case VersionCopyOperation: 1562 | { 1563 | assert(Operation.EntryA); 1564 | assert(Operation.EntryB); 1565 | assert(VersionA); 1566 | 1567 | bool Deleted = VersionA->Deleted; 1568 | bool OldStyle = VersionA->OldStyle; 1569 | GetVersionFileName(*Context.Config, EntryA, TypeA, VersionIndex, Deleted, OldStyle, BufferA); 1570 | GetVersionFileName(*Context.Config, EntryB, TypeB, VersionIndex, Deleted, false , BufferB); 1571 | 1572 | EnsureDirectoryExistsForFile(BufferA); 1573 | EnsureDirectoryExistsForFile(BufferB); 1574 | 1575 | if((TypeB != WorkspaceFileType) || Config.WritableWorkspace) 1576 | { 1577 | if(CMirrorCopyFile(Config, TypeA, TypeB, BufferA, BufferB, Operation.Overwrite)) 1578 | { 1579 | // NOTE: We re-check the file times here instead of just 1580 | // using the ones we assume it will be, because it's possible 1581 | // that someone has changed the file during the cmirror sync 1582 | // operation and we don't want to leave the cache in an 1583 | // invalid state if caching is active. 1584 | WIN32_FIND_DATA FindData; 1585 | if(Win32FindFile(BufferB, FindData)) 1586 | { 1587 | AddFileVersion(EntryB->File[TypeB], VersionIndex, 1588 | FindData.ftLastWriteTime.dwLowDateTime, 1589 | FindData.ftLastWriteTime.dwHighDateTime, 1590 | Deleted, false); 1591 | } else { 1592 | // do something besides Result = true ??? 1593 | } 1594 | 1595 | Result = true; 1596 | } 1597 | else 1598 | { 1599 | AddError(Context, "Copy failed from %s to %s", BufferA, BufferB); 1600 | } 1601 | } 1602 | else 1603 | { 1604 | AddNotice(Context, "Suppressed write of %s (workspace is not writable)", BufferB); 1605 | } 1606 | } break; 1607 | 1608 | case VersionDeleteOperation: 1609 | { 1610 | assert(Operation.EntryA); 1611 | 1612 | if ( VersionA ) 1613 | { 1614 | bool Deleted = VersionA->Deleted; 1615 | bool OldStyle = VersionA->OldStyle; 1616 | GetVersionFileName(*Context.Config, EntryA, TypeA, VersionIndex, Deleted, OldStyle, BufferA); 1617 | 1618 | if((TypeA != WorkspaceFileType) || Config.WritableWorkspace) 1619 | { 1620 | if(CMirrorDeleteFile(Config, TypeA, BufferA)) 1621 | { 1622 | if (TypeA == CentralFileType && *Config.CentralCache) 1623 | { 1624 | RemoveFileVersion(EntryA->File[TypeA], VersionIndex); 1625 | } 1626 | Result = true; 1627 | } 1628 | else 1629 | { 1630 | AddError(Context, "Deletion failed on file %s", BufferA); 1631 | } 1632 | } 1633 | else 1634 | { 1635 | AddNotice(Context, "Suppressed deletion of %s (workspace is not writable)", BufferA); 1636 | } 1637 | } 1638 | else 1639 | { 1640 | AddNotice(Context, "Version %i didn't exist for %s", VersionIndex, EntryA->Name ); 1641 | } 1642 | } break; 1643 | 1644 | case VersionMoveOperation: 1645 | { 1646 | assert(Operation.EntryA); 1647 | assert(Operation.EntryB); 1648 | assert(VersionA); 1649 | 1650 | bool Deleted = VersionA->Deleted; 1651 | bool OldStyle = VersionA->OldStyle; 1652 | GetVersionFileName(*Context.Config, EntryA, TypeA, VersionIndex, Deleted, OldStyle, BufferA); 1653 | GetVersionFileName(*Context.Config, EntryB, TypeB, VersionIndex, Deleted, false , BufferB); 1654 | 1655 | EnsureDirectoryExistsForFile(BufferA); 1656 | EnsureDirectoryExistsForFile(BufferB); 1657 | 1658 | if((TypeB != WorkspaceFileType) || Config.WritableWorkspace) 1659 | { 1660 | if(CMirrorCopyFile(Config, TypeA, TypeB, BufferA, BufferB, Operation.Overwrite)) 1661 | { 1662 | // NOTE: We re-check the file times here instead of just 1663 | // using the ones we assume it will be, because it's possible 1664 | // that someone has changed the file during the cmirror sync 1665 | // operation and we don't want to leave the cache in an 1666 | // invalid state if caching is active. 1667 | WIN32_FIND_DATA FindData; 1668 | if(Win32FindFile(BufferB, FindData)) 1669 | { 1670 | AddFileVersion(EntryB->File[TypeB], VersionIndex, 1671 | FindData.ftLastWriteTime.dwLowDateTime, 1672 | FindData.ftLastWriteTime.dwHighDateTime, 1673 | Deleted, false); 1674 | } else { 1675 | // do something besides Result = true ??? 1676 | } 1677 | 1678 | if(CMirrorDeleteFile(Config, TypeA, BufferA)) 1679 | { 1680 | if (TypeA == CentralFileType && *Config.CentralCache) 1681 | { 1682 | RemoveFileVersion(EntryA->File[TypeA], VersionIndex); 1683 | } 1684 | Result = true; 1685 | } 1686 | else 1687 | { 1688 | AddError(Context, "Deletion-for-move failed on file %s", BufferA); 1689 | } 1690 | } 1691 | else 1692 | { 1693 | AddError(Context, "Copy-for-move failed from %s to %s", BufferA, BufferB); 1694 | } 1695 | } 1696 | else 1697 | { 1698 | AddNotice(Context, "Suppressed write of %s (workspace is not writable)", BufferB); 1699 | } 1700 | break; 1701 | } 1702 | 1703 | case MarkAsDeletedOperation: 1704 | { 1705 | GetVersionFileName(*Context.Config, EntryA, TypeA, VersionIndex, true, false, BufferA); 1706 | HANDLE File = CreateFile(BufferA, 0, FILE_SHARE_WRITE, 0, CREATE_NEW, 0, 0); 1707 | if(File != INVALID_HANDLE_VALUE) 1708 | { 1709 | WIN32_FIND_DATA FindData; 1710 | 1711 | CloseHandle(File); 1712 | 1713 | // now get the file and reinsert into the list 1714 | if(Win32FindFile( BufferA, FindData)) 1715 | { 1716 | AddFileVersion(EntryA->File[TypeA], VersionIndex, FindData.ftLastWriteTime.dwLowDateTime, FindData.ftLastWriteTime.dwHighDateTime, true, false); 1717 | } 1718 | 1719 | Result = true; 1720 | } 1721 | else 1722 | { 1723 | AddError(Context, "Creation of deletion marker failed on file %s", BufferA); 1724 | } 1725 | } break; 1726 | 1727 | case PreCheckinOperation: 1728 | { 1729 | assert(TypeA == WorkspaceFileType); 1730 | assert(Operation.EntryA); 1731 | assert(VersionA); 1732 | 1733 | GetVersionFileName(*Context.Config, EntryA, TypeA, 1734 | VersionIndex, false, false, BufferA); 1735 | if(CMirrorPreCheckin(Context, Operation.Command, Operation.Parameters, 1736 | BufferA, EntryA->Name, VersionIndex)) 1737 | { 1738 | Result = true; 1739 | } 1740 | else 1741 | { 1742 | AddError(Context, "Pre-checkin operation [%s %s] failed on file \"%s\"", 1743 | Operation.Command, Operation.Parameters, BufferA); 1744 | } 1745 | } break; 1746 | 1747 | default: 1748 | { 1749 | AddError(Context, "INTERNAL ERROR: Unrecognized operation type"); 1750 | } break; 1751 | } 1752 | 1753 | return(Result); 1754 | } 1755 | 1756 | static bool 1757 | ExecuteSyncContext(sync_context &Context) 1758 | { 1759 | bool Success = true; 1760 | 1761 | {for(int SyncIndex = 0; 1762 | (SyncIndex < stb_arr_len(Context.Operations)) && Success; 1763 | ++SyncIndex) 1764 | { 1765 | printf("\r Executing: %d / %d ", SyncIndex + 1, stb_arr_len(Context.Operations)); 1766 | sync_operation &Operation = Context.Operations[SyncIndex]; 1767 | if(!Execute(Context, Operation)) 1768 | { 1769 | Success = ShouldContinue(Context.ContinueAlways, NULL); 1770 | } 1771 | }} 1772 | printf("\n"); 1773 | 1774 | return(Success); 1775 | } 1776 | 1777 | static void 1778 | FreeSyncContext(sync_context &Context) 1779 | { 1780 | Context.Operations = NULL; 1781 | } 1782 | 1783 | // TODO: Pick this buffer size with a little less arbitrariness, shall we? 1784 | static unsigned char CompareBufferA[1 << 16]; 1785 | static unsigned char CompareBufferB[1 << 16]; 1786 | static bool 1787 | FilesAreEquivalentByteForByte(sync_context &Context, char *FileNameA, char *FileNameB) 1788 | { 1789 | bool Result = false; 1790 | 1791 | FILE *FileA = fopen(FileNameA, "rb"); 1792 | FILE *FileB = fopen(FileNameB, "rb"); 1793 | if(FileA && FileB) 1794 | { 1795 | int ReadSizeA, ReadSizeB; 1796 | Result = true; 1797 | while(Result && 1798 | (ReadSizeA = fread(CompareBufferA, 1, sizeof(CompareBufferA), FileA)) && 1799 | (ReadSizeB = fread(CompareBufferB, 1, sizeof(CompareBufferB), FileB))) 1800 | { 1801 | Result = ((ReadSizeA == ReadSizeB) && 1802 | (memcmp(CompareBufferA, CompareBufferB, ReadSizeA) == 0)); 1803 | } 1804 | } 1805 | else 1806 | { 1807 | AddError(Context, "Unable to byte-compare %s and %s", FileNameA, FileNameB); 1808 | } 1809 | 1810 | // TODO: Does fclose() accept 0 nicely? 1811 | if(FileA) {fclose(FileA);} 1812 | if(FileB) {fclose(FileB);} 1813 | 1814 | return(Result); 1815 | } 1816 | 1817 | /*static bool 1818 | Win32GetFileDate(char *FileName, FILETIME &Date) 1819 | { 1820 | bool DateDetermined = false; 1821 | 1822 | WIN32_FIND_DATA Data; 1823 | if(Win32FindFile(FileName, Data)) 1824 | { 1825 | Date = Data.ftLastWriteTime; 1826 | DateDetermined = true; 1827 | } 1828 | 1829 | return(DateDetermined); 1830 | } 1831 | 1832 | static bool 1833 | FilesHaveEquivalentModificationStamps(sync_context &Context, char *FileNameA, char *FileNameB) 1834 | { 1835 | bool Result = false; 1836 | 1837 | FILETIME DateA, DateB; 1838 | if(Win32GetFileDate(FileNameA, DateA) && 1839 | Win32GetFileDate(FileNameB, DateB)) 1840 | { 1841 | Result = ((DateA.dwHighDateTime == DateB.dwHighDateTime) && 1842 | (DateA.dwLowDateTime == DateB.dwLowDateTime)); 1843 | } 1844 | else 1845 | { 1846 | AddError(Context, "Unable to date-compare %s and %s", BufferA, BufferB); 1847 | } 1848 | 1849 | return(Result); 1850 | }*/ 1851 | 1852 | static bool 1853 | AreEquivalentMethod(sync_context &Context, 1854 | directory_entry &EntryA, file_type TypeA, 1855 | directory_entry &EntryB, file_type TypeB, 1856 | int VersionIndex, diff_method Method) 1857 | { 1858 | bool Result = false; 1859 | 1860 | file_version *VersionA = GetVersion(&EntryA, TypeA, VersionIndex); 1861 | file_version *VersionB = GetVersion(&EntryB, TypeB, VersionIndex); 1862 | 1863 | if(VersionA && VersionB && 1864 | (VersionA->Deleted == VersionB->Deleted)) 1865 | { 1866 | switch(Method) 1867 | { 1868 | case ByteByByteDiff: 1869 | { 1870 | GetVersionFileName(*Context.Config, &EntryA, TypeA, 1871 | VersionIndex, VersionA->Deleted, VersionA->OldStyle, BufferA); 1872 | GetVersionFileName(*Context.Config, &EntryB, TypeB, 1873 | VersionIndex, VersionB->Deleted, VersionA->OldStyle, BufferB); 1874 | 1875 | Result = FilesAreEquivalentByteForByte(Context, BufferA, BufferB); 1876 | } break; 1877 | 1878 | case ModificationStampDiff: 1879 | { 1880 | Result = ((VersionA->dwHighDateTime == VersionB->dwHighDateTime) && 1881 | (VersionA->dwLowDateTime == VersionB->dwLowDateTime)); 1882 | 1883 | //Result = FilesHaveEquivalentModificationStamps(Context, BufferA, BufferB); 1884 | } break; 1885 | 1886 | case TimestampRepresentationTolerantStampDiff: 1887 | { 1888 | __int64 time_A = ((__int64) VersionA->dwHighDateTime << 32) + 1889 | (VersionA->dwLowDateTime); 1890 | __int64 time_B = ((__int64) VersionB->dwHighDateTime << 32) + 1891 | (VersionB->dwLowDateTime); 1892 | __int64 dt = time_A - time_B; 1893 | 1894 | Result = ((VersionA->dwHighDateTime == VersionB->dwHighDateTime) && 1895 | (VersionA->dwLowDateTime == VersionB->dwLowDateTime)); 1896 | 1897 | // Times are in nanoseconds. Different filesystems have different 1898 | // precision as to what they represent, so when converting between 1899 | // file systems we can expect a certain amount of error. 1900 | // 1901 | // on a NAS using the Linux ext3 file system, the largest error 1902 | // in I saw was: 5,600,000 1903 | // 1904 | // one second = 1,000,000,000 1905 | // one tenth = 100,000,000 1906 | // one one-hundredth = 10,000,000 1907 | // 1908 | // The bottom 7 digits of the time from the NAS are consistently 0, 1909 | // so basically it's just rounding the NTFS time to 100ths of a second. 1910 | // However, because the error was a little MORE than 5,000,000, that 1911 | // means it's not _exactly_ rounding, so we can't detect specifically 1912 | // that. 1913 | // 1914 | // One solution would be to accept times that are within 6,000,000 1915 | // nanoseconds. This would correctly address the behavior I see from 1916 | // the ext3 NAS. However, it is probably more future-proof to 1917 | // accept a wider range. We're not worried about clock drift, just 1918 | // errors due to storing _our_ times on a foreign filesystem. Since 1919 | // it's infeasible to run cmirror 10 times a second (much less have 1920 | // change files at that rate simultaneously), we can safely set a tolerance 1921 | // on the order of 1/10th of a second. If we hypothesize a file system 1922 | // that has only 1/10th second storage precision, we need to deal 1923 | // with rounding to that level, so we'll use 2/10ths of a second. 1924 | 1925 | // @TODO: allow putting this in cmirror.config ? 1926 | #define TimestampTolerance 200000000 1927 | 1928 | Result = (-TimestampTolerance <= dt && dt <= TimestampTolerance); 1929 | } break; 1930 | } 1931 | } 1932 | 1933 | return(Result); 1934 | } 1935 | 1936 | static bool 1937 | AreEquivalent(sync_context &Context, 1938 | directory_entry &EntryA, file_type TypeA, 1939 | directory_entry &EntryB, file_type TypeB, 1940 | int VersionIndex) 1941 | { 1942 | bool Result = false; 1943 | 1944 | diff_method Method = Context.Config->DiffMethod[TypeA][TypeB]; 1945 | switch(Method) 1946 | { 1947 | case TRTSWithByteFallbackDiff: 1948 | { 1949 | Result = AreEquivalentMethod(Context, EntryA, TypeA, EntryB, TypeB, VersionIndex, TimestampRepresentationTolerantStampDiff); 1950 | if(!Result) 1951 | { 1952 | Result = AreEquivalentMethod(Context, EntryA, TypeA, EntryB, TypeB, VersionIndex, ByteByByteDiff); 1953 | } 1954 | } break; 1955 | 1956 | default: 1957 | { 1958 | Result = AreEquivalentMethod(Context, EntryA, TypeA, EntryB, TypeB, VersionIndex, Method); 1959 | } break; 1960 | } 1961 | 1962 | return(Result); 1963 | } 1964 | 1965 | static void 1966 | AddVersionCopyOperation(sync_context &Context, sync_reason Reason, 1967 | directory_entry &EntryA, file_type TypeA, 1968 | directory_entry &EntryB, file_type TypeB, 1969 | int VersionIndex) 1970 | { 1971 | AddOperation(Context, VersionCopyOperation, Reason, &EntryA, TypeA, &EntryB, TypeB, VersionIndex); 1972 | } 1973 | 1974 | static void 1975 | AddVersionOverwriteOperation(sync_context &Context, sync_reason Reason, 1976 | directory_entry &EntryA, file_type TypeA, 1977 | directory_entry &EntryB, file_type TypeB, 1978 | int VersionIndex) 1979 | { 1980 | sync_operation *Operation = 1981 | AddOperation(Context, VersionCopyOperation, Reason, 1982 | &EntryA, TypeA, 1983 | &EntryB, TypeB, VersionIndex); 1984 | if(Operation) 1985 | { 1986 | Operation->Overwrite = true; 1987 | } 1988 | } 1989 | 1990 | static void 1991 | AddVersionMoveOperation(sync_context &Context, sync_reason Reason, 1992 | directory_entry &EntryA, file_type TypeA, 1993 | directory_entry &EntryB, file_type TypeB, 1994 | int VersionIndex) 1995 | { 1996 | AddOperation(Context, VersionMoveOperation, Reason, &EntryA, TypeA, &EntryB, TypeB, VersionIndex); 1997 | } 1998 | 1999 | static void 2000 | AddVersionDeleteOperation(sync_context &Context, sync_reason Reason, 2001 | directory_entry &EntryA, file_type TypeA, 2002 | int VersionIndex) 2003 | { 2004 | AddOperation(Context, VersionDeleteOperation, Reason, &EntryA, TypeA, 0, NullFileType, VersionIndex); 2005 | } 2006 | 2007 | static void 2008 | AddMarkAsDeletedOperation(sync_context &Context, sync_reason Reason, 2009 | directory_entry &Entry, file_type Type, 2010 | int VersionIndex) 2011 | { 2012 | AddOperation(Context, MarkAsDeletedOperation, Reason, &Entry, Type, 0, NullFileType, VersionIndex); 2013 | } 2014 | 2015 | static void 2016 | AddPreCheckinOperation(sync_context &Context, 2017 | directory_entry &EntryA, int VersionIndex) 2018 | { 2019 | file_rule_list *FileRuleList = &Context.Config->FileRuleList; 2020 | {for(file_rule *Rule = *FileRuleList; 2021 | !stb_arr_end(*FileRuleList, Rule); 2022 | ++Rule) 2023 | { 2024 | if(Rule->PreCheckinCommand && 2025 | WildCardMatch(EntryA.Name, Rule->WildCard)) 2026 | { 2027 | sync_operation *Operation = 2028 | AddOperation(Context, PreCheckinOperation, PreCheckinReason, 2029 | &EntryA, WorkspaceFileType, 2030 | 0, NullFileType, VersionIndex); 2031 | Operation->Command = Rule->PreCheckinCommand; 2032 | Operation->Parameters = Rule->PreCheckinCommandParameters; 2033 | } 2034 | }} 2035 | } 2036 | 2037 | static int 2038 | GetLatestVersion(file_record &Record) 2039 | { 2040 | assert(Record.MaxVersion != -1); 2041 | return(Record.MaxVersion); 2042 | } 2043 | 2044 | static int 2045 | GetNextVersion(file_record &Record) 2046 | { 2047 | return(Record.MaxVersion + 1); 2048 | } 2049 | 2050 | bool 2051 | SynchronizeLocalAndWorkspace(sync_context &Context, directory_contents &RootContents) 2052 | { 2053 | bool Result = true; 2054 | 2055 | int EntryIndex = 0; 2056 | {for(directory_entry *Entry = DirFirst(&RootContents); 2057 | Entry && Result; 2058 | Entry = DirNext(&RootContents, Entry)) 2059 | { 2060 | ++EntryIndex; 2061 | 2062 | if ( ( EntryIndex & 127 ) == 0 ) 2063 | { 2064 | printf("\r Workspace <-> Local: %d ", EntryIndex); 2065 | } 2066 | 2067 | // 2068 | // First, sync the workspace and local files 2069 | // 2070 | if(Entry->File[WorkspaceFileType].Present) 2071 | { 2072 | // See if we have any local versions of this file at all 2073 | if(Entry->File[LocalFileType].Present) 2074 | { 2075 | if(AreEquivalent(Context, 2076 | *Entry, WorkspaceFileType, 2077 | *Entry, LocalFileType, 2078 | GetLatestVersion(Entry->File[LocalFileType]))) 2079 | { 2080 | // The file has not changed 2081 | } 2082 | else 2083 | { 2084 | // The file has changed - sync it back to local as a new version 2085 | int VersionIndex = GetNextVersion(Entry->File[LocalFileType]); 2086 | AddPreCheckinOperation(Context, *Entry, VersionIndex); 2087 | AddVersionCopyOperation(Context, WorkspaceChangedReason, 2088 | *Entry, WorkspaceFileType, 2089 | *Entry, LocalFileType, 2090 | VersionIndex); 2091 | } 2092 | } 2093 | else 2094 | { 2095 | // This is a new file, copy it to the local directory 2096 | int VersionIndex = 1; 2097 | AddPreCheckinOperation(Context, *Entry, VersionIndex); 2098 | AddVersionCopyOperation(Context, WorkspaceNewReason, 2099 | *Entry, WorkspaceFileType, 2100 | *Entry, LocalFileType, 2101 | VersionIndex); 2102 | } 2103 | } 2104 | else 2105 | { 2106 | // See if we ever had a version of this file 2107 | if(Entry->File[LocalFileType].Present) 2108 | { 2109 | // We did, so it must have been deleted 2110 | if(!Entry->File[LocalFileType].Deleted) 2111 | { 2112 | AddMarkAsDeletedOperation(Context, WorkspaceDeletedReason, 2113 | *Entry, LocalFileType, 2114 | GetNextVersion(Entry->File[LocalFileType])); 2115 | } 2116 | } 2117 | else 2118 | { 2119 | // The server alone has this file. We don't do 2120 | // anything with it here, then, we just let the 2121 | // server sync pick it up. 2122 | } 2123 | } 2124 | }} 2125 | 2126 | if ( EntryIndex ) 2127 | { 2128 | printf("\r Workspace <-> Local: %d \n", EntryIndex); 2129 | } 2130 | 2131 | return(Result); 2132 | } 2133 | 2134 | bool 2135 | SynchronizeLocalAndCentral(sync_context &Context, directory_contents &RootContents) 2136 | { 2137 | bool Result = true; 2138 | 2139 | // TODO: I should probably issue an overwrite operation ONLY if the workspace 2140 | // is marked as having the file, to guard against the possibility of overwriting 2141 | // a file I missed somehow... 2142 | 2143 | int EntryIndex = 0; 2144 | {for(directory_entry *Entry = DirFirst(&RootContents); 2145 | Entry && Result; 2146 | Entry = DirNext(&RootContents, Entry)) 2147 | { 2148 | ++EntryIndex; 2149 | 2150 | if ( ( EntryIndex & 127 ) == 0 ) 2151 | { 2152 | printf("\r Local <-> Central: %d ", EntryIndex); 2153 | } 2154 | 2155 | // 2156 | // Second, sync the local and server files 2157 | // 2158 | if(Entry->File[CentralFileType].Present) 2159 | { 2160 | if(Entry->File[LocalFileType].Present) 2161 | { 2162 | int MaxVersion = Entry->File[CentralFileType].MaxVersion; 2163 | if(MaxVersion < Entry->File[LocalFileType].MaxVersion) 2164 | { 2165 | MaxVersion = Entry->File[LocalFileType].MaxVersion; 2166 | } 2167 | 2168 | {for(int VersionIndex = Entry->File[LocalFileType].MinVersion; 2169 | VersionIndex < MaxVersion; 2170 | ++VersionIndex) 2171 | { 2172 | file_version *LocalVersion = GetVersion(Entry, LocalFileType, VersionIndex); 2173 | file_version *CentralVersion = GetVersion(Entry, CentralFileType, VersionIndex); 2174 | 2175 | if(LocalVersion) 2176 | { 2177 | if(!CentralVersion) 2178 | { 2179 | AddVersionMoveOperation(Context, LocalOnlyNonLatestReason, 2180 | *Entry, LocalFileType, 2181 | *Entry, CentralFileType, 2182 | VersionIndex); 2183 | } 2184 | else 2185 | { 2186 | if(AreEquivalent(Context, 2187 | *Entry, CentralFileType, 2188 | *Entry, LocalFileType, 2189 | VersionIndex)) 2190 | { 2191 | AddVersionDeleteOperation(Context, LocalNonLatestReason, *Entry, 2192 | LocalFileType, 2193 | VersionIndex); 2194 | } 2195 | else if (GetConflictBehaviorFor(*Context.Config, Entry) == 2196 | IgnoreConflicts) 2197 | { 2198 | AddVersionDeleteOperation(Context, LocalCentralConflictIgnoreReason, *Entry, 2199 | LocalFileType, 2200 | VersionIndex); 2201 | } 2202 | else 2203 | { 2204 | AddError(Context, 2205 | "Local \"%s\" differs with central version with same index (%d) - " 2206 | "please repair manually.", Entry->Name, VersionIndex); 2207 | 2208 | Result = false; 2209 | } 2210 | } 2211 | } 2212 | else 2213 | { 2214 | // Nothing to do 2215 | } 2216 | }} 2217 | 2218 | // Handle the max version specially 2219 | { 2220 | file_version *LocalVersion = GetVersion(Entry, LocalFileType, MaxVersion); 2221 | file_version *CentralVersion = GetVersion(Entry, CentralFileType, MaxVersion); 2222 | 2223 | if(LocalVersion) 2224 | { 2225 | if(!CentralVersion) 2226 | { 2227 | AddVersionCopyOperation(Context, LocalOnlyLatestReason, 2228 | *Entry, LocalFileType, 2229 | *Entry, CentralFileType, 2230 | MaxVersion); 2231 | } 2232 | else if(AreEquivalent(Context, 2233 | *Entry, CentralFileType, 2234 | *Entry, LocalFileType, 2235 | MaxVersion) || 2236 | (GetConflictBehaviorFor(*Context.Config, Entry) == 2237 | IgnoreConflicts)) 2238 | { 2239 | // Nothing to do 2240 | } 2241 | else 2242 | { 2243 | AddError(Context, 2244 | "Local \"%s\" differs with central version with same index (%d) - " 2245 | "please repair manually.", Entry->Name, MaxVersion); 2246 | 2247 | Result = false; 2248 | } 2249 | } 2250 | else 2251 | { 2252 | if(CentralVersion) 2253 | { 2254 | AddVersionCopyOperation(Context, CentralOnlyLatestReason, 2255 | *Entry, CentralFileType, 2256 | *Entry, LocalFileType, 2257 | MaxVersion); 2258 | 2259 | if(Entry->File[WorkspaceFileType].Present) 2260 | { 2261 | if(Entry->File[CentralFileType].Deleted) 2262 | { 2263 | AddVersionDeleteOperation(Context, CentralLatestDeletedReason, *Entry, 2264 | WorkspaceFileType, 2265 | MaxVersion); 2266 | } 2267 | else 2268 | { 2269 | AddVersionOverwriteOperation(Context, CentralOnlyLatestReason, 2270 | *Entry, LocalFileType, 2271 | *Entry, WorkspaceFileType, 2272 | MaxVersion); 2273 | } 2274 | } 2275 | else 2276 | { 2277 | AddVersionCopyOperation(Context, CentralOnlyLatestReason, 2278 | *Entry, LocalFileType, 2279 | *Entry, WorkspaceFileType, 2280 | MaxVersion); 2281 | } 2282 | } 2283 | else 2284 | { 2285 | AddError(Context, 2286 | "INTERNAL ERROR: It should not be possible to have " 2287 | "neither a local nor a server version at this point"); 2288 | 2289 | Result = false; 2290 | } 2291 | } 2292 | } 2293 | } 2294 | else 2295 | { 2296 | // This file exists only on the server - copy down the latest version. 2297 | AddVersionCopyOperation(Context, CentralNewReason, 2298 | *Entry, CentralFileType, 2299 | *Entry, LocalFileType, 2300 | GetLatestVersion(Entry->File[CentralFileType])); 2301 | if(!Entry->File[CentralFileType].Deleted) 2302 | { 2303 | AddVersionOverwriteOperation(Context, CentralNewReason, 2304 | *Entry, LocalFileType, 2305 | *Entry, WorkspaceFileType, 2306 | GetLatestVersion(Entry->File[CentralFileType])); 2307 | } 2308 | } 2309 | } 2310 | else 2311 | { 2312 | if(Entry->File[LocalFileType].Present) 2313 | { 2314 | // The local machine has this file, but the server does not, so we 2315 | // copy the local machines' versions to the server and delete them 2316 | {for(int VersionIndex = Entry->File[LocalFileType].MinVersion; 2317 | VersionIndex <= Entry->File[LocalFileType].MaxVersion; 2318 | ++VersionIndex) 2319 | { 2320 | if(VersionIndex == Entry->File[LocalFileType].MaxVersion) 2321 | { 2322 | AddVersionCopyOperation(Context, LocalOnlyLatestReason, 2323 | *Entry, LocalFileType, 2324 | *Entry, CentralFileType, 2325 | VersionIndex); 2326 | } 2327 | else 2328 | { 2329 | AddVersionMoveOperation(Context, LocalOnlyNonLatestReason, 2330 | *Entry, LocalFileType, 2331 | *Entry, CentralFileType, 2332 | VersionIndex); 2333 | } 2334 | }} 2335 | } 2336 | else 2337 | { 2338 | // Neither the server nor the local machine have this file. 2339 | // How is that possible??? 2340 | AddError(Context, 2341 | "INTERNAL ERROR: It should not be possible to only " 2342 | "have a workspace file at this point"); 2343 | Result = ShouldContinue( Context.ContinueAlways, NULL ); 2344 | } 2345 | } 2346 | 2347 | // If this file has a version cap imposed, clean up all versions 2348 | // that exceed the cap 2349 | 2350 | // Subtlety: this cleanup check is done here, which is before any deletions 2351 | // from this particular sync have been completed. That's so that it won't 2352 | // immediately use the deletion file cap on the sync in which the file is 2353 | // first deleted, in case you suddenly realize you've made a mistake deleting 2354 | // the file. So, it will wait until the NEXT time you cmirror to attempt the 2355 | // delete, and hopefully you'll realize it then when it prints out the deletion 2356 | // action. 2357 | int VersionCap; 2358 | if(GetFileVersionCap(*Context.Config, Entry, VersionCap)) 2359 | { 2360 | {for(int VersionIndex = Entry->File[CentralFileType].MinVersion; 2361 | VersionIndex <= (Entry->File[CentralFileType].MaxVersion - VersionCap); 2362 | ++VersionIndex) 2363 | { 2364 | // Subtlety: we must check for existence of each version here because 2365 | // a sync copies filse up from local before executing this loop. Hence, 2366 | // a machine that's been out of sync for a while might copy up an old 2367 | // version of a file from its local which has already been deleted from 2368 | // central (due to the version cap) by a more recent sync on another machine. 2369 | // This will leave a gap between the min versions and the max versions, 2370 | // which would cause us to issue an invalid deletion request for some 2371 | // versions in the middle, which don't actually exist anywhere. 2372 | file_version *CentralVersion = GetVersion(Entry, CentralFileType, VersionIndex); 2373 | if(CentralVersion) 2374 | { 2375 | AddVersionDeleteOperation(Context, VersionCapReason, 2376 | *Entry, CentralFileType, 2377 | VersionIndex); 2378 | } 2379 | }} 2380 | } 2381 | }} 2382 | if ( EntryIndex ) 2383 | { 2384 | printf("\r Local <-> Central: %d \n", EntryIndex); 2385 | } 2386 | 2387 | return(Result); 2388 | } 2389 | 2390 | // squish a qualified filename for printing by eliding some of the path 2391 | bool did_squish; 2392 | 2393 | void 2394 | SquishFilename(char *filename, int length) 2395 | { 2396 | int n,k; 2397 | --length; // printing in last column might force a newline 2398 | if ((int) strlen(filename) <= length) return; 2399 | char *s = stb_strrchr2(filename, '/', '\\'); 2400 | char *t = stb_strchr2(filename, '/', '\\'); 2401 | if (s == NULL || t == NULL || s==t) return; 2402 | n = strlen(s); 2403 | // we need to insert '...' 2404 | length -= 5; 2405 | if (n + (t-filename) > length) return; 2406 | // now we want to choose some k such that: 2407 | // t += k 2408 | // s -= k 2409 | // (strlen(s) + (t-filename) == length), +-1 2410 | // this would mean: 2411 | // ((n+k) + (t+k-filename) == length) 2412 | // n+2k+t-filename = length 2413 | k = (length - n - (t-filename))/2; 2414 | if (k <= 0) return; 2415 | s -= k; 2416 | t += k; 2417 | if ((int) strlen(s) + (t-filename) < length) --s; 2418 | assert((int) strlen(s) + (t-filename) == length); 2419 | if (s < t+5) return; 2420 | strcpy(t, " ... "); 2421 | memmove(t+5, s, strlen(s)+1); 2422 | did_squish = true; 2423 | } 2424 | 2425 | void 2426 | Print(sync_context &Context, sync_operation &Operation, bool Squish) 2427 | { 2428 | char *Reason="", *Pad=""; 2429 | int ReasonLen = 0; 2430 | 2431 | int VersionIndex = Operation.VersionIndex; 2432 | 2433 | directory_entry *EntryA = Operation.EntryA; 2434 | file_type TypeA = Operation.EntryTypeA; 2435 | file_version *VersionA = GetVersion(EntryA, TypeA, VersionIndex); 2436 | 2437 | directory_entry *EntryB = Operation.EntryB; 2438 | file_type TypeB = Operation.EntryTypeB; 2439 | file_version *VersionB = GetVersion(EntryB, TypeB, VersionIndex); 2440 | 2441 | if (Context.Config->PrintReason) { 2442 | ReasonLen = 4; 2443 | Pad = " "; 2444 | switch(Operation.Reason) { 2445 | case PreCheckinReason: Reason = " "; break; 2446 | case WorkspaceChangedReason: Reason = "wc "; break; 2447 | case WorkspaceNewReason: Reason = "WA "; break; 2448 | case WorkspaceDeletedReason: Reason = "WD "; break; 2449 | case LocalNonLatestReason: Reason = "lo "; break; 2450 | case LocalOnlyNonLatestReason: Reason = "ln "; break; 2451 | case LocalCentralConflictIgnoreReason: Reason = "C! "; break; 2452 | case LocalOnlyLatestReason: Reason = "ln "; break; 2453 | case CentralOnlyLatestReason: Reason = "CC "; break; 2454 | case CentralLatestDeletedReason: Reason = "CD "; break; 2455 | case CentralNewReason: Reason = "CA "; break; 2456 | case VersionCapReason: Reason = "vc "; break; 2457 | default: AddError(Context, "INTERNAL ERROR: Unrecognized reason"); 2458 | } 2459 | } 2460 | 2461 | switch(Operation.OperationType) 2462 | { 2463 | case VersionCopyOperation: 2464 | { 2465 | assert(Operation.EntryA); 2466 | assert(Operation.EntryB); 2467 | 2468 | bool Deleted = false; 2469 | bool OldStyle = false; // if doesn't exist yet, it will be new style 2470 | if(VersionA) 2471 | { 2472 | // TODO: This has to happen because sometimes you're copying from 2473 | // a version that won't be there unless a previous command has 2474 | // executed - what to do in this case? Perhaps that's bad because 2475 | // it means you can't continue after failure? Etc., etc. 2476 | Deleted = VersionA->Deleted; 2477 | OldStyle = VersionA->OldStyle; 2478 | } 2479 | 2480 | GetVersionFileName(*Context.Config, EntryA, TypeA, 2481 | VersionIndex, Deleted, OldStyle, BufferA); 2482 | GetVersionFileName(*Context.Config, EntryB, TypeB, 2483 | VersionIndex, Deleted, false, BufferB); 2484 | 2485 | if (Squish) { 2486 | if (Context.Config->LineLength) SquishFilename(BufferA, Context.Config->LineLength - 6 - ReasonLen); 2487 | if (Context.Config->LineLength) SquishFilename(BufferB, Context.Config->LineLength - 6 - ReasonLen); 2488 | } 2489 | printf("%scopy: %s\n", Reason, BufferA); 2490 | printf("%s -> %s\n", Pad, BufferB); 2491 | } break; 2492 | 2493 | case VersionDeleteOperation: 2494 | { 2495 | assert(Operation.EntryA); 2496 | if ( VersionA == 0 ) 2497 | { 2498 | printf("missing version: %s (%i)\n", EntryA->Name, VersionIndex ); 2499 | } 2500 | else 2501 | { 2502 | GetVersionFileName(*Context.Config, EntryA, TypeA, 2503 | VersionIndex, VersionA->Deleted, VersionA->OldStyle, BufferA); 2504 | 2505 | if (Squish && Context.Config->LineLength) SquishFilename(BufferA, Context.Config->LineLength - 8 - ReasonLen); 2506 | printf("%sdelete: %s\n", Reason, BufferA); 2507 | } 2508 | } break; 2509 | 2510 | case VersionMoveOperation: 2511 | { 2512 | assert(Operation.EntryA); 2513 | assert(Operation.EntryB); 2514 | 2515 | bool Deleted = false; 2516 | bool OldStyle = false; // if doesn't exist yet, it will be new style 2517 | if(VersionA) 2518 | { 2519 | // TODO: This has to happen because sometimes you're copying from 2520 | // a version that won't be there unless a previous command has 2521 | // executed - what to do in this case? Perhaps that's bad because 2522 | // it means you can't continue after failure? Etc., etc. 2523 | Deleted = VersionA->Deleted; 2524 | OldStyle = VersionA->OldStyle; 2525 | } 2526 | 2527 | GetVersionFileName(*Context.Config, EntryA, TypeA, 2528 | VersionIndex, Deleted, OldStyle, BufferA); 2529 | GetVersionFileName(*Context.Config, EntryB, TypeB, 2530 | VersionIndex, Deleted, false, BufferB); 2531 | 2532 | if (Squish) { 2533 | if (Context.Config->LineLength) SquishFilename(BufferA, Context.Config->LineLength - 6 - ReasonLen); 2534 | if (Context.Config->LineLength) SquishFilename(BufferB, Context.Config->LineLength - 6 - ReasonLen); 2535 | } 2536 | printf("%smove: %s\n", Reason, BufferA); 2537 | printf("%s -> %s\n", Pad, BufferB); 2538 | } break; 2539 | 2540 | case MarkAsDeletedOperation: 2541 | { 2542 | GetVersionFileName(*Context.Config, EntryA, TypeA, VersionIndex, true, false, BufferA); 2543 | if (Squish && Context.Config->LineLength) SquishFilename(BufferA, Context.Config->LineLength - 8 - ReasonLen); 2544 | printf("%sd-mark: %s\n", Reason, BufferA); 2545 | } break; 2546 | 2547 | case PreCheckinOperation: 2548 | { 2549 | GetVersionFileName(*Context.Config, EntryA, TypeA, VersionIndex, true, false, BufferA); 2550 | // @TODO: I don't use pre-checkin so I'm not sure how best to approach SquishFilenaming it 2551 | printf("%spre-checkin: %s %s %s\n", Reason, Operation.Command, Operation.Parameters, BufferA); 2552 | } break; 2553 | 2554 | default: 2555 | { 2556 | AddError(Context, "INTERNAL ERROR: Unrecognized operation type"); 2557 | } break; 2558 | } 2559 | } 2560 | 2561 | char * 2562 | FirstFileName(sync_context &Context, sync_operation &Operation) 2563 | { 2564 | int VersionIndex = Operation.VersionIndex; 2565 | 2566 | directory_entry *EntryA = Operation.EntryA; 2567 | file_type TypeA = Operation.EntryTypeA; 2568 | file_version *VersionA = GetVersion(EntryA, TypeA, VersionIndex); 2569 | 2570 | directory_entry *EntryB = Operation.EntryB; 2571 | file_type TypeB = Operation.EntryTypeB; 2572 | file_version *VersionB = GetVersion(EntryB, TypeB, VersionIndex); 2573 | 2574 | switch(Operation.OperationType) 2575 | { 2576 | case VersionCopyOperation: 2577 | { 2578 | assert(Operation.EntryA); 2579 | assert(Operation.EntryB); 2580 | 2581 | bool Deleted = false; 2582 | bool OldStyle = false; // if doesn't exist yet, it will be new style 2583 | if(VersionA) 2584 | { 2585 | // TODO: This has to happen because sometimes you're copying from 2586 | // a version that won't be there unless a previous command has 2587 | // executed - what to do in this case? Perhaps that's bad because 2588 | // it means you can't continue after failure? Etc., etc. 2589 | Deleted = VersionA->Deleted; 2590 | OldStyle = VersionA->OldStyle; 2591 | } 2592 | 2593 | GetVersionFileName(*Context.Config, EntryA, TypeA, 2594 | VersionIndex, Deleted, OldStyle, BufferA); 2595 | return BufferA; 2596 | } break; 2597 | 2598 | case VersionDeleteOperation: 2599 | { 2600 | assert(Operation.EntryA); 2601 | if ( VersionA == 0 ) 2602 | { 2603 | printf("missing version: %s (%i)\n", EntryA->Name, VersionIndex ); 2604 | } 2605 | else 2606 | { 2607 | GetVersionFileName(*Context.Config, EntryA, TypeA, 2608 | VersionIndex, VersionA->Deleted, VersionA->OldStyle, BufferA); 2609 | 2610 | return BufferA; 2611 | } 2612 | } break; 2613 | 2614 | case VersionMoveOperation: 2615 | { 2616 | assert(Operation.EntryA); 2617 | assert(Operation.EntryB); 2618 | 2619 | bool Deleted = false; 2620 | bool OldStyle = false; // if doesn't exist yet, it will be new style 2621 | if(VersionA) 2622 | { 2623 | // TODO: This has to happen because sometimes you're copying from 2624 | // a version that won't be there unless a previous command has 2625 | // executed - what to do in this case? Perhaps that's bad because 2626 | // it means you can't continue after failure? Etc., etc. 2627 | Deleted = VersionA->Deleted; 2628 | OldStyle = VersionA->OldStyle; 2629 | } 2630 | 2631 | GetVersionFileName(*Context.Config, EntryA, TypeA, 2632 | VersionIndex, Deleted, OldStyle, BufferA); 2633 | return BufferA; 2634 | } break; 2635 | 2636 | case MarkAsDeletedOperation: 2637 | { 2638 | GetVersionFileName(*Context.Config, EntryA, TypeA, VersionIndex, true, false, BufferA); 2639 | return BufferA; 2640 | } break; 2641 | 2642 | case PreCheckinOperation: 2643 | { 2644 | GetVersionFileName(*Context.Config, EntryA, TypeA, VersionIndex, true, false, BufferA); 2645 | return BufferA; 2646 | } break; 2647 | 2648 | default: 2649 | { 2650 | AddError(Context, "INTERNAL ERROR: Unrecognized operation type"); 2651 | } break; 2652 | } 2653 | return NULL; 2654 | } 2655 | 2656 | void 2657 | SummarizeOperation(int Operation, int *OpsPerReason, char *Pad) // Operation is really sync_operation_type 2658 | { 2659 | int NumDifferentReasons=0, FirstReason = -1, TotalOps=0; 2660 | {for (int i=0; i < ReasonCount; ++i) 2661 | { 2662 | TotalOps += OpsPerReason[i]; 2663 | if (OpsPerReason[i]) 2664 | { 2665 | ++NumDifferentReasons; 2666 | if (FirstReason == -1) FirstReason = i; 2667 | } 2668 | }} 2669 | 2670 | if (NumDifferentReasons == 0) 2671 | return; 2672 | 2673 | if (NumDifferentReasons == 1) 2674 | { 2675 | printf("%s%5d %s due to %s\n", Pad, OpsPerReason[FirstReason], 2676 | OperationName[Operation], 2677 | ReasonName[FirstReason]); 2678 | return; 2679 | } 2680 | 2681 | printf("%s%5d %s\n", Pad, TotalOps, 2682 | OperationName[Operation]); 2683 | {for (int i=0; i < ReasonCount; ++i) 2684 | { 2685 | if (OpsPerReason[i]) 2686 | { 2687 | printf("%s %5d due to %s\n", Pad, OpsPerReason[i], ReasonName[i]); 2688 | } 2689 | }} 2690 | } 2691 | 2692 | static void 2693 | PrintMatrixOpCount(int *ReasonArray) 2694 | { 2695 | int Total = 0; 2696 | 2697 | {for(int ReasonIndex = 0; 2698 | ReasonIndex < ReasonCount; 2699 | ++ReasonIndex) 2700 | { 2701 | Total += ReasonArray[ReasonIndex]; 2702 | }} 2703 | 2704 | if(Total) 2705 | { 2706 | printf("%3d ", Total); 2707 | } 2708 | else 2709 | { 2710 | printf(" - "); 2711 | } 2712 | } 2713 | 2714 | #define MatrixStyle 1 2715 | 2716 | void 2717 | SummarizeRecursively(stb_dirtree2 *d, stb_ptrmap *map, sync_context &Context) 2718 | { 2719 | // if there are non-zero files, print a summary record 2720 | if (stb_arr_len(d->files)) { 2721 | int OpCounts[OperationCount][ReasonCount]; 2722 | memset(OpCounts, 0, sizeof(OpCounts)); 2723 | {for (int i=0; i < stb_arr_len(d->files); ++i) { 2724 | sync_operation *s = (sync_operation *) stb_ptrmap_get(map, d->files[i]); 2725 | OpCounts[s->OperationType][s->Reason] += 1; 2726 | }} 2727 | 2728 | if(MatrixStyle) 2729 | { 2730 | printf(" "); 2731 | PrintMatrixOpCount(OpCounts[VersionCopyOperation]); 2732 | PrintMatrixOpCount(OpCounts[VersionDeleteOperation]); 2733 | PrintMatrixOpCount(OpCounts[VersionMoveOperation]); 2734 | PrintMatrixOpCount(OpCounts[MarkAsDeletedOperation]); 2735 | PrintMatrixOpCount(OpCounts[PreCheckinOperation]); 2736 | printf(" %s\n", d->fullpath); 2737 | } 2738 | else 2739 | { 2740 | printf(" %s\n", d->fullpath); 2741 | {for (int i=0; i < OperationCount; ++i) { 2742 | SummarizeOperation(i, OpCounts[i], " "); 2743 | }} 2744 | } 2745 | } 2746 | 2747 | {for (int i=0; i < stb_arr_len(d->subdirs); ++i) { 2748 | SummarizeRecursively(d->subdirs[i], map, Context); 2749 | }} 2750 | } 2751 | 2752 | 2753 | void 2754 | DoSummarizeDirs(sync_context &Context) 2755 | { 2756 | // there are easier ways to do this -- we don't need to build a 2757 | // tree structure, just find operations whose names have common 2758 | // paths... but since the tree structure DOES gather them together 2759 | // automatically, might as well use it 2760 | 2761 | // map between filenames and operations 2762 | stb_ptrmap *map = stb_ptrmap_new(); 2763 | 2764 | // gather up a list of all source filenames 2765 | char** files=NULL; 2766 | for (int i=0; i < stb_arr_len(Context.Operations); ++i) 2767 | { 2768 | char *name = FirstFileName(Context, Context.Operations[i]); 2769 | if (name) { 2770 | name = strdup(name); 2771 | stb_arr_push(files, name); 2772 | // create a mapping from the filename to the operation 2773 | stb_ptrmap_add(map, name, &Context.Operations[i]); 2774 | } 2775 | } 2776 | 2777 | // build a tree out of the list 2778 | stb_dirtree2 *d = stb_dirtree2_from_files(files, stb_arr_len(files)); 2779 | 2780 | printf("Directory Summary\n"); 2781 | if(MatrixStyle) 2782 | { 2783 | printf(" cp rm mv +d pc Path\n"); 2784 | } 2785 | SummarizeRecursively(d, map, Context); 2786 | 2787 | stb_ptrmap_destroy(map); 2788 | stb_pointer_array_free((void **) (char **) files, stb_arr_len(files)); 2789 | stb_arr_free(files); 2790 | } 2791 | 2792 | 2793 | bool 2794 | PromptSyncContext(sync_context &Context) 2795 | { 2796 | if(Context.Operations) 2797 | { 2798 | int Count = stb_arr_len(Context.Operations); 2799 | bool Suppress = (Count >= Context.Config->SyncPrintThreshold); 2800 | bool allow_squish = true; 2801 | retry: 2802 | did_squish = false; 2803 | if (!Suppress) 2804 | { 2805 | {for(int SyncIndex = 0; 2806 | SyncIndex < Count; 2807 | ++SyncIndex) 2808 | { 2809 | sync_operation &Operation = Context.Operations[SyncIndex]; 2810 | Print(Context, Operation, allow_squish); 2811 | }} 2812 | } 2813 | else 2814 | { 2815 | printf("Synchronization requires %d operations (over the printable limit of %d)\n", 2816 | Count, Context.Config->SyncPrintThreshold); 2817 | } 2818 | 2819 | if (Context.Config->SummarizeDirs || (Suppress && Context.Config->SummarizeDirsIfNotPrinted)) 2820 | { 2821 | DoSummarizeDirs(Context); 2822 | } 2823 | if (Context.Config->SummarizeSync || (Suppress && Context.Config->SummarizeSyncIfNotPrinted)) 2824 | { 2825 | printf("Summary:\n"); 2826 | int OpCounts[OperationCount][ReasonCount]; 2827 | memset(OpCounts, 0, sizeof(OpCounts)); 2828 | {for(int SyncIndex = 0; 2829 | SyncIndex < Count; 2830 | ++SyncIndex) 2831 | { 2832 | sync_operation &Operation = Context.Operations[SyncIndex]; 2833 | OpCounts[Operation.OperationType][Operation.Reason] += 1; 2834 | }} 2835 | {for(int i=0; i < OperationCount; ++i) 2836 | { 2837 | SummarizeOperation(i, OpCounts[i], ""); 2838 | }} 2839 | } 2840 | bool Choice; 2841 | if (Suppress || did_squish) 2842 | { 2843 | char Selection=0; 2844 | Choice = ShouldContinue(Context.ContinueAlways, 2845 | "Continue? (y/n/a/v to view operations)", "v", &Selection); 2846 | if (!Choice && Selection == 'v') 2847 | { 2848 | Suppress = false; 2849 | allow_squish = false; 2850 | goto retry; // could turn this into a loop, but it's the abnormal case so I hate that 2851 | } 2852 | } 2853 | else 2854 | { 2855 | Choice = ShouldContinue(Context.ContinueAlways, NULL); 2856 | } 2857 | return Choice; 2858 | } 2859 | else 2860 | { 2861 | return(true); 2862 | } 2863 | } 2864 | 2865 | static bool 2866 | DirectoryIsAccessible(char *DirectoryName, char * Description, int RetryTimes) 2867 | { 2868 | bool result; 2869 | int times = RetryTimes; 2870 | 2871 | while ( 1 ) 2872 | { 2873 | #define INVALID_FILE_ATTRIBUTES (-1) 2874 | 2875 | result = (GetFileAttributes(DirectoryName) != INVALID_FILE_ATTRIBUTES); 2876 | 2877 | --times; 2878 | 2879 | if ( ( result == true ) || ( times <= 0 ) ) 2880 | { 2881 | return( result ); 2882 | } 2883 | 2884 | printf("** %s is unavailable: %s. Retrying... **\n", Description, DirectoryName); 2885 | 2886 | Sleep( 60000 ); // wait one minute 2887 | } 2888 | 2889 | // will never get here 2890 | } 2891 | 2892 | bool 2893 | Synchronize(mirror_config &Config, directory_contents &RootContents) 2894 | { 2895 | bool Result = true; 2896 | 2897 | bool LocalIsAvailable = 2898 | DirectoryIsAccessible(Config.Directory[LocalFileType], 2899 | "Local", Config.RetryTimes[LocalFileType]); 2900 | if(LocalIsAvailable) 2901 | { 2902 | printf("Synchronizing:\n"); 2903 | 2904 | if ( Config.SyncWorkspace ) 2905 | { 2906 | bool WorkspaceIsAvailable = 2907 | DirectoryIsAccessible(Config.Directory[WorkspaceFileType], 2908 | "Workspace", Config.RetryTimes[WorkspaceFileType]); 2909 | 2910 | if(WorkspaceIsAvailable) 2911 | { 2912 | sync_context Context; 2913 | 2914 | InitializeSyncContext(Context, Config); 2915 | Result = Result && SynchronizeLocalAndWorkspace(Context, RootContents); 2916 | Result = Result && PromptSyncContext(Context); 2917 | Result = Result && ExecuteSyncContext(Context); 2918 | FreeSyncContext(Context); 2919 | } 2920 | else 2921 | { 2922 | printf("** Workspace is unavailable **\n"); 2923 | return( false ); 2924 | } 2925 | } 2926 | 2927 | bool ServerIsAvailable = 2928 | DirectoryIsAccessible(Config.Directory[CentralFileType], 2929 | "Central", Config.RetryTimes[CentralFileType]); 2930 | if(Result) 2931 | { 2932 | if(ServerIsAvailable) 2933 | { 2934 | sync_context Context; 2935 | 2936 | InitializeSyncContext(Context, Config); 2937 | Result = Result && SynchronizeLocalAndCentral(Context, RootContents); 2938 | Result = Result && PromptSyncContext(Context); 2939 | Result = Result && ExecuteSyncContext(Context); 2940 | FreeSyncContext(Context); 2941 | } 2942 | else 2943 | { 2944 | printf("** Server is unavailable **\n"); 2945 | } 2946 | } 2947 | 2948 | printf("\n"); 2949 | } 2950 | else 2951 | { 2952 | printf("** Local is unavailable **\n"); 2953 | } 2954 | 2955 | return(Result); 2956 | } 2957 | 2958 | struct file_rule_expander 2959 | { 2960 | file_rule_list *RuleList; 2961 | char **z; 2962 | file_rule *Rule; 2963 | }; 2964 | 2965 | static bool 2966 | Continue(file_rule_expander &Expander) 2967 | { 2968 | return((*Expander.z) != 0); 2969 | } 2970 | 2971 | static void 2972 | Next(file_rule_expander &Expander) 2973 | { 2974 | ++Expander.z; 2975 | Expander.Rule = Continue(Expander) ? UpdateRuleFor(Expander.RuleList, *Expander.z) : 0; 2976 | } 2977 | 2978 | static file_rule_expander 2979 | ExpandRule(file_rule_list *RuleList, char *WildCard) 2980 | { 2981 | file_rule_expander Expander; 2982 | Expander.RuleList = RuleList; 2983 | Expander.z = stb_tokens(WildCard, ";", 0); 2984 | Expander.Rule = Continue(Expander) ? UpdateRuleFor(Expander.RuleList, *Expander.z) : 0; 2985 | 2986 | // NOTE: we never free the z array, as the rule stores what we passed in, rather than copying 2987 | 2988 | return(Expander); 2989 | } 2990 | 2991 | void 2992 | MakeIgnoreRule(file_rule_list *RuleList, char *Wildcard, ignore_behavior behavior) 2993 | { 2994 | {for(file_rule_expander RuleExpand = ExpandRule(RuleList, Wildcard); 2995 | Continue(RuleExpand); 2996 | Next(RuleExpand)) 2997 | { 2998 | file_rule *Rule = RuleExpand.Rule; 2999 | Rule->Ignore = behavior; 3000 | }} 3001 | } 3002 | 3003 | char * 3004 | FindConfigFile(string_table &Strings, char *CurrentDirectory) 3005 | { 3006 | char *BaseName = "cmirror.config"; 3007 | // NOTE: This is braindead. 3008 | // NOTE: we don't bother pushing & popping strings anymore because we need to keep the filename around! 3009 | 3010 | while(1) 3011 | { 3012 | // cmirror.config is a file? 3013 | char *ConfigFileName = StoreDirectoryFile(Strings, CurrentDirectory, BaseName); 3014 | FILE *ConfigFile = fopen(ConfigFileName, "rb"); 3015 | if(ConfigFile) 3016 | { 3017 | fclose(ConfigFile); 3018 | return ConfigFileName; 3019 | } 3020 | 3021 | // cmirror.config is a directory? 3022 | WIN32_FIND_DATA FindData; 3023 | if(Win32FindFile(ConfigFileName, FindData)) 3024 | { 3025 | if (FindData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) 3026 | { 3027 | ConfigFileName = StoreDirectoryFile(Strings, ConfigFileName, BaseName); 3028 | ConfigFile = fopen(ConfigFileName, "rb"); 3029 | if (ConfigFile) 3030 | { 3031 | fclose(ConfigFile); 3032 | return(ConfigFileName); 3033 | } 3034 | fprintf(stderr, "Found cmirror.config directory in %s, but no cmirror.config file within.\n", CurrentDirectory); 3035 | exit(0); 3036 | } 3037 | // or there was a file according to FindFile, but not fopen! 3038 | return 0; 3039 | } 3040 | 3041 | char *NewCurrentDirectory = FindLastSlash(CurrentDirectory); 3042 | if(NewCurrentDirectory != CurrentDirectory) 3043 | { 3044 | *NewCurrentDirectory = '\0'; 3045 | } 3046 | else 3047 | { 3048 | return(0); 3049 | } 3050 | } 3051 | } 3052 | 3053 | struct stream 3054 | { 3055 | int LineIndex; 3056 | char *At; 3057 | }; 3058 | 3059 | enum token_type 3060 | { 3061 | UnrecongizedTokenType, 3062 | IdentifierToken, 3063 | EqualsToken, 3064 | StringToken, 3065 | IntegerToken, 3066 | SemiColonToken, 3067 | EndOfStreamToken, 3068 | }; 3069 | 3070 | struct token 3071 | { 3072 | token_type Type; 3073 | char *Content; 3074 | int Integer; 3075 | }; 3076 | 3077 | static bool 3078 | IsWhitespace(char Character) 3079 | { 3080 | return((Character == ' ') || 3081 | (Character == '\t')); 3082 | } 3083 | 3084 | static bool 3085 | IsEOL(char Character) 3086 | { 3087 | return((Character == '\n') || 3088 | (Character == '\r')); 3089 | } 3090 | 3091 | static bool 3092 | IsAlpha(char Character) 3093 | { 3094 | return(((Character >= 'a') && (Character <= 'z')) || 3095 | ((Character >= 'A') && (Character <= 'Z'))); 3096 | } 3097 | 3098 | static bool 3099 | IsNumeric(char Character) 3100 | { 3101 | return((Character >= '0') && (Character <= '9')); 3102 | } 3103 | 3104 | static void 3105 | SkipToNextLine(stream &Stream) 3106 | { 3107 | while(*Stream.At && !IsEOL(*Stream.At)) 3108 | { 3109 | ++Stream.At; 3110 | } 3111 | 3112 | if(((Stream.At[0] == '\n') && (Stream.At[1] == '\r')) || 3113 | ((Stream.At[0] == '\r') && (Stream.At[1] == '\n'))) 3114 | { 3115 | Stream.At += 2; 3116 | } 3117 | else if(IsEOL(*Stream.At)) 3118 | { 3119 | ++Stream.At; 3120 | } 3121 | 3122 | ++Stream.LineIndex; 3123 | } 3124 | 3125 | static void 3126 | StripQuotesInPlace(char *String) 3127 | { 3128 | char *Source = String; 3129 | char *Dest = String; 3130 | 3131 | bool Escaped = false; 3132 | while(*Source) 3133 | { 3134 | if(!Escaped && 3135 | ((*Source == '"') || 3136 | (*Source == '\''))) 3137 | { 3138 | ++Source; 3139 | } 3140 | else if(!Escaped && 3141 | (*Source == '\\')) 3142 | { 3143 | ++Source; 3144 | Escaped = true; 3145 | } 3146 | else 3147 | { 3148 | *Dest++ = *Source++; 3149 | Escaped = false; 3150 | } 3151 | } 3152 | 3153 | *Dest = '\0'; 3154 | } 3155 | 3156 | static char * 3157 | ParseString(stream &Stream, string_table &Strings) 3158 | { 3159 | char start_quote; 3160 | 3161 | start_quote = *Stream.At; 3162 | 3163 | assert((start_quote == '"') || (start_quote == '\'')); 3164 | ++Stream.At; 3165 | 3166 | char *Result = Strings.StoreCurrent; 3167 | while(*Stream.At && (*Stream.At != start_quote)) 3168 | { 3169 | *Strings.StoreCurrent++ = *Stream.At++; 3170 | } 3171 | *Strings.StoreCurrent++ = '\0'; 3172 | 3173 | if(*Stream.At == start_quote) 3174 | { 3175 | ++Stream.At; 3176 | } 3177 | else 3178 | { 3179 | printf("Unterminated string constant\n"); 3180 | } 3181 | 3182 | return(Result); 3183 | } 3184 | 3185 | static char * 3186 | ParseIdentifier(stream &Stream, string_table &Strings) 3187 | { 3188 | assert(IsAlpha(*Stream.At)); 3189 | 3190 | char *Result = Strings.StoreCurrent; 3191 | while(IsAlpha(*Stream.At) || IsNumeric(*Stream.At)) 3192 | { 3193 | *Strings.StoreCurrent++ = *Stream.At++; 3194 | } 3195 | *Strings.StoreCurrent++ = '\0'; 3196 | 3197 | return(Result); 3198 | } 3199 | 3200 | static token 3201 | GetToken(stream &Stream, string_table &Strings) 3202 | { 3203 | while(*Stream.At) 3204 | { 3205 | if((Stream.At[0] == '/') && (Stream.At[1] == '/')) 3206 | { 3207 | Stream.At += 2; 3208 | SkipToNextLine(Stream); 3209 | } 3210 | else if(IsWhitespace(*Stream.At)) 3211 | { 3212 | ++Stream.At; 3213 | } 3214 | else if(IsEOL(*Stream.At)) 3215 | { 3216 | SkipToNextLine(Stream); 3217 | } 3218 | else 3219 | { 3220 | break; 3221 | } 3222 | } 3223 | 3224 | token Token; 3225 | Token.Type = UnrecongizedTokenType; 3226 | Token.Content = Stream.At; 3227 | switch(*Stream.At) 3228 | { 3229 | case '=': 3230 | { 3231 | Token.Type = EqualsToken; 3232 | ++Stream.At; 3233 | } break; 3234 | 3235 | case ';': 3236 | { 3237 | Token.Type = SemiColonToken; 3238 | ++Stream.At; 3239 | } break; 3240 | 3241 | case '\0': 3242 | { 3243 | Token.Type = EndOfStreamToken; 3244 | } break; 3245 | 3246 | case '\'': 3247 | case '"': 3248 | { 3249 | Token.Type = StringToken; 3250 | Token.Content = ParseString(Stream, Strings); 3251 | } break; 3252 | 3253 | default: 3254 | { 3255 | if(IsAlpha(*Stream.At)) 3256 | { 3257 | Token.Type = IdentifierToken; 3258 | Token.Content = ParseIdentifier(Stream, Strings); 3259 | } 3260 | else if(IsNumeric(*Stream.At)) 3261 | { 3262 | Token.Type = IntegerToken; 3263 | Token.Integer = atoi(Stream.At); 3264 | while(IsNumeric(*Stream.At)) {++Stream.At;} 3265 | } 3266 | 3267 | } break; 3268 | } 3269 | 3270 | return(Token); 3271 | } 3272 | 3273 | bool 3274 | TrueOrFalse(char *Value) 3275 | { 3276 | if((stricmp(Value, "true") == 0) || 3277 | (stricmp(Value, "yes") == 0) || 3278 | (stricmp(Value, "1") == 0)) 3279 | { 3280 | return(true); 3281 | } 3282 | 3283 | if((stricmp(Value, "false") == 0) || 3284 | (stricmp(Value, "no") == 0) || 3285 | (stricmp(Value, "0") == 0)) 3286 | { 3287 | return(false); 3288 | } 3289 | 3290 | printf("Unrecognized true-or-false value \"%s\"\n", Value); 3291 | return(false); 3292 | } 3293 | 3294 | diff_method 3295 | GetDiffTypeFromString(char *Value) 3296 | { 3297 | if(stricmp(Value, "ByteByByte") == 0) 3298 | { 3299 | return(ByteByByteDiff); 3300 | } 3301 | 3302 | if(stricmp(Value, "ModificationStamp") == 0) 3303 | { 3304 | return(ModificationStampDiff); 3305 | } 3306 | 3307 | if(stricmp(Value, "TimestampRepresentationTolerantStamp") == 0) 3308 | { 3309 | return(TimestampRepresentationTolerantStampDiff); 3310 | } 3311 | 3312 | if(stricmp(Value, "TRTSWithByteFallback") == 0) 3313 | { 3314 | return(TRTSWithByteFallbackDiff); 3315 | } 3316 | 3317 | fprintf(stderr, "Unrecognized diff type %s\n", Value); 3318 | return(TimestampRepresentationTolerantStampDiff); 3319 | } 3320 | 3321 | char * 3322 | GetDiffString(diff_method DiffMethod) 3323 | { 3324 | switch(DiffMethod) 3325 | { 3326 | case ByteByByteDiff: {return("ByteByByte");} break; 3327 | case ModificationStampDiff: {return("ModificationStamp");} break; 3328 | case TimestampRepresentationTolerantStampDiff: {return("TimestampRepresentationTolerantStamp");} break; 3329 | case TRTSWithByteFallbackDiff: {return("TRTSWithByteFallback");} break; 3330 | } 3331 | 3332 | return("Unknown"); 3333 | } 3334 | 3335 | void 3336 | ParseConfigFile(stream &Stream, string_table &Strings, mirror_config &Config) 3337 | { 3338 | bool Parsing = true; 3339 | while(Parsing) 3340 | { 3341 | token Token = GetToken(Stream, Strings); 3342 | switch(Token.Type) 3343 | { 3344 | case IdentifierToken: 3345 | { 3346 | token Equals = GetToken(Stream, Strings); 3347 | if(Equals.Type == EqualsToken) 3348 | { 3349 | token Value = GetToken(Stream, Strings); 3350 | if((Value.Type == StringToken) || 3351 | (Value.Type == IntegerToken) || 3352 | (Value.Type == IdentifierToken)) 3353 | { 3354 | Unixify(Value.Content); 3355 | if(stricmp(Token.Content, "Central") == 0) 3356 | { 3357 | StripQuotesInPlace(Value.Content); 3358 | Config.Directory[CentralFileType] = Value.Content; 3359 | } 3360 | else if(stricmp(Token.Content, "CentralCache") == 0) 3361 | { 3362 | StripQuotesInPlace(Value.Content); 3363 | Config.CentralCache = Value.Content; 3364 | } 3365 | else if(stricmp(Token.Content, "Local") == 0) 3366 | { 3367 | StripQuotesInPlace(Value.Content); 3368 | Config.Directory[LocalFileType] = Value.Content; 3369 | } 3370 | else if(stricmp(Token.Content, "Workspace") == 0) 3371 | { 3372 | StripQuotesInPlace(Value.Content); 3373 | Config.Directory[WorkspaceFileType] = Value.Content; 3374 | } 3375 | else if(stricmp(Token.Content, "LocalRetryTimes") == 0) 3376 | { 3377 | if ( Value.Type == IntegerToken ) 3378 | { 3379 | Config.RetryTimes[LocalFileType] = Value.Integer; 3380 | } 3381 | } 3382 | else if(stricmp(Token.Content, "WorkspaceRetryTimes") == 0) 3383 | { 3384 | if ( Value.Type == IntegerToken ) 3385 | { 3386 | Config.RetryTimes[WorkspaceFileType] = Value.Integer; 3387 | } 3388 | } 3389 | else if(stricmp(Token.Content, "CentralRetryTimes") == 0) 3390 | { 3391 | if ( Value.Type == IntegerToken ) 3392 | { 3393 | Config.RetryTimes[CentralFileType] = Value.Integer; 3394 | } 3395 | } 3396 | else if(stricmp(Token.Content, "Ignore") == 0) 3397 | { 3398 | StripQuotesInPlace(Value.Content); 3399 | MakeIgnoreRule(&Config.FileRuleList, Value.Content, DoIgnore); 3400 | } 3401 | else if(stricmp(Token.Content, "Allow") == 0) 3402 | { 3403 | StripQuotesInPlace(Value.Content); 3404 | MakeIgnoreRule(&Config.FileRuleList, Value.Content, DoNotIgnore); 3405 | } 3406 | else if(stricmp(Token.Content, "IgnoreConflicts") == 0) 3407 | { 3408 | StripQuotesInPlace(Value.Content); 3409 | 3410 | {for(file_rule_expander RuleExpand = ExpandRule(&Config.FileRuleList, Value.Content); 3411 | Continue(RuleExpand); 3412 | Next(RuleExpand)) 3413 | { 3414 | file_rule *Rule = RuleExpand.Rule; 3415 | 3416 | Rule->Behavior = IgnoreConflicts; 3417 | }} 3418 | } 3419 | else if(stricmp(Token.Content, "HaltOnConflicts") == 0) 3420 | { 3421 | StripQuotesInPlace(Value.Content); 3422 | 3423 | {for(file_rule_expander RuleExpand = ExpandRule(&Config.FileRuleList, Value.Content); 3424 | Continue(RuleExpand); 3425 | Next(RuleExpand)) 3426 | { 3427 | file_rule *Rule = RuleExpand.Rule; 3428 | 3429 | Rule->Behavior = HaltOnConflicts; 3430 | }} 3431 | } 3432 | else if(stricmp(Token.Content, "VersionCap") == 0) 3433 | { 3434 | token VersionCountToken = GetToken(Stream, Strings); 3435 | if(VersionCountToken.Type == IntegerToken) 3436 | { 3437 | token DeletedVersionCountToken = GetToken(Stream, Strings); 3438 | if(DeletedVersionCountToken.Type == IntegerToken) 3439 | { 3440 | StripQuotesInPlace(Value.Content); 3441 | 3442 | {for(file_rule_expander RuleExpand = ExpandRule(&Config.FileRuleList, Value.Content); 3443 | Continue(RuleExpand); 3444 | Next(RuleExpand)) 3445 | { 3446 | file_rule *Rule = RuleExpand.Rule; 3447 | 3448 | Rule->CapVersionCount = true; 3449 | Rule->MaxVersionCount = VersionCountToken.Integer; 3450 | Rule->MaxVersionCountIfDeleted = DeletedVersionCountToken.Integer; 3451 | }} 3452 | } 3453 | else 3454 | { 3455 | printf("Missing second integer in version cap definition\n"); 3456 | } 3457 | } 3458 | else 3459 | { 3460 | printf("Missing first integer in version cap definition\n"); 3461 | } 3462 | } 3463 | else if(stricmp(Token.Content, "PreCheckin") == 0) 3464 | { 3465 | token CommandToken = GetToken(Stream, Strings); 3466 | if((CommandToken.Type == IdentifierToken) || 3467 | (CommandToken.Type == StringToken)) 3468 | { 3469 | token ParamsToken = GetToken(Stream, Strings); 3470 | if(ParamsToken.Type == StringToken) 3471 | { 3472 | StripQuotesInPlace(Value.Content); 3473 | StripQuotesInPlace(CommandToken.Content); 3474 | StripQuotesInPlace(ParamsToken.Content); 3475 | 3476 | {for(file_rule_expander RuleExpand = ExpandRule(&Config.FileRuleList, Value.Content); 3477 | Continue(RuleExpand); 3478 | Next(RuleExpand)) 3479 | { 3480 | file_rule *Rule = RuleExpand.Rule; 3481 | Rule->PreCheckinCommand = CommandToken.Content; 3482 | Rule->PreCheckinCommandParameters = ParamsToken.Content; 3483 | }} 3484 | } 3485 | else 3486 | { 3487 | printf("Missing parameter string in pre-checkin definition\n"); 3488 | } 3489 | } 3490 | else 3491 | { 3492 | printf("Missing command string in pre-checkin definition\n"); 3493 | } 3494 | } 3495 | else if(stricmp(Token.Content, "WritableWorkspace") == 0) 3496 | { 3497 | Config.WritableWorkspace = TrueOrFalse(Value.Content); 3498 | } 3499 | else if(stricmp(Token.Content, "SyncWorkspace") == 0) 3500 | { 3501 | Config.SyncWorkspace = TrueOrFalse(Value.Content); 3502 | } 3503 | else if(stricmp(Token.Content, "SyncCentral") == 0) 3504 | { 3505 | Config.SyncCentral = TrueOrFalse(Value.Content); 3506 | } 3507 | else if(stricmp(Token.Content, "ContinueAlways") == 0) 3508 | { 3509 | Config.ContinueAlways = TrueOrFalse(Value.Content); 3510 | } 3511 | else if(stricmp(Token.Content, "SuppressIgnores") == 0) 3512 | { 3513 | Config.SuppressIgnores = TrueOrFalse(Value.Content); 3514 | } 3515 | else if(stricmp(Token.Content, "SummarizeIgnores") == 0) 3516 | { 3517 | Config.SummarizeIgnores = TrueOrFalse(Value.Content); 3518 | } 3519 | else if(stricmp(Token.Content, "PrintReason") == 0) 3520 | { 3521 | Config.PrintReason = TrueOrFalse(Value.Content); 3522 | } 3523 | else if(stricmp(Token.Content, "LineLength") == 0) 3524 | { 3525 | if(Value.Type == IntegerToken) 3526 | { 3527 | Config.LineLength = Value.Integer; 3528 | } 3529 | else 3530 | { 3531 | printf("Missing first integer in LineLength definition\n"); 3532 | } 3533 | } 3534 | else if(stricmp(Token.Content, "SummarizeSync") == 0) 3535 | { 3536 | Config.SummarizeSync = TrueOrFalse(Value.Content); 3537 | } 3538 | else if(stricmp(Token.Content, "SummarizeSyncIfNotPrinted") == 0) 3539 | { 3540 | Config.SummarizeSyncIfNotPrinted = TrueOrFalse(Value.Content); 3541 | } 3542 | else if(stricmp(Token.Content, "SummarizeDirs") == 0) 3543 | { 3544 | Config.SummarizeDirs = TrueOrFalse(Value.Content); 3545 | } 3546 | else if(stricmp(Token.Content, "SummarizeDirsIfNotPrinted") == 0) 3547 | { 3548 | Config.SummarizeDirsIfNotPrinted = TrueOrFalse(Value.Content); 3549 | } 3550 | else if(stricmp(Token.Content, "SyncPrintThreshold") == 0) 3551 | { 3552 | if(Value.Type == IntegerToken) 3553 | { 3554 | Config.SyncPrintThreshold = Value.Integer; 3555 | } 3556 | else 3557 | { 3558 | printf("Missing first integer in SyncPrintThreshold definition\n"); 3559 | } 3560 | } 3561 | else if(stricmp(Token.Content, "CopyCommand") == 0) 3562 | { 3563 | StripQuotesInPlace( Value.Content ); 3564 | Config.CopyCommand = Value.Content; 3565 | } 3566 | else if(stricmp(Token.Content, "DeleteCommand") == 0) 3567 | { 3568 | StripQuotesInPlace( Value.Content ); 3569 | Config.DeleteCommand = Value.Content; 3570 | } 3571 | else if(stricmp(Token.Content, "WorkspaceToLocalDiff") == 0) 3572 | { 3573 | Config.DiffMethod[WorkspaceFileType][LocalFileType] = 3574 | Config.DiffMethod[LocalFileType][WorkspaceFileType] = 3575 | GetDiffTypeFromString(Value.Content); 3576 | } 3577 | else if(stricmp(Token.Content, "LocalToCentralDiff") == 0) 3578 | { 3579 | Config.DiffMethod[LocalFileType][CentralFileType] = 3580 | Config.DiffMethod[CentralFileType][LocalFileType] = 3581 | GetDiffTypeFromString(Value.Content); 3582 | } 3583 | else 3584 | { 3585 | printf("Unrecognized configuration variable \"%s\"\n", 3586 | Token.Content); 3587 | } 3588 | 3589 | token SemiColon = GetToken(Stream, Strings); 3590 | if(SemiColon.Type != SemiColonToken) 3591 | { 3592 | printf("Expected semi-colon after assignment on line %d\n", 3593 | Stream.LineIndex); 3594 | SkipToNextLine(Stream); 3595 | } 3596 | } 3597 | else 3598 | { 3599 | printf("Expected string after %s assignment on line %d\n", 3600 | Token.Content, Stream.LineIndex); 3601 | SkipToNextLine(Stream); 3602 | } 3603 | } 3604 | else 3605 | { 3606 | printf("Expected = after %s on line %d\n", 3607 | Token.Content, Stream.LineIndex); 3608 | SkipToNextLine(Stream); 3609 | } 3610 | } break; 3611 | 3612 | case EndOfStreamToken: 3613 | { 3614 | Parsing = false; 3615 | } break; 3616 | 3617 | default: 3618 | { 3619 | printf("Unexpected token type begins line %d ('%c')\n", 3620 | Stream.LineIndex, *Stream.At); 3621 | SkipToNextLine(Stream); 3622 | } break; 3623 | } 3624 | } 3625 | } 3626 | 3627 | static void 3628 | ProcessCommandLine(mirror_config &Config, string_table &Strings, 3629 | int ArgCount, char **Args, 3630 | bool AllowConfigs, char *&ConfigFileName) 3631 | { 3632 | bool ConfigParam = false; 3633 | {for(int ArgIndex = 1; 3634 | ArgIndex < ArgCount; 3635 | ++ArgIndex) 3636 | { 3637 | char *Arg = Args[ArgIndex]; 3638 | 3639 | if(ConfigParam) 3640 | { 3641 | if(AllowConfigs) 3642 | { 3643 | stream Stream; 3644 | Stream.At = Arg; 3645 | Stream.LineIndex = 1; 3646 | ParseConfigFile(Stream, Strings, Config); 3647 | } 3648 | 3649 | ConfigParam = false; 3650 | } 3651 | else 3652 | { 3653 | if(strcmp(Arg, "-config") == 0) 3654 | { 3655 | ConfigParam = true; 3656 | } 3657 | else 3658 | { 3659 | ConfigFileName = Arg; 3660 | } 3661 | } 3662 | }} 3663 | } 3664 | 3665 | void 3666 | InitializeCacheIgnore(mirror_config &Config) 3667 | { 3668 | if (Config.CentralCache) { 3669 | char *s = Config.CentralCache; 3670 | char *t = Config.Directory[CentralFileType]; 3671 | int n = strlen(t); 3672 | if (strnicmp(s,t,n) == 0) { 3673 | s += n; 3674 | // if the directory doesn't end in '/', pretend it did 3675 | if (t[n-1] != '/') { 3676 | if (s[0] != '/') return; 3677 | ++s; 3678 | } 3679 | MakeIgnoreRule(&Config.FileRuleList, s, DoIgnore); 3680 | } 3681 | } 3682 | } 3683 | 3684 | static bool check(char *text, char *id) 3685 | { 3686 | // check for "unwrapped" calls to 'id'... note that this code is not 3687 | // smart and looks inside comments and strings. 3688 | int n = strlen(id); 3689 | char *t; 3690 | t = strstr(text, id); 3691 | for (; t; t = strstr(t+1,id) ) 3692 | { 3693 | // store the length of the identifier we're looking for in 'len' 3694 | int len = n; 3695 | // allow an identifier that's two longer if there's an 'Ex' at the end 3696 | if (stb_prefix(t+len, "Ex")) len += 2; 3697 | // allow an identifier that's prefixed with a "_" at the beginning 3698 | if (t[-1] == '_') { --t; ++len; } 3699 | // if there are identifier characters BEFORE the id, it's ok 3700 | if (__iscsym(t[ -1])) continue; 3701 | // if there are identifier characters AFTER the id, it's ok 3702 | if (__iscsym(t[len])) continue; 3703 | // neither before nor after, so now check the tail 3704 | t += len; 3705 | while (*t && isspace(*t)) ++t; 3706 | if (*t == '(') 3707 | return true; // whoops! 3708 | } 3709 | return false; // none found 3710 | } 3711 | 3712 | void CheckCmirrorSource(void) 3713 | { 3714 | bool ok = true; 3715 | 3716 | // if cmirror.cpp is present, check it for any egregious violations 3717 | char *text = stb_filec("cmirror.cpp", NULL); 3718 | if (!text) return; 3719 | char *s = strstr(text, "// EOLLFO"); 3720 | // windows versions 3721 | if (check(s, "MoveFile" )) ok=false, printf("Warning: cmirror.cpp contains unwrapped MoveFile call\n"); 3722 | if (check(s, "DeleteFile")) ok=false, printf("Warning: cmirror.cpp contains unwrapped DeleteFile call\n"); 3723 | if (check(s, "CopyFile" )) ok=false, printf("Warning: cmirror.cpp contains unwrapped CopyFile call\n"); 3724 | // portable versions 3725 | if (check(s, "remove" )) ok=false, printf("Warning: cmirror.cpp contains unwrapped remove call\n"); 3726 | if (check(s, "rename" )) ok=false, printf("Warning: cmirror.cpp contains unwrapped rename call\n"); 3727 | // no portable copy?!? 3728 | 3729 | if (ok) 3730 | printf("Checked cmirror.cpp successfully.\n"); 3731 | printf("\n"); 3732 | } 3733 | 3734 | int 3735 | main(int ArgCount, char **Args) 3736 | { 3737 | printf("CMirror v1.3c by Casey Muratori, Sean Barrett, and Jeff Roberts\n"); 3738 | printf("NO WARRANTY IS EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.\n"); 3739 | printf("This program and its source code are in public domain. For the latest\n"); 3740 | printf("version or the source code, see http://www.mollyrocket.com/tools\n"); 3741 | printf("\n"); 3742 | 3743 | assert(sizeof(ReasonName)/sizeof(ReasonName[0]) == ReasonCount); 3744 | assert(sizeof(OperationName)/sizeof(OperationName[0]) == OperationCount); 3745 | 3746 | string_table Strings; 3747 | InitializeStringTable(Strings); 3748 | 3749 | CheckCmirrorSource(); 3750 | 3751 | char CurrentDirectory[MAX_PATH]; 3752 | GetCurrentDirectory(sizeof(CurrentDirectory), CurrentDirectory); 3753 | char *LastSlash = FindLastSlash(CurrentDirectory); 3754 | if(!LastSlash[1]) 3755 | { 3756 | *LastSlash = 0; 3757 | } 3758 | Unixify(CurrentDirectory); 3759 | 3760 | mirror_config Config; 3761 | Config.FileRuleList = NULL; 3762 | Config.MirrorMode = FullSyncMode; 3763 | {for(int FileTypeA = 0; 3764 | FileTypeA < OnePastLastFileType; 3765 | ++FileTypeA) 3766 | { 3767 | {for(int FileTypeB = 0; 3768 | FileTypeB < OnePastLastFileType; 3769 | ++FileTypeB) 3770 | { 3771 | Config.DiffMethod[FileTypeA][FileTypeB] = TimestampRepresentationTolerantStampDiff; 3772 | }} 3773 | }} 3774 | Config.CopyCommand = 0; 3775 | Config.DeleteCommand = 0; 3776 | Config.WritableWorkspace = true; 3777 | Config.ContinueAlways = false; 3778 | Config.Directory[WorkspaceFileType] = CurrentDirectory; 3779 | Config.Directory[CentralFileType] = ""; 3780 | Config.Directory[LocalFileType] = ""; 3781 | Config.RetryTimes[WorkspaceFileType] = 0; 3782 | Config.RetryTimes[CentralFileType] = 0; 3783 | Config.RetryTimes[LocalFileType] = 0; 3784 | Config.SyncWorkspace = true; 3785 | Config.SyncCentral = true; 3786 | Config.CentralCache = ""; 3787 | Config.SuppressIgnores = false; 3788 | Config.SummarizeIgnores = false; 3789 | Config.SyncPrintThreshold = 256; 3790 | Config.PrintReason = true; 3791 | Config.SummarizeSync = true; 3792 | Config.SummarizeSyncIfNotPrinted = true; 3793 | Config.SummarizeDirs = true; 3794 | Config.SummarizeDirsIfNotPrinted = true; 3795 | Config.LineLength = 0; 3796 | 3797 | FILE *ConfigFile = 0; 3798 | 3799 | char *ConfigFileName = 0; 3800 | ProcessCommandLine(Config, Strings, ArgCount, Args, false, ConfigFileName); 3801 | if(!ConfigFileName) 3802 | { 3803 | ConfigFileName = FindConfigFile(Strings, CurrentDirectory); 3804 | SetCurrentDirectory(CurrentDirectory); 3805 | } 3806 | 3807 | if(ConfigFileName) 3808 | { 3809 | printf("Looking for config file at \"%s\"...\n", ConfigFileName); 3810 | size_t FileSize; 3811 | char *ConfigFileBuffer = stb_filec(ConfigFileName, &FileSize); 3812 | if (ConfigFileBuffer) 3813 | { 3814 | stream Stream; 3815 | Stream.At = ConfigFileBuffer; 3816 | Stream.LineIndex = 1; 3817 | ParseConfigFile(Stream, Strings, Config); 3818 | ProcessCommandLine(Config, Strings, ArgCount, Args, true, ConfigFileName); 3819 | 3820 | InitializeCacheIgnore(Config); 3821 | 3822 | printf("Configuration:\n"); 3823 | printf(" Workspace: \"%s\" (%s)\n", Config.Directory[WorkspaceFileType], 3824 | Config.WritableWorkspace ? "writable" : "read-only"); 3825 | printf(" Central: \"%s\"\n", Config.Directory[CentralFileType]); 3826 | printf(" Central cache: \"%s\"\n", Config.CentralCache); 3827 | printf(" Local: \"%s\"\n", Config.Directory[LocalFileType]); 3828 | printf(" Workspace <-> Local: \"%s\"\n", GetDiffString(Config.DiffMethod[WorkspaceFileType][LocalFileType])); 3829 | printf(" Local <-> Central: \"%s\"\n", GetDiffString(Config.DiffMethod[LocalFileType][CentralFileType])); 3830 | printf(" File rules:\n"); 3831 | {for(file_rule *FileRule = Config.FileRuleList; 3832 | !stb_arr_end(Config.FileRuleList, FileRule); 3833 | ++FileRule) 3834 | { 3835 | printf(" "); 3836 | if(FileRule->Ignore == DoIgnore) 3837 | { 3838 | printf("Ignore "); 3839 | } 3840 | if(FileRule->Ignore == DoNotIgnore) 3841 | { 3842 | printf("Include "); 3843 | } 3844 | if(FileRule->CapVersionCount) 3845 | { 3846 | printf("Keep%d(Del%d) ", 3847 | FileRule->MaxVersionCount, 3848 | FileRule->MaxVersionCountIfDeleted); 3849 | } 3850 | switch(FileRule->Behavior) 3851 | { 3852 | case HaltOnConflicts: 3853 | { 3854 | // This is the default 3855 | } break; 3856 | 3857 | case IgnoreConflicts: 3858 | { 3859 | printf("IgnoreConflicts "); 3860 | } break; 3861 | 3862 | default: 3863 | { 3864 | printf("\n** WARNING ** UNKNOWN CONFLICT BEHAVIOR\n"); 3865 | } break; 3866 | } 3867 | if(FileRule->PreCheckinCommand) 3868 | { 3869 | printf("PreCheck[%s] ", 3870 | FileRule->PreCheckinCommand); 3871 | } 3872 | printf("- \"%s\"\n", FileRule->WildCard); 3873 | }} 3874 | if(Config.CopyCommand) 3875 | { 3876 | printf(" Custom copy: \"%s\"\n", Config.CopyCommand); 3877 | } 3878 | if(Config.DeleteCommand) 3879 | { 3880 | printf(" Custom delete: \"%s\"\n", Config.DeleteCommand); 3881 | } 3882 | if(Config.ContinueAlways) 3883 | { 3884 | printf(" Continue always\n"); 3885 | } 3886 | 3887 | printf("\n"); 3888 | 3889 | directory_contents RootContents; 3890 | DirInit(&RootContents); 3891 | BuildDirectory(Config, RootContents, Strings); 3892 | Synchronize(Config, RootContents); 3893 | if(*Config.CentralCache) 3894 | { 3895 | if(!WriteDirectoryToCache(RootContents, CentralFileType, 3896 | "Writing central cache", Config.CentralCache)) 3897 | { 3898 | printf("Unable to write to central cache.\n"); 3899 | } 3900 | } 3901 | free(ConfigFileBuffer); 3902 | } 3903 | else 3904 | { 3905 | printf("Unable to read configuration file.\n"); 3906 | } 3907 | } 3908 | else 3909 | { 3910 | fprintf(stderr, "No config file found along path %s\n", CurrentDirectory); 3911 | } 3912 | 3913 | return(0); 3914 | } 3915 | --------------------------------------------------------------------------------