Fossil SCM

fossil-scm / src / path.c
Blame History Raw 738 lines
1
/*
2
** Copyright (c) 2011 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
**
15
*******************************************************************************
16
**
17
** This file contains code used to trace paths of through the
18
** directed acyclic graph (DAG) of check-ins.
19
*/
20
#include "config.h"
21
#include "path.h"
22
#include <assert.h>
23
#include <math.h>
24
25
#if INTERFACE
26
/* Nodes for the paths through the DAG.
27
*/
28
struct PathNode {
29
int rid; /* ID for this node */
30
u8 fromIsParent; /* True if pFrom is the parent of rid */
31
u8 isPrim; /* True if primary side of common ancestor */
32
u8 isHidden; /* Abbreviate output in "fossil bisect ls" */
33
char *zBranch; /* Branch name for this node. Might be NULL */
34
double mtime; /* Date/time of this check-in */
35
PathNode *pFrom; /* Node we came from */
36
union {
37
double rCost; /* Cost of getting to this node from pStart */
38
PathNode *pTo; /* Next on path from beginning to end */
39
} u;
40
PathNode *pAll; /* List of all nodes */
41
};
42
#endif
43
44
/*
45
** Local variables for this module
46
*/
47
static struct {
48
PQueue pending; /* Nodes pending review for inclusion in the graph */
49
PathNode *pAll; /* All nodes */
50
int nStep; /* Number of steps from first to last */
51
int nNotHidden; /* Number of steps not counting hidden nodes */
52
int brCost; /* Extra cost for moving to a different branch */
53
int revCost; /* Extra cost for changing directions */
54
PathNode *pStart; /* Earliest node */
55
PathNode *pEnd; /* Most recent */
56
} path;
57
static int path_debug = 0; /* Flag to enable debugging */
58
59
/*
60
** Return the first (last) element of the computed path.
61
*/
62
PathNode *path_first(void){ return path.pStart; }
63
PathNode *path_last(void){ return path.pEnd; }
64
65
/*
66
** Return the number of steps in the computed path.
67
*/
68
int path_length(void){ return path.nStep; }
69
70
/*
71
** Return the number of non-hidden steps in the computed path.
72
*/
73
int path_length_not_hidden(void){ return path.nNotHidden; }
74
75
/*
76
** Used for debugging only.
77
**
78
** Given a RID, return the ISO date/time string and branch for the
79
** corresponding check-in. Memory is held locally and is overwritten
80
** with each call.
81
*/
82
char *path_rid_desc(int rid){
83
static Stmt q;
84
static char *zDesc = 0;
85
db_static_prepare(&q,
86
"SELECT concat(strftime('%%Y%%m%%d%%H%%M',event.mtime),'/',value)"
87
" FROM event, tagxref"
88
" WHERE event.objid=:rid"
89
" AND tagxref.rid=:rid"
90
" AND tagxref.tagid=%d"
91
" AND tagxref.tagtype>0",
92
TAG_BRANCH
93
);
94
fossil_free(zDesc);
95
db_bind_int(&q, ":rid", rid);
96
if( db_step(&q)==SQLITE_ROW ){
97
zDesc = fossil_strdup(db_column_text(&q,0));
98
}
99
db_reset(&q);
100
return zDesc ? zDesc : "???";
101
}
102
103
/*
104
** Create a new node and insert it into the path.pending queue.
105
*/
106
static PathNode *path_new_node(int rid, PathNode *pFrom, int isParent){
107
PathNode *p;
108
109
p = fossil_malloc( sizeof(*p) );
110
memset(p, 0, sizeof(*p));
111
p->pAll = path.pAll;
112
path.pAll = p;
113
p->rid = rid;
114
p->fromIsParent = isParent;
115
p->pFrom = pFrom;
116
p->u.rCost = pFrom ? pFrom->u.rCost : 0.0;
117
if( path.brCost ){
118
p->zBranch = branch_of_rid(rid);
119
p->mtime = mtime_of_rid(rid, 0.0);
120
if( pFrom ){
121
p->u.rCost += fabs(pFrom->mtime - p->mtime);
122
if( fossil_strcmp(p->zBranch, pFrom->zBranch)!=0 ){
123
p->u.rCost += path.brCost;
124
}
125
}
126
}else{
127
/* When brCost==0, we try to minimize the number of nodes
128
** along the path. The cost is just the number of nodes back
129
** to the start. We do not need to know the branch name nor
130
** the mtime */
131
p->u.rCost += 1.0;
132
}
133
if( path_debug ){
134
fossil_print("PUSH %-50s cost = %g\n", path_rid_desc(p->rid), p->u.rCost);
135
}
136
pqueuex_insert_ptr(&path.pending, (void*)p, p->u.rCost);
137
return p;
138
}
139
140
/*
141
** Reset memory used by the shortest path algorithm.
142
*/
143
void path_reset(void){
144
PathNode *p;
145
while( path.pAll ){
146
p = path.pAll;
147
path.pAll = p->pAll;
148
fossil_free(p->zBranch);
149
fossil_free(p);
150
}
151
pqueuex_clear(&path.pending);
152
memset(&path, 0, sizeof(path));
153
}
154
155
/*
156
** Construct the path from path.pStart to path.pEnd in the u.pTo fields.
157
*/
158
static void path_reverse_path(void){
159
PathNode *p;
160
assert( path.pEnd!=0 );
161
for(p=path.pEnd; p && p->pFrom; p = p->pFrom){
162
p->pFrom->u.pTo = p;
163
}
164
path.pEnd->u.pTo = 0;
165
assert( p==path.pStart );
166
}
167
168
/*
169
** Compute the shortest path from iFrom to iTo
170
**
171
** If directOnly is true, then use only the "primary" links from parent to
172
** child. In other words, ignore merges.
173
**
174
** Return a pointer to the beginning of the path (the iFrom node).
175
** Elements of the path can be traversed by following the PathNode.u.pTo
176
** pointer chain.
177
**
178
** Return NULL if no path is found.
179
*/
180
PathNode *path_shortest(
181
int iFrom, /* Path starts here */
182
int iTo, /* Path ends here */
183
int directOnly, /* No merge links if true */
184
int oneWayOnly, /* Parent->child only if true */
185
Bag *pHidden, /* Hidden nodes */
186
int branchCost /* Add extra cost to changing branches */
187
){
188
Stmt s;
189
Bag seen;
190
PathNode *p;
191
192
path_reset();
193
path.brCost = branchCost;
194
path.pStart = path_new_node(iFrom, 0, 0);
195
if( iTo==iFrom ){
196
path.pEnd = path.pStart;
197
path.pEnd->u.pTo = 0;
198
return path.pStart;
199
}
200
if( oneWayOnly && directOnly ){
201
db_prepare(&s,
202
"SELECT cid, 1 FROM plink WHERE pid=:pid AND isprim"
203
);
204
}else if( oneWayOnly ){
205
db_prepare(&s,
206
"SELECT cid, 1 FROM plink WHERE pid=:pid "
207
);
208
}else if( directOnly ){
209
db_prepare(&s,
210
"SELECT cid, 1 FROM plink WHERE pid=:pid AND isprim "
211
"UNION ALL "
212
"SELECT pid, 0 FROM plink WHERE :back AND cid=:pid AND isprim"
213
);
214
}else{
215
db_prepare(&s,
216
"SELECT cid, 1 FROM plink WHERE pid=:pid "
217
"UNION ALL "
218
"SELECT pid, 0 FROM plink WHERE :back AND cid=:pid"
219
);
220
}
221
bag_init(&seen);
222
while( (p = pqueuex_extract_ptr(&path.pending))!=0 ){
223
if( path_debug ){
224
printf("PULL %s %g\n", path_rid_desc(p->rid), p->u.rCost);
225
}
226
if( p->rid==iTo ){
227
db_finalize(&s);
228
path.pEnd = p;
229
path_reverse_path();
230
for(p=path.pStart->u.pTo; p; p=p->u.pTo ){
231
if( !p->isHidden ) path.nNotHidden++;
232
}
233
return path.pStart;
234
}
235
if( bag_find(&seen, p->rid) ) continue;
236
bag_insert(&seen, p->rid);
237
db_bind_int(&s, ":pid", p->rid);
238
if( !oneWayOnly ) db_bind_int(&s, ":back", !p->fromIsParent);
239
while( db_step(&s)==SQLITE_ROW ){
240
int cid = db_column_int(&s, 0);
241
int isParent = db_column_int(&s, 1);
242
PathNode *pNew;
243
if( bag_find(&seen, cid) ) continue;
244
pNew = path_new_node(cid, p, isParent);
245
if( pHidden && bag_find(pHidden,cid) ) pNew->isHidden = 1;
246
}
247
db_reset(&s);
248
}
249
db_finalize(&s);
250
path_reset();
251
return 0;
252
}
253
254
/*
255
** Find the mid-point of the path. If the path contains fewer than
256
** 2 steps, return 0.
257
*/
258
PathNode *path_midpoint(void){
259
PathNode *p;
260
int i;
261
if( path.nNotHidden<2 ) return 0;
262
for(p=path.pEnd, i=0; p && (p->isHidden || i<path.nNotHidden/2); p=p->pFrom){
263
if( !p->isHidden ) i++;
264
}
265
return p;
266
}
267
268
/*
269
** Find the next most recent node on a path.
270
*/
271
PathNode *path_next(void){
272
PathNode *p;
273
p = path.pStart;
274
if( p ) p = p->u.pTo;
275
return p;
276
}
277
278
/*
279
** Return the branch for a path node.
280
**
281
** Storage space is managed by the path subsystem. The returned value
282
** is valid until the path is reset.
283
*/
284
const char *path_branch(PathNode *p){
285
if( p==0 ) return 0;
286
if( p->zBranch==0 ) p->zBranch = branch_of_rid(p->rid);
287
return p->zBranch;
288
}
289
290
/*
291
** Return an estimate of the number of comparisons remaining in order
292
** to bisect path. This is based on the log2() of path.nStep.
293
*/
294
int path_search_depth(void){
295
int i, j;
296
for(i=0, j=1; j<path.nNotHidden; i++, j+=j){}
297
return i;
298
}
299
300
/*
301
** Compute the shortest path between two check-ins and then transfer
302
** that path into the "ancestor" table. This is a utility used by
303
** both /annotate and /finfo. See also: compute_direct_ancestors().
304
*/
305
void path_shortest_stored_in_ancestor_table(
306
int origid, /* RID for check-in at start of the path */
307
int cid /* RID for check-in at the end of the path */
308
){
309
PathNode *pPath;
310
int gen = 0;
311
Stmt ins;
312
pPath = path_shortest(cid, origid, 1, 0, 0, 0);
313
db_multi_exec(
314
"CREATE TEMP TABLE IF NOT EXISTS ancestor("
315
" rid INT UNIQUE,"
316
" generation INTEGER PRIMARY KEY"
317
");"
318
"DELETE FROM ancestor;"
319
);
320
db_prepare(&ins, "INSERT INTO ancestor(rid, generation) VALUES(:rid,:gen)");
321
while( pPath ){
322
db_bind_int(&ins, ":rid", pPath->rid);
323
db_bind_int(&ins, ":gen", ++gen);
324
db_step(&ins);
325
db_reset(&ins);
326
pPath = pPath->u.pTo;
327
}
328
db_finalize(&ins);
329
path_reset();
330
}
331
332
/*
333
** COMMAND: test-shortest-path
334
**
335
** Usage: %fossil test-shortest-path [OPTIONS] VERSION1 VERSION2
336
**
337
** Report the shortest path between two check-ins. Options:
338
**
339
** --branch-cost N Additional cost N for changing branches
340
** --debug Show debugging output
341
** --one-way One-way forwards in time, parent->child only
342
** --no-merge Follow only direct parent-child paths and omit
343
** merge links.
344
*/
345
void shortest_path_test_cmd(void){
346
int iFrom;
347
int iTo;
348
PathNode *p;
349
int n;
350
int directOnly;
351
int oneWay;
352
const char *zBrCost;
353
354
db_find_and_open_repository(0,0);
355
directOnly = find_option("no-merge",0,0)!=0;
356
oneWay = find_option("one-way",0,0)!=0;
357
zBrCost = find_option("branch-cost",0,1);
358
if( find_option("debug",0,0)!=0 ) path_debug = 1;
359
if( g.argc!=4 ) usage("VERSION1 VERSION2");
360
iFrom = name_to_rid(g.argv[2]);
361
iTo = name_to_rid(g.argv[3]);
362
p = path_shortest(iFrom, iTo, directOnly, oneWay, 0,
363
zBrCost ? atoi(zBrCost) : 0);
364
if( p==0 ){
365
fossil_fatal("no path from %s to %s", g.argv[1], g.argv[2]);
366
}
367
for(n=1, p=path.pStart; p; p=p->u.pTo, n++){
368
fossil_print("%4d: %s\n", n, path_rid_desc(p->rid));
369
}
370
path_debug = 0;
371
}
372
373
/*
374
** Find the closest common ancestor of two nodes. "Closest" means the
375
** fewest number of arcs.
376
*/
377
int path_common_ancestor(int iMe, int iYou){
378
Stmt s;
379
PathNode *pThis;
380
PathNode *p;
381
Bag me, you;
382
383
if( iMe==iYou ) return iMe;
384
if( iMe==0 || iYou==0 ) return 0;
385
path_reset();
386
path.pStart = path_new_node(iMe, 0, 0);
387
path.pStart->isPrim = 1;
388
path.pEnd = path_new_node(iYou, 0, 0);
389
db_prepare(&s, "SELECT pid FROM plink WHERE cid=:cid");
390
bag_init(&me);
391
bag_insert(&me, iMe);
392
bag_init(&you);
393
bag_insert(&you, iYou);
394
while( (pThis = pqueuex_extract_ptr(&path.pending))!=0 ){
395
db_bind_int(&s, ":cid", pThis->rid);
396
while( db_step(&s)==SQLITE_ROW ){
397
int pid = db_column_int(&s, 0);
398
if( bag_find(pThis->isPrim ? &you : &me, pid) ){
399
/* pid is the common ancestor */
400
PathNode *pNext;
401
for(p=path.pAll; p && p->rid!=pid; p=p->pAll){}
402
assert( p!=0 );
403
pNext = p;
404
while( pNext ){
405
pNext = p->pFrom;
406
p->pFrom = pThis;
407
pThis = p;
408
p = pNext;
409
}
410
if( pThis==path.pStart ) path.pStart = path.pEnd;
411
path.pEnd = pThis;
412
path_reverse_path();
413
db_finalize(&s);
414
return pid;
415
}else if( bag_find(pThis->isPrim ? &me : &you, pid) ){
416
/* pid is just an alternative path to a node we've already visited */
417
continue;
418
}
419
p = path_new_node(pid, pThis, 0);
420
p->isPrim = pThis->isPrim;
421
bag_insert(pThis->isPrim ? &me : &you, pid);
422
}
423
db_reset(&s);
424
}
425
db_finalize(&s);
426
path_reset();
427
return 0;
428
}
429
430
/*
431
** COMMAND: test-ancestor-path
432
**
433
** Usage: %fossil test-ancestor-path VERSION1 VERSION2
434
**
435
** Report the path from VERSION1 to VERSION2 through their most recent
436
** common ancestor.
437
*/
438
void ancestor_path_test_cmd(void){
439
int iFrom;
440
int iTo;
441
int iPivot;
442
PathNode *p;
443
int n;
444
445
db_find_and_open_repository(0,0);
446
if( g.argc!=4 ) usage("VERSION1 VERSION2");
447
iFrom = name_to_rid(g.argv[2]);
448
iTo = name_to_rid(g.argv[3]);
449
iPivot = path_common_ancestor(iFrom, iTo);
450
for(n=1, p=path.pStart; p; p=p->u.pTo, n++){
451
char *z;
452
z = db_text(0,
453
"SELECT substr(uuid,1,12) || ' ' || datetime(mtime)"
454
" FROM blob, event"
455
" WHERE blob.rid=%d AND event.objid=%d AND event.type='ci'",
456
p->rid, p->rid);
457
fossil_print("%4d: %5d %s", n, p->rid, z);
458
fossil_free(z);
459
if( p->rid==iFrom ) fossil_print(" VERSION1");
460
if( p->rid==iTo ) fossil_print(" VERSION2");
461
if( p->rid==iPivot ) fossil_print(" PIVOT");
462
fossil_print("\n");
463
}
464
}
465
466
467
/*
468
** A record of a file rename operation.
469
*/
470
typedef struct NameChange NameChange;
471
struct NameChange {
472
int origName; /* Original name of file */
473
int curName; /* Current name of the file */
474
int newName; /* Name of file in next version */
475
NameChange *pNext; /* List of all name changes */
476
};
477
478
/*
479
** Compute all file name changes that occur going from check-in iFrom
480
** to check-in iTo.
481
**
482
** The number of name changes is written into *pnChng. For each name
483
** change, two integers are allocated for *piChng. The first is the
484
** filename.fnid for the original name as seen in check-in iFrom and
485
** the second is for new name as it is used in check-in iTo.
486
**
487
** Space to hold *piChng is obtained from fossil_malloc() and should
488
** be released by the caller.
489
**
490
** This routine really has nothing to do with path. It is located
491
** in this path.c module in order to leverage some of the path
492
** infrastructure.
493
*/
494
void find_filename_changes(
495
int iFrom, /* Ancestor check-in */
496
int iTo, /* Recent check-in */
497
int revOK, /* OK to move backwards (child->parent) if true */
498
int *pnChng, /* Number of name changes along the path */
499
int **aiChng, /* Name changes */
500
const char *zDebug /* Generate trace output if no NULL */
501
){
502
PathNode *p; /* For looping over path from iFrom to iTo */
503
NameChange *pAll = 0; /* List of all name changes seen so far */
504
NameChange *pChng; /* For looping through the name change list */
505
int nChng = 0; /* Number of files whose names have changed */
506
int *aChng; /* Two integers per name change */
507
int i; /* Loop counter */
508
Stmt q1; /* Query of name changes */
509
510
*pnChng = 0;
511
*aiChng = 0;
512
if(0==iFrom){
513
fossil_fatal("Invalid 'from' RID: 0");
514
}else if(0==iTo){
515
fossil_fatal("Invalid 'to' RID: 0");
516
}
517
if( iFrom==iTo ) return;
518
path_reset();
519
p = path_shortest(iFrom, iTo, 1, revOK==0, 0, 0);
520
if( p==0 ) return;
521
path_reverse_path();
522
db_prepare(&q1,
523
"SELECT pfnid, fnid FROM mlink"
524
" WHERE mid=:mid AND (pfnid>0 OR fid==0)"
525
" ORDER BY pfnid"
526
);
527
for(p=path.pStart; p; p=p->u.pTo){
528
int fnid, pfnid;
529
if( !p->fromIsParent && (p->u.pTo==0 || p->u.pTo->fromIsParent) ){
530
/* Skip nodes where the parent is not on the path */
531
continue;
532
}
533
db_bind_int(&q1, ":mid", p->rid);
534
if( zDebug ){
535
fossil_print("%s check-in %.16z %z rid %d\n",
536
zDebug,
537
db_text(0, "SELECT uuid FROM blob WHERE rid=%d", p->rid),
538
db_text(0, "SELECT date(mtime) FROM event WHERE objid=%d", p->rid),
539
p->rid
540
);
541
}
542
while( db_step(&q1)==SQLITE_ROW ){
543
fnid = db_column_int(&q1, 1);
544
pfnid = db_column_int(&q1, 0);
545
if( pfnid==0 ){
546
pfnid = fnid;
547
fnid = 0;
548
}
549
if( !p->fromIsParent ){
550
int t = fnid;
551
fnid = pfnid;
552
pfnid = t;
553
}
554
if( zDebug ){
555
fossil_print("%s %d[%z] -> %d[%z]\n",
556
zDebug,
557
pfnid,
558
db_text(0, "SELECT name FROM filename WHERE fnid=%d", pfnid),
559
fnid,
560
db_text(0, "SELECT name FROM filename WHERE fnid=%d", fnid));
561
}
562
for(pChng=pAll; pChng; pChng=pChng->pNext){
563
if( pChng->curName==pfnid ){
564
pChng->newName = fnid;
565
break;
566
}
567
}
568
if( pChng==0 && fnid>0 ){
569
pChng = fossil_malloc( sizeof(*pChng) );
570
pChng->pNext = pAll;
571
pAll = pChng;
572
pChng->origName = pfnid;
573
pChng->curName = pfnid;
574
pChng->newName = fnid;
575
nChng++;
576
}
577
}
578
for(pChng=pAll; pChng; pChng=pChng->pNext){
579
pChng->curName = pChng->newName;
580
}
581
db_reset(&q1);
582
}
583
db_finalize(&q1);
584
if( nChng ){
585
aChng = *aiChng = fossil_malloc( nChng*2*sizeof(int) );
586
for(pChng=pAll, i=0; pChng; pChng=pChng->pNext){
587
if( pChng->newName==0 ) continue;
588
if( pChng->origName==0 ) continue;
589
aChng[i] = pChng->origName;
590
aChng[i+1] = pChng->newName;
591
if( zDebug ){
592
fossil_print("%s summary %d[%z] -> %d[%z]\n",
593
zDebug,
594
aChng[i],
595
db_text(0, "SELECT name FROM filename WHERE fnid=%d", aChng[i]),
596
aChng[i+1],
597
db_text(0, "SELECT name FROM filename WHERE fnid=%d", aChng[i+1]));
598
}
599
i += 2;
600
}
601
*pnChng = i/2;
602
while( pAll ){
603
pChng = pAll;
604
pAll = pAll->pNext;
605
fossil_free(pChng);
606
}
607
}
608
path_reset();
609
}
610
611
/*
612
** COMMAND: test-name-changes
613
**
614
** Usage: %fossil test-name-changes [--debug] VERSION1 VERSION2
615
**
616
** Show all filename changes that occur going from VERSION1 to VERSION2
617
*/
618
void test_name_change(void){
619
int iFrom;
620
int iTo;
621
int *aChng;
622
int nChng;
623
int i;
624
const char *zDebug = 0;
625
int revOK = 0;
626
627
db_find_and_open_repository(0,0);
628
zDebug = find_option("debug",0,0)!=0 ? "debug" : 0;
629
revOK = find_option("bidirectional",0,0)!=0;
630
if( g.argc<4 ) usage("VERSION1 VERSION2");
631
while( g.argc>=4 ){
632
iFrom = name_to_rid(g.argv[2]);
633
iTo = name_to_rid(g.argv[3]);
634
find_filename_changes(iFrom, iTo, revOK, &nChng, &aChng, zDebug);
635
fossil_print("------ Changes for (%d) %s -> (%d) %s\n",
636
iFrom, g.argv[2], iTo, g.argv[3]);
637
for(i=0; i<nChng; i++){
638
char *zFrom, *zTo;
639
640
zFrom = db_text(0, "SELECT name FROM filename WHERE fnid=%d", aChng[i*2]);
641
zTo = db_text(0, "SELECT name FROM filename WHERE fnid=%d", aChng[i*2+1]);
642
fossil_print("[%s] -> [%s]\n", zFrom, zTo);
643
fossil_free(zFrom);
644
fossil_free(zTo);
645
}
646
fossil_free(aChng);
647
g.argv += 2;
648
g.argc -= 2;
649
}
650
}
651
652
/* Query to extract all rename operations */
653
static const char zRenameQuery[] =
654
@ CREATE TEMP TABLE renames AS
655
@ SELECT
656
@ datetime(event.mtime) AS date,
657
@ F.name AS old_name,
658
@ T.name AS new_name,
659
@ blob.uuid AS checkin
660
@ FROM mlink, filename F, filename T, event, blob
661
@ WHERE coalesce(mlink.pfnid,0)!=0 AND mlink.pfnid!=mlink.fnid
662
@ AND F.fnid=mlink.pfnid
663
@ AND T.fnid=mlink.fnid
664
@ AND event.objid=mlink.mid
665
@ AND event.type='ci'
666
@ AND blob.rid=mlink.mid;
667
;
668
669
/* Query to extract distinct rename operations */
670
static const char zDistinctRenameQuery[] =
671
@ CREATE TEMP TABLE renames AS
672
@ SELECT
673
@ min(datetime(event.mtime)) AS date,
674
@ F.name AS old_name,
675
@ T.name AS new_name,
676
@ blob.uuid AS checkin
677
@ FROM mlink, filename F, filename T, event, blob
678
@ WHERE coalesce(mlink.pfnid,0)!=0 AND mlink.pfnid!=mlink.fnid
679
@ AND F.fnid=mlink.pfnid
680
@ AND T.fnid=mlink.fnid
681
@ AND event.objid=mlink.mid
682
@ AND event.type='ci'
683
@ AND blob.rid=mlink.mid
684
@ GROUP BY 2, 3;
685
;
686
687
/*
688
** WEBPAGE: test-rename-list
689
**
690
** Print a list of all file rename operations throughout history.
691
** This page is intended for testing purposes only and may change
692
** or be discontinued without notice.
693
*/
694
void test_rename_list_page(void){
695
Stmt q;
696
int nRename;
697
int nCheckin;
698
699
login_check_credentials();
700
if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
701
style_set_current_feature("test");
702
if( P("all")!=0 ){
703
style_header("List Of All Filename Changes");
704
db_multi_exec("%s", zRenameQuery/*safe-for-%s*/);
705
style_submenu_element("Distinct", "%R/test-rename-list");
706
}else{
707
style_header("List Of Distinct Filename Changes");
708
db_multi_exec("%s", zDistinctRenameQuery/*safe-for-%s*/);
709
style_submenu_element("All", "%R/test-rename-list?all");
710
}
711
nRename = db_int(0, "SELECT count(*) FROM renames;");
712
nCheckin = db_int(0, "SELECT count(DISTINCT checkin) FROM renames;");
713
db_prepare(&q, "SELECT date, old_name, new_name, checkin FROM renames"
714
" ORDER BY date DESC, old_name ASC");
715
@ <h1>%d(nRename) filename changes in %d(nCheckin) check-ins</h1>
716
@ <table class='sortable' data-column-types='tttt' data-init-sort='1'\
717
@ border="1" cellpadding="2" cellspacing="0">
718
@ <thead><tr><th>Date &amp; Time</th>
719
@ <th>Old Name</th>
720
@ <th>New Name</th>
721
@ <th>Check-in</th></tr></thead><tbody>
722
while( db_step(&q)==SQLITE_ROW ){
723
const char *zDate = db_column_text(&q, 0);
724
const char *zOld = db_column_text(&q, 1);
725
const char *zNew = db_column_text(&q, 2);
726
const char *zUuid = db_column_text(&q, 3);
727
@ <tr>
728
@ <td>%z(href("%R/timeline?c=%t",zDate))%s(zDate)</a></td>
729
@ <td>%z(href("%R/finfo?name=%t",zOld))%h(zOld)</a></td>
730
@ <td>%z(href("%R/finfo?name=%t",zNew))%h(zNew)</a></td>
731
@ <td>%z(href("%R/info/%!S",zUuid))%S(zUuid)</a></td></tr>
732
}
733
@ </tbody></table>
734
db_finalize(&q);
735
style_table_sorter();
736
style_finish_page();
737
}
738

Keyboard Shortcuts

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