Fossil SCM

fossil-scm / src / patch.c
Blame History Raw 1207 lines
1
/*
2
** Copyright (c) 2021 D. Richard Hipp
3
**
4
** This program is free software; you can redistribute it and/or
5
** modify it under the terms of the Simplified BSD License (also
6
** known as the "2-Clause License" or "FreeBSD License".)
7
**
8
** This program is distributed in the hope that it will be useful,
9
** but without any warranty; without even the implied warranty of
10
** merchantability or fitness for a particular purpose.
11
**
12
** Author contact information:
13
** [email protected]
14
** http://www.hwaci.com/drh/
15
**
16
*******************************************************************************
17
**
18
** This file contains code used to implement the "patch" command
19
*/
20
#include "config.h"
21
#include "patch.h"
22
#include <assert.h>
23
24
/*
25
** Try to compute the name of the computer on which this process
26
** is running.
27
*/
28
char *fossil_hostname(void){
29
FILE *in;
30
char zBuf[200];
31
in = popen("hostname","r");
32
if( in ){
33
int n = fread(zBuf, 1, sizeof(zBuf)-1, in);
34
while( n>0 && fossil_isspace(zBuf[n-1]) ){ n--; }
35
if( n<0 ) n = 0;
36
zBuf[n] = 0;
37
pclose(in);
38
return fossil_strdup(zBuf);
39
}
40
return 0;
41
}
42
43
/*
44
** Flags passed from the main patch_cmd() routine into subfunctions used
45
** to implement the various subcommands.
46
*/
47
#define PATCH_DRYRUN 0x0001
48
#define PATCH_VERBOSE 0x0002
49
#define PATCH_FORCE 0x0004
50
#define PATCH_RETRY 0x0008 /* Second attempt */
51
52
/*
53
** Implementation of the "readfile(X)" SQL function. The entire content
54
** of the check-out file named X is read and returned as a BLOB.
55
*/
56
static void readfileFunc(
57
sqlite3_context *context,
58
int argc,
59
sqlite3_value **argv
60
){
61
const char *zName;
62
Blob x;
63
sqlite3_int64 sz;
64
(void)(argc); /* Unused parameter */
65
zName = (const char*)sqlite3_value_text(argv[0]);
66
if( zName==0 || (zName[0]=='-' && zName[1]==0) ) return;
67
sz = blob_read_from_file(&x, zName, RepoFILE);
68
sqlite3_result_blob64(context, x.aData, sz, SQLITE_TRANSIENT);
69
blob_reset(&x);
70
}
71
72
/*
73
** mkdelta(X,Y)
74
**
75
** X is an numeric artifact id. Y is a filename.
76
**
77
** Compute a compressed delta that carries X into Y. Or return
78
** and zero-length blob if X is equal to Y.
79
*/
80
static void mkdeltaFunc(
81
sqlite3_context *context,
82
int argc,
83
sqlite3_value **argv
84
){
85
const char *zFile;
86
Blob x, y, out;
87
int rid;
88
int nOut;
89
sqlite3_int64 sz;
90
91
rid = sqlite3_value_int(argv[0]);
92
if( !content_get(rid, &x) ){
93
sqlite3_result_error(context, "mkdelta(X,Y): no content for X", -1);
94
return;
95
}
96
zFile = (const char*)sqlite3_value_text(argv[1]);
97
if( zFile==0 ){
98
sqlite3_result_error(context, "mkdelta(X,Y): NULL Y argument", -1);
99
blob_reset(&x);
100
return;
101
}
102
sz = blob_read_from_file(&y, zFile, RepoFILE);
103
if( sz<0 ){
104
sqlite3_result_error(context, "mkdelta(X,Y): cannot read file Y", -1);
105
blob_reset(&x);
106
return;
107
}
108
blob_init(&out, 0, 0);
109
blob_resize(&out, sz+70);
110
if( blob_size(&x)==blob_size(&y)
111
&& memcmp(blob_buffer(&x), blob_buffer(&y), blob_size(&x))==0
112
){
113
blob_reset(&y);
114
blob_reset(&x);
115
sqlite3_result_blob64(context, "", 0, SQLITE_STATIC);
116
return;
117
}
118
nOut = delta_create(blob_buffer(&x),blob_size(&x),
119
blob_buffer(&y),blob_size(&y), blob_buffer(&out));
120
blob_resize(&out, nOut);
121
blob_reset(&x);
122
blob_reset(&y);
123
blob_compress(&out, &out);
124
sqlite3_result_blob64(context, blob_buffer(&out), blob_size(&out),
125
SQLITE_TRANSIENT);
126
blob_reset(&out);
127
}
128
129
130
/*
131
** Generate a binary patch file and store it into the file
132
** named zOut. Or if zOut is NULL, write it into out.
133
**
134
** Return the number of errors.
135
*/
136
void patch_create(unsigned mFlags, const char *zOut, FILE *out){
137
int vid;
138
char *z;
139
140
if( zOut && file_isdir(zOut, ExtFILE)!=0 ){
141
if( mFlags & PATCH_FORCE ){
142
file_delete(zOut);
143
}
144
if( file_isdir(zOut, ExtFILE)!=0 ){
145
fossil_fatal("patch file already exists: %s", zOut);
146
}
147
}
148
add_content_sql_commands(g.db);
149
deltafunc_init(g.db);
150
sqlite3_create_function(g.db, "read_co_file", 1, SQLITE_UTF8, 0,
151
readfileFunc, 0, 0);
152
sqlite3_create_function(g.db, "mkdelta", 2, SQLITE_UTF8, 0,
153
mkdeltaFunc, 0, 0);
154
db_multi_exec("ATTACH %Q AS patch;", zOut ? zOut : ":memory:");
155
db_multi_exec(
156
"PRAGMA patch.journal_mode=OFF;\n"
157
"PRAGMA patch.page_size=512;\n"
158
"CREATE TABLE patch.chng(\n"
159
" pathname TEXT,\n" /* Filename */
160
" origname TEXT,\n" /* Name before rename. NULL if not renamed */
161
" hash TEXT,\n" /* Baseline hash. NULL for new files. */
162
" isexe BOOL,\n" /* True if executable */
163
" islink BOOL,\n" /* True if is a symbolic link */
164
" delta BLOB\n" /* compressed delta. NULL if deleted.
165
** length 0 if unchanged */
166
");"
167
"CREATE TABLE patch.cfg(\n"
168
" key TEXT,\n"
169
" value ANY\n"
170
");"
171
);
172
vid = db_lget_int("checkout", 0);
173
vfile_check_signature(vid, CKSIG_ENOTFILE);
174
user_select();
175
db_multi_exec(
176
"INSERT INTO patch.cfg(key,value)"
177
"SELECT 'baseline',uuid FROM blob WHERE rid=%d "
178
"UNION ALL"
179
" SELECT 'ckout',rtrim(%Q,'/')"
180
"UNION ALL"
181
" SELECT 'repo',%Q "
182
"UNION ALL"
183
" SELECT 'user',%Q "
184
"UNION ALL"
185
" SELECT 'date',julianday('now')"
186
"UNION ALL"
187
" SELECT name,value FROM repository.config"
188
" WHERE name IN ('project-code','project-name') "
189
"UNION ALL"
190
" SELECT 'fossil-date',julianday('" MANIFEST_DATE "')"
191
";", vid, g.zLocalRoot, g.zRepositoryName, g.zLogin);
192
z = fossil_hostname();
193
if( z ){
194
db_multi_exec(
195
"INSERT INTO patch.cfg(key,value)VALUES('hostname',%Q)", z);
196
fossil_free(z);
197
}
198
199
/* New files */
200
db_multi_exec(
201
"INSERT INTO patch.chng(pathname,hash,isexe,islink,delta)"
202
" SELECT pathname, NULL, isexe, islink,"
203
" compress(read_co_file(%Q||pathname))"
204
" FROM vfile WHERE rid==0;",
205
g.zLocalRoot
206
);
207
208
/* Deleted files */
209
db_multi_exec(
210
"INSERT INTO patch.chng(pathname,hash,isexe,islink,delta)"
211
" SELECT pathname, NULL, 0, 0, NULL"
212
" FROM vfile WHERE deleted;"
213
);
214
215
/* Changed files */
216
db_multi_exec(
217
"INSERT INTO patch.chng(pathname,origname,hash,isexe,islink,delta)"
218
" SELECT pathname, nullif(origname,pathname), blob.uuid, isexe, islink,"
219
" mkdelta(blob.rid, %Q||pathname)"
220
" FROM vfile, blob"
221
" WHERE blob.rid=vfile.rid"
222
" AND NOT deleted AND (chnged OR origname<>pathname);",
223
g.zLocalRoot
224
);
225
226
/* Merges */
227
if( db_exists("SELECT 1 FROM localdb.vmerge WHERE id<=0") ){
228
db_multi_exec(
229
"CREATE TABLE patch.patchmerge(type TEXT,mhash TEXT);\n"
230
"WITH tmap(id,type) AS (VALUES(0,'merge'),(-1,'cherrypick'),"
231
"(-2,'backout'),(-4,'integrate'))"
232
"INSERT INTO patch.patchmerge(type,mhash)"
233
" SELECT tmap.type,vmerge.mhash FROM vmerge, tmap"
234
" WHERE tmap.id=vmerge.id;"
235
);
236
}
237
238
/* Write the database to standard output if zOut==0 */
239
if( zOut==0 ){
240
sqlite3_int64 sz;
241
unsigned char *pData;
242
pData = sqlite3_serialize(g.db, "patch", &sz, 0);
243
if( pData==0 ){
244
fossil_fatal("out of memory");
245
}
246
#ifdef _WIN32
247
fflush(out);
248
_setmode(_fileno(out), _O_BINARY);
249
#endif
250
fwrite(pData, 1, sz, out);
251
fflush(out);
252
sqlite3_free(pData);
253
}
254
db_multi_exec("DETACH patch;");
255
}
256
257
/*
258
** Attempt to load and validate a patchfile identified by the first
259
** argument.
260
*/
261
void patch_attach(const char *zIn, FILE *in, int bIgnoreEmptyPatch){
262
Stmt q;
263
if( g.db==0 ){
264
sqlite3_open(":memory:", &g.db);
265
}
266
if( zIn==0 ){
267
Blob buf;
268
int rc;
269
int sz;
270
unsigned char *pData;
271
blob_init(&buf, 0, 0);
272
#ifdef _WIN32
273
_setmode(_fileno(in), _O_BINARY);
274
#endif
275
sz = blob_read_from_channel(&buf, in, -1);
276
pData = (unsigned char*)blob_buffer(&buf);
277
if( sz<512 ){
278
blob_reset(&buf);
279
if( bIgnoreEmptyPatch ) return;
280
fossil_fatal("input is too small to be a patch file");
281
}
282
db_multi_exec("ATTACH ':memory:' AS patch");
283
if( g.fSqlTrace ){
284
fossil_trace("-- deserialize(\"patch\", pData, %lld);\n", sz);
285
}
286
rc = sqlite3_deserialize(g.db, "patch", pData, sz, sz, 0);
287
if( rc ){
288
fossil_fatal("cannot open patch database: %s", sqlite3_errmsg(g.db));
289
}
290
}else if( !file_isfile(zIn, ExtFILE) ){
291
fossil_fatal("no such file: %s", zIn);
292
}else{
293
db_multi_exec("ATTACH %Q AS patch", zIn);
294
}
295
db_prepare(&q, "PRAGMA patch.quick_check");
296
while( db_step(&q)==SQLITE_ROW ){
297
if( fossil_strcmp(db_column_text(&q,0),"ok")!=0 ){
298
fossil_fatal("file %s is not a well-formed Fossil patchfile", zIn);
299
}
300
}
301
db_finalize(&q);
302
}
303
304
/*
305
** Show a summary of the content of a patch on standard output
306
*/
307
void patch_view(unsigned mFlags){
308
Stmt q;
309
db_prepare(&q,
310
"WITH nmap(nkey,nm) AS (VALUES"
311
"('baseline','BASELINE'),"
312
"('project-name','PROJECT-NAME'))"
313
"SELECT nm, value FROM nmap, patch.cfg WHERE nkey=key;"
314
);
315
while( db_step(&q)==SQLITE_ROW ){
316
fossil_print("%-12s %s\n", db_column_text(&q,0), db_column_text(&q,1));
317
}
318
db_finalize(&q);
319
if( mFlags & PATCH_VERBOSE ){
320
db_prepare(&q,
321
"WITH nmap(nkey,nm,isDate) AS (VALUES"
322
"('project-code','PROJECT-CODE',0),"
323
"('date','TIMESTAMP',1),"
324
"('user','USER',0),"
325
"('hostname','HOSTNAME',0),"
326
"('ckout','CHECKOUT',0),"
327
"('repo','REPOSITORY',0))"
328
"SELECT nm, CASE WHEN isDate THEN datetime(value) ELSE value END"
329
" FROM nmap, patch.cfg WHERE nkey=key;"
330
);
331
while( db_step(&q)==SQLITE_ROW ){
332
fossil_print("%-12s %s\n", db_column_text(&q,0), db_column_text(&q,1));
333
}
334
db_finalize(&q);
335
}
336
if( db_table_exists("patch","patchmerge") ){
337
db_prepare(&q, "SELECT upper(type),mhash FROM patchmerge");
338
while( db_step(&q)==SQLITE_ROW ){
339
fossil_print("%-12s %s\n",
340
db_column_text(&q,0),
341
db_column_text(&q,1));
342
}
343
db_finalize(&q);
344
}
345
db_prepare(&q,
346
"SELECT pathname," /* 0: new name */
347
" hash IS NULL AND delta IS NOT NULL," /* 1: isNew */
348
" delta IS NULL," /* 2: isDeleted */
349
" origname" /* 3: old name or NULL */
350
" FROM patch.chng ORDER BY 1");
351
while( db_step(&q)==SQLITE_ROW ){
352
const char *zClass = "EDIT";
353
const char *zName = db_column_text(&q,0);
354
const char *zOrigName = db_column_text(&q, 3);
355
if( db_column_int(&q, 1) && zOrigName==0 ){
356
zClass = "NEW";
357
}else if( db_column_int(&q, 2) ){
358
zClass = zOrigName==0 ? "DELETE" : 0;
359
}
360
if( zOrigName!=0 && zOrigName[0]!=0 ){
361
fossil_print("%-12s %s -> %s\n", "RENAME",zOrigName,zName);
362
}
363
if( zClass ){
364
fossil_print("%-12s %s\n", zClass, zName);
365
}
366
}
367
db_finalize(&q);
368
}
369
370
/*
371
** Apply the patch currently attached as database "patch".
372
**
373
** First update the check-out to be at "baseline". Then loop through
374
** and update all files.
375
*/
376
void patch_apply(unsigned mFlags){
377
Stmt q;
378
Blob cmd;
379
380
blob_init(&cmd, 0, 0);
381
if( unsaved_changes(0) ){
382
if( (mFlags & PATCH_FORCE)==0 ){
383
fossil_fatal("Cannot apply patch: there are unsaved changes "
384
"in the current check-out");
385
}else{
386
blob_appendf(&cmd, "%$ revert --noundo", g.nameOfExe);
387
if( mFlags & PATCH_DRYRUN ){
388
fossil_print("%s\n", blob_str(&cmd));
389
}else{
390
int rc = fossil_system(blob_str(&cmd));
391
if( rc ){
392
fossil_fatal("unable to revert preexisting changes: %s",
393
blob_str(&cmd));
394
}
395
}
396
blob_reset(&cmd);
397
}
398
}
399
file_chdir(g.zLocalRoot, 0);
400
db_prepare(&q,
401
"SELECT patch.cfg.value"
402
" FROM patch.cfg, localdb.vvar"
403
" WHERE patch.cfg.key='baseline'"
404
" AND localdb.vvar.name='checkout-hash'"
405
" AND patch.cfg.key<>localdb.vvar.name"
406
);
407
if( db_step(&q)==SQLITE_ROW ){
408
blob_append_escaped_arg(&cmd, g.nameOfExe, 1);
409
blob_appendf(&cmd, " update %s", db_column_text(&q, 0));
410
if( mFlags & PATCH_VERBOSE ){
411
fossil_print("%-10s %s\n", "BASELINE", db_column_text(&q,0));
412
}
413
}
414
db_finalize(&q);
415
if( blob_size(&cmd)>0 ){
416
if( mFlags & PATCH_DRYRUN ){
417
fossil_print("%s\n", blob_str(&cmd));
418
}else{
419
int rc = fossil_system(blob_str(&cmd));
420
if( rc ){
421
fossil_fatal("unable to update to the baseline check-out: %s",
422
blob_str(&cmd));
423
}
424
}
425
}
426
blob_reset(&cmd);
427
if( db_table_exists("patch","patchmerge") ){
428
int nMerge = 0;
429
db_prepare(&q,
430
"SELECT type, mhash, upper(type) FROM patch.patchmerge"
431
" WHERE type IN ('merge','cherrypick','backout','integrate')"
432
" AND mhash NOT GLOB '*[^a-fA-F0-9]*';"
433
);
434
while( db_step(&q)==SQLITE_ROW ){
435
const char *zType = db_column_text(&q,0);
436
blob_append_escaped_arg(&cmd, g.nameOfExe, 1);
437
if( strcmp(zType,"merge")==0 ){
438
blob_appendf(&cmd, " merge --noundo --nosync %s\n",
439
db_column_text(&q,1));
440
}else{
441
blob_appendf(&cmd, " merge --%s --noundo --nosync %s\n",
442
zType, db_column_text(&q,1));
443
}
444
nMerge++;
445
if( mFlags & PATCH_VERBOSE ){
446
fossil_print("%-10s %s\n", db_column_text(&q,2),
447
db_column_text(&q,0));
448
}
449
}
450
db_finalize(&q);
451
if( mFlags & PATCH_DRYRUN ){
452
fossil_print("%s", blob_str(&cmd));
453
}else{
454
int rc = fossil_unsafe_system(blob_str(&cmd));
455
if( rc ){
456
fossil_fatal("unable to do merges:\n%s",
457
blob_str(&cmd));
458
}
459
}
460
blob_reset(&cmd);
461
462
/* 2024-12-16 https://fossil-scm.org/home/forumpost/51a37054
463
** If one or more merge operations occurred in the patch and there are
464
** files that are marked as "chnged' in the local VFILE but which
465
** are not mentioned as having been modified in the patch, then
466
** revert those files.
467
*/
468
if( nMerge ){
469
int vid = db_lget_int("checkout", 0);
470
int nRevert = 0;
471
blob_append_escaped_arg(&cmd, g.nameOfExe, 1);
472
blob_appendf(&cmd, " revert --noundo ");
473
db_prepare(&q,
474
"SELECT pathname FROM vfile WHERE vid=%d AND chnged "
475
"EXCEPT SELECT pathname FROM chng",
476
vid
477
);
478
while( db_step(&q)==SQLITE_ROW ){
479
blob_append_escaped_arg(&cmd, db_column_text(&q,0), 1);
480
nRevert++;
481
}
482
db_finalize(&q);
483
if( nRevert ){
484
if( mFlags & PATCH_DRYRUN ){
485
fossil_print("%s", blob_str(&cmd));
486
}else{
487
int rc = fossil_unsafe_system(blob_str(&cmd));
488
if( rc ){
489
fossil_fatal("unable to do reverts:\n%s",
490
blob_str(&cmd));
491
}
492
}
493
}
494
blob_reset(&cmd);
495
}
496
}
497
498
/* Deletions */
499
db_prepare(&q, "SELECT pathname FROM patch.chng"
500
" WHERE origname IS NULL AND delta IS NULL");
501
while( db_step(&q)==SQLITE_ROW ){
502
if( blob_size(&cmd)==0 ){
503
blob_append_escaped_arg(&cmd, g.nameOfExe, 1);
504
blob_appendf(&cmd, " rm --hard");
505
}
506
blob_appendf(&cmd, " %$", db_column_text(&q,0));
507
if( mFlags & PATCH_VERBOSE ){
508
fossil_print("%-10s %s\n", "DELETE", db_column_text(&q,0));
509
}
510
}
511
db_finalize(&q);
512
if( blob_size(&cmd)>0 ){
513
blob_appendf(&cmd, "\n");
514
if( mFlags & PATCH_DRYRUN ){
515
fossil_print("%s", blob_str(&cmd));
516
}else{
517
int rc = fossil_unsafe_system(blob_str(&cmd));
518
if( rc ){
519
fossil_fatal("unable to do merges:\n%s",
520
blob_str(&cmd));
521
}
522
}
523
blob_reset(&cmd);
524
}
525
526
/* Renames */
527
db_prepare(&q,
528
"SELECT origname, pathname FROM patch.chng"
529
" WHERE origname IS NOT NULL"
530
" AND origname<>pathname"
531
);
532
while( db_step(&q)==SQLITE_ROW ){
533
blob_append_escaped_arg(&cmd, g.nameOfExe, 1);
534
blob_appendf(&cmd, " mv --hard %$ %$\n",
535
db_column_text(&q,0), db_column_text(&q,1));
536
if( mFlags & PATCH_VERBOSE ){
537
fossil_print("%-10s %s -> %s\n", "RENAME",
538
db_column_text(&q,0), db_column_text(&q,1));
539
}
540
}
541
db_finalize(&q);
542
if( blob_size(&cmd)>0 ){
543
if( mFlags & PATCH_DRYRUN ){
544
fossil_print("%s", blob_str(&cmd));
545
}else{
546
int rc = fossil_unsafe_system(blob_str(&cmd));
547
if( rc ){
548
fossil_print("%-10s unable to rename files:\n%s", "WARNING!",
549
blob_str(&cmd));
550
}
551
}
552
blob_reset(&cmd);
553
}
554
555
/* Edits and new files */
556
db_prepare(&q,
557
"SELECT pathname, hash, isexe, islink, delta FROM patch.chng"
558
" WHERE delta IS NOT NULL"
559
);
560
while( db_step(&q)==SQLITE_ROW ){
561
const char *zPathname = db_column_text(&q,0);
562
const char *zHash = db_column_text(&q,1);
563
int isExe = db_column_int(&q,2);
564
int isLink = db_column_int(&q,3);
565
Blob data;
566
567
blob_init(&data, 0, 0);
568
db_ephemeral_blob(&q, 4, &data);
569
if( blob_size(&data) ){
570
blob_uncompress(&data, &data);
571
}
572
if( blob_size(&data)==0 ){
573
/* No changes to the file */
574
continue;
575
}else if( zHash ){
576
Blob basis;
577
int rid = fast_uuid_to_rid(zHash);
578
int outSize, sz;
579
char *aOut;
580
if( rid==0 ){
581
fossil_fatal("cannot locate basis artifact %s for %s",
582
zHash, zPathname);
583
}
584
if( !content_get(rid, &basis) ){
585
fossil_fatal("cannot load basis artifact %d for %s", rid, zPathname);
586
}
587
outSize = delta_output_size(blob_buffer(&data),blob_size(&data));
588
if( outSize<=0 ){
589
fossil_fatal("malformed delta for %s", zPathname);
590
}
591
aOut = sqlite3_malloc64( outSize+1 );
592
if( aOut==0 ){
593
fossil_fatal("out of memory");
594
}
595
sz = delta_apply(blob_buffer(&basis), blob_size(&basis),
596
blob_buffer(&data), blob_size(&data), aOut);
597
if( sz<0 ){
598
fossil_fatal("malformed delta for %s", zPathname);
599
}
600
blob_reset(&basis);
601
blob_reset(&data);
602
blob_append(&data, aOut, sz);
603
sqlite3_free(aOut);
604
if( mFlags & PATCH_VERBOSE ){
605
fossil_print("%-10s %s\n", "EDIT", zPathname);
606
}
607
}else{
608
blob_append_escaped_arg(&cmd, g.nameOfExe, 1);
609
blob_appendf(&cmd, " add %$\n", zPathname);
610
if( mFlags & PATCH_VERBOSE ){
611
fossil_print("%-10s %s\n", "NEW", zPathname);
612
}
613
}
614
if( (mFlags & PATCH_DRYRUN)==0 ){
615
if( isLink ){
616
symlink_create(blob_str(&data), zPathname);
617
}else{
618
blob_write_to_file(&data, zPathname);
619
}
620
file_setexe(zPathname, isExe);
621
blob_reset(&data);
622
}
623
}
624
db_finalize(&q);
625
if( blob_size(&cmd)>0 ){
626
if( mFlags & PATCH_DRYRUN ){
627
fossil_print("%s", blob_str(&cmd));
628
}else{
629
int rc = fossil_unsafe_system(blob_str(&cmd));
630
if( rc ){
631
fossil_fatal("unable to add new files:\n%s",
632
blob_str(&cmd));
633
}
634
}
635
blob_reset(&cmd);
636
}
637
}
638
639
/*
640
** This routine processes the
641
**
642
** ... [--dir64 DIR64] [DIRECTORY] FILENAME
643
**
644
** part of various "fossil patch" subcommands.
645
**
646
** Find and return the filename of the patch file to be used by
647
** "fossil patch apply" or "fossil patch create". Space to hold
648
** the returned name is obtained from fossil_malloc() and should
649
** be freed by the caller.
650
**
651
** If the name is "-" return NULL. The caller will interpret this
652
** to mean the patch is coming in over stdin or going out over
653
** stdout.
654
**
655
** If there is a prior DIRECTORY argument, or if
656
** the --dir64 option is present, first chdir to the specified
657
** directory, and adjust the path of FILENAME as appropriate so
658
** that it still points to the same file.
659
**
660
** The --dir64 option is undocumented. The argument to --dir64
661
** is a base64-encoded directory name. The --dir64 option is used
662
** to transmit the directory as part of the command argument to
663
** a "ssh" command without having to worry about quoting
664
** any special characters in the filename.
665
**
666
** The returned name is obtained from fossil_malloc() and should
667
** be freed by the caller.
668
*/
669
static char *patch_find_patch_filename(const char *zCmdName){
670
const char *zDir64 = find_option("dir64",0,1);
671
const char *zDir = 0;
672
const char *zBaseName;
673
char *zToFree = 0;
674
char *zPatchFile = 0;
675
if( zDir64 ){
676
int n = 0;
677
zToFree = decode64(zDir64, &n);
678
zDir = zToFree;
679
}
680
verify_all_options();
681
if( g.argc!=4 && g.argc!=5 ){
682
usage(mprintf("%s [DIRECTORY] FILENAME", zCmdName));
683
}
684
if( g.argc==5 ){
685
zDir = g.argv[3];
686
zBaseName = g.argv[4];
687
}else{
688
zBaseName = g.argv[3];
689
}
690
if( fossil_strcmp(zBaseName, "-")==0 ){
691
zPatchFile = 0;
692
}else if( zDir ){
693
zPatchFile = file_canonical_name_dup(g.argv[4]);
694
}else{
695
zPatchFile = fossil_strdup(g.argv[3]);
696
}
697
if( zDir && file_chdir(zDir,0) ){
698
fossil_fatal("cannot change to directory \"%s\"", zDir);
699
}
700
fossil_free(zToFree);
701
return zPatchFile;
702
}
703
704
/*
705
** Resolves a patch-command remote system name, accounting for patch
706
** aliases.
707
**
708
** If a CONFIG table entry matching name='patch-alias:$zKey' is found,
709
** the corresponding value is returned, else a fossil_strdup() of zKey
710
** is returned. The caller is responsible for passing the resulting
711
** string to fossil_free().
712
*/
713
static char *patch_resolve_remote(const char *zKey){
714
char *zAlias = db_text(0, "SELECT value FROM config "
715
"WHERE name = 'patch-alias:%q'",
716
zKey);
717
return zAlias ? zAlias : fossil_strdup(zKey);
718
}
719
720
/*
721
** Create a FILE* that will execute the remote side of a push or pull
722
** using ssh (probably) or fossil for local pushes and pulls. Return
723
** a FILE* obtained from popen() into which we write the patch, or from
724
** which we read the patch, depending on whether this is a push or pull.
725
*/
726
static FILE *patch_remote_command(
727
unsigned mFlags, /* flags */
728
const char *zThisCmd, /* "push" or "pull" */
729
const char *zRemoteCmd, /* "apply" or "create" */
730
const char *zFossilCmd, /* Name of "fossil" on remote system */
731
const char *zRW /* "w" or "r" */
732
){
733
char *zRemote = 0;
734
char *zDir = 0;
735
Blob cmd;
736
FILE *f = 0;
737
Blob flgs;
738
char *zForce = 0;
739
int isRetry = (mFlags & PATCH_RETRY)!=0;
740
741
blob_init(&flgs, 0, 0);
742
blob_init(&cmd, 0, 0);
743
if( mFlags & PATCH_FORCE ) blob_appendf(&flgs, " -f");
744
if( mFlags & PATCH_VERBOSE ) blob_appendf(&flgs, " -v");
745
if( mFlags & PATCH_DRYRUN ) blob_appendf(&flgs, " -n");
746
zForce = blob_size(&flgs)>0 ? blob_str(&flgs) : "";
747
if( g.argc!=4 ){
748
usage(mprintf("%s [USER@]HOST:DIRECTORY", zThisCmd));
749
}
750
zRemote = patch_resolve_remote(g.argv[3]);
751
zDir = (char*)file_skip_userhost(zRemote);
752
if( zDir==0 ){
753
if( isRetry ) goto remote_command_error;
754
zDir = zRemote;
755
blob_append_escaped_arg(&cmd, g.nameOfExe, 1);
756
blob_appendf(&cmd, " patch %s%s %$ -", zRemoteCmd, zForce, zDir);
757
}else{
758
Blob remote;
759
*(char*)(zDir-1) = 0;
760
transport_ssh_command(&cmd);
761
blob_appendf(&cmd, " -T");
762
blob_append_escaped_arg(&cmd, zRemote, 0);
763
blob_init(&remote, 0, 0);
764
if( zFossilCmd==0 ){
765
if( ssh_needs_path_argument(zRemote,-1) ^ isRetry ){
766
ssh_add_path_argument(&cmd);
767
}
768
zFossilCmd = "fossil";
769
}else if( mFlags & PATCH_RETRY ){
770
goto remote_command_error;
771
}
772
blob_appendf(&remote, "%$ patch %s%s --dir64 %z -",
773
zFossilCmd, zRemoteCmd, zForce, encode64(zDir, -1));
774
blob_append_escaped_arg(&cmd, blob_str(&remote), 0);
775
blob_reset(&remote);
776
}
777
if( isRetry ){
778
fossil_print("First attempt to run \"fossil\" on %s failed\n"
779
"Retry: ", zRemote);
780
}
781
fossil_print("%s\n", blob_str(&cmd));
782
fflush(stdout);
783
f = popen(blob_str(&cmd), zRW);
784
if( f==0 ){
785
fossil_fatal("cannot run command: %s", blob_str(&cmd));
786
}
787
remote_command_error:
788
fossil_free(zRemote);
789
blob_reset(&cmd);
790
blob_reset(&flgs);
791
return f;
792
}
793
794
/*
795
** Toggle the use-path-for-ssh setting for the remote host defined
796
** by g.argv[3].
797
*/
798
static void patch_toggle_ssh_needs_path(void){
799
char *zRemote = patch_resolve_remote(g.argv[3]);
800
char *zDir = (char*)file_skip_userhost(zRemote);
801
if( zDir ){
802
*(char*)(zDir - 1) = 0;
803
ssh_needs_path_argument(zRemote, 99);
804
}
805
fossil_free(zRemote);
806
}
807
808
/*
809
** Show a diff for the patch currently loaded into database "patch".
810
*/
811
static void patch_diff(
812
unsigned mFlags, /* Patch flags. only -f is allowed */
813
DiffConfig *pCfg /* Diff options */
814
){
815
int nErr = 0;
816
Stmt q;
817
int bWebpage = (pCfg->diffFlags & DIFF_WEBPAGE)!=0;
818
Blob empty;
819
blob_zero(&empty);
820
821
if( (mFlags & PATCH_FORCE)==0 ){
822
/* Check to ensure that the patch is against the repository that
823
** we have opened.
824
**
825
** To do: If there is a mismatch, should we scan all of the repositories
826
** listed in the global_config table looking for a match?
827
*/
828
if( db_exists(
829
"SELECT 1 FROM patch.cfg"
830
" WHERE cfg.key='baseline'"
831
" AND NOT EXISTS(SELECT 1 FROM blob WHERE uuid=cfg.value)"
832
)){
833
char *zBaseline;
834
db_prepare(&q,
835
"SELECT config.value, cfg.value FROM config, cfg"
836
" WHERE config.name='project-name'"
837
" AND cfg.key='project-name'"
838
" AND config.value<>cfg.value"
839
);
840
if( db_step(&q)==SQLITE_ROW ){
841
char *zRepo = fossil_strdup(db_column_text(&q,0));
842
char *zPatch = fossil_strdup(db_column_text(&q,1));
843
db_finalize(&q);
844
fossil_fatal("the patch is against project \"%z\" but you are using "
845
"project \"%z\"", zPatch, zRepo);
846
}
847
db_finalize(&q);
848
zBaseline = db_text(0, "SELECT value FROM patch.cfg"
849
" WHERE key='baseline'");
850
if( zBaseline ){
851
fossil_fatal("the baseline of the patch (check-in %S) is not found "
852
"in the %s repository", zBaseline, g.zRepositoryName);
853
}
854
}
855
}
856
857
diff_begin(pCfg);
858
db_prepare(&q,
859
"SELECT"
860
" (SELECT blob.rid FROM blob WHERE blob.uuid=chng.hash),"
861
" pathname," /* 1: new pathname */
862
" origname," /* 2: original pathname. Null if not renamed */
863
" delta," /* 3: delta. NULL if deleted. empty is no change */
864
" hash" /* 4: baseline hash */
865
" FROM patch.chng"
866
" ORDER BY pathname"
867
);
868
while( db_step(&q)==SQLITE_ROW ){
869
int rid;
870
const char *zName;
871
Blob a, b;
872
873
if( db_column_type(&q,0)!=SQLITE_INTEGER
874
&& db_column_type(&q,4)==SQLITE_TEXT
875
){
876
char *zUuid = fossil_strdup(db_column_text(&q,4));
877
char *zName = fossil_strdup(db_column_text(&q,1));
878
if( mFlags & PATCH_FORCE ){
879
fossil_print("ERROR cannot find base artifact %S for file \"%s\"\n",
880
zUuid, zName);
881
nErr++;
882
fossil_free(zUuid);
883
fossil_free(zName);
884
continue;
885
}else{
886
db_finalize(&q);
887
fossil_fatal("base artifact %S for file \"%s\" not found",
888
zUuid, zName);
889
}
890
}
891
zName = db_column_text(&q, 1);
892
rid = db_column_int(&q, 0);
893
894
pCfg->diffFlags &= (~DIFF_FILE_MASK);
895
if( db_column_type(&q,3)==SQLITE_NULL ){
896
if( !bWebpage ) fossil_print("DELETE %s\n", zName);
897
pCfg->diffFlags |= DIFF_FILE_DELETED;
898
diff_print_index(zName, pCfg, 0);
899
content_get(rid, &a);
900
diff_file_mem(&a, &empty, zName, pCfg);
901
}else if( rid==0 ){
902
db_ephemeral_blob(&q, 3, &a);
903
blob_uncompress(&a, &a);
904
if( !bWebpage ) fossil_print("ADDED %s\n", zName);
905
pCfg->diffFlags |= DIFF_FILE_ADDED;
906
diff_print_index(zName, pCfg, 0);
907
diff_file_mem(&empty, &a, zName, pCfg);
908
blob_reset(&a);
909
}else if( db_column_bytes(&q, 3)>0 ){
910
Blob delta;
911
db_ephemeral_blob(&q, 3, &delta);
912
blob_uncompress(&delta, &delta);
913
content_get(rid, &a);
914
blob_delta_apply(&a, &delta, &b);
915
diff_file_mem(&a, &b, zName, pCfg);
916
blob_reset(&a);
917
blob_reset(&b);
918
blob_reset(&delta);
919
}
920
}
921
db_finalize(&q);
922
diff_end(pCfg, nErr);
923
if( nErr ) fossil_fatal("abort due to prior errors");
924
}
925
926
/*
927
** COMMAND: patch
928
**
929
** Usage: %fossil patch SUBCOMMAND ?ARGS ..?
930
**
931
** This command is used to create, view, and apply Fossil binary patches.
932
** A Fossil binary patch is a single (binary) file that captures all of the
933
** uncommitted changes of a check-out. Use Fossil binary patches to transfer
934
** proposed or incomplete changes between machines for testing or analysis.
935
**
936
** > fossil patch alias add|rm|ls|list ?ARGS?
937
**
938
** Manage remote-name aliases, which act as short-form
939
** equivalents to REMOTE-CHECKOUT strings. Aliases are local to
940
** a given repository and do not sync. Subcommands:
941
**
942
** ... add ALIAS REMOTE-CHECKOUT Add ALIAS as an alias
943
** for REMOTE-CHECKOUT.
944
** ... ls|list List all local aliases.
945
** ... rm ALIAS [ALIAS...] Remove named aliases
946
** ... rm --all Remove all aliases
947
**
948
** > fossil patch create [DIRECTORY] PATCHFILE
949
**
950
** Create a new binary patch in PATCHFILE that captures all uncommitted
951
** changes in the check-out at DIRECTORY, or the current directory if
952
** DIRECTORY is omitted. If PATCHFILE is "-" then the binary patch
953
** is written to standard output.
954
**
955
** Options:
956
** -f|--force Overwrite an existing patch with the same name
957
**
958
** > fossil patch apply [DIRECTORY] PATCHFILE
959
**
960
** Apply the changes in PATCHFILE to the check-out at DIRECTORY, or
961
** in the current directory if DIRECTORY is omitted.
962
**
963
** Options:
964
** -f|--force Apply the patch even though there are unsaved
965
** changes in the current check-out. Unsaved changes
966
** are reverted and permanently lost.
967
** -n|--dry-run Do nothing, but print what would have happened
968
** -v|--verbose Extra output explaining what happens
969
**
970
** > fossil patch diff [DIRECTORY] PATCHFILE
971
** > fossil patch gdiff [DIRECTORY] PATCHFILE
972
**
973
** Show a human-readable diff for the patch in PATCHFILE and associated
974
** with the repository checked out in DIRECTORY. The current
975
** directory is used if DIRECTORY is omitted. All the usual
976
** diff flags described at "fossil help diff" apply. With gdiff,
977
** gdiff-command is used instead of internal diff logic. In addition:
978
**
979
** -f|--force Continue trying to perform the diff even if
980
** baseline information is missing from the current
981
** repository
982
**
983
** > fossil patch push REMOTE-CHECKOUT
984
**
985
** Create a patch for the current check-out, transfer that patch to
986
** a remote machine (using ssh) and apply the patch there. The
987
** REMOTE-CHECKOUT is in one of the following formats:
988
**
989
** * DIRECTORY
990
** * HOST:DIRECTORY
991
** * USER@HOST:DIRECTORY
992
**
993
** The name of the fossil executable on the remote host is specified
994
** by the --fossilcmd option, or if there is no --fossilcmd, it first
995
** tries "$HOME/bin/fossil" and if not found there it searches for any
996
** executable named "fossil" on the default $PATH set by SSH on the
997
** remote.
998
**
999
** Command-line options:
1000
**
1001
** -f|--force Apply the patch even though there are unsaved
1002
** changes in the current check-out. Unsaved
1003
** changes will be reverted and then the patch is
1004
** applied.
1005
** --fossilcmd EXE Name of the "fossil" executable on the remote
1006
** -n|--dry-run Do nothing, but print what would have happened
1007
** -v|--verbose Extra output explaining what happens
1008
**
1009
**
1010
** > fossil patch pull REMOTE-CHECKOUT
1011
**
1012
** Like "fossil patch push" except that the transfer is from remote
1013
** to local. All the same command-line options apply.
1014
**
1015
** > fossil patch view PATCHFILE
1016
**
1017
** View a summary of the changes in the binary patch in PATCHFILE.
1018
** Use "fossil patch diff" for detailed patch content.
1019
**
1020
** -v|--verbose Show extra detail about the patch
1021
**
1022
*/
1023
void patch_cmd(void){
1024
const char *zCmd;
1025
size_t n;
1026
if( g.argc<3 ){
1027
patch_usage:
1028
usage("alias|apply|create|diff|gdiff|pull|push|view");
1029
}
1030
zCmd = g.argv[2];
1031
n = strlen(zCmd);
1032
if( strncmp(zCmd, "alias", n)==0 ){
1033
const char * zArg = g.argc>3 ? g.argv[3] : 0;
1034
db_must_be_within_tree();
1035
if( 0==zArg ){
1036
goto usage_patch_alias;
1037
}else if( 0==strcmp("ls",zArg) || 0==strcmp("list",zArg) ){
1038
/* alias ls|list */
1039
Stmt q;
1040
int nAlias = 0;
1041
1042
verify_all_options();
1043
db_prepare(&q, "SELECT substr(name,13), value FROM config "
1044
"WHERE name GLOB 'patch-alias:*' ORDER BY name");
1045
while( SQLITE_ROW==db_step(&q) ){
1046
const char *zName = db_column_text(&q, 0);
1047
const char *zVal = db_column_text(&q, 1);
1048
++nAlias;
1049
fossil_print("%s = %s\n", zName, zVal);
1050
}
1051
db_finalize(&q);
1052
if( 0==nAlias ){
1053
fossil_print("No patch aliases defined\n");
1054
}
1055
}else if( 0==strcmp("add", zArg) ){
1056
/* alias add localName remote */
1057
verify_all_options();
1058
if( 6!=g.argc ){
1059
usage("alias add localName remote");
1060
}
1061
db_unprotect(PROTECT_CONFIG);
1062
db_multi_exec("REPLACE INTO config (name, value, mtime) "
1063
"VALUES ('patch-alias:%q', %Q, unixepoch())",
1064
g.argv[4], g.argv[5]);
1065
db_protect_pop();
1066
}else if( 0==strcmp("rm", zArg) ){
1067
/* alias rm */
1068
const int fAll = 0!=find_option("all", 0, 0);
1069
if( fAll ? g.argc<4 : g.argc<5 ){
1070
usage("alias rm [-all] [aliasGlob [...aliasGlobN]]");
1071
}
1072
verify_all_options();
1073
db_unprotect(PROTECT_CONFIG);
1074
if( 0!=fAll ){
1075
db_multi_exec("DELETE FROM config WHERE name GLOB 'patch-alias:*'");
1076
}else{
1077
Stmt q;
1078
int i;
1079
db_prepare(&q, "DELETE FROM config WHERE name "
1080
"GLOB 'patch-alias:' || :pattern");
1081
for(i = 4; i < g.argc; ++i){
1082
db_bind_text(&q, ":pattern", g.argv[i]);
1083
db_step(&q);
1084
db_reset(&q);
1085
}
1086
db_finalize(&q);
1087
}
1088
db_protect_pop();
1089
}else{
1090
usage_patch_alias:
1091
usage("alias ls|list|add|rm ...");
1092
}
1093
}else
1094
if( strncmp(zCmd, "apply", n)==0 ){
1095
char *zIn;
1096
unsigned flags = 0;
1097
if( find_option("dry-run","n",0) ) flags |= PATCH_DRYRUN;
1098
if( find_option("verbose","v",0) ) flags |= PATCH_VERBOSE;
1099
if( find_option("force","f",0) ) flags |= PATCH_FORCE;
1100
zIn = patch_find_patch_filename("apply");
1101
db_must_be_within_tree();
1102
patch_attach(zIn, stdin, 0);
1103
patch_apply(flags);
1104
fossil_free(zIn);
1105
}else
1106
if( strncmp(zCmd, "create", n)==0 ){
1107
char *zOut;
1108
unsigned flags = 0;
1109
if( find_option("force","f",0) ) flags |= PATCH_FORCE;
1110
zOut = patch_find_patch_filename("create");
1111
verify_all_options();
1112
db_must_be_within_tree();
1113
patch_create(flags, zOut, stdout);
1114
fossil_free(zOut);
1115
}else
1116
if( (strncmp(zCmd, "diff", n)==0) || (strncmp(zCmd, "gdiff", n)==0) ){
1117
char *zIn;
1118
unsigned flags = 0;
1119
DiffConfig DCfg;
1120
1121
if( find_option("tk",0,0)!=0 ){
1122
db_close(0);
1123
diff_tk("patch diff", 3);
1124
return;
1125
}
1126
db_find_and_open_repository(0, 0);
1127
if( gdiff_using_tk(zCmd[0]=='g') ){
1128
diff_tk("patch diff", 3);
1129
return;
1130
}
1131
if( find_option("force","f",0) ) flags |= PATCH_FORCE;
1132
diff_options(&DCfg, zCmd[0]=='g', 0);
1133
verify_all_options();
1134
zIn = patch_find_patch_filename("diff");
1135
patch_attach(zIn, stdin, 0);
1136
patch_diff(flags, &DCfg);
1137
fossil_free(zIn);
1138
}else
1139
if( strncmp(zCmd, "pull", n)==0 ){
1140
FILE *pIn = 0;
1141
unsigned flags = 0;
1142
const char *zFossilCmd = find_option("fossilcmd",0,1);
1143
if( find_option("dry-run","n",0) ) flags |= PATCH_DRYRUN;
1144
if( find_option("verbose","v",0) ) flags |= PATCH_VERBOSE;
1145
if( find_option("force","f",0) ) flags |= PATCH_FORCE;
1146
db_must_be_within_tree();
1147
verify_all_options();
1148
pIn = patch_remote_command(flags & (~PATCH_FORCE),
1149
"pull", "create", zFossilCmd, "r");
1150
if( pIn ){
1151
patch_attach(0, pIn, 1);
1152
if( pclose(pIn) ){
1153
flags |= PATCH_RETRY;
1154
pIn = patch_remote_command(flags & (~PATCH_FORCE),
1155
"pull", "create", zFossilCmd, "r");
1156
if( pIn ){
1157
patch_attach(0, pIn, 0);
1158
if( pclose(pIn)==0 ){
1159
patch_toggle_ssh_needs_path();
1160
}
1161
}
1162
}
1163
patch_apply(flags);
1164
}
1165
}else
1166
if( strncmp(zCmd, "push", n)==0 ){
1167
FILE *pOut = 0;
1168
unsigned flags = 0;
1169
const char *zFossilCmd = find_option("fossilcmd",0,1);
1170
if( find_option("dry-run","n",0) ) flags |= PATCH_DRYRUN;
1171
if( find_option("verbose","v",0) ) flags |= PATCH_VERBOSE;
1172
if( find_option("force","f",0) ) flags |= PATCH_FORCE;
1173
db_must_be_within_tree();
1174
verify_all_options();
1175
pOut = patch_remote_command(flags, "push", "apply", zFossilCmd, "w");
1176
if( pOut ){
1177
patch_create(0, 0, pOut);
1178
if( pclose(pOut)!=0 ){
1179
flags |= PATCH_RETRY;
1180
pOut = patch_remote_command(flags, "push", "apply", zFossilCmd, "w");
1181
if( pOut ){
1182
patch_create(0, 0, pOut);
1183
if( pclose(pOut)==0 ){
1184
patch_toggle_ssh_needs_path();
1185
}
1186
}
1187
}
1188
}
1189
}else
1190
if( strncmp(zCmd, "view", n)==0 ){
1191
const char *zIn;
1192
unsigned int flags = 0;
1193
if( find_option("verbose","v",0) ) flags |= PATCH_VERBOSE;
1194
verify_all_options();
1195
if( g.argc!=4 ){
1196
usage("view FILENAME");
1197
}
1198
zIn = g.argv[3];
1199
if( fossil_strcmp(zIn, "-")==0 ) zIn = 0;
1200
patch_attach(zIn, stdin, 0);
1201
patch_view(flags);
1202
}else
1203
{
1204
goto patch_usage;
1205
}
1206
}
1207

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button