Fossil SCM

Merged in trunk.

tinus 2019-01-04 21:29 vor0nwe-wiki-wysiwyg merge
Commit eb942bab480d0038d154509f5c7fe7352386ce4e6ed5609b2b963cf28dfe66e9
--- skins/default/header.txt
+++ skins/default/header.txt
@@ -17,11 +17,11 @@
1717
html "<a href='$home$url' class='active $cls'>$name</a>\n"
1818
} else {
1919
html "<a href='$home$url' class='$cls'>$name</a>\n"
2020
}
2121
}
22
-html "<a href='#'>&#9776;</a>"
22
+html "<a id='hbbtn' href='#'>&#9776;</a>"
2323
menulink $index_page Home {}
2424
if {[anycap jor]} {
2525
menulink /timeline Timeline {}
2626
}
2727
if {[hascap oh]} {
2828
--- skins/default/header.txt
+++ skins/default/header.txt
@@ -17,11 +17,11 @@
17 html "<a href='$home$url' class='active $cls'>$name</a>\n"
18 } else {
19 html "<a href='$home$url' class='$cls'>$name</a>\n"
20 }
21 }
22 html "<a href='#'>&#9776;</a>"
23 menulink $index_page Home {}
24 if {[anycap jor]} {
25 menulink /timeline Timeline {}
26 }
27 if {[hascap oh]} {
28
--- skins/default/header.txt
+++ skins/default/header.txt
@@ -17,11 +17,11 @@
17 html "<a href='$home$url' class='active $cls'>$name</a>\n"
18 } else {
19 html "<a href='$home$url' class='$cls'>$name</a>\n"
20 }
21 }
22 html "<a id='hbbtn' href='#'>&#9776;</a>"
23 menulink $index_page Home {}
24 if {[anycap jor]} {
25 menulink /timeline Timeline {}
26 }
27 if {[hascap oh]} {
28
--- skins/default/js.txt
+++ skins/default/js.txt
@@ -16,27 +16,53 @@
1616
** This file contains the JS code specific to the Fossil default skin.
1717
** Currently, the only thing this does is handle clicks on its hamburger
1818
** menu button.
1919
*/
2020
(function() {
21
+ var hbButton = document.getElementById("hbbtn");
22
+ if (!hbButton) return; // no hamburger button
23
+ if (!document.addEventListener) {
24
+ // Turn the button into a link to the sitemap for incompatible browsers.
25
+ hbButton.href = "$home/sitemap";
26
+ return;
27
+ }
2128
var panel = document.getElementById("hbdrop");
2229
if (!panel) return; // site admin might've nuked it
2330
if (!panel.style) return; // shouldn't happen, but be sure
2431
var panelBorder = panel.style.border;
32
+ var panelInitialized = false; // reset if browser window is resized
33
+ var panelResetBorderTimerID = 0; // used to cancel post-animation tasks
2534
2635
// Disable animation if this browser doesn't support CSS transitions.
2736
//
2837
// We need this ugly calling form for old browsers that don't allow
2938
// panel.style.hasOwnProperty('transition'); catering to old browsers
3039
// is the whole point here.
3140
var animate = panel.style.transition !== null && (typeof(panel.style.transition) == "string");
32
- var animMS = 400;
41
+
42
+ // The duration of the animation can be overridden from the default skin
43
+ // header.txt by setting the "data-anim-ms" attribute of the panel.
44
+ var animMS = panel.getAttribute("data-anim-ms");
45
+ if (animMS) { // not null or empty string, parse it
46
+ animMS = parseInt(animMS);
47
+ if (isNaN(animMS) || animMS == 0)
48
+ animate = false; // disable animation if non-numeric or zero
49
+ else if (animMS < 0)
50
+ animMS = 400; // set default animation duration if negative
51
+ }
52
+ else // attribute is null or empty string, use default
53
+ animMS = 400;
3354
3455
// Calculate panel height despite its being hidden at call time.
3556
// Based on https://stackoverflow.com/a/29047447/142454
36
- var panelHeight; // computed on sitemap load
57
+ var panelHeight; // computed on first panel display
3758
function calculatePanelHeight() {
59
+
60
+ // Clear the max-height CSS property in case the panel size is recalculated
61
+ // after the browser window was resized.
62
+ panel.style.maxHeight = '';
63
+
3864
// Get initial panel styles so we can restore them below.
3965
var es = window.getComputedStyle(panel),
4066
edis = es.display,
4167
epos = es.position,
4268
evis = es.visibility;
@@ -62,20 +88,55 @@
6288
// instead, that would prevent the browser from seeing the height
6389
// change as a state transition, so it'd skip the CSS transition:
6490
//
6591
// https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Transitions/Using_CSS_transitions#JavaScript_examples
6692
function showPanel() {
93
+ // Cancel the timer to remove the panel border after the closing animation,
94
+ // otherwise double-clicking the hamburger button with the panel opened will
95
+ // remove the borders from the (closed and immediately reopened) panel.
96
+ if (panelResetBorderTimerID) {
97
+ clearTimeout(panelResetBorderTimerID);
98
+ panelResetBorderTimerID = 0;
99
+ }
67100
if (animate) {
101
+ if (!panelInitialized) {
102
+ panelInitialized = true;
103
+ // Set up a CSS transition to animate the panel open and
104
+ // closed. Only needs to be done once per page load.
105
+ // Based on https://stackoverflow.com/a/29047447/142454
106
+ calculatePanelHeight();
107
+ panel.style.transition = 'max-height ' + animMS +
108
+ 'ms ease-in-out';
109
+ panel.style.overflowY = 'hidden';
110
+ panel.style.maxHeight = '0';
111
+ }
68112
setTimeout(function() {
69113
panel.style.maxHeight = panelHeight;
70114
panel.style.border = panelBorder;
71115
}, 40); // 25ms is insufficient with Firefox 62
72116
}
73
- else {
74
- panel.style.display = 'block';
75
- }
117
+ panel.style.display = 'block';
118
+ document.addEventListener('keydown',panelKeydown,/* useCapture == */true);
119
+ document.addEventListener('click',panelClick,false);
76120
}
121
+
122
+ var panelKeydown = function(event) {
123
+ var key = event.which || event.keyCode;
124
+ if (key == 27) {
125
+ event.stopPropagation(); // ignore other keydown handlers
126
+ panelToggle(true);
127
+ }
128
+ };
129
+
130
+ var panelClick = function(event) {
131
+ if (!panel.contains(event.target)) {
132
+ // Call event.preventDefault() to have clicks outside the opened panel
133
+ // just close the panel, and swallow clicks on links or form elements.
134
+ //event.preventDefault();
135
+ panelToggle(true);
136
+ }
137
+ };
77138
78139
// Return true if the panel is showing.
79140
function panelShowing() {
80141
if (animate) {
81142
return panel.style.maxHeight == panelHeight;
@@ -82,63 +143,92 @@
82143
}
83144
else {
84145
return panel.style.display == 'block';
85146
}
86147
}
148
+
149
+ // Check if the specified HTML element has any child elements. Note that plain
150
+ // text nodes, comments, and any spaces (presentational or not) are ignored.
151
+ function hasChildren(element) {
152
+ var childElement = element.firstChild;
153
+ while (childElement) {
154
+ if (childElement.nodeType == 1) // Node.ELEMENT_NODE == 1
155
+ return true;
156
+ childElement = childElement.nextSibling;
157
+ }
158
+ return false;
159
+ }
160
+
161
+ // Reset the state of the panel to uninitialized if the browser window is
162
+ // resized, so the dimensions are recalculated the next time it's opened.
163
+ window.addEventListener('resize',function(event) {
164
+ panelInitialized = false;
165
+ },false);
87166
88167
// Click handler for the hamburger button.
89
- var needSitemapHTML = true;
90
- document.querySelector("div.mainmenu > a").onclick = function() {
168
+ hbButton.addEventListener('click',function(event) {
169
+ // Break the event handler chain, or the handler for document → click
170
+ // (about to be installed) may already be triggered by the current event.
171
+ event.stopPropagation();
172
+ event.preventDefault(); // prevent browser from acting on <a> click
173
+ panelToggle(false);
174
+ },false);
175
+
176
+ function panelToggle(suppressAnimation) {
91177
if (panelShowing()) {
178
+ document.removeEventListener('keydown',panelKeydown,/* useCapture == */true);
179
+ document.removeEventListener('click',panelClick,false);
92180
// Transition back to hidden state.
93181
if (animate) {
94
- panel.style.maxHeight = '0';
95
- setTimeout(function() {
96
- // Browsers show a 1px high border line when maxHeight == 0,
97
- // our "hidden" state, so hide the borders in that state, too.
182
+ if (suppressAnimation) {
183
+ var transition = panel.style.transition;
184
+ panel.style.transition = '';
185
+ panel.style.maxHeight = '0';
98186
panel.style.border = 'none';
99
- }, animMS);
187
+ setTimeout(function() {
188
+ // Make sure CSS transition won't take effect now, so restore it
189
+ // asynchronously. Outer variable 'transition' still valid here.
190
+ panel.style.transition = transition;
191
+ }, 40); // 25ms is insufficient with Firefox 62
192
+ }
193
+ else {
194
+ panel.style.maxHeight = '0';
195
+ panelResetBorderTimerID = setTimeout(function() {
196
+ // Browsers show a 1px high border line when maxHeight == 0,
197
+ // our "hidden" state, so hide the borders in that state, too.
198
+ panel.style.border = 'none';
199
+ panelResetBorderTimerID = 0; // clear ID of completed timer
200
+ }, animMS);
201
+ }
100202
}
101203
else {
102204
panel.style.display = 'none';
103205
}
104206
}
105
- else if (needSitemapHTML) {
106
- // Only get it once per page load: it isn't likely to
107
- // change on us.
108
- var xhr = new XMLHttpRequest();
109
- xhr.onload = function() {
110
- var doc = xhr.responseXML;
111
- if (doc) {
112
- var sm = doc.querySelector("ul#sitemap");
113
- if (sm && xhr.status == 200) {
114
- // Got sitemap. Insert it into the drop-down panel.
115
- needSitemapHTML = false;
116
- panel.innerHTML = sm.outerHTML;
117
-
118
- // Display the panel
119
- if (animate) {
120
- // Set up a CSS transition to animate the panel open and
121
- // closed. Only needs to be done once per page load.
122
- // Based on https://stackoverflow.com/a/29047447/142454
123
- calculatePanelHeight();
124
- panel.style.transition = 'max-height ' + animMS +
125
- 'ms ease-in-out';
126
- panel.style.overflowY = 'hidden';
127
- panel.style.maxHeight = '0';
207
+ else {
208
+ if (!hasChildren(panel)) {
209
+ // Only get the sitemap once per page load: it isn't likely to
210
+ // change on us.
211
+ var xhr = new XMLHttpRequest();
212
+ xhr.onload = function() {
213
+ var doc = xhr.responseXML;
214
+ if (doc) {
215
+ var sm = doc.querySelector("ul#sitemap");
216
+ if (sm && xhr.status == 200) {
217
+ // Got sitemap. Insert it into the drop-down panel.
218
+ panel.innerHTML = sm.outerHTML;
219
+ // Display the panel
128220
showPanel();
129221
}
130
- panel.style.display = 'block';
131
- }
132
- }
133
- // else, can't parse response as HTML or XML
134
- }
135
- xhr.open("GET", "$home/sitemap?popup"); // note the TH1 substitution!
136
- xhr.responseType = "document";
137
- xhr.send();
138
- }
139
- else {
140
- showPanel(); // just show what we built above
141
- }
142
- return false; // prevent browser from acting on <a> click
222
+ }
223
+ // else, can't parse response as HTML or XML
224
+ }
225
+ xhr.open("GET", "$home/sitemap?popup"); // note the TH1 substitution!
226
+ xhr.responseType = "document";
227
+ xhr.send();
228
+ }
229
+ else {
230
+ showPanel(); // just show what we built above
231
+ }
232
+ }
143233
}
144234
})();
145235
--- skins/default/js.txt
+++ skins/default/js.txt
@@ -16,27 +16,53 @@
16 ** This file contains the JS code specific to the Fossil default skin.
17 ** Currently, the only thing this does is handle clicks on its hamburger
18 ** menu button.
19 */
20 (function() {
 
 
 
 
 
 
 
21 var panel = document.getElementById("hbdrop");
22 if (!panel) return; // site admin might've nuked it
23 if (!panel.style) return; // shouldn't happen, but be sure
24 var panelBorder = panel.style.border;
 
 
25
26 // Disable animation if this browser doesn't support CSS transitions.
27 //
28 // We need this ugly calling form for old browsers that don't allow
29 // panel.style.hasOwnProperty('transition'); catering to old browsers
30 // is the whole point here.
31 var animate = panel.style.transition !== null && (typeof(panel.style.transition) == "string");
32 var animMS = 400;
 
 
 
 
 
 
 
 
 
 
 
 
33
34 // Calculate panel height despite its being hidden at call time.
35 // Based on https://stackoverflow.com/a/29047447/142454
36 var panelHeight; // computed on sitemap load
37 function calculatePanelHeight() {
 
 
 
 
 
38 // Get initial panel styles so we can restore them below.
39 var es = window.getComputedStyle(panel),
40 edis = es.display,
41 epos = es.position,
42 evis = es.visibility;
@@ -62,20 +88,55 @@
62 // instead, that would prevent the browser from seeing the height
63 // change as a state transition, so it'd skip the CSS transition:
64 //
65 // https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Transitions/Using_CSS_transitions#JavaScript_examples
66 function showPanel() {
 
 
 
 
 
 
 
67 if (animate) {
 
 
 
 
 
 
 
 
 
 
 
68 setTimeout(function() {
69 panel.style.maxHeight = panelHeight;
70 panel.style.border = panelBorder;
71 }, 40); // 25ms is insufficient with Firefox 62
72 }
73 else {
74 panel.style.display = 'block';
75 }
76 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
78 // Return true if the panel is showing.
79 function panelShowing() {
80 if (animate) {
81 return panel.style.maxHeight == panelHeight;
@@ -82,63 +143,92 @@
82 }
83 else {
84 return panel.style.display == 'block';
85 }
86 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
88 // Click handler for the hamburger button.
89 var needSitemapHTML = true;
90 document.querySelector("div.mainmenu > a").onclick = function() {
 
 
 
 
 
 
 
91 if (panelShowing()) {
 
 
92 // Transition back to hidden state.
93 if (animate) {
94 panel.style.maxHeight = '0';
95 setTimeout(function() {
96 // Browsers show a 1px high border line when maxHeight == 0,
97 // our "hidden" state, so hide the borders in that state, too.
98 panel.style.border = 'none';
99 }, animMS);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100 }
101 else {
102 panel.style.display = 'none';
103 }
104 }
105 else if (needSitemapHTML) {
106 // Only get it once per page load: it isn't likely to
107 // change on us.
108 var xhr = new XMLHttpRequest();
109 xhr.onload = function() {
110 var doc = xhr.responseXML;
111 if (doc) {
112 var sm = doc.querySelector("ul#sitemap");
113 if (sm && xhr.status == 200) {
114 // Got sitemap. Insert it into the drop-down panel.
115 needSitemapHTML = false;
116 panel.innerHTML = sm.outerHTML;
117
118 // Display the panel
119 if (animate) {
120 // Set up a CSS transition to animate the panel open and
121 // closed. Only needs to be done once per page load.
122 // Based on https://stackoverflow.com/a/29047447/142454
123 calculatePanelHeight();
124 panel.style.transition = 'max-height ' + animMS +
125 'ms ease-in-out';
126 panel.style.overflowY = 'hidden';
127 panel.style.maxHeight = '0';
128 showPanel();
129 }
130 panel.style.display = 'block';
131 }
132 }
133 // else, can't parse response as HTML or XML
134 }
135 xhr.open("GET", "$home/sitemap?popup"); // note the TH1 substitution!
136 xhr.responseType = "document";
137 xhr.send();
138 }
139 else {
140 showPanel(); // just show what we built above
141 }
142 return false; // prevent browser from acting on <a> click
143 }
144 })();
145
--- skins/default/js.txt
+++ skins/default/js.txt
@@ -16,27 +16,53 @@
16 ** This file contains the JS code specific to the Fossil default skin.
17 ** Currently, the only thing this does is handle clicks on its hamburger
18 ** menu button.
19 */
20 (function() {
21 var hbButton = document.getElementById("hbbtn");
22 if (!hbButton) return; // no hamburger button
23 if (!document.addEventListener) {
24 // Turn the button into a link to the sitemap for incompatible browsers.
25 hbButton.href = "$home/sitemap";
26 return;
27 }
28 var panel = document.getElementById("hbdrop");
29 if (!panel) return; // site admin might've nuked it
30 if (!panel.style) return; // shouldn't happen, but be sure
31 var panelBorder = panel.style.border;
32 var panelInitialized = false; // reset if browser window is resized
33 var panelResetBorderTimerID = 0; // used to cancel post-animation tasks
34
35 // Disable animation if this browser doesn't support CSS transitions.
36 //
37 // We need this ugly calling form for old browsers that don't allow
38 // panel.style.hasOwnProperty('transition'); catering to old browsers
39 // is the whole point here.
40 var animate = panel.style.transition !== null && (typeof(panel.style.transition) == "string");
41
42 // The duration of the animation can be overridden from the default skin
43 // header.txt by setting the "data-anim-ms" attribute of the panel.
44 var animMS = panel.getAttribute("data-anim-ms");
45 if (animMS) { // not null or empty string, parse it
46 animMS = parseInt(animMS);
47 if (isNaN(animMS) || animMS == 0)
48 animate = false; // disable animation if non-numeric or zero
49 else if (animMS < 0)
50 animMS = 400; // set default animation duration if negative
51 }
52 else // attribute is null or empty string, use default
53 animMS = 400;
54
55 // Calculate panel height despite its being hidden at call time.
56 // Based on https://stackoverflow.com/a/29047447/142454
57 var panelHeight; // computed on first panel display
58 function calculatePanelHeight() {
59
60 // Clear the max-height CSS property in case the panel size is recalculated
61 // after the browser window was resized.
62 panel.style.maxHeight = '';
63
64 // Get initial panel styles so we can restore them below.
65 var es = window.getComputedStyle(panel),
66 edis = es.display,
67 epos = es.position,
68 evis = es.visibility;
@@ -62,20 +88,55 @@
88 // instead, that would prevent the browser from seeing the height
89 // change as a state transition, so it'd skip the CSS transition:
90 //
91 // https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Transitions/Using_CSS_transitions#JavaScript_examples
92 function showPanel() {
93 // Cancel the timer to remove the panel border after the closing animation,
94 // otherwise double-clicking the hamburger button with the panel opened will
95 // remove the borders from the (closed and immediately reopened) panel.
96 if (panelResetBorderTimerID) {
97 clearTimeout(panelResetBorderTimerID);
98 panelResetBorderTimerID = 0;
99 }
100 if (animate) {
101 if (!panelInitialized) {
102 panelInitialized = true;
103 // Set up a CSS transition to animate the panel open and
104 // closed. Only needs to be done once per page load.
105 // Based on https://stackoverflow.com/a/29047447/142454
106 calculatePanelHeight();
107 panel.style.transition = 'max-height ' + animMS +
108 'ms ease-in-out';
109 panel.style.overflowY = 'hidden';
110 panel.style.maxHeight = '0';
111 }
112 setTimeout(function() {
113 panel.style.maxHeight = panelHeight;
114 panel.style.border = panelBorder;
115 }, 40); // 25ms is insufficient with Firefox 62
116 }
117 panel.style.display = 'block';
118 document.addEventListener('keydown',panelKeydown,/* useCapture == */true);
119 document.addEventListener('click',panelClick,false);
120 }
121
122 var panelKeydown = function(event) {
123 var key = event.which || event.keyCode;
124 if (key == 27) {
125 event.stopPropagation(); // ignore other keydown handlers
126 panelToggle(true);
127 }
128 };
129
130 var panelClick = function(event) {
131 if (!panel.contains(event.target)) {
132 // Call event.preventDefault() to have clicks outside the opened panel
133 // just close the panel, and swallow clicks on links or form elements.
134 //event.preventDefault();
135 panelToggle(true);
136 }
137 };
138
139 // Return true if the panel is showing.
140 function panelShowing() {
141 if (animate) {
142 return panel.style.maxHeight == panelHeight;
@@ -82,63 +143,92 @@
143 }
144 else {
145 return panel.style.display == 'block';
146 }
147 }
148
149 // Check if the specified HTML element has any child elements. Note that plain
150 // text nodes, comments, and any spaces (presentational or not) are ignored.
151 function hasChildren(element) {
152 var childElement = element.firstChild;
153 while (childElement) {
154 if (childElement.nodeType == 1) // Node.ELEMENT_NODE == 1
155 return true;
156 childElement = childElement.nextSibling;
157 }
158 return false;
159 }
160
161 // Reset the state of the panel to uninitialized if the browser window is
162 // resized, so the dimensions are recalculated the next time it's opened.
163 window.addEventListener('resize',function(event) {
164 panelInitialized = false;
165 },false);
166
167 // Click handler for the hamburger button.
168 hbButton.addEventListener('click',function(event) {
169 // Break the event handler chain, or the handler for document → click
170 // (about to be installed) may already be triggered by the current event.
171 event.stopPropagation();
172 event.preventDefault(); // prevent browser from acting on <a> click
173 panelToggle(false);
174 },false);
175
176 function panelToggle(suppressAnimation) {
177 if (panelShowing()) {
178 document.removeEventListener('keydown',panelKeydown,/* useCapture == */true);
179 document.removeEventListener('click',panelClick,false);
180 // Transition back to hidden state.
181 if (animate) {
182 if (suppressAnimation) {
183 var transition = panel.style.transition;
184 panel.style.transition = '';
185 panel.style.maxHeight = '0';
186 panel.style.border = 'none';
187 setTimeout(function() {
188 // Make sure CSS transition won't take effect now, so restore it
189 // asynchronously. Outer variable 'transition' still valid here.
190 panel.style.transition = transition;
191 }, 40); // 25ms is insufficient with Firefox 62
192 }
193 else {
194 panel.style.maxHeight = '0';
195 panelResetBorderTimerID = setTimeout(function() {
196 // Browsers show a 1px high border line when maxHeight == 0,
197 // our "hidden" state, so hide the borders in that state, too.
198 panel.style.border = 'none';
199 panelResetBorderTimerID = 0; // clear ID of completed timer
200 }, animMS);
201 }
202 }
203 else {
204 panel.style.display = 'none';
205 }
206 }
207 else {
208 if (!hasChildren(panel)) {
209 // Only get the sitemap once per page load: it isn't likely to
210 // change on us.
211 var xhr = new XMLHttpRequest();
212 xhr.onload = function() {
213 var doc = xhr.responseXML;
214 if (doc) {
215 var sm = doc.querySelector("ul#sitemap");
216 if (sm && xhr.status == 200) {
217 // Got sitemap. Insert it into the drop-down panel.
218 panel.innerHTML = sm.outerHTML;
219 // Display the panel
 
 
 
 
 
 
 
 
 
 
220 showPanel();
221 }
222 }
223 // else, can't parse response as HTML or XML
224 }
225 xhr.open("GET", "$home/sitemap?popup"); // note the TH1 substitution!
226 xhr.responseType = "document";
227 xhr.send();
228 }
229 else {
230 showPanel(); // just show what we built above
231 }
232 }
 
 
233 }
234 })();
235
+39 -12
--- src/branch.c
+++ src/branch.c
@@ -20,11 +20,11 @@
2020
#include "config.h"
2121
#include "branch.h"
2222
#include <assert.h>
2323
2424
/*
25
-** fossil branch new NAME BASIS ?OPTIONS?
25
+** fossil branch new NAME BASIS ?OPTIONS?
2626
** argv0 argv1 argv2 argv3 argv4
2727
*/
2828
void branch_new(void){
2929
int rootid; /* RID of the root check-in - what we branch off of */
3030
int brid; /* RID of the branch check-in */
@@ -435,11 +435,11 @@
435435
rNow = db_double(0.0, "SELECT julianday('now')");
436436
@ <div class="brlist">
437437
@ <table class='sortable' data-column-types='tkNtt' data-init-sort='2'>
438438
@ <thead><tr>
439439
@ <th>Branch Name</th>
440
- @ <th>Age</th>
440
+ @ <th>Last Change</th>
441441
@ <th>Check-ins</th>
442442
@ <th>Status</th>
443443
@ <th>Resolution</th>
444444
@ </tr></thead><tbody>
445445
while( db_step(&q)==SQLITE_ROW ){
@@ -463,11 +463,11 @@
463463
if( zBgClr && zBgClr[0] && show_colors ){
464464
@ <tr style="background-color:%s(zBgClr)">
465465
}else{
466466
@ <tr>
467467
}
468
- @ <td>%z(href("%R/timeline?n=100&r=%T",zBranch))%h(zBranch)</a></td>
468
+ @ <td>%z(href("%R/timeline?r=%T",zBranch))%h(zBranch)</a></td>
469469
@ <td data-sortkey="%016llx(iMtime)">%s(zAge)</td>
470470
@ <td>%d(nCkin)</td>
471471
fossil_free(zAge);
472472
@ <td>%s(isClosed?"closed":"")</td>
473473
if( zMergeTo ){
@@ -576,11 +576,11 @@
576576
if( colorTest ){
577577
const char *zColor = hash_color(zBr);
578578
@ <li><span style="background-color: %s(zColor)">
579579
@ %h(zBr) &rarr; %s(zColor)</span></li>
580580
}else{
581
- @ <li>%z(href("%R/timeline?r=%T&n=200",zBr))%h(zBr)</a></li>
581
+ @ <li>%z(href("%R/timeline?r=%T",zBr))%h(zBr)</a></li>
582582
}
583583
}
584584
if( cnt ){
585585
@ </ul>
586586
}
@@ -604,35 +604,62 @@
604604
" AND tag.tagname GLOB 'sym-*'",
605605
rid
606606
);
607607
while( db_step(&q)==SQLITE_ROW ){
608608
const char *zTagName = db_column_text(&q, 0);
609
- @ %z(href("%R/timeline?r=%T&n=200",zTagName))[timeline]</a>
609
+ @ %z(href("%R/timeline?r=%T",zTagName))[timeline]</a>
610610
}
611611
db_finalize(&q);
612612
}
613613
614614
/*
615615
** WEBPAGE: brtimeline
616616
**
617617
** Show a timeline of all branches
618
+**
619
+** Query parameters:
620
+**
621
+** ng No graph
622
+** nohidden Hide check-ins with "hidden" tag
623
+** onlyhidden Show only check-ins with "hidden" tag
624
+** brbg Background color by branch name
625
+** ubg Background color by user name
618626
*/
619627
void brtimeline_page(void){
628
+ Blob sql = empty_blob;
620629
Stmt q;
630
+ int tmFlags; /* Timeline display flags */
631
+ int fNoHidden = PB("nohidden")!=0; /* The "nohidden" query parameter */
632
+ int fOnlyHidden = PB("onlyhidden")!=0; /* The "onlyhidden" query parameter */
621633
622634
login_check_credentials();
623635
if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
624636
625637
style_header("Branches");
626638
style_submenu_element("List", "brlist");
627639
login_anonymous_available();
640
+ timeline_ss_submenu();
641
+ cookie_render();
628642
@ <h2>The initial check-in for each branch:</h2>
629
- db_prepare(&q,
630
- "%s AND blob.rid IN (SELECT rid FROM tagxref"
631
- " WHERE tagtype>0 AND tagid=%d AND srcid!=0)"
632
- " ORDER BY event.mtime DESC",
633
- timeline_query_for_www(), TAG_BRANCH
634
- );
635
- www_print_timeline(&q, 0, 0, 0, 0, brtimeline_extra);
643
+ blob_append(&sql, timeline_query_for_www(), -1);
644
+ blob_append_sql(&sql,
645
+ "AND blob.rid IN (SELECT rid FROM tagxref"
646
+ " WHERE tagtype>0 AND tagid=%d AND srcid!=0)", TAG_BRANCH);
647
+ if( fNoHidden || fOnlyHidden ){
648
+ const char* zUnaryOp = fNoHidden ? "NOT" : "";
649
+ blob_append_sql(&sql,
650
+ " AND %s EXISTS(SELECT 1 FROM tagxref"
651
+ " WHERE tagid=%d AND tagtype>0 AND rid=blob.rid)\n",
652
+ zUnaryOp/*safe-for-%s*/, TAG_HIDDEN);
653
+ }
654
+ db_prepare(&q, "%s ORDER BY event.mtime DESC", blob_sql_text(&sql));
655
+ blob_reset(&sql);
656
+ /* Always specify TIMELINE_DISJOINT, or graph_finish() may fail because of too
657
+ ** many descenders to (off-screen) parents. */
658
+ tmFlags = TIMELINE_DISJOINT | TIMELINE_NOSCROLL;
659
+ if( PB("ng")==0 ) tmFlags |= TIMELINE_GRAPH;
660
+ if( PB("brbg")!=0 ) tmFlags |= TIMELINE_BRCOLOR;
661
+ if( PB("ubg")!=0 ) tmFlags |= TIMELINE_UCOLOR;
662
+ www_print_timeline(&q, tmFlags, 0, 0, 0, brtimeline_extra);
636663
db_finalize(&q);
637664
style_footer();
638665
}
639666
--- src/branch.c
+++ src/branch.c
@@ -20,11 +20,11 @@
20 #include "config.h"
21 #include "branch.h"
22 #include <assert.h>
23
24 /*
25 ** fossil branch new NAME BASIS ?OPTIONS?
26 ** argv0 argv1 argv2 argv3 argv4
27 */
28 void branch_new(void){
29 int rootid; /* RID of the root check-in - what we branch off of */
30 int brid; /* RID of the branch check-in */
@@ -435,11 +435,11 @@
435 rNow = db_double(0.0, "SELECT julianday('now')");
436 @ <div class="brlist">
437 @ <table class='sortable' data-column-types='tkNtt' data-init-sort='2'>
438 @ <thead><tr>
439 @ <th>Branch Name</th>
440 @ <th>Age</th>
441 @ <th>Check-ins</th>
442 @ <th>Status</th>
443 @ <th>Resolution</th>
444 @ </tr></thead><tbody>
445 while( db_step(&q)==SQLITE_ROW ){
@@ -463,11 +463,11 @@
463 if( zBgClr && zBgClr[0] && show_colors ){
464 @ <tr style="background-color:%s(zBgClr)">
465 }else{
466 @ <tr>
467 }
468 @ <td>%z(href("%R/timeline?n=100&r=%T",zBranch))%h(zBranch)</a></td>
469 @ <td data-sortkey="%016llx(iMtime)">%s(zAge)</td>
470 @ <td>%d(nCkin)</td>
471 fossil_free(zAge);
472 @ <td>%s(isClosed?"closed":"")</td>
473 if( zMergeTo ){
@@ -576,11 +576,11 @@
576 if( colorTest ){
577 const char *zColor = hash_color(zBr);
578 @ <li><span style="background-color: %s(zColor)">
579 @ %h(zBr) &rarr; %s(zColor)</span></li>
580 }else{
581 @ <li>%z(href("%R/timeline?r=%T&n=200",zBr))%h(zBr)</a></li>
582 }
583 }
584 if( cnt ){
585 @ </ul>
586 }
@@ -604,35 +604,62 @@
604 " AND tag.tagname GLOB 'sym-*'",
605 rid
606 );
607 while( db_step(&q)==SQLITE_ROW ){
608 const char *zTagName = db_column_text(&q, 0);
609 @ %z(href("%R/timeline?r=%T&n=200",zTagName))[timeline]</a>
610 }
611 db_finalize(&q);
612 }
613
614 /*
615 ** WEBPAGE: brtimeline
616 **
617 ** Show a timeline of all branches
 
 
 
 
 
 
 
 
618 */
619 void brtimeline_page(void){
 
620 Stmt q;
 
 
 
621
622 login_check_credentials();
623 if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
624
625 style_header("Branches");
626 style_submenu_element("List", "brlist");
627 login_anonymous_available();
 
 
628 @ <h2>The initial check-in for each branch:</h2>
629 db_prepare(&q,
630 "%s AND blob.rid IN (SELECT rid FROM tagxref"
631 " WHERE tagtype>0 AND tagid=%d AND srcid!=0)"
632 " ORDER BY event.mtime DESC",
633 timeline_query_for_www(), TAG_BRANCH
634 );
635 www_print_timeline(&q, 0, 0, 0, 0, brtimeline_extra);
 
 
 
 
 
 
 
 
 
 
 
 
 
636 db_finalize(&q);
637 style_footer();
638 }
639
--- src/branch.c
+++ src/branch.c
@@ -20,11 +20,11 @@
20 #include "config.h"
21 #include "branch.h"
22 #include <assert.h>
23
24 /*
25 ** fossil branch new NAME BASIS ?OPTIONS?
26 ** argv0 argv1 argv2 argv3 argv4
27 */
28 void branch_new(void){
29 int rootid; /* RID of the root check-in - what we branch off of */
30 int brid; /* RID of the branch check-in */
@@ -435,11 +435,11 @@
435 rNow = db_double(0.0, "SELECT julianday('now')");
436 @ <div class="brlist">
437 @ <table class='sortable' data-column-types='tkNtt' data-init-sort='2'>
438 @ <thead><tr>
439 @ <th>Branch Name</th>
440 @ <th>Last Change</th>
441 @ <th>Check-ins</th>
442 @ <th>Status</th>
443 @ <th>Resolution</th>
444 @ </tr></thead><tbody>
445 while( db_step(&q)==SQLITE_ROW ){
@@ -463,11 +463,11 @@
463 if( zBgClr && zBgClr[0] && show_colors ){
464 @ <tr style="background-color:%s(zBgClr)">
465 }else{
466 @ <tr>
467 }
468 @ <td>%z(href("%R/timeline?r=%T",zBranch))%h(zBranch)</a></td>
469 @ <td data-sortkey="%016llx(iMtime)">%s(zAge)</td>
470 @ <td>%d(nCkin)</td>
471 fossil_free(zAge);
472 @ <td>%s(isClosed?"closed":"")</td>
473 if( zMergeTo ){
@@ -576,11 +576,11 @@
576 if( colorTest ){
577 const char *zColor = hash_color(zBr);
578 @ <li><span style="background-color: %s(zColor)">
579 @ %h(zBr) &rarr; %s(zColor)</span></li>
580 }else{
581 @ <li>%z(href("%R/timeline?r=%T",zBr))%h(zBr)</a></li>
582 }
583 }
584 if( cnt ){
585 @ </ul>
586 }
@@ -604,35 +604,62 @@
604 " AND tag.tagname GLOB 'sym-*'",
605 rid
606 );
607 while( db_step(&q)==SQLITE_ROW ){
608 const char *zTagName = db_column_text(&q, 0);
609 @ %z(href("%R/timeline?r=%T",zTagName))[timeline]</a>
610 }
611 db_finalize(&q);
612 }
613
614 /*
615 ** WEBPAGE: brtimeline
616 **
617 ** Show a timeline of all branches
618 **
619 ** Query parameters:
620 **
621 ** ng No graph
622 ** nohidden Hide check-ins with "hidden" tag
623 ** onlyhidden Show only check-ins with "hidden" tag
624 ** brbg Background color by branch name
625 ** ubg Background color by user name
626 */
627 void brtimeline_page(void){
628 Blob sql = empty_blob;
629 Stmt q;
630 int tmFlags; /* Timeline display flags */
631 int fNoHidden = PB("nohidden")!=0; /* The "nohidden" query parameter */
632 int fOnlyHidden = PB("onlyhidden")!=0; /* The "onlyhidden" query parameter */
633
634 login_check_credentials();
635 if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
636
637 style_header("Branches");
638 style_submenu_element("List", "brlist");
639 login_anonymous_available();
640 timeline_ss_submenu();
641 cookie_render();
642 @ <h2>The initial check-in for each branch:</h2>
643 blob_append(&sql, timeline_query_for_www(), -1);
644 blob_append_sql(&sql,
645 "AND blob.rid IN (SELECT rid FROM tagxref"
646 " WHERE tagtype>0 AND tagid=%d AND srcid!=0)", TAG_BRANCH);
647 if( fNoHidden || fOnlyHidden ){
648 const char* zUnaryOp = fNoHidden ? "NOT" : "";
649 blob_append_sql(&sql,
650 " AND %s EXISTS(SELECT 1 FROM tagxref"
651 " WHERE tagid=%d AND tagtype>0 AND rid=blob.rid)\n",
652 zUnaryOp/*safe-for-%s*/, TAG_HIDDEN);
653 }
654 db_prepare(&q, "%s ORDER BY event.mtime DESC", blob_sql_text(&sql));
655 blob_reset(&sql);
656 /* Always specify TIMELINE_DISJOINT, or graph_finish() may fail because of too
657 ** many descenders to (off-screen) parents. */
658 tmFlags = TIMELINE_DISJOINT | TIMELINE_NOSCROLL;
659 if( PB("ng")==0 ) tmFlags |= TIMELINE_GRAPH;
660 if( PB("brbg")!=0 ) tmFlags |= TIMELINE_BRCOLOR;
661 if( PB("ubg")!=0 ) tmFlags |= TIMELINE_UCOLOR;
662 www_print_timeline(&q, tmFlags, 0, 0, 0, brtimeline_extra);
663 db_finalize(&q);
664 style_footer();
665 }
666
+2 -2
--- src/clone.c
+++ src/clone.c
@@ -314,11 +314,11 @@
314314
style_header("Download Page");
315315
if( !g.perm.Zip ){
316316
@ <p>Bummer. You do not have permission to download.
317317
if( g.zLogin==0 || g.zLogin[0]==0 ){
318318
@ Maybe it would work better if you
319
- @ <a href="../login">logged in</a>.
319
+ @ %z(href("%R/login"))logged in</a>.
320320
}else{
321321
@ Contact the site administrator and ask them to give
322322
@ you "Download Zip" privileges.
323323
}
324324
}else{
@@ -333,11 +333,11 @@
333333
}
334334
if( !g.perm.Clone ){
335335
@ <p>You are not authorized to clone this repository.
336336
if( g.zLogin==0 || g.zLogin[0]==0 ){
337337
@ Maybe you would be able to clone if you
338
- @ <a href="../login">logged in</a>.
338
+ @ %z(href("%R/login"))logged in</a>.
339339
}else{
340340
@ Contact the site administrator and ask them to give
341341
@ you "Clone" privileges in order to clone.
342342
}
343343
}else{
344344
--- src/clone.c
+++ src/clone.c
@@ -314,11 +314,11 @@
314 style_header("Download Page");
315 if( !g.perm.Zip ){
316 @ <p>Bummer. You do not have permission to download.
317 if( g.zLogin==0 || g.zLogin[0]==0 ){
318 @ Maybe it would work better if you
319 @ <a href="../login">logged in</a>.
320 }else{
321 @ Contact the site administrator and ask them to give
322 @ you "Download Zip" privileges.
323 }
324 }else{
@@ -333,11 +333,11 @@
333 }
334 if( !g.perm.Clone ){
335 @ <p>You are not authorized to clone this repository.
336 if( g.zLogin==0 || g.zLogin[0]==0 ){
337 @ Maybe you would be able to clone if you
338 @ <a href="../login">logged in</a>.
339 }else{
340 @ Contact the site administrator and ask them to give
341 @ you "Clone" privileges in order to clone.
342 }
343 }else{
344
--- src/clone.c
+++ src/clone.c
@@ -314,11 +314,11 @@
314 style_header("Download Page");
315 if( !g.perm.Zip ){
316 @ <p>Bummer. You do not have permission to download.
317 if( g.zLogin==0 || g.zLogin[0]==0 ){
318 @ Maybe it would work better if you
319 @ %z(href("%R/login"))logged in</a>.
320 }else{
321 @ Contact the site administrator and ask them to give
322 @ you "Download Zip" privileges.
323 }
324 }else{
@@ -333,11 +333,11 @@
333 }
334 if( !g.perm.Clone ){
335 @ <p>You are not authorized to clone this repository.
336 if( g.zLogin==0 || g.zLogin[0]==0 ){
337 @ Maybe you would be able to clone if you
338 @ %z(href("%R/login"))logged in</a>.
339 }else{
340 @ Contact the site administrator and ask them to give
341 @ you "Clone" privileges in order to clone.
342 }
343 }else{
344
+1 -1
--- src/configure.c
+++ src/configure.c
@@ -419,11 +419,11 @@
419419
}else{
420420
blob_append_sql(&sql, "INSERT OR IGNORE INTO ");
421421
}
422422
blob_append_sql(&sql, "\"%w\"(\"%w\",mtime",
423423
&zName[1], aType[ii].zPrimKey);
424
- if( fossil_stricmp(zName,"/subscriber") ) alert_schema(0);
424
+ if( fossil_stricmp(zName,"/subscriber")==0 ) alert_schema(0);
425425
for(jj=2; jj<nToken; jj+=2){
426426
blob_append_sql(&sql, ",\"%w\"", azToken[jj]);
427427
}
428428
blob_append_sql(&sql,") VALUES(%s,%s",
429429
azToken[1] /*safe-for-%s*/, azToken[0]/*safe-for-%s*/);
430430
--- src/configure.c
+++ src/configure.c
@@ -419,11 +419,11 @@
419 }else{
420 blob_append_sql(&sql, "INSERT OR IGNORE INTO ");
421 }
422 blob_append_sql(&sql, "\"%w\"(\"%w\",mtime",
423 &zName[1], aType[ii].zPrimKey);
424 if( fossil_stricmp(zName,"/subscriber") ) alert_schema(0);
425 for(jj=2; jj<nToken; jj+=2){
426 blob_append_sql(&sql, ",\"%w\"", azToken[jj]);
427 }
428 blob_append_sql(&sql,") VALUES(%s,%s",
429 azToken[1] /*safe-for-%s*/, azToken[0]/*safe-for-%s*/);
430
--- src/configure.c
+++ src/configure.c
@@ -419,11 +419,11 @@
419 }else{
420 blob_append_sql(&sql, "INSERT OR IGNORE INTO ");
421 }
422 blob_append_sql(&sql, "\"%w\"(\"%w\",mtime",
423 &zName[1], aType[ii].zPrimKey);
424 if( fossil_stricmp(zName,"/subscriber")==0 ) alert_schema(0);
425 for(jj=2; jj<nToken; jj+=2){
426 blob_append_sql(&sql, ",\"%w\"", azToken[jj]);
427 }
428 blob_append_sql(&sql,") VALUES(%s,%s",
429 azToken[1] /*safe-for-%s*/, azToken[0]/*safe-for-%s*/);
430
--- src/default_css.txt
+++ src/default_css.txt
@@ -159,10 +159,32 @@
159159
border-left: 3px solid #000;
160160
}
161161
.tl-line.merge {
162162
width: 1px;
163163
}
164
+.tl-arrow.cherrypick {
165
+ height: 1px;
166
+ border-width: 2px 0;
167
+}
168
+.tl-arrow.cherrypick.l {
169
+ border-right: 3px solid #000;
170
+}
171
+.tl-arrow.cherrypick.r {
172
+ border-left: 3px solid #000;
173
+}
174
+.tl-line.cherrypick.h {
175
+ width: 0px;
176
+ border-top: 1px dashed #000;
177
+ border-left: 0px dashed #000;
178
+ background: #fff;
179
+}
180
+.tl-line.cherrypick.v {
181
+ width: 0px;
182
+ border-top: 0px dashed #000;
183
+ border-left: 1px dashed #000;
184
+ background: #fff;
185
+}
164186
.tl-arrow.warp {
165187
margin-left: 1px;
166188
border-width: 3px 0;
167189
border-left: 7px solid #600000;
168190
}
@@ -197,10 +219,11 @@
197219
div.columns > ul li:first-child {
198220
margin-top:0px;
199221
}
200222
.columns li {
201223
break-inside: avoid;
224
+ page-break-inside: avoid;
202225
}
203226
.filetree {
204227
margin: 1em 0;
205228
line-height: 1.5;
206229
}
207230
--- src/default_css.txt
+++ src/default_css.txt
@@ -159,10 +159,32 @@
159 border-left: 3px solid #000;
160 }
161 .tl-line.merge {
162 width: 1px;
163 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164 .tl-arrow.warp {
165 margin-left: 1px;
166 border-width: 3px 0;
167 border-left: 7px solid #600000;
168 }
@@ -197,10 +219,11 @@
197 div.columns > ul li:first-child {
198 margin-top:0px;
199 }
200 .columns li {
201 break-inside: avoid;
 
202 }
203 .filetree {
204 margin: 1em 0;
205 line-height: 1.5;
206 }
207
--- src/default_css.txt
+++ src/default_css.txt
@@ -159,10 +159,32 @@
159 border-left: 3px solid #000;
160 }
161 .tl-line.merge {
162 width: 1px;
163 }
164 .tl-arrow.cherrypick {
165 height: 1px;
166 border-width: 2px 0;
167 }
168 .tl-arrow.cherrypick.l {
169 border-right: 3px solid #000;
170 }
171 .tl-arrow.cherrypick.r {
172 border-left: 3px solid #000;
173 }
174 .tl-line.cherrypick.h {
175 width: 0px;
176 border-top: 1px dashed #000;
177 border-left: 0px dashed #000;
178 background: #fff;
179 }
180 .tl-line.cherrypick.v {
181 width: 0px;
182 border-top: 0px dashed #000;
183 border-left: 1px dashed #000;
184 background: #fff;
185 }
186 .tl-arrow.warp {
187 margin-left: 1px;
188 border-width: 3px 0;
189 border-left: 7px solid #600000;
190 }
@@ -197,10 +219,11 @@
219 div.columns > ul li:first-child {
220 margin-top:0px;
221 }
222 .columns li {
223 break-inside: avoid;
224 page-break-inside: avoid;
225 }
226 .filetree {
227 margin: 1em 0;
228 line-height: 1.5;
229 }
230
--- src/descendants.c
+++ src/descendants.c
@@ -454,31 +454,51 @@
454454
**
455455
** Query parameters:
456456
**
457457
** all Show all leaves
458458
** closed Show only closed leaves
459
+** ng No graph
460
+** nohidden Hide check-ins with "hidden" tag
461
+** onlyhidden Show only check-ins with "hidden" tag
462
+** brbg Background color by branch name
463
+** ubg Background color by user name
459464
*/
460465
void leaves_page(void){
461466
Blob sql;
462467
Stmt q;
463468
int showAll = P("all")!=0;
464469
int showClosed = P("closed")!=0;
470
+ int fNg = PB("ng")!=0; /* Flag for the "ng" query parameter */
471
+ int fNoHidden = PB("nohidden")!=0; /* "nohidden" query parameter */
472
+ int fOnlyHidden = PB("onlyhidden")!=0; /* "onlyhidden" query parameter */
473
+ int fBrBg = PB("brbg")!=0; /* Flag for the "brbg" query parameter */
474
+ int fUBg = PB("ubg")!=0; /* Flag for the "ubg" query parameter */
475
+ HQuery url; /* URL to /leaves plus query parameters */
476
+ int tmFlags; /* Timeline display flags */
465477
466478
login_check_credentials();
467479
if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
468
-
480
+ url_initialize(&url, "leaves");
481
+ if( fNg ) url_add_parameter(&url, "ng", "");
482
+ if( fNoHidden ) url_add_parameter(&url, "nohidden", "");
483
+ if( fOnlyHidden ) url_add_parameter(&url, "onlyhidden", "");
484
+ if( fBrBg ) url_add_parameter(&url, "brbg", "");
485
+ if( fUBg ) url_add_parameter(&url, "ubg", "");
469486
if( !showAll ){
470
- style_submenu_element("All", "leaves?all");
487
+ style_submenu_element("All", "%s", url_render(&url, "all", "", 0, 0));
471488
}
472489
if( !showClosed ){
473
- style_submenu_element("Closed", "leaves?closed");
490
+ style_submenu_element("Closed", "%s", url_render(&url, "closed", "", 0, 0));
474491
}
475492
if( showClosed || showAll ){
476
- style_submenu_element("Open", "leaves");
493
+ style_submenu_element("Open", "%s", url_render(&url, 0, 0, 0, 0));
477494
}
495
+ url_reset(&url);
478496
style_header("Leaves");
479497
login_anonymous_available();
498
+ timeline_ss_submenu();
499
+ cookie_render();
480500
#if 0
481501
style_sidebox_begin("Nomenclature:", "33%");
482502
@ <ol>
483503
@ <li> A <div class="sideboxDescribed">leaf</div>
484504
@ is a check-in with no descendants in the same branch.</li>
@@ -505,13 +525,26 @@
505525
if( showClosed ){
506526
blob_append_sql(&sql," AND %z", leaf_is_closed_sql("blob.rid"));
507527
}else if( !showAll ){
508528
blob_append_sql(&sql," AND NOT %z", leaf_is_closed_sql("blob.rid"));
509529
}
530
+ if( fNoHidden || fOnlyHidden ){
531
+ const char* zUnaryOp = fNoHidden ? "NOT" : "";
532
+ blob_append_sql(&sql,
533
+ " AND %s EXISTS(SELECT 1 FROM tagxref"
534
+ " WHERE tagid=%d AND tagtype>0 AND rid=blob.rid)\n",
535
+ zUnaryOp/*safe-for-%s*/, TAG_HIDDEN);
536
+ }
510537
db_prepare(&q, "%s ORDER BY event.mtime DESC", blob_sql_text(&sql));
511538
blob_reset(&sql);
512
- www_print_timeline(&q, TIMELINE_LEAFONLY, 0, 0, 0, 0);
539
+ /* Always specify TIMELINE_DISJOINT, or graph_finish() may fail because of too
540
+ ** many descenders to (off-screen) parents. */
541
+ tmFlags = TIMELINE_LEAFONLY | TIMELINE_DISJOINT | TIMELINE_NOSCROLL;
542
+ if( fNg==0 ) tmFlags |= TIMELINE_GRAPH;
543
+ if( fBrBg ) tmFlags |= TIMELINE_BRCOLOR;
544
+ if( fUBg ) tmFlags |= TIMELINE_UCOLOR;
545
+ www_print_timeline(&q, tmFlags, 0, 0, 0, 0);
513546
db_finalize(&q);
514547
@ <br />
515548
style_footer();
516549
}
517550
518551
--- src/descendants.c
+++ src/descendants.c
@@ -454,31 +454,51 @@
454 **
455 ** Query parameters:
456 **
457 ** all Show all leaves
458 ** closed Show only closed leaves
 
 
 
 
 
459 */
460 void leaves_page(void){
461 Blob sql;
462 Stmt q;
463 int showAll = P("all")!=0;
464 int showClosed = P("closed")!=0;
 
 
 
 
 
 
 
465
466 login_check_credentials();
467 if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
468
 
 
 
 
 
469 if( !showAll ){
470 style_submenu_element("All", "leaves?all");
471 }
472 if( !showClosed ){
473 style_submenu_element("Closed", "leaves?closed");
474 }
475 if( showClosed || showAll ){
476 style_submenu_element("Open", "leaves");
477 }
 
478 style_header("Leaves");
479 login_anonymous_available();
 
 
480 #if 0
481 style_sidebox_begin("Nomenclature:", "33%");
482 @ <ol>
483 @ <li> A <div class="sideboxDescribed">leaf</div>
484 @ is a check-in with no descendants in the same branch.</li>
@@ -505,13 +525,26 @@
505 if( showClosed ){
506 blob_append_sql(&sql," AND %z", leaf_is_closed_sql("blob.rid"));
507 }else if( !showAll ){
508 blob_append_sql(&sql," AND NOT %z", leaf_is_closed_sql("blob.rid"));
509 }
 
 
 
 
 
 
 
510 db_prepare(&q, "%s ORDER BY event.mtime DESC", blob_sql_text(&sql));
511 blob_reset(&sql);
512 www_print_timeline(&q, TIMELINE_LEAFONLY, 0, 0, 0, 0);
 
 
 
 
 
 
513 db_finalize(&q);
514 @ <br />
515 style_footer();
516 }
517
518
--- src/descendants.c
+++ src/descendants.c
@@ -454,31 +454,51 @@
454 **
455 ** Query parameters:
456 **
457 ** all Show all leaves
458 ** closed Show only closed leaves
459 ** ng No graph
460 ** nohidden Hide check-ins with "hidden" tag
461 ** onlyhidden Show only check-ins with "hidden" tag
462 ** brbg Background color by branch name
463 ** ubg Background color by user name
464 */
465 void leaves_page(void){
466 Blob sql;
467 Stmt q;
468 int showAll = P("all")!=0;
469 int showClosed = P("closed")!=0;
470 int fNg = PB("ng")!=0; /* Flag for the "ng" query parameter */
471 int fNoHidden = PB("nohidden")!=0; /* "nohidden" query parameter */
472 int fOnlyHidden = PB("onlyhidden")!=0; /* "onlyhidden" query parameter */
473 int fBrBg = PB("brbg")!=0; /* Flag for the "brbg" query parameter */
474 int fUBg = PB("ubg")!=0; /* Flag for the "ubg" query parameter */
475 HQuery url; /* URL to /leaves plus query parameters */
476 int tmFlags; /* Timeline display flags */
477
478 login_check_credentials();
479 if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
480 url_initialize(&url, "leaves");
481 if( fNg ) url_add_parameter(&url, "ng", "");
482 if( fNoHidden ) url_add_parameter(&url, "nohidden", "");
483 if( fOnlyHidden ) url_add_parameter(&url, "onlyhidden", "");
484 if( fBrBg ) url_add_parameter(&url, "brbg", "");
485 if( fUBg ) url_add_parameter(&url, "ubg", "");
486 if( !showAll ){
487 style_submenu_element("All", "%s", url_render(&url, "all", "", 0, 0));
488 }
489 if( !showClosed ){
490 style_submenu_element("Closed", "%s", url_render(&url, "closed", "", 0, 0));
491 }
492 if( showClosed || showAll ){
493 style_submenu_element("Open", "%s", url_render(&url, 0, 0, 0, 0));
494 }
495 url_reset(&url);
496 style_header("Leaves");
497 login_anonymous_available();
498 timeline_ss_submenu();
499 cookie_render();
500 #if 0
501 style_sidebox_begin("Nomenclature:", "33%");
502 @ <ol>
503 @ <li> A <div class="sideboxDescribed">leaf</div>
504 @ is a check-in with no descendants in the same branch.</li>
@@ -505,13 +525,26 @@
525 if( showClosed ){
526 blob_append_sql(&sql," AND %z", leaf_is_closed_sql("blob.rid"));
527 }else if( !showAll ){
528 blob_append_sql(&sql," AND NOT %z", leaf_is_closed_sql("blob.rid"));
529 }
530 if( fNoHidden || fOnlyHidden ){
531 const char* zUnaryOp = fNoHidden ? "NOT" : "";
532 blob_append_sql(&sql,
533 " AND %s EXISTS(SELECT 1 FROM tagxref"
534 " WHERE tagid=%d AND tagtype>0 AND rid=blob.rid)\n",
535 zUnaryOp/*safe-for-%s*/, TAG_HIDDEN);
536 }
537 db_prepare(&q, "%s ORDER BY event.mtime DESC", blob_sql_text(&sql));
538 blob_reset(&sql);
539 /* Always specify TIMELINE_DISJOINT, or graph_finish() may fail because of too
540 ** many descenders to (off-screen) parents. */
541 tmFlags = TIMELINE_LEAFONLY | TIMELINE_DISJOINT | TIMELINE_NOSCROLL;
542 if( fNg==0 ) tmFlags |= TIMELINE_GRAPH;
543 if( fBrBg ) tmFlags |= TIMELINE_BRCOLOR;
544 if( fUBg ) tmFlags |= TIMELINE_UCOLOR;
545 www_print_timeline(&q, tmFlags, 0, 0, 0, 0);
546 db_finalize(&q);
547 @ <br />
548 style_footer();
549 }
550
551
+2 -1
--- src/dispatch.c
+++ src/dispatch.c
@@ -265,11 +265,12 @@
265265
266266
if( find_option("www","w",0) ){
267267
mask = CMDFLAG_WEBPAGE;
268268
}
269269
if( find_option("everything","e",0) ){
270
- mask = CMDFLAG_1ST_TIER | CMDFLAG_2ND_TIER | CMDFLAG_WEBPAGE;
270
+ mask = CMDFLAG_1ST_TIER | CMDFLAG_2ND_TIER | CMDFLAG_WEBPAGE |
271
+ CMDFLAG_SETTING | CMDFLAG_TEST;
271272
}
272273
if( find_option("settings","s",0) ){
273274
mask = CMDFLAG_SETTING;
274275
}
275276
if( find_option("test","t",0) ){
276277
--- src/dispatch.c
+++ src/dispatch.c
@@ -265,11 +265,12 @@
265
266 if( find_option("www","w",0) ){
267 mask = CMDFLAG_WEBPAGE;
268 }
269 if( find_option("everything","e",0) ){
270 mask = CMDFLAG_1ST_TIER | CMDFLAG_2ND_TIER | CMDFLAG_WEBPAGE;
 
271 }
272 if( find_option("settings","s",0) ){
273 mask = CMDFLAG_SETTING;
274 }
275 if( find_option("test","t",0) ){
276
--- src/dispatch.c
+++ src/dispatch.c
@@ -265,11 +265,12 @@
265
266 if( find_option("www","w",0) ){
267 mask = CMDFLAG_WEBPAGE;
268 }
269 if( find_option("everything","e",0) ){
270 mask = CMDFLAG_1ST_TIER | CMDFLAG_2ND_TIER | CMDFLAG_WEBPAGE |
271 CMDFLAG_SETTING | CMDFLAG_TEST;
272 }
273 if( find_option("settings","s",0) ){
274 mask = CMDFLAG_SETTING;
275 }
276 if( find_option("test","t",0) ){
277
+1 -1
--- src/doc.c
+++ src/doc.c
@@ -510,11 +510,11 @@
510510
** action="$ROOT/
511511
**
512512
** Convert $ROOT to the root URI of the repository. Allow ' in place of "
513513
** and any case for href or action.
514514
*/
515
-static void convert_href_and_output(Blob *pIn){
515
+void convert_href_and_output(Blob *pIn){
516516
int i, base;
517517
int n = blob_size(pIn);
518518
char *z = blob_buffer(pIn);
519519
for(base=0, i=7; i<n; i++){
520520
if( z[i]=='$'
521521
--- src/doc.c
+++ src/doc.c
@@ -510,11 +510,11 @@
510 ** action="$ROOT/
511 **
512 ** Convert $ROOT to the root URI of the repository. Allow ' in place of "
513 ** and any case for href or action.
514 */
515 static void convert_href_and_output(Blob *pIn){
516 int i, base;
517 int n = blob_size(pIn);
518 char *z = blob_buffer(pIn);
519 for(base=0, i=7; i<n; i++){
520 if( z[i]=='$'
521
--- src/doc.c
+++ src/doc.c
@@ -510,11 +510,11 @@
510 ** action="$ROOT/
511 **
512 ** Convert $ROOT to the root URI of the repository. Allow ' in place of "
513 ** and any case for href or action.
514 */
515 void convert_href_and_output(Blob *pIn){
516 int i, base;
517 int n = blob_size(pIn);
518 char *z = blob_buffer(pIn);
519 for(base=0, i=7; i<n; i++){
520 if( z[i]=='$'
521
+4 -2
--- src/finfo.c
+++ src/finfo.c
@@ -330,10 +330,12 @@
330330
zStyle = "Columnar";
331331
}else if( tmFlags & TIMELINE_COMPACT ){
332332
zStyle = "Compact";
333333
}else if( tmFlags & TIMELINE_VERBOSE ){
334334
zStyle = "Verbose";
335
+ }else if( tmFlags & TIMELINE_CLASSIC ){
336
+ zStyle = "Classic";
335337
}else{
336338
zStyle = "Modern";
337339
}
338340
url_initialize(&url, "finfo");
339341
if( brBg ) url_add_parameter(&url, "brbg", 0);
@@ -503,11 +505,11 @@
503505
zBgClr = hash_color(zUser);
504506
}else if( brBg || zBgClr==0 || zBgClr[0]==0 ){
505507
zBgClr = strcmp(zBr,"trunk")==0 ? "" : hash_color(zBr);
506508
}
507509
gidx = graph_add_row(pGraph, frid>0 ? frid : fpid+1000000000,
508
- nParent, aParent, zBr, zBgClr,
510
+ nParent, 0, aParent, zBr, zBgClr,
509511
zUuid, 0);
510512
if( strncmp(zDate, zPrevDate, 10) ){
511513
sqlite3_snprintf(sizeof(zPrevDate), zPrevDate, "%.10s", zDate);
512514
@ <tr><td>
513515
@ <div class="divider timelineDate">%s(zPrevDate)</div>
@@ -573,11 +575,11 @@
573575
if( fShowId ){
574576
@ (%d(fmid))
575577
}
576578
@ user:&nbsp;\
577579
hyperlink_to_user(zUser, zDate, ",");
578
- @ branch:&nbsp;%z(href("%R/timeline?t=%T&n=200",zBr))%h(zBr)</a>,
580
+ @ branch:&nbsp;%z(href("%R/timeline?t=%T",zBr))%h(zBr)</a>,
579581
if( tmFlags & (TIMELINE_COMPACT|TIMELINE_VERBOSE) ){
580582
@ size:&nbsp;%d(szFile))
581583
}else{
582584
@ size:&nbsp;%d(szFile)
583585
}
584586
--- src/finfo.c
+++ src/finfo.c
@@ -330,10 +330,12 @@
330 zStyle = "Columnar";
331 }else if( tmFlags & TIMELINE_COMPACT ){
332 zStyle = "Compact";
333 }else if( tmFlags & TIMELINE_VERBOSE ){
334 zStyle = "Verbose";
 
 
335 }else{
336 zStyle = "Modern";
337 }
338 url_initialize(&url, "finfo");
339 if( brBg ) url_add_parameter(&url, "brbg", 0);
@@ -503,11 +505,11 @@
503 zBgClr = hash_color(zUser);
504 }else if( brBg || zBgClr==0 || zBgClr[0]==0 ){
505 zBgClr = strcmp(zBr,"trunk")==0 ? "" : hash_color(zBr);
506 }
507 gidx = graph_add_row(pGraph, frid>0 ? frid : fpid+1000000000,
508 nParent, aParent, zBr, zBgClr,
509 zUuid, 0);
510 if( strncmp(zDate, zPrevDate, 10) ){
511 sqlite3_snprintf(sizeof(zPrevDate), zPrevDate, "%.10s", zDate);
512 @ <tr><td>
513 @ <div class="divider timelineDate">%s(zPrevDate)</div>
@@ -573,11 +575,11 @@
573 if( fShowId ){
574 @ (%d(fmid))
575 }
576 @ user:&nbsp;\
577 hyperlink_to_user(zUser, zDate, ",");
578 @ branch:&nbsp;%z(href("%R/timeline?t=%T&n=200",zBr))%h(zBr)</a>,
579 if( tmFlags & (TIMELINE_COMPACT|TIMELINE_VERBOSE) ){
580 @ size:&nbsp;%d(szFile))
581 }else{
582 @ size:&nbsp;%d(szFile)
583 }
584
--- src/finfo.c
+++ src/finfo.c
@@ -330,10 +330,12 @@
330 zStyle = "Columnar";
331 }else if( tmFlags & TIMELINE_COMPACT ){
332 zStyle = "Compact";
333 }else if( tmFlags & TIMELINE_VERBOSE ){
334 zStyle = "Verbose";
335 }else if( tmFlags & TIMELINE_CLASSIC ){
336 zStyle = "Classic";
337 }else{
338 zStyle = "Modern";
339 }
340 url_initialize(&url, "finfo");
341 if( brBg ) url_add_parameter(&url, "brbg", 0);
@@ -503,11 +505,11 @@
505 zBgClr = hash_color(zUser);
506 }else if( brBg || zBgClr==0 || zBgClr[0]==0 ){
507 zBgClr = strcmp(zBr,"trunk")==0 ? "" : hash_color(zBr);
508 }
509 gidx = graph_add_row(pGraph, frid>0 ? frid : fpid+1000000000,
510 nParent, 0, aParent, zBr, zBgClr,
511 zUuid, 0);
512 if( strncmp(zDate, zPrevDate, 10) ){
513 sqlite3_snprintf(sizeof(zPrevDate), zPrevDate, "%.10s", zDate);
514 @ <tr><td>
515 @ <div class="divider timelineDate">%s(zPrevDate)</div>
@@ -573,11 +575,11 @@
575 if( fShowId ){
576 @ (%d(fmid))
577 }
578 @ user:&nbsp;\
579 hyperlink_to_user(zUser, zDate, ",");
580 @ branch:&nbsp;%z(href("%R/timeline?t=%T",zBr))%h(zBr)</a>,
581 if( tmFlags & (TIMELINE_COMPACT|TIMELINE_VERBOSE) ){
582 @ size:&nbsp;%d(szFile))
583 }else{
584 @ size:&nbsp;%d(szFile)
585 }
586
+47 -13
--- src/graph.c
+++ src/graph.c
@@ -34,10 +34,12 @@
3434
** but which are included just so that we can capture their background color.
3535
*/
3636
struct GraphRow {
3737
int rid; /* The rid for the check-in */
3838
i8 nParent; /* Number of parents. -1 for technote lines */
39
+ i8 nCherrypick; /* Subset of aParent that are cherrypicks */
40
+ i8 nNonCherrypick; /* Number of non-cherrypick parents */
3941
int *aParent; /* Array of parents. 0 element is primary .*/
4042
char *zBranch; /* Branch name */
4143
char *zBgClr; /* Background Color */
4244
char zUuid[HNAME_MAX+1]; /* Check-in for file ID */
4345
@@ -47,18 +49,21 @@
4749
int idx; /* Row index. First is 1. 0 used for "none" */
4850
int idxTop; /* Direct descendent highest up on the graph */
4951
GraphRow *pChild; /* Child immediately above this node */
5052
u8 isDup; /* True if this is duplicate of a prior entry */
5153
u8 isLeaf; /* True if this is a leaf node */
54
+ u8 hasNormalOutMerge; /* Is parent of at laest 1 non-cherrypick merge */
5255
u8 timeWarp; /* Child is earlier in time */
5356
u8 bDescender; /* True if riser from bottom of graph to here. */
5457
i8 iRail; /* Which rail this check-in appears on. 0-based.*/
5558
i8 mergeOut; /* Merge out to this rail. -1 if no merge-out */
5659
u8 mergeIn[GR_MAX_RAIL]; /* Merge in from non-zero rails */
5760
int aiRiser[GR_MAX_RAIL]; /* Risers from this node to a higher row. */
5861
int mergeUpto; /* Draw the mergeOut rail up to this level */
62
+ int cherrypickUpto; /* Continue the mergeOut rail up to here */
5963
u64 mergeDown; /* Draw merge lines up from bottom of graph */
64
+ u64 cherrypickDown; /* Draw cherrypick lines up from bottom */
6065
6166
u64 railInUse; /* Mask of occupied rails at this row */
6267
};
6368
6469
/* Context while building a graph
@@ -179,10 +184,11 @@
179184
*/
180185
int graph_add_row(
181186
GraphContext *p, /* The context to which the row is added */
182187
int rid, /* RID for the check-in */
183188
int nParent, /* Number of parents */
189
+ int nCherrypick, /* How many of aParent[] are actually cherrypicks */
184190
int *aParent, /* Array of parents */
185191
const char *zBranch, /* Branch for this check-in */
186192
const char *zBgClr, /* Background color. NULL or "" for white. */
187193
const char *zUuid, /* hash name of the object being graphed */
188194
int isLeaf /* True if this row is a leaf */
@@ -195,11 +201,16 @@
195201
nByte = sizeof(GraphRow);
196202
if( nParent>0 ) nByte += sizeof(pRow->aParent[0])*nParent;
197203
pRow = (GraphRow*)safeMalloc( nByte );
198204
pRow->aParent = nParent>0 ? (int*)&pRow[1] : 0;
199205
pRow->rid = rid;
206
+ if( nCherrypick>=nParent ){
207
+ nCherrypick = nParent-1; /* Safety. Should never happen. */
208
+ }
200209
pRow->nParent = nParent;
210
+ pRow->nCherrypick = nCherrypick;
211
+ pRow->nNonCherrypick = nParent - nCherrypick;
201212
pRow->zBranch = persistBranchName(p, zBranch);
202213
if( zUuid==0 ) zUuid = "";
203214
sqlite3_snprintf(sizeof(pRow->zUuid), pRow->zUuid, "%s", zUuid);
204215
pRow->isLeaf = isLeaf;
205216
memset(pRow->aiRiser, -1, sizeof(pRow->aiRiser));
@@ -284,11 +295,12 @@
284295
** Create a merge-arrow riser going from pParent up to pChild.
285296
*/
286297
static void createMergeRiser(
287298
GraphContext *p,
288299
GraphRow *pParent,
289
- GraphRow *pChild
300
+ GraphRow *pChild,
301
+ int isCherrypick
290302
){
291303
int u;
292304
u64 mask;
293305
GraphRow *pLoop;
294306
@@ -297,25 +309,33 @@
297309
if( u>=0 && u<pChild->idx ){
298310
/* The thick arrow up to the next primary child of pDesc goes
299311
** further up than the thin merge arrow riser, so draw them both
300312
** on the same rail. */
301313
pParent->mergeOut = pParent->iRail;
302
- pParent->mergeUpto = pChild->idx;
303
- }else{
314
+ }else{
304315
/* The thin merge arrow riser is taller than the thick primary
305316
** child riser, so use separate rails. */
306317
int iTarget = pParent->iRail;
307318
pParent->mergeOut = findFreeRail(p, pChild->idx, pParent->idx-1, iTarget);
308
- pParent->mergeUpto = pChild->idx;
309319
mask = BIT(pParent->mergeOut);
310320
for(pLoop=pChild->pNext; pLoop && pLoop->rid!=pParent->rid;
311321
pLoop=pLoop->pNext){
312322
pLoop->railInUse |= mask;
313323
}
314324
}
315325
}
316
- pChild->mergeIn[pParent->mergeOut] = 1;
326
+ if( isCherrypick ){
327
+ if( pParent->cherrypickUpto==0 || pParent->cherrypickUpto > pChild->idx ){
328
+ pParent->cherrypickUpto = pChild->idx;
329
+ }
330
+ }else{
331
+ pParent->hasNormalOutMerge = 1;
332
+ if( pParent->mergeUpto==0 || pParent->mergeUpto > pChild->idx ){
333
+ pParent->mergeUpto = pChild->idx;
334
+ }
335
+ }
336
+ pChild->mergeIn[pParent->mergeOut] = isCherrypick ? 2 : 1;
317337
}
318338
319339
/*
320340
** Compute the maximum rail number.
321341
*/
@@ -323,11 +343,13 @@
323343
GraphRow *pRow;
324344
p->mxRail = 0;
325345
for(pRow=p->pFirst; pRow; pRow=pRow->pNext){
326346
if( pRow->iRail>p->mxRail ) p->mxRail = pRow->iRail;
327347
if( pRow->mergeOut>p->mxRail ) p->mxRail = pRow->mergeOut;
328
- while( p->mxRail<GR_MAX_RAIL && pRow->mergeDown>(BIT(p->mxRail+1)-1) ){
348
+ while( p->mxRail<GR_MAX_RAIL
349
+ && (pRow->mergeDown|pRow->cherrypickDown)>(BIT(p->mxRail+1)-1)
350
+ ){
329351
p->mxRail++;
330352
}
331353
}
332354
}
333355
@@ -395,11 +417,18 @@
395417
*/
396418
if( omitDescenders ){
397419
for(pRow=p->pFirst; pRow; pRow=pRow->pNext){
398420
for(i=1; i<pRow->nParent; i++){
399421
if( hashFind(p, pRow->aParent[i])==0 ){
400
- pRow->aParent[i] = pRow->aParent[--pRow->nParent];
422
+ memmove(pRow->aParent+i, pRow->aParent+i+1,
423
+ sizeof(pRow->aParent[0])*(pRow->nParent-i-1));
424
+ pRow->nParent--;
425
+ if( i<pRow->nNonCherrypick ){
426
+ pRow->nNonCherrypick--;
427
+ }else{
428
+ pRow->nCherrypick--;
429
+ }
401430
i--;
402431
}
403432
}
404433
}
405434
}
@@ -408,15 +437,15 @@
408437
** other parents in the same branch, reorder the parents to make
409438
** the parent from the same branch the primary parent.
410439
*/
411440
for(pRow=p->pFirst; pRow; pRow=pRow->pNext){
412441
if( pRow->isDup ) continue;
413
- if( pRow->nParent<2 ) continue; /* Not a fork */
442
+ if( pRow->nNonCherrypick<2 ) continue; /* Not a fork */
414443
pParent = hashFind(p, pRow->aParent[0]);
415444
if( pParent==0 ) continue; /* Parent off-screen */
416445
if( pParent->zBranch==pRow->zBranch ) continue; /* Same branch */
417
- for(i=1; i<pRow->nParent; i++){
446
+ for(i=1; i<pRow->nNonCherrypick; i++){
418447
pParent = hashFind(p, pRow->aParent[i]);
419448
if( pParent && pParent->zBranch==pRow->zBranch ){
420449
int t = pRow->aParent[0];
421450
pRow->aParent[0] = pRow->aParent[i];
422451
pRow->aParent[i] = t;
@@ -565,18 +594,23 @@
565594
iMrail = findFreeRail(p, pRow->idx, p->nRow, 0);
566595
if( p->mxRail>=GR_MAX_RAIL ) return;
567596
mergeRiserFrom[iMrail] = parentRid;
568597
}
569598
mask = BIT(iMrail);
570
- pRow->mergeIn[iMrail] = 1;
571
- pRow->mergeDown |= mask;
599
+ if( i>=pRow->nNonCherrypick ){
600
+ pRow->mergeIn[iMrail] = 2;
601
+ pRow->cherrypickDown |= mask;
602
+ }else{
603
+ pRow->mergeIn[iMrail] = 1;
604
+ pRow->mergeDown |= mask;
605
+ }
572606
for(pLoop=pRow->pNext; pLoop; pLoop=pLoop->pNext){
573607
pLoop->railInUse |= mask;
574608
}
575609
}else{
576610
/* Merge from an on-screen node */
577
- createMergeRiser(p, pDesc, pRow);
611
+ createMergeRiser(p, pDesc, pRow, i>=pRow->nNonCherrypick);
578612
if( p->mxRail>=GR_MAX_RAIL ) return;
579613
}
580614
}
581615
}
582616
@@ -593,11 +627,11 @@
593627
for(pRow=p->pFirst; pRow; pRow=pRow->pNext){
594628
if( !pRow->isDup ) continue;
595629
pRow->iRail = dupRail;
596630
pDesc = hashFind(p, pRow->rid);
597631
assert( pDesc!=0 && pDesc!=pRow );
598
- createMergeRiser(p, pDesc, pRow);
632
+ createMergeRiser(p, pDesc, pRow, 0);
599633
if( pDesc->mergeOut>mxRail ) mxRail = pDesc->mergeOut;
600634
}
601635
if( dupRail<=mxRail ){
602636
dupRail = mxRail+1;
603637
for(pRow=p->pFirst; pRow; pRow=pRow->pNext){
604638
--- src/graph.c
+++ src/graph.c
@@ -34,10 +34,12 @@
34 ** but which are included just so that we can capture their background color.
35 */
36 struct GraphRow {
37 int rid; /* The rid for the check-in */
38 i8 nParent; /* Number of parents. -1 for technote lines */
 
 
39 int *aParent; /* Array of parents. 0 element is primary .*/
40 char *zBranch; /* Branch name */
41 char *zBgClr; /* Background Color */
42 char zUuid[HNAME_MAX+1]; /* Check-in for file ID */
43
@@ -47,18 +49,21 @@
47 int idx; /* Row index. First is 1. 0 used for "none" */
48 int idxTop; /* Direct descendent highest up on the graph */
49 GraphRow *pChild; /* Child immediately above this node */
50 u8 isDup; /* True if this is duplicate of a prior entry */
51 u8 isLeaf; /* True if this is a leaf node */
 
52 u8 timeWarp; /* Child is earlier in time */
53 u8 bDescender; /* True if riser from bottom of graph to here. */
54 i8 iRail; /* Which rail this check-in appears on. 0-based.*/
55 i8 mergeOut; /* Merge out to this rail. -1 if no merge-out */
56 u8 mergeIn[GR_MAX_RAIL]; /* Merge in from non-zero rails */
57 int aiRiser[GR_MAX_RAIL]; /* Risers from this node to a higher row. */
58 int mergeUpto; /* Draw the mergeOut rail up to this level */
 
59 u64 mergeDown; /* Draw merge lines up from bottom of graph */
 
60
61 u64 railInUse; /* Mask of occupied rails at this row */
62 };
63
64 /* Context while building a graph
@@ -179,10 +184,11 @@
179 */
180 int graph_add_row(
181 GraphContext *p, /* The context to which the row is added */
182 int rid, /* RID for the check-in */
183 int nParent, /* Number of parents */
 
184 int *aParent, /* Array of parents */
185 const char *zBranch, /* Branch for this check-in */
186 const char *zBgClr, /* Background color. NULL or "" for white. */
187 const char *zUuid, /* hash name of the object being graphed */
188 int isLeaf /* True if this row is a leaf */
@@ -195,11 +201,16 @@
195 nByte = sizeof(GraphRow);
196 if( nParent>0 ) nByte += sizeof(pRow->aParent[0])*nParent;
197 pRow = (GraphRow*)safeMalloc( nByte );
198 pRow->aParent = nParent>0 ? (int*)&pRow[1] : 0;
199 pRow->rid = rid;
 
 
 
200 pRow->nParent = nParent;
 
 
201 pRow->zBranch = persistBranchName(p, zBranch);
202 if( zUuid==0 ) zUuid = "";
203 sqlite3_snprintf(sizeof(pRow->zUuid), pRow->zUuid, "%s", zUuid);
204 pRow->isLeaf = isLeaf;
205 memset(pRow->aiRiser, -1, sizeof(pRow->aiRiser));
@@ -284,11 +295,12 @@
284 ** Create a merge-arrow riser going from pParent up to pChild.
285 */
286 static void createMergeRiser(
287 GraphContext *p,
288 GraphRow *pParent,
289 GraphRow *pChild
 
290 ){
291 int u;
292 u64 mask;
293 GraphRow *pLoop;
294
@@ -297,25 +309,33 @@
297 if( u>=0 && u<pChild->idx ){
298 /* The thick arrow up to the next primary child of pDesc goes
299 ** further up than the thin merge arrow riser, so draw them both
300 ** on the same rail. */
301 pParent->mergeOut = pParent->iRail;
302 pParent->mergeUpto = pChild->idx;
303 }else{
304 /* The thin merge arrow riser is taller than the thick primary
305 ** child riser, so use separate rails. */
306 int iTarget = pParent->iRail;
307 pParent->mergeOut = findFreeRail(p, pChild->idx, pParent->idx-1, iTarget);
308 pParent->mergeUpto = pChild->idx;
309 mask = BIT(pParent->mergeOut);
310 for(pLoop=pChild->pNext; pLoop && pLoop->rid!=pParent->rid;
311 pLoop=pLoop->pNext){
312 pLoop->railInUse |= mask;
313 }
314 }
315 }
316 pChild->mergeIn[pParent->mergeOut] = 1;
 
 
 
 
 
 
 
 
 
 
317 }
318
319 /*
320 ** Compute the maximum rail number.
321 */
@@ -323,11 +343,13 @@
323 GraphRow *pRow;
324 p->mxRail = 0;
325 for(pRow=p->pFirst; pRow; pRow=pRow->pNext){
326 if( pRow->iRail>p->mxRail ) p->mxRail = pRow->iRail;
327 if( pRow->mergeOut>p->mxRail ) p->mxRail = pRow->mergeOut;
328 while( p->mxRail<GR_MAX_RAIL && pRow->mergeDown>(BIT(p->mxRail+1)-1) ){
 
 
329 p->mxRail++;
330 }
331 }
332 }
333
@@ -395,11 +417,18 @@
395 */
396 if( omitDescenders ){
397 for(pRow=p->pFirst; pRow; pRow=pRow->pNext){
398 for(i=1; i<pRow->nParent; i++){
399 if( hashFind(p, pRow->aParent[i])==0 ){
400 pRow->aParent[i] = pRow->aParent[--pRow->nParent];
 
 
 
 
 
 
 
401 i--;
402 }
403 }
404 }
405 }
@@ -408,15 +437,15 @@
408 ** other parents in the same branch, reorder the parents to make
409 ** the parent from the same branch the primary parent.
410 */
411 for(pRow=p->pFirst; pRow; pRow=pRow->pNext){
412 if( pRow->isDup ) continue;
413 if( pRow->nParent<2 ) continue; /* Not a fork */
414 pParent = hashFind(p, pRow->aParent[0]);
415 if( pParent==0 ) continue; /* Parent off-screen */
416 if( pParent->zBranch==pRow->zBranch ) continue; /* Same branch */
417 for(i=1; i<pRow->nParent; i++){
418 pParent = hashFind(p, pRow->aParent[i]);
419 if( pParent && pParent->zBranch==pRow->zBranch ){
420 int t = pRow->aParent[0];
421 pRow->aParent[0] = pRow->aParent[i];
422 pRow->aParent[i] = t;
@@ -565,18 +594,23 @@
565 iMrail = findFreeRail(p, pRow->idx, p->nRow, 0);
566 if( p->mxRail>=GR_MAX_RAIL ) return;
567 mergeRiserFrom[iMrail] = parentRid;
568 }
569 mask = BIT(iMrail);
570 pRow->mergeIn[iMrail] = 1;
571 pRow->mergeDown |= mask;
 
 
 
 
 
572 for(pLoop=pRow->pNext; pLoop; pLoop=pLoop->pNext){
573 pLoop->railInUse |= mask;
574 }
575 }else{
576 /* Merge from an on-screen node */
577 createMergeRiser(p, pDesc, pRow);
578 if( p->mxRail>=GR_MAX_RAIL ) return;
579 }
580 }
581 }
582
@@ -593,11 +627,11 @@
593 for(pRow=p->pFirst; pRow; pRow=pRow->pNext){
594 if( !pRow->isDup ) continue;
595 pRow->iRail = dupRail;
596 pDesc = hashFind(p, pRow->rid);
597 assert( pDesc!=0 && pDesc!=pRow );
598 createMergeRiser(p, pDesc, pRow);
599 if( pDesc->mergeOut>mxRail ) mxRail = pDesc->mergeOut;
600 }
601 if( dupRail<=mxRail ){
602 dupRail = mxRail+1;
603 for(pRow=p->pFirst; pRow; pRow=pRow->pNext){
604
--- src/graph.c
+++ src/graph.c
@@ -34,10 +34,12 @@
34 ** but which are included just so that we can capture their background color.
35 */
36 struct GraphRow {
37 int rid; /* The rid for the check-in */
38 i8 nParent; /* Number of parents. -1 for technote lines */
39 i8 nCherrypick; /* Subset of aParent that are cherrypicks */
40 i8 nNonCherrypick; /* Number of non-cherrypick parents */
41 int *aParent; /* Array of parents. 0 element is primary .*/
42 char *zBranch; /* Branch name */
43 char *zBgClr; /* Background Color */
44 char zUuid[HNAME_MAX+1]; /* Check-in for file ID */
45
@@ -47,18 +49,21 @@
49 int idx; /* Row index. First is 1. 0 used for "none" */
50 int idxTop; /* Direct descendent highest up on the graph */
51 GraphRow *pChild; /* Child immediately above this node */
52 u8 isDup; /* True if this is duplicate of a prior entry */
53 u8 isLeaf; /* True if this is a leaf node */
54 u8 hasNormalOutMerge; /* Is parent of at laest 1 non-cherrypick merge */
55 u8 timeWarp; /* Child is earlier in time */
56 u8 bDescender; /* True if riser from bottom of graph to here. */
57 i8 iRail; /* Which rail this check-in appears on. 0-based.*/
58 i8 mergeOut; /* Merge out to this rail. -1 if no merge-out */
59 u8 mergeIn[GR_MAX_RAIL]; /* Merge in from non-zero rails */
60 int aiRiser[GR_MAX_RAIL]; /* Risers from this node to a higher row. */
61 int mergeUpto; /* Draw the mergeOut rail up to this level */
62 int cherrypickUpto; /* Continue the mergeOut rail up to here */
63 u64 mergeDown; /* Draw merge lines up from bottom of graph */
64 u64 cherrypickDown; /* Draw cherrypick lines up from bottom */
65
66 u64 railInUse; /* Mask of occupied rails at this row */
67 };
68
69 /* Context while building a graph
@@ -179,10 +184,11 @@
184 */
185 int graph_add_row(
186 GraphContext *p, /* The context to which the row is added */
187 int rid, /* RID for the check-in */
188 int nParent, /* Number of parents */
189 int nCherrypick, /* How many of aParent[] are actually cherrypicks */
190 int *aParent, /* Array of parents */
191 const char *zBranch, /* Branch for this check-in */
192 const char *zBgClr, /* Background color. NULL or "" for white. */
193 const char *zUuid, /* hash name of the object being graphed */
194 int isLeaf /* True if this row is a leaf */
@@ -195,11 +201,16 @@
201 nByte = sizeof(GraphRow);
202 if( nParent>0 ) nByte += sizeof(pRow->aParent[0])*nParent;
203 pRow = (GraphRow*)safeMalloc( nByte );
204 pRow->aParent = nParent>0 ? (int*)&pRow[1] : 0;
205 pRow->rid = rid;
206 if( nCherrypick>=nParent ){
207 nCherrypick = nParent-1; /* Safety. Should never happen. */
208 }
209 pRow->nParent = nParent;
210 pRow->nCherrypick = nCherrypick;
211 pRow->nNonCherrypick = nParent - nCherrypick;
212 pRow->zBranch = persistBranchName(p, zBranch);
213 if( zUuid==0 ) zUuid = "";
214 sqlite3_snprintf(sizeof(pRow->zUuid), pRow->zUuid, "%s", zUuid);
215 pRow->isLeaf = isLeaf;
216 memset(pRow->aiRiser, -1, sizeof(pRow->aiRiser));
@@ -284,11 +295,12 @@
295 ** Create a merge-arrow riser going from pParent up to pChild.
296 */
297 static void createMergeRiser(
298 GraphContext *p,
299 GraphRow *pParent,
300 GraphRow *pChild,
301 int isCherrypick
302 ){
303 int u;
304 u64 mask;
305 GraphRow *pLoop;
306
@@ -297,25 +309,33 @@
309 if( u>=0 && u<pChild->idx ){
310 /* The thick arrow up to the next primary child of pDesc goes
311 ** further up than the thin merge arrow riser, so draw them both
312 ** on the same rail. */
313 pParent->mergeOut = pParent->iRail;
314 }else{
 
315 /* The thin merge arrow riser is taller than the thick primary
316 ** child riser, so use separate rails. */
317 int iTarget = pParent->iRail;
318 pParent->mergeOut = findFreeRail(p, pChild->idx, pParent->idx-1, iTarget);
 
319 mask = BIT(pParent->mergeOut);
320 for(pLoop=pChild->pNext; pLoop && pLoop->rid!=pParent->rid;
321 pLoop=pLoop->pNext){
322 pLoop->railInUse |= mask;
323 }
324 }
325 }
326 if( isCherrypick ){
327 if( pParent->cherrypickUpto==0 || pParent->cherrypickUpto > pChild->idx ){
328 pParent->cherrypickUpto = pChild->idx;
329 }
330 }else{
331 pParent->hasNormalOutMerge = 1;
332 if( pParent->mergeUpto==0 || pParent->mergeUpto > pChild->idx ){
333 pParent->mergeUpto = pChild->idx;
334 }
335 }
336 pChild->mergeIn[pParent->mergeOut] = isCherrypick ? 2 : 1;
337 }
338
339 /*
340 ** Compute the maximum rail number.
341 */
@@ -323,11 +343,13 @@
343 GraphRow *pRow;
344 p->mxRail = 0;
345 for(pRow=p->pFirst; pRow; pRow=pRow->pNext){
346 if( pRow->iRail>p->mxRail ) p->mxRail = pRow->iRail;
347 if( pRow->mergeOut>p->mxRail ) p->mxRail = pRow->mergeOut;
348 while( p->mxRail<GR_MAX_RAIL
349 && (pRow->mergeDown|pRow->cherrypickDown)>(BIT(p->mxRail+1)-1)
350 ){
351 p->mxRail++;
352 }
353 }
354 }
355
@@ -395,11 +417,18 @@
417 */
418 if( omitDescenders ){
419 for(pRow=p->pFirst; pRow; pRow=pRow->pNext){
420 for(i=1; i<pRow->nParent; i++){
421 if( hashFind(p, pRow->aParent[i])==0 ){
422 memmove(pRow->aParent+i, pRow->aParent+i+1,
423 sizeof(pRow->aParent[0])*(pRow->nParent-i-1));
424 pRow->nParent--;
425 if( i<pRow->nNonCherrypick ){
426 pRow->nNonCherrypick--;
427 }else{
428 pRow->nCherrypick--;
429 }
430 i--;
431 }
432 }
433 }
434 }
@@ -408,15 +437,15 @@
437 ** other parents in the same branch, reorder the parents to make
438 ** the parent from the same branch the primary parent.
439 */
440 for(pRow=p->pFirst; pRow; pRow=pRow->pNext){
441 if( pRow->isDup ) continue;
442 if( pRow->nNonCherrypick<2 ) continue; /* Not a fork */
443 pParent = hashFind(p, pRow->aParent[0]);
444 if( pParent==0 ) continue; /* Parent off-screen */
445 if( pParent->zBranch==pRow->zBranch ) continue; /* Same branch */
446 for(i=1; i<pRow->nNonCherrypick; i++){
447 pParent = hashFind(p, pRow->aParent[i]);
448 if( pParent && pParent->zBranch==pRow->zBranch ){
449 int t = pRow->aParent[0];
450 pRow->aParent[0] = pRow->aParent[i];
451 pRow->aParent[i] = t;
@@ -565,18 +594,23 @@
594 iMrail = findFreeRail(p, pRow->idx, p->nRow, 0);
595 if( p->mxRail>=GR_MAX_RAIL ) return;
596 mergeRiserFrom[iMrail] = parentRid;
597 }
598 mask = BIT(iMrail);
599 if( i>=pRow->nNonCherrypick ){
600 pRow->mergeIn[iMrail] = 2;
601 pRow->cherrypickDown |= mask;
602 }else{
603 pRow->mergeIn[iMrail] = 1;
604 pRow->mergeDown |= mask;
605 }
606 for(pLoop=pRow->pNext; pLoop; pLoop=pLoop->pNext){
607 pLoop->railInUse |= mask;
608 }
609 }else{
610 /* Merge from an on-screen node */
611 createMergeRiser(p, pDesc, pRow, i>=pRow->nNonCherrypick);
612 if( p->mxRail>=GR_MAX_RAIL ) return;
613 }
614 }
615 }
616
@@ -593,11 +627,11 @@
627 for(pRow=p->pFirst; pRow; pRow=pRow->pNext){
628 if( !pRow->isDup ) continue;
629 pRow->iRail = dupRail;
630 pDesc = hashFind(p, pRow->rid);
631 assert( pDesc!=0 && pDesc!=pRow );
632 createMergeRiser(p, pDesc, pRow, 0);
633 if( pDesc->mergeOut>mxRail ) mxRail = pDesc->mergeOut;
634 }
635 if( dupRail<=mxRail ){
636 dupRail = mxRail+1;
637 for(pRow=p->pFirst; pRow; pRow=pRow->pNext){
638
+113 -52
--- src/graph.js
+++ src/graph.js
@@ -21,34 +21,42 @@
2121
** The rowinfo field is an array of structures, one per entry in the timeline,
2222
** where each structure has the following fields:
2323
**
2424
** id: The id of the <div> element for the row. This is an integer.
2525
** to get an actual id, prepend "m" to the integer. The top node
26
-** is iTopRow and numbers increase moving down the tx.
26
+** is iTopRow and numbers increase moving down the timeline.
2727
** bg: The background color for this row
2828
** r: The "rail" that the node for this row sits on. The left-most
2929
** rail is 0 and the number increases to the right.
30
-** d: True if there is a "descender" - an arrow coming from the bottom
31
-** of the page straight up to this node.
32
-** mo: "merge-out". If non-negative, this is the rail position
33
-** for the upward portion of a merge arrow. The merge arrow goes up
34
-** to the row identified by mu:. If this value is negative then
35
-** node has no merge children and no merge-out line is drawn.
30
+** d: If exists and true then there is a "descender" - an arrow
31
+** coming from the bottom of the page straight up to this node.
32
+** mo: "merge-out". If it exists, this is the rail position
33
+** for the upward portion of a merge arrow. The merge arrow goes as
34
+** a solid normal merge line up to the row identified by "mu" and
35
+** then as a dashed cherrypick merge line up further to "cu".
36
+** If this value is omitted if there are no merge children.
3637
** mu: The id of the row which is the top of the merge-out arrow.
38
+** Only exists if "mo" exists.
39
+** cu: Extend the mu merge arrow up to this row as a cherrypick
40
+** merge line, if this value exists.
3741
** u: Draw a thick child-line out of the top of this node and up to
3842
** the node with an id equal to this value. 0 if it is straight to
3943
** the top of the page, -1 if there is no thick-line riser.
4044
** f: 0x01: a leaf node.
4145
** au: An array of integers that define thick-line risers for branches.
4246
** The integers are in pairs. For each pair, the first integer is
4347
** is the rail on which the riser should run and the second integer
44
-** is the id of the node upto which the riser should run.
48
+** is the id of the node upto which the riser should run. If there
49
+** are no risers, this array does not exist.
4550
** mi: "merge-in". An array of integer rail positions from which
4651
** merge arrows should be drawn into this node. If the value is
4752
** negative, then the rail position is the absolute value of mi[]
4853
** and a thin merge-arrow descender is drawn to the bottom of
49
-** the screen.
54
+** the screen. This array is omitted if there are no inbound
55
+** merges.
56
+** ci: "cherrypick-in". Like "mi" except for cherrypick merges.
57
+** omitted if there are no cherrypick merges.
5058
** h: The artifact hash of the object being graphed
5159
*/
5260
var amendCssOnce = 1; // Only change the CSS one time
5361
function amendCss(circleNodes,showArrowheads){
5462
if( !amendCssOnce ) return;
@@ -83,11 +91,12 @@
8391
parent.appendChild(canvasDiv);
8492
8593
var elems = {};
8694
var elemClasses = [
8795
"rail", "mergeoffset", "node", "arrow u", "arrow u sm", "line",
88
- "arrow merge r", "line merge", "arrow warp", "line warp"
96
+ "arrow merge r", "line merge", "arrow warp", "line warp",
97
+ "line cherrypick"
8998
];
9099
for( var i=0; i<elemClasses.length; i++ ){
91100
var cls = elemClasses[i];
92101
var elem = document.createElement("div");
93102
elem.className = "tl-" + cls;
@@ -103,10 +112,11 @@
103112
arrow = elems.arrow_u;
104113
arrowSmall = elems.arrow_u_sm;
105114
line = elems.line;
106115
mArrow = elems.arrow_merge_r;
107116
mLine = elems.line_merge;
117
+ cpLine = elems.line_cherrypick;
108118
wArrow = elems.arrow_warp;
109119
wLine = elems.line_warp;
110120
111121
var minRailPitch = Math.ceil((node.w+line.w)/2 + mArrow.w + 1);
112122
if( window.innerWidth<400 ){
@@ -186,14 +196,20 @@
186196
drawLine(line,color,x,y0,null,y1);
187197
x = to.x + (node.w-arw.w)/2;
188198
var n = drawBox(arw.cls,null,x,y);
189199
if(color) n.style.borderBottomColor = color;
190200
}
201
+ /* Draw thin horizontal or vertical lines representing merges */
191202
function drawMergeLine(x0,y0,x1,y1){
192203
drawLine(mLine,null,x0,y0,x1,y1);
193204
}
194
- function drawMergeArrow(p,rail){
205
+ function drawCherrypickLine(x0,y0,x1,y1){
206
+ drawLine(cpLine,null,x0,y0,x1,y1);
207
+ }
208
+ /* Draw an arrow representing an in-bound merge from the "rail"-th rail
209
+ ** over to the node of "p". Make is a checkpoint merge is "isCP" is true */
210
+ function drawMergeArrow(p,rail,isCP){
195211
var x0 = rail*railPitch + node.w/2;
196212
if( rail in mergeLines ){
197213
x0 += mergeLines[rail];
198214
if( p.r<rail ) x0 += mLine.w;
199215
}else{
@@ -200,13 +216,19 @@
200216
x0 += (p.r<rail ? -1 : 1)*line.w/2;
201217
}
202218
var x1 = mArrow.w ? mArrow.w/2 : -node.w/2;
203219
x1 = p.x + (p.r<rail ? node.w + Math.ceil(x1) : -x1);
204220
var y = miLineY(p);
205
- drawMergeLine(x0,y,x1,null);
206221
var x = p.x + (p.r<rail ? node.w : -mArrow.w);
207
- var cls = "arrow merge " + (p.r<rail ? "l" : "r");
222
+ var cls;
223
+ if( isCP ){
224
+ drawCherrypickLine(x0,y,x1,null);
225
+ cls = "arrow cherrypick " + (p.r<rail ? "l" : "r");
226
+ }else{
227
+ drawMergeLine(x0,y,x1,null);
228
+ cls = "arrow merge " + (p.r<rail ? "l" : "r");
229
+ }
208230
drawBox(cls,null,x,y+(mLine.w-mArrow.h)/2);
209231
}
210232
function drawNode(p, btm){
211233
if( p.bg ){
212234
var e = document.getElementById("mc"+p.id);
@@ -215,74 +237,112 @@
215237
if(e) e.style.backgroundColor = p.bg;
216238
}
217239
if( p.r<0 ) return;
218240
if( p.u>0 ) drawUpArrow(p,tx.rowinfo[p.u-tx.iTopRow],p.fg);
219241
var cls = node.cls;
220
- if( p.mi.length ) cls += " merge";
242
+ if( p.hasOwnProperty('mi') && p.mi.length ) cls += " merge";
221243
if( p.f&1 ) cls += " leaf";
222244
var n = drawBox(cls,p.bg,p.x,p.y);
223245
n.id = "tln"+p.id;
224246
n.onclick = clickOnNode;
225247
n.style.zIndex = 10;
226248
if( !tx.omitDescenders ){
227249
if( p.u==0 ) drawUpArrow(p,{x: p.x, y: -node.h},p.fg);
228
- if( p.d ) drawUpArrow({x: p.x, y: btm-node.h/2},p,p.fg);
250
+ if( p.hasOwnProperty('d') ) drawUpArrow({x: p.x, y: btm-node.h/2},p,p.fg);
229251
}
230
- if( p.mo>=0 ){
252
+ if( p.hasOwnProperty('mo') ){
231253
var x0 = p.x + node.w/2;
232254
var x1 = p.mo*railPitch + node.w/2;
233255
var u = tx.rowinfo[p.mu-tx.iTopRow];
234256
var y1 = miLineY(u);
235257
if( p.u<0 || p.mo!=p.r ){
236258
x1 += mergeLines[p.mo] = -mLine.w/2;
237259
var y0 = p.y+2;
238
- if( p.r!=p.mo ) drawMergeLine(x0,y0,x1+(x0<x1 ? mLine.w : 0),null);
239
- drawMergeLine(x1,y0+mLine.w,null,y1);
260
+ if( p.mu==p.id ){
261
+ drawCherrypickLine(x0,y0,x1+(x0<x1 ? mLine.w : 0),null);
262
+ y1 = y0;
263
+ }else{
264
+ drawMergeLine(x0,y0,x1+(x0<x1 ? mLine.w : 0),null);
265
+ drawMergeLine(x1,y0+mLine.w,null,y1);
266
+ }
267
+ if( p.hasOwnProperty('cu') ){
268
+ var u2 = tx.rowinfo[p.cu-tx.iTopRow];
269
+ var y2 = miLineY(u2);
270
+ drawCherrypickLine(x1,y1,null,y2);
271
+ }
240272
}else if( mergeOffset ){
241273
mergeLines[p.mo] = u.r<p.r ? -mergeOffset-mLine.w : mergeOffset;
242274
x1 += mergeLines[p.mo];
243
- drawMergeLine(x1,p.y+node.h/2,null,y1);
275
+ if( p.mo<p.id ){
276
+ drawMergeLine(x1,p.y+node.h/2,null,y1);
277
+ }
278
+ if( p.hasOwnProperty('cu') ){
279
+ var u2 = tx.rowinfo[p.cu-tx.iTopRow];
280
+ var y2 = miLineY(u2);
281
+ drawCherrypickLine(x1,y1,null,y2);
282
+ }
244283
}else{
245284
delete mergeLines[p.mo];
246285
}
247286
}
248
- for( var i=0; i<p.au.length; i+=2 ){
249
- var rail = p.au[i];
250
- var x0 = p.x + node.w/2;
251
- var x1 = rail*railPitch + (node.w-line.w)/2;
252
- if( x0<x1 ){
253
- x0 = Math.ceil(x0);
254
- x1 += line.w;
255
- }
256
- var y0 = p.y + (node.h-line.w)/2;
257
- var u = tx.rowinfo[p.au[i+1]-tx.iTopRow];
258
- if( u.id<p.id ){
259
- drawLine(line,u.fg,x0,y0,x1,null);
260
- drawUpArrow(p,u,u.fg);
261
- }else{
262
- var y1 = u.y + (node.h-line.w)/2;
263
- drawLine(wLine,u.fg,x0,y0,x1,null);
264
- drawLine(wLine,u.fg,x1-line.w,y0,null,y1+line.w);
265
- drawLine(wLine,u.fg,x1,y1,u.x-wArrow.w/2,null);
266
- var x = u.x-wArrow.w;
267
- var y = u.y+(node.h-wArrow.h)/2;
268
- var n = drawBox(wArrow.cls,null,x,y);
269
- if( u.fg ) n.style.borderLeftColor = u.fg;
270
- }
271
- }
272
- for( var i=0; i<p.mi.length; i++ ){
273
- var rail = p.mi[i];
274
- if( rail<0 ){
275
- rail = -rail;
276
- mergeLines[rail] = -mLine.w/2;
277
- var x = rail*railPitch + (node.w-mLine.w)/2;
278
- drawMergeLine(x,miLineY(p),null,btm);
279
- }
280
- drawMergeArrow(p,rail);
287
+ if( p.hasOwnProperty('au') ){
288
+ for( var i=0; i<p.au.length; i+=2 ){
289
+ var rail = p.au[i];
290
+ var x0 = p.x + node.w/2;
291
+ var x1 = rail*railPitch + (node.w-line.w)/2;
292
+ if( x0<x1 ){
293
+ x0 = Math.ceil(x0);
294
+ x1 += line.w;
295
+ }
296
+ var y0 = p.y + (node.h-line.w)/2;
297
+ var u = tx.rowinfo[p.au[i+1]-tx.iTopRow];
298
+ if( u.id<p.id ){
299
+ drawLine(line,u.fg,x0,y0,x1,null);
300
+ drawUpArrow(p,u,u.fg);
301
+ }else{
302
+ var y1 = u.y + (node.h-line.w)/2;
303
+ drawLine(wLine,u.fg,x0,y0,x1,null);
304
+ drawLine(wLine,u.fg,x1-line.w,y0,null,y1+line.w);
305
+ drawLine(wLine,u.fg,x1,y1,u.x-wArrow.w/2,null);
306
+ var x = u.x-wArrow.w;
307
+ var y = u.y+(node.h-wArrow.h)/2;
308
+ var n = drawBox(wArrow.cls,null,x,y);
309
+ if( u.fg ) n.style.borderLeftColor = u.fg;
310
+ }
311
+ }
312
+ }
313
+ if( p.hasOwnProperty('mi') ){
314
+ for( var i=0; i<p.mi.length; i++ ){
315
+ var rail = p.mi[i];
316
+ if( rail<0 ){
317
+ rail = -rail;
318
+ mergeLines[rail] = -mLine.w/2;
319
+ var x = rail*railPitch + (node.w-mLine.w)/2;
320
+ var y = miLineY(p);
321
+ drawMergeLine(x,y,null,mergeBtm[rail]);
322
+ mergeBtm[rail] = y;
323
+ }
324
+ drawMergeArrow(p,rail,0);
325
+ }
326
+ }
327
+ if( p.hasOwnProperty('ci') ){
328
+ for( var i=0; i<p.ci.length; i++ ){
329
+ var rail = p.ci[i];
330
+ if( rail<0 ){
331
+ rail = -rail;
332
+ mergeLines[rail] = -mLine.w/2;
333
+ var x = rail*railPitch + (node.w-mLine.w)/2;
334
+ var y = miLineY(p);
335
+ drawCherrypickLine(x,y,null,mergeBtm[rail]);
336
+ mergeBtm[rail] = y;
337
+ }
338
+ drawMergeArrow(p,rail,1);
339
+ }
281340
}
282341
}
283342
var mergeLines;
343
+ var mergeBtm = new Array;
284344
function renderGraph(){
285345
mergeLines = {};
286346
canvasDiv.innerHTML = "";
287347
var canvasY = absoluteY(canvasDiv);
288348
for(var i=0; i<tx.rowinfo.length; i++ ){
@@ -293,10 +353,11 @@
293353
var tlBtm = document.querySelector(".timelineBottom");
294354
if( tlBtm.offsetHeight<node.h ){
295355
tlBtm.style.height = node.h + "px";
296356
}
297357
var btm = absoluteY(tlBtm) - canvasY + tlBtm.offsetHeight;
358
+ for( var i=0; i<tx.nrail; i++) mergeBtm[i] = btm;
298359
for( var i=tx.rowinfo.length-1; i>=0; i-- ){
299360
drawNode(tx.rowinfo[i], btm);
300361
}
301362
}
302363
var selRow;
303364
--- src/graph.js
+++ src/graph.js
@@ -21,34 +21,42 @@
21 ** The rowinfo field is an array of structures, one per entry in the timeline,
22 ** where each structure has the following fields:
23 **
24 ** id: The id of the <div> element for the row. This is an integer.
25 ** to get an actual id, prepend "m" to the integer. The top node
26 ** is iTopRow and numbers increase moving down the tx.
27 ** bg: The background color for this row
28 ** r: The "rail" that the node for this row sits on. The left-most
29 ** rail is 0 and the number increases to the right.
30 ** d: True if there is a "descender" - an arrow coming from the bottom
31 ** of the page straight up to this node.
32 ** mo: "merge-out". If non-negative, this is the rail position
33 ** for the upward portion of a merge arrow. The merge arrow goes up
34 ** to the row identified by mu:. If this value is negative then
35 ** node has no merge children and no merge-out line is drawn.
 
36 ** mu: The id of the row which is the top of the merge-out arrow.
 
 
 
37 ** u: Draw a thick child-line out of the top of this node and up to
38 ** the node with an id equal to this value. 0 if it is straight to
39 ** the top of the page, -1 if there is no thick-line riser.
40 ** f: 0x01: a leaf node.
41 ** au: An array of integers that define thick-line risers for branches.
42 ** The integers are in pairs. For each pair, the first integer is
43 ** is the rail on which the riser should run and the second integer
44 ** is the id of the node upto which the riser should run.
 
45 ** mi: "merge-in". An array of integer rail positions from which
46 ** merge arrows should be drawn into this node. If the value is
47 ** negative, then the rail position is the absolute value of mi[]
48 ** and a thin merge-arrow descender is drawn to the bottom of
49 ** the screen.
 
 
 
50 ** h: The artifact hash of the object being graphed
51 */
52 var amendCssOnce = 1; // Only change the CSS one time
53 function amendCss(circleNodes,showArrowheads){
54 if( !amendCssOnce ) return;
@@ -83,11 +91,12 @@
83 parent.appendChild(canvasDiv);
84
85 var elems = {};
86 var elemClasses = [
87 "rail", "mergeoffset", "node", "arrow u", "arrow u sm", "line",
88 "arrow merge r", "line merge", "arrow warp", "line warp"
 
89 ];
90 for( var i=0; i<elemClasses.length; i++ ){
91 var cls = elemClasses[i];
92 var elem = document.createElement("div");
93 elem.className = "tl-" + cls;
@@ -103,10 +112,11 @@
103 arrow = elems.arrow_u;
104 arrowSmall = elems.arrow_u_sm;
105 line = elems.line;
106 mArrow = elems.arrow_merge_r;
107 mLine = elems.line_merge;
 
108 wArrow = elems.arrow_warp;
109 wLine = elems.line_warp;
110
111 var minRailPitch = Math.ceil((node.w+line.w)/2 + mArrow.w + 1);
112 if( window.innerWidth<400 ){
@@ -186,14 +196,20 @@
186 drawLine(line,color,x,y0,null,y1);
187 x = to.x + (node.w-arw.w)/2;
188 var n = drawBox(arw.cls,null,x,y);
189 if(color) n.style.borderBottomColor = color;
190 }
 
191 function drawMergeLine(x0,y0,x1,y1){
192 drawLine(mLine,null,x0,y0,x1,y1);
193 }
194 function drawMergeArrow(p,rail){
 
 
 
 
 
195 var x0 = rail*railPitch + node.w/2;
196 if( rail in mergeLines ){
197 x0 += mergeLines[rail];
198 if( p.r<rail ) x0 += mLine.w;
199 }else{
@@ -200,13 +216,19 @@
200 x0 += (p.r<rail ? -1 : 1)*line.w/2;
201 }
202 var x1 = mArrow.w ? mArrow.w/2 : -node.w/2;
203 x1 = p.x + (p.r<rail ? node.w + Math.ceil(x1) : -x1);
204 var y = miLineY(p);
205 drawMergeLine(x0,y,x1,null);
206 var x = p.x + (p.r<rail ? node.w : -mArrow.w);
207 var cls = "arrow merge " + (p.r<rail ? "l" : "r");
 
 
 
 
 
 
 
208 drawBox(cls,null,x,y+(mLine.w-mArrow.h)/2);
209 }
210 function drawNode(p, btm){
211 if( p.bg ){
212 var e = document.getElementById("mc"+p.id);
@@ -215,74 +237,112 @@
215 if(e) e.style.backgroundColor = p.bg;
216 }
217 if( p.r<0 ) return;
218 if( p.u>0 ) drawUpArrow(p,tx.rowinfo[p.u-tx.iTopRow],p.fg);
219 var cls = node.cls;
220 if( p.mi.length ) cls += " merge";
221 if( p.f&1 ) cls += " leaf";
222 var n = drawBox(cls,p.bg,p.x,p.y);
223 n.id = "tln"+p.id;
224 n.onclick = clickOnNode;
225 n.style.zIndex = 10;
226 if( !tx.omitDescenders ){
227 if( p.u==0 ) drawUpArrow(p,{x: p.x, y: -node.h},p.fg);
228 if( p.d ) drawUpArrow({x: p.x, y: btm-node.h/2},p,p.fg);
229 }
230 if( p.mo>=0 ){
231 var x0 = p.x + node.w/2;
232 var x1 = p.mo*railPitch + node.w/2;
233 var u = tx.rowinfo[p.mu-tx.iTopRow];
234 var y1 = miLineY(u);
235 if( p.u<0 || p.mo!=p.r ){
236 x1 += mergeLines[p.mo] = -mLine.w/2;
237 var y0 = p.y+2;
238 if( p.r!=p.mo ) drawMergeLine(x0,y0,x1+(x0<x1 ? mLine.w : 0),null);
239 drawMergeLine(x1,y0+mLine.w,null,y1);
 
 
 
 
 
 
 
 
 
 
240 }else if( mergeOffset ){
241 mergeLines[p.mo] = u.r<p.r ? -mergeOffset-mLine.w : mergeOffset;
242 x1 += mergeLines[p.mo];
243 drawMergeLine(x1,p.y+node.h/2,null,y1);
 
 
 
 
 
 
 
244 }else{
245 delete mergeLines[p.mo];
246 }
247 }
248 for( var i=0; i<p.au.length; i+=2 ){
249 var rail = p.au[i];
250 var x0 = p.x + node.w/2;
251 var x1 = rail*railPitch + (node.w-line.w)/2;
252 if( x0<x1 ){
253 x0 = Math.ceil(x0);
254 x1 += line.w;
255 }
256 var y0 = p.y + (node.h-line.w)/2;
257 var u = tx.rowinfo[p.au[i+1]-tx.iTopRow];
258 if( u.id<p.id ){
259 drawLine(line,u.fg,x0,y0,x1,null);
260 drawUpArrow(p,u,u.fg);
261 }else{
262 var y1 = u.y + (node.h-line.w)/2;
263 drawLine(wLine,u.fg,x0,y0,x1,null);
264 drawLine(wLine,u.fg,x1-line.w,y0,null,y1+line.w);
265 drawLine(wLine,u.fg,x1,y1,u.x-wArrow.w/2,null);
266 var x = u.x-wArrow.w;
267 var y = u.y+(node.h-wArrow.h)/2;
268 var n = drawBox(wArrow.cls,null,x,y);
269 if( u.fg ) n.style.borderLeftColor = u.fg;
270 }
271 }
272 for( var i=0; i<p.mi.length; i++ ){
273 var rail = p.mi[i];
274 if( rail<0 ){
275 rail = -rail;
276 mergeLines[rail] = -mLine.w/2;
277 var x = rail*railPitch + (node.w-mLine.w)/2;
278 drawMergeLine(x,miLineY(p),null,btm);
279 }
280 drawMergeArrow(p,rail);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
281 }
282 }
283 var mergeLines;
 
284 function renderGraph(){
285 mergeLines = {};
286 canvasDiv.innerHTML = "";
287 var canvasY = absoluteY(canvasDiv);
288 for(var i=0; i<tx.rowinfo.length; i++ ){
@@ -293,10 +353,11 @@
293 var tlBtm = document.querySelector(".timelineBottom");
294 if( tlBtm.offsetHeight<node.h ){
295 tlBtm.style.height = node.h + "px";
296 }
297 var btm = absoluteY(tlBtm) - canvasY + tlBtm.offsetHeight;
 
298 for( var i=tx.rowinfo.length-1; i>=0; i-- ){
299 drawNode(tx.rowinfo[i], btm);
300 }
301 }
302 var selRow;
303
--- src/graph.js
+++ src/graph.js
@@ -21,34 +21,42 @@
21 ** The rowinfo field is an array of structures, one per entry in the timeline,
22 ** where each structure has the following fields:
23 **
24 ** id: The id of the <div> element for the row. This is an integer.
25 ** to get an actual id, prepend "m" to the integer. The top node
26 ** is iTopRow and numbers increase moving down the timeline.
27 ** bg: The background color for this row
28 ** r: The "rail" that the node for this row sits on. The left-most
29 ** rail is 0 and the number increases to the right.
30 ** d: If exists and true then there is a "descender" - an arrow
31 ** coming from the bottom of the page straight up to this node.
32 ** mo: "merge-out". If it exists, this is the rail position
33 ** for the upward portion of a merge arrow. The merge arrow goes as
34 ** a solid normal merge line up to the row identified by "mu" and
35 ** then as a dashed cherrypick merge line up further to "cu".
36 ** If this value is omitted if there are no merge children.
37 ** mu: The id of the row which is the top of the merge-out arrow.
38 ** Only exists if "mo" exists.
39 ** cu: Extend the mu merge arrow up to this row as a cherrypick
40 ** merge line, if this value exists.
41 ** u: Draw a thick child-line out of the top of this node and up to
42 ** the node with an id equal to this value. 0 if it is straight to
43 ** the top of the page, -1 if there is no thick-line riser.
44 ** f: 0x01: a leaf node.
45 ** au: An array of integers that define thick-line risers for branches.
46 ** The integers are in pairs. For each pair, the first integer is
47 ** is the rail on which the riser should run and the second integer
48 ** is the id of the node upto which the riser should run. If there
49 ** are no risers, this array does not exist.
50 ** mi: "merge-in". An array of integer rail positions from which
51 ** merge arrows should be drawn into this node. If the value is
52 ** negative, then the rail position is the absolute value of mi[]
53 ** and a thin merge-arrow descender is drawn to the bottom of
54 ** the screen. This array is omitted if there are no inbound
55 ** merges.
56 ** ci: "cherrypick-in". Like "mi" except for cherrypick merges.
57 ** omitted if there are no cherrypick merges.
58 ** h: The artifact hash of the object being graphed
59 */
60 var amendCssOnce = 1; // Only change the CSS one time
61 function amendCss(circleNodes,showArrowheads){
62 if( !amendCssOnce ) return;
@@ -83,11 +91,12 @@
91 parent.appendChild(canvasDiv);
92
93 var elems = {};
94 var elemClasses = [
95 "rail", "mergeoffset", "node", "arrow u", "arrow u sm", "line",
96 "arrow merge r", "line merge", "arrow warp", "line warp",
97 "line cherrypick"
98 ];
99 for( var i=0; i<elemClasses.length; i++ ){
100 var cls = elemClasses[i];
101 var elem = document.createElement("div");
102 elem.className = "tl-" + cls;
@@ -103,10 +112,11 @@
112 arrow = elems.arrow_u;
113 arrowSmall = elems.arrow_u_sm;
114 line = elems.line;
115 mArrow = elems.arrow_merge_r;
116 mLine = elems.line_merge;
117 cpLine = elems.line_cherrypick;
118 wArrow = elems.arrow_warp;
119 wLine = elems.line_warp;
120
121 var minRailPitch = Math.ceil((node.w+line.w)/2 + mArrow.w + 1);
122 if( window.innerWidth<400 ){
@@ -186,14 +196,20 @@
196 drawLine(line,color,x,y0,null,y1);
197 x = to.x + (node.w-arw.w)/2;
198 var n = drawBox(arw.cls,null,x,y);
199 if(color) n.style.borderBottomColor = color;
200 }
201 /* Draw thin horizontal or vertical lines representing merges */
202 function drawMergeLine(x0,y0,x1,y1){
203 drawLine(mLine,null,x0,y0,x1,y1);
204 }
205 function drawCherrypickLine(x0,y0,x1,y1){
206 drawLine(cpLine,null,x0,y0,x1,y1);
207 }
208 /* Draw an arrow representing an in-bound merge from the "rail"-th rail
209 ** over to the node of "p". Make is a checkpoint merge is "isCP" is true */
210 function drawMergeArrow(p,rail,isCP){
211 var x0 = rail*railPitch + node.w/2;
212 if( rail in mergeLines ){
213 x0 += mergeLines[rail];
214 if( p.r<rail ) x0 += mLine.w;
215 }else{
@@ -200,13 +216,19 @@
216 x0 += (p.r<rail ? -1 : 1)*line.w/2;
217 }
218 var x1 = mArrow.w ? mArrow.w/2 : -node.w/2;
219 x1 = p.x + (p.r<rail ? node.w + Math.ceil(x1) : -x1);
220 var y = miLineY(p);
 
221 var x = p.x + (p.r<rail ? node.w : -mArrow.w);
222 var cls;
223 if( isCP ){
224 drawCherrypickLine(x0,y,x1,null);
225 cls = "arrow cherrypick " + (p.r<rail ? "l" : "r");
226 }else{
227 drawMergeLine(x0,y,x1,null);
228 cls = "arrow merge " + (p.r<rail ? "l" : "r");
229 }
230 drawBox(cls,null,x,y+(mLine.w-mArrow.h)/2);
231 }
232 function drawNode(p, btm){
233 if( p.bg ){
234 var e = document.getElementById("mc"+p.id);
@@ -215,74 +237,112 @@
237 if(e) e.style.backgroundColor = p.bg;
238 }
239 if( p.r<0 ) return;
240 if( p.u>0 ) drawUpArrow(p,tx.rowinfo[p.u-tx.iTopRow],p.fg);
241 var cls = node.cls;
242 if( p.hasOwnProperty('mi') && p.mi.length ) cls += " merge";
243 if( p.f&1 ) cls += " leaf";
244 var n = drawBox(cls,p.bg,p.x,p.y);
245 n.id = "tln"+p.id;
246 n.onclick = clickOnNode;
247 n.style.zIndex = 10;
248 if( !tx.omitDescenders ){
249 if( p.u==0 ) drawUpArrow(p,{x: p.x, y: -node.h},p.fg);
250 if( p.hasOwnProperty('d') ) drawUpArrow({x: p.x, y: btm-node.h/2},p,p.fg);
251 }
252 if( p.hasOwnProperty('mo') ){
253 var x0 = p.x + node.w/2;
254 var x1 = p.mo*railPitch + node.w/2;
255 var u = tx.rowinfo[p.mu-tx.iTopRow];
256 var y1 = miLineY(u);
257 if( p.u<0 || p.mo!=p.r ){
258 x1 += mergeLines[p.mo] = -mLine.w/2;
259 var y0 = p.y+2;
260 if( p.mu==p.id ){
261 drawCherrypickLine(x0,y0,x1+(x0<x1 ? mLine.w : 0),null);
262 y1 = y0;
263 }else{
264 drawMergeLine(x0,y0,x1+(x0<x1 ? mLine.w : 0),null);
265 drawMergeLine(x1,y0+mLine.w,null,y1);
266 }
267 if( p.hasOwnProperty('cu') ){
268 var u2 = tx.rowinfo[p.cu-tx.iTopRow];
269 var y2 = miLineY(u2);
270 drawCherrypickLine(x1,y1,null,y2);
271 }
272 }else if( mergeOffset ){
273 mergeLines[p.mo] = u.r<p.r ? -mergeOffset-mLine.w : mergeOffset;
274 x1 += mergeLines[p.mo];
275 if( p.mo<p.id ){
276 drawMergeLine(x1,p.y+node.h/2,null,y1);
277 }
278 if( p.hasOwnProperty('cu') ){
279 var u2 = tx.rowinfo[p.cu-tx.iTopRow];
280 var y2 = miLineY(u2);
281 drawCherrypickLine(x1,y1,null,y2);
282 }
283 }else{
284 delete mergeLines[p.mo];
285 }
286 }
287 if( p.hasOwnProperty('au') ){
288 for( var i=0; i<p.au.length; i+=2 ){
289 var rail = p.au[i];
290 var x0 = p.x + node.w/2;
291 var x1 = rail*railPitch + (node.w-line.w)/2;
292 if( x0<x1 ){
293 x0 = Math.ceil(x0);
294 x1 += line.w;
295 }
296 var y0 = p.y + (node.h-line.w)/2;
297 var u = tx.rowinfo[p.au[i+1]-tx.iTopRow];
298 if( u.id<p.id ){
299 drawLine(line,u.fg,x0,y0,x1,null);
300 drawUpArrow(p,u,u.fg);
301 }else{
302 var y1 = u.y + (node.h-line.w)/2;
303 drawLine(wLine,u.fg,x0,y0,x1,null);
304 drawLine(wLine,u.fg,x1-line.w,y0,null,y1+line.w);
305 drawLine(wLine,u.fg,x1,y1,u.x-wArrow.w/2,null);
306 var x = u.x-wArrow.w;
307 var y = u.y+(node.h-wArrow.h)/2;
308 var n = drawBox(wArrow.cls,null,x,y);
309 if( u.fg ) n.style.borderLeftColor = u.fg;
310 }
311 }
312 }
313 if( p.hasOwnProperty('mi') ){
314 for( var i=0; i<p.mi.length; i++ ){
315 var rail = p.mi[i];
316 if( rail<0 ){
317 rail = -rail;
318 mergeLines[rail] = -mLine.w/2;
319 var x = rail*railPitch + (node.w-mLine.w)/2;
320 var y = miLineY(p);
321 drawMergeLine(x,y,null,mergeBtm[rail]);
322 mergeBtm[rail] = y;
323 }
324 drawMergeArrow(p,rail,0);
325 }
326 }
327 if( p.hasOwnProperty('ci') ){
328 for( var i=0; i<p.ci.length; i++ ){
329 var rail = p.ci[i];
330 if( rail<0 ){
331 rail = -rail;
332 mergeLines[rail] = -mLine.w/2;
333 var x = rail*railPitch + (node.w-mLine.w)/2;
334 var y = miLineY(p);
335 drawCherrypickLine(x,y,null,mergeBtm[rail]);
336 mergeBtm[rail] = y;
337 }
338 drawMergeArrow(p,rail,1);
339 }
340 }
341 }
342 var mergeLines;
343 var mergeBtm = new Array;
344 function renderGraph(){
345 mergeLines = {};
346 canvasDiv.innerHTML = "";
347 var canvasY = absoluteY(canvasDiv);
348 for(var i=0; i<tx.rowinfo.length; i++ ){
@@ -293,10 +353,11 @@
353 var tlBtm = document.querySelector(".timelineBottom");
354 if( tlBtm.offsetHeight<node.h ){
355 tlBtm.style.height = node.h + "px";
356 }
357 var btm = absoluteY(tlBtm) - canvasY + tlBtm.offsetHeight;
358 for( var i=0; i<tx.nrail; i++) mergeBtm[i] = btm;
359 for( var i=tx.rowinfo.length-1; i>=0; i-- ){
360 drawNode(tx.rowinfo[i], btm);
361 }
362 }
363 var selRow;
364
+141 -25
--- src/info.c
+++ src/info.c
@@ -267,15 +267,28 @@
267267
);
268268
if( !parentsOnly ){
269269
db_multi_exec(
270270
"INSERT OR IGNORE INTO ok SELECT cid FROM plink WHERE pid=%d;", rid
271271
);
272
+ if( db_table_exists("repository","cherrypick") ){
273
+ db_multi_exec(
274
+ "INSERT OR IGNORE INTO ok "
275
+ " SELECT parentid FROM cherrypick WHERE childid=%d;"
276
+ "INSERT OR IGNORE INTO ok "
277
+ " SELECT childid FROM cherrypick WHERE parentid=%d;",
278
+ rid, rid
279
+ );
280
+ }
272281
}
273282
blob_append_sql(&sql, " AND event.objid IN ok ORDER BY mtime DESC");
274283
db_prepare(&q, "%s", blob_sql_text(&sql));
275
- www_print_timeline(&q, TIMELINE_DISJOINT|TIMELINE_GRAPH|TIMELINE_NOSCROLL,
276
- 0, 0, rid, 0);
284
+ www_print_timeline(&q,
285
+ TIMELINE_DISJOINT
286
+ |TIMELINE_GRAPH
287
+ |TIMELINE_NOSCROLL
288
+ |TIMELINE_CHPICK,
289
+ 0, 0, rid, 0);
277290
db_finalize(&q);
278291
}
279292
280293
/*
281294
** Show a graph all wiki, tickets, and check-ins that refer to object zUuid.
@@ -612,20 +625,19 @@
612625
}
613626
614627
/*
615628
** WEBPAGE: vinfo
616629
** WEBPAGE: ci
617
-** URL: /ci?name=ARTIFACTID
618
-** URL: /vinfo?name=ARTIFACTID
619
-**
620
-** Display information about a particular check-in.
621
-**
622
-** We also jump here from /info if the name is a check-in
623
-**
624
-** If the /ci and /vinfo pages used to differ in their default
625
-** diff settings, but now diff settings persist with a cookie and
626
-** so /ci and /vinfo behave the same.
630
+** URL: /ci/ARTIFACTID
631
+** OR: /ci?name=ARTIFACTID
632
+**
633
+** Display information about a particular check-in. The exact
634
+** same information is shown on the /info page if the name query
635
+** parameter to /info describes a check-in.
636
+**
637
+** The ARTIFACTID can be a unique prefix for the HASH of the check-in,
638
+** or a tag or branch name that identifies the check-in.
627639
*/
628640
void ci_page(void){
629641
Stmt q1, q2, q3;
630642
int rid;
631643
int isLeaf;
@@ -677,10 +689,14 @@
677689
const char *zUser;
678690
const char *zOrigUser;
679691
const char *zComment;
680692
const char *zDate;
681693
const char *zOrigDate;
694
+ const char *zBrName;
695
+ int okWiki = 0;
696
+ Blob wiki_read_links = BLOB_INITIALIZER;
697
+ Blob wiki_add_links = BLOB_INITIALIZER;
682698
683699
style_header("Check-in [%S]", zUuid);
684700
login_anonymous_available();
685701
zEUser = db_text(0,
686702
"SELECT value FROM tagxref"
@@ -687,10 +703,13 @@
687703
" WHERE tagid=%d AND rid=%d AND tagtype>0",
688704
TAG_USER, rid);
689705
zEComment = db_text(0,
690706
"SELECT value FROM tagxref WHERE tagid=%d AND rid=%d",
691707
TAG_COMMENT, rid);
708
+ zBrName = db_text(0,
709
+ "SELECT value FROM tagxref WHERE tagid=%d AND rid=%d",
710
+ TAG_BRANCH, rid);
692711
zOrigUser = db_column_text(&q1, 2);
693712
zUser = zEUser ? zEUser : zOrigUser;
694713
zComment = db_column_text(&q1, 3);
695714
zDate = db_column_text(&q1,1);
696715
zOrigDate = db_column_text(&q1, 4);
@@ -741,11 +760,29 @@
741760
" WHERE rid=%d AND tagtype>0 "
742761
" AND tag.tagid=tagxref.tagid "
743762
" AND +tag.tagname GLOB 'sym-*'", rid);
744763
while( db_step(&q2)==SQLITE_ROW ){
745764
const char *zTagName = db_column_text(&q2, 0);
746
- @ | %z(href("%R/timeline?r=%T&unhide",zTagName))%h(zTagName)</a>
765
+ if( fossil_strcmp(zTagName,zBrName)==0 ){
766
+ @ | %z(href("%R/timeline?r=%T&unhide",zTagName))%h(zTagName)</a>
767
+ if( wiki_tagid2("branch",zTagName)!=0 ){
768
+ blob_appendf(&wiki_read_links, " | %z%h</a>",
769
+ href("%R/wiki?name=branch/%h",zTagName), zTagName);
770
+ }else if( g.perm.Write && g.perm.WrWiki ){
771
+ blob_appendf(&wiki_add_links, " | %z%h</a>",
772
+ href("%R/wikiedit?name=branch/%h",zTagName), zTagName);
773
+ }
774
+ }else{
775
+ @ | %z(href("%R/timeline?t=%T&unhide",zTagName))%h(zTagName)</a>
776
+ if( wiki_tagid2("tag",zTagName)!=0 ){
777
+ blob_appendf(&wiki_read_links, " | %z%h</a>",
778
+ href("%R/wiki?name=tag/%h",zTagName), zTagName);
779
+ }else if( g.perm.Write && g.perm.WrWiki ){
780
+ blob_appendf(&wiki_add_links, " | %z%h</a>",
781
+ href("%R/wikiedit?name=tag/%h",zTagName), zTagName);
782
+ }
783
+ }
747784
}
748785
db_finalize(&q2);
749786
@ </td></tr>
750787
751788
@ <tr><th>Files:</th>
@@ -790,10 +827,46 @@
790827
@ <tr><th>Received&nbsp;From:</th>
791828
@ <td>%h(zUser) @ %h(zIpAddr) on %s(zDate)</td></tr>
792829
}
793830
db_finalize(&q2);
794831
}
832
+
833
+ /* Only show links to read wiki pages if the users can read wiki
834
+ ** and if the wiki pages already exist */
835
+ if( g.perm.RdWiki
836
+ && ((okWiki = wiki_tagid2("checkin",zUuid))!=0 ||
837
+ blob_size(&wiki_read_links)>0)
838
+ && db_get_boolean("wiki-about",1)
839
+ ){
840
+ const char *zLinks = blob_str(&wiki_read_links);
841
+ @ <tr><th>Wiki:</th><td>\
842
+ if( okWiki ){
843
+ @ %z(href("%R/wiki?name=checkin/%s",zUuid))this checkin</a>\
844
+ }else if( zLinks[0] ){
845
+ zLinks += 3;
846
+ }
847
+ @ %s(zLinks)</td></tr>
848
+ }
849
+
850
+ /* Only show links to create new wiki pages if the users can write wiki
851
+ ** and if the wiki pages do not already exist */
852
+ if( g.perm.WrWiki
853
+ && g.perm.RdWiki
854
+ && g.perm.Write
855
+ && (blob_size(&wiki_add_links)>0 || !okWiki)
856
+ && db_get_boolean("wiki-about",1)
857
+ ){
858
+ const char *zLinks = blob_str(&wiki_add_links);
859
+ @ <tr><th>Add&nbsp;Wiki:</th><td>\
860
+ if( !okWiki ){
861
+ @ %z(href("%R/wikiedit?name=checkin/%s",zUuid))this checkin</a>\
862
+ }else if( zLinks[0] ){
863
+ zLinks += 3;
864
+ }
865
+ @ %s(zLinks)</td></tr>
866
+ }
867
+
795868
if( g.perm.Hyperlink ){
796869
@ <tr><th>Other&nbsp;Links:</th>
797870
@ <td>
798871
@ %z(href("%R/artifact/%!S",zUuid))manifest</a>
799872
@ | %z(href("%R/ci_tags/%!S",zUuid))tags</a>
@@ -805,15 +878,20 @@
805878
}
806879
@ </td>
807880
@ </tr>
808881
}
809882
@ </table>
883
+ blob_reset(&wiki_read_links);
884
+ blob_reset(&wiki_add_links);
810885
}else{
811886
style_header("Check-in Information");
812887
login_anonymous_available();
813888
}
814889
db_finalize(&q1);
890
+ if( !PB("nowiki") ){
891
+ wiki_render_associated("checkin", zUuid, 0);
892
+ }
815893
render_backlink_graph(zUuid, "<div class=\"section\">References</div>\n");
816894
@ <div class="section">Context</div>
817895
render_checkin_context(rid, 0);
818896
@ <div class="section">Changes</div>
819897
@ <div class="sectionmenu">
@@ -891,10 +969,12 @@
891969
char *zUuid;
892970
char *zDate;
893971
Blob wiki;
894972
int modPending;
895973
const char *zModAction;
974
+ int tagid;
975
+ int ridNext;
896976
897977
login_check_credentials();
898978
if( !g.perm.RdWiki ){ login_needed(g.anon.RdWiki); return; }
899979
rid = name_to_rid_www("name");
900980
if( rid==0 || (pWiki = manifest_get(rid, CFTYPE_WIKI, 0))==0 ){
@@ -937,11 +1017,13 @@
9371017
if( g.perm.Setup ){
9381018
@ (%d(rid))
9391019
}
9401020
modPending = moderation_pending_www(rid);
9411021
@ </td></tr>
942
- @ <tr><th>Page&nbsp;Name:</th><td>%h(pWiki->zWikiTitle)</td></tr>
1022
+ @ <tr><th>Page&nbsp;Name:</th>\
1023
+ @ <td>%z(href("%R/whistory?name=%h",pWiki->zWikiTitle))\
1024
+ @ %h(pWiki->zWikiTitle)</a></td></tr>
9431025
@ <tr><th>Date:</th><td>
9441026
hyperlink_to_date(zDate, "</td></tr>");
9451027
@ <tr><th>Original&nbsp;User:</th><td>
9461028
hyperlink_to_user(pWiki->zUser, zDate, "</td></tr>");
9471029
if( pWiki->zMimetype ){
@@ -951,13 +1033,20 @@
9511033
int i;
9521034
@ <tr><th>Parent%s(pWiki->nParent==1?"":"s"):</th><td>
9531035
for(i=0; i<pWiki->nParent; i++){
9541036
char *zParent = pWiki->azParent[i];
9551037
@ %z(href("info/%!S",zParent))%s(zParent)</a>
1038
+ @ %z(href("%R/wdiff?id=%!S&pid=%!S",zUuid,zParent))(diff)</a>
9561039
}
9571040
@ </td></tr>
9581041
}
1042
+ tagid = wiki_tagid(pWiki->zWikiTitle);
1043
+ if( tagid>0 && (ridNext = wiki_next(tagid, pWiki->rDate))>0 ){
1044
+ char *zId = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", ridNext);
1045
+ @ <tr><th>Next</th>
1046
+ @ <td>%z(href("%R/info/%!S",zId))%s(zId)</a></td>
1047
+ }
9591048
@ </table>
9601049
9611050
if( g.perm.ModWiki && modPending ){
9621051
@ <div class="section">Moderation</div>
9631052
@ <blockquote>
@@ -2478,11 +2567,17 @@
24782567
24792568
/*
24802569
** The apply_newtags method is called after all newtags have been added
24812570
** and the control artifact is completed and then written to the DB.
24822571
*/
2483
-static void apply_newtags(Blob *ctrl, int rid, const char *zUuid){
2572
+static void apply_newtags(
2573
+ Blob *ctrl,
2574
+ int rid,
2575
+ const char *zUuid,
2576
+ const char *zUserOvrd, /* The user name on the control artifact */
2577
+ int fDryRun /* Print control artifact, but make no changes */
2578
+){
24842579
Stmt q;
24852580
int nChng = 0;
24862581
24872582
db_prepare(&q, "SELECT tag, prefix, value FROM newtags"
24882583
" ORDER BY prefix || tag");
@@ -2499,19 +2594,29 @@
24992594
}
25002595
db_finalize(&q);
25012596
if( nChng>0 ){
25022597
int nrid;
25032598
Blob cksum;
2504
- blob_appendf(ctrl, "U %F\n", login_name());
2599
+ if( zUserOvrd && zUserOvrd[0] ){
2600
+ blob_appendf(ctrl, "U %F\n", zUserOvrd);
2601
+ }else{
2602
+ blob_appendf(ctrl, "U %F\n", login_name());
2603
+ }
25052604
md5sum_blob(ctrl, &cksum);
25062605
blob_appendf(ctrl, "Z %b\n", &cksum);
2507
- db_begin_transaction();
2508
- g.markPrivate = content_is_private(rid);
2509
- nrid = content_put(ctrl);
2510
- manifest_crosslink(nrid, ctrl, MC_PERMIT_HOOKS);
2606
+ if( fDryRun ){
2607
+ assert( g.isHTTP==0 ); /* Only print control artifact in console mode. */
2608
+ fossil_print("%s", blob_str(ctrl));
2609
+ blob_reset(ctrl);
2610
+ }else{
2611
+ db_begin_transaction();
2612
+ g.markPrivate = content_is_private(rid);
2613
+ nrid = content_put(ctrl);
2614
+ manifest_crosslink(nrid, ctrl, MC_PERMIT_HOOKS);
2615
+ db_end_transaction(0);
2616
+ }
25112617
assert( blob_is_reset(ctrl) );
2512
- db_end_transaction(0);
25132618
}
25142619
}
25152620
25162621
/*
25172622
** This method checks that the date can be parsed.
@@ -2647,11 +2752,11 @@
26472752
db_finalize(&q);
26482753
if( zHideFlag[0] ) hide_branch();
26492754
if( zCloseFlag[0] ) close_leaf(rid);
26502755
if( zNewTagFlag[0] && zNewTag[0] ) add_tag(zNewTag);
26512756
if( zNewBrFlag[0] && zNewBranch[0] ) change_branch(rid,zNewBranch);
2652
- apply_newtags(&ctrl, rid, zUuid);
2757
+ apply_newtags(&ctrl, rid, zUuid, 0, 0);
26532758
cgi_redirectf("%R/ci/%S", zUuid);
26542759
}
26552760
blob_zero(&comment);
26562761
blob_append(&comment, zNewComment, -1);
26572762
zUuid[10] = 0;
@@ -2901,10 +3006,13 @@
29013006
** --tag TAG Add new TAG to this check-in
29023007
** --cancel TAG Cancel TAG from this check-in
29033008
** --branch NAME Make this check-in the start of branch NAME
29043009
** --hide Hide branch starting from this check-in
29053010
** --close Mark this "leaf" as closed
3011
+** -n|--dry-run Print control artifact, but make no changes
3012
+** --date-override DATETIME Set the change time on the control artifact
3013
+** --user-override USER Set the user name on the control artifact
29063014
**
29073015
** DATETIME may be "now" or "YYYY-MM-DDTHH:MM:SS.SSS". If in
29083016
** year-month-day form, it may be truncated, the "T" may be replaced by
29093017
** a space, and it may also name a timezone offset from UTC as "-HH:MM"
29103018
** (westward) or "+HH:MM" (eastward). Either no timezone suffix or "Z"
@@ -2930,11 +3038,13 @@
29303038
int fPropagateColor; /* True if color propagates before amend */
29313039
int fNewPropagateColor = 0; /* True if color propagates after amend */
29323040
int fHasHidden = 0; /* True if hidden tag already set */
29333041
int fHasClosed = 0; /* True if closed tag already set */
29343042
int fEditComment; /* True if editor to be used for comment */
3043
+ int fDryRun; /* Print control artifact, make no changes */
29353044
const char *zChngTime; /* The change time on the control artifact */
3045
+ const char *zUserOvrd; /* The user name on the control artifact */
29363046
const char *zUuid;
29373047
Blob ctrl;
29383048
Blob comment;
29393049
char *zNow;
29403050
int nTags, nCancels;
@@ -2956,11 +3066,15 @@
29563066
zNewUser = find_option("author",0,1);
29573067
pzNewTags = find_repeatable_option("tag",0,&nTags);
29583068
pzCancelTags = find_repeatable_option("cancel",0,&nCancels);
29593069
fClose = find_option("close",0,0)!=0;
29603070
fHide = find_option("hide",0,0)!=0;
2961
- zChngTime = find_option("chngtime",0,1);
3071
+ fDryRun = find_option("dry-run","n",0)!=0;
3072
+ if( fDryRun==0 ) fDryRun = find_option("dryrun","n",0)!=0;
3073
+ zChngTime = find_option("date-override",0,1);
3074
+ if( zChngTime==0 ) zChngTime = find_option("chngtime",0,1);
3075
+ zUserOvrd = find_option("user-override",0,1);
29623076
db_find_and_open_repository(0,0);
29633077
user_select();
29643078
verify_all_options();
29653079
if( g.argc<3 || g.argc>=4 ) usage(AMEND_USAGE_STMT);
29663080
rid = name_to_typed_rid(g.argv[2], "ci");
@@ -3052,8 +3166,10 @@
30523166
fossil_free((void *)pzCancelTags);
30533167
}
30543168
if( fHide && !fHasHidden ) hide_branch();
30553169
if( fClose && !fHasClosed ) close_leaf(rid);
30563170
if( zNewBranch && zNewBranch[0] ) change_branch(rid,zNewBranch);
3057
- apply_newtags(&ctrl, rid, zUuid);
3058
- show_common_info(rid, "uuid:", 1, 0);
3171
+ apply_newtags(&ctrl, rid, zUuid, zUserOvrd, fDryRun);
3172
+ if( fDryRun==0 ){
3173
+ show_common_info(rid, "uuid:", 1, 0);
3174
+ }
30593175
}
30603176
--- src/info.c
+++ src/info.c
@@ -267,15 +267,28 @@
267 );
268 if( !parentsOnly ){
269 db_multi_exec(
270 "INSERT OR IGNORE INTO ok SELECT cid FROM plink WHERE pid=%d;", rid
271 );
 
 
 
 
 
 
 
 
 
272 }
273 blob_append_sql(&sql, " AND event.objid IN ok ORDER BY mtime DESC");
274 db_prepare(&q, "%s", blob_sql_text(&sql));
275 www_print_timeline(&q, TIMELINE_DISJOINT|TIMELINE_GRAPH|TIMELINE_NOSCROLL,
276 0, 0, rid, 0);
 
 
 
 
277 db_finalize(&q);
278 }
279
280 /*
281 ** Show a graph all wiki, tickets, and check-ins that refer to object zUuid.
@@ -612,20 +625,19 @@
612 }
613
614 /*
615 ** WEBPAGE: vinfo
616 ** WEBPAGE: ci
617 ** URL: /ci?name=ARTIFACTID
618 ** URL: /vinfo?name=ARTIFACTID
619 **
620 ** Display information about a particular check-in.
621 **
622 ** We also jump here from /info if the name is a check-in
623 **
624 ** If the /ci and /vinfo pages used to differ in their default
625 ** diff settings, but now diff settings persist with a cookie and
626 ** so /ci and /vinfo behave the same.
627 */
628 void ci_page(void){
629 Stmt q1, q2, q3;
630 int rid;
631 int isLeaf;
@@ -677,10 +689,14 @@
677 const char *zUser;
678 const char *zOrigUser;
679 const char *zComment;
680 const char *zDate;
681 const char *zOrigDate;
 
 
 
 
682
683 style_header("Check-in [%S]", zUuid);
684 login_anonymous_available();
685 zEUser = db_text(0,
686 "SELECT value FROM tagxref"
@@ -687,10 +703,13 @@
687 " WHERE tagid=%d AND rid=%d AND tagtype>0",
688 TAG_USER, rid);
689 zEComment = db_text(0,
690 "SELECT value FROM tagxref WHERE tagid=%d AND rid=%d",
691 TAG_COMMENT, rid);
 
 
 
692 zOrigUser = db_column_text(&q1, 2);
693 zUser = zEUser ? zEUser : zOrigUser;
694 zComment = db_column_text(&q1, 3);
695 zDate = db_column_text(&q1,1);
696 zOrigDate = db_column_text(&q1, 4);
@@ -741,11 +760,29 @@
741 " WHERE rid=%d AND tagtype>0 "
742 " AND tag.tagid=tagxref.tagid "
743 " AND +tag.tagname GLOB 'sym-*'", rid);
744 while( db_step(&q2)==SQLITE_ROW ){
745 const char *zTagName = db_column_text(&q2, 0);
746 @ | %z(href("%R/timeline?r=%T&unhide",zTagName))%h(zTagName)</a>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
747 }
748 db_finalize(&q2);
749 @ </td></tr>
750
751 @ <tr><th>Files:</th>
@@ -790,10 +827,46 @@
790 @ <tr><th>Received&nbsp;From:</th>
791 @ <td>%h(zUser) @ %h(zIpAddr) on %s(zDate)</td></tr>
792 }
793 db_finalize(&q2);
794 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
795 if( g.perm.Hyperlink ){
796 @ <tr><th>Other&nbsp;Links:</th>
797 @ <td>
798 @ %z(href("%R/artifact/%!S",zUuid))manifest</a>
799 @ | %z(href("%R/ci_tags/%!S",zUuid))tags</a>
@@ -805,15 +878,20 @@
805 }
806 @ </td>
807 @ </tr>
808 }
809 @ </table>
 
 
810 }else{
811 style_header("Check-in Information");
812 login_anonymous_available();
813 }
814 db_finalize(&q1);
 
 
 
815 render_backlink_graph(zUuid, "<div class=\"section\">References</div>\n");
816 @ <div class="section">Context</div>
817 render_checkin_context(rid, 0);
818 @ <div class="section">Changes</div>
819 @ <div class="sectionmenu">
@@ -891,10 +969,12 @@
891 char *zUuid;
892 char *zDate;
893 Blob wiki;
894 int modPending;
895 const char *zModAction;
 
 
896
897 login_check_credentials();
898 if( !g.perm.RdWiki ){ login_needed(g.anon.RdWiki); return; }
899 rid = name_to_rid_www("name");
900 if( rid==0 || (pWiki = manifest_get(rid, CFTYPE_WIKI, 0))==0 ){
@@ -937,11 +1017,13 @@
937 if( g.perm.Setup ){
938 @ (%d(rid))
939 }
940 modPending = moderation_pending_www(rid);
941 @ </td></tr>
942 @ <tr><th>Page&nbsp;Name:</th><td>%h(pWiki->zWikiTitle)</td></tr>
 
 
943 @ <tr><th>Date:</th><td>
944 hyperlink_to_date(zDate, "</td></tr>");
945 @ <tr><th>Original&nbsp;User:</th><td>
946 hyperlink_to_user(pWiki->zUser, zDate, "</td></tr>");
947 if( pWiki->zMimetype ){
@@ -951,13 +1033,20 @@
951 int i;
952 @ <tr><th>Parent%s(pWiki->nParent==1?"":"s"):</th><td>
953 for(i=0; i<pWiki->nParent; i++){
954 char *zParent = pWiki->azParent[i];
955 @ %z(href("info/%!S",zParent))%s(zParent)</a>
 
956 }
957 @ </td></tr>
958 }
 
 
 
 
 
 
959 @ </table>
960
961 if( g.perm.ModWiki && modPending ){
962 @ <div class="section">Moderation</div>
963 @ <blockquote>
@@ -2478,11 +2567,17 @@
2478
2479 /*
2480 ** The apply_newtags method is called after all newtags have been added
2481 ** and the control artifact is completed and then written to the DB.
2482 */
2483 static void apply_newtags(Blob *ctrl, int rid, const char *zUuid){
 
 
 
 
 
 
2484 Stmt q;
2485 int nChng = 0;
2486
2487 db_prepare(&q, "SELECT tag, prefix, value FROM newtags"
2488 " ORDER BY prefix || tag");
@@ -2499,19 +2594,29 @@
2499 }
2500 db_finalize(&q);
2501 if( nChng>0 ){
2502 int nrid;
2503 Blob cksum;
2504 blob_appendf(ctrl, "U %F\n", login_name());
 
 
 
 
2505 md5sum_blob(ctrl, &cksum);
2506 blob_appendf(ctrl, "Z %b\n", &cksum);
2507 db_begin_transaction();
2508 g.markPrivate = content_is_private(rid);
2509 nrid = content_put(ctrl);
2510 manifest_crosslink(nrid, ctrl, MC_PERMIT_HOOKS);
 
 
 
 
 
 
 
2511 assert( blob_is_reset(ctrl) );
2512 db_end_transaction(0);
2513 }
2514 }
2515
2516 /*
2517 ** This method checks that the date can be parsed.
@@ -2647,11 +2752,11 @@
2647 db_finalize(&q);
2648 if( zHideFlag[0] ) hide_branch();
2649 if( zCloseFlag[0] ) close_leaf(rid);
2650 if( zNewTagFlag[0] && zNewTag[0] ) add_tag(zNewTag);
2651 if( zNewBrFlag[0] && zNewBranch[0] ) change_branch(rid,zNewBranch);
2652 apply_newtags(&ctrl, rid, zUuid);
2653 cgi_redirectf("%R/ci/%S", zUuid);
2654 }
2655 blob_zero(&comment);
2656 blob_append(&comment, zNewComment, -1);
2657 zUuid[10] = 0;
@@ -2901,10 +3006,13 @@
2901 ** --tag TAG Add new TAG to this check-in
2902 ** --cancel TAG Cancel TAG from this check-in
2903 ** --branch NAME Make this check-in the start of branch NAME
2904 ** --hide Hide branch starting from this check-in
2905 ** --close Mark this "leaf" as closed
 
 
 
2906 **
2907 ** DATETIME may be "now" or "YYYY-MM-DDTHH:MM:SS.SSS". If in
2908 ** year-month-day form, it may be truncated, the "T" may be replaced by
2909 ** a space, and it may also name a timezone offset from UTC as "-HH:MM"
2910 ** (westward) or "+HH:MM" (eastward). Either no timezone suffix or "Z"
@@ -2930,11 +3038,13 @@
2930 int fPropagateColor; /* True if color propagates before amend */
2931 int fNewPropagateColor = 0; /* True if color propagates after amend */
2932 int fHasHidden = 0; /* True if hidden tag already set */
2933 int fHasClosed = 0; /* True if closed tag already set */
2934 int fEditComment; /* True if editor to be used for comment */
 
2935 const char *zChngTime; /* The change time on the control artifact */
 
2936 const char *zUuid;
2937 Blob ctrl;
2938 Blob comment;
2939 char *zNow;
2940 int nTags, nCancels;
@@ -2956,11 +3066,15 @@
2956 zNewUser = find_option("author",0,1);
2957 pzNewTags = find_repeatable_option("tag",0,&nTags);
2958 pzCancelTags = find_repeatable_option("cancel",0,&nCancels);
2959 fClose = find_option("close",0,0)!=0;
2960 fHide = find_option("hide",0,0)!=0;
2961 zChngTime = find_option("chngtime",0,1);
 
 
 
 
2962 db_find_and_open_repository(0,0);
2963 user_select();
2964 verify_all_options();
2965 if( g.argc<3 || g.argc>=4 ) usage(AMEND_USAGE_STMT);
2966 rid = name_to_typed_rid(g.argv[2], "ci");
@@ -3052,8 +3166,10 @@
3052 fossil_free((void *)pzCancelTags);
3053 }
3054 if( fHide && !fHasHidden ) hide_branch();
3055 if( fClose && !fHasClosed ) close_leaf(rid);
3056 if( zNewBranch && zNewBranch[0] ) change_branch(rid,zNewBranch);
3057 apply_newtags(&ctrl, rid, zUuid);
3058 show_common_info(rid, "uuid:", 1, 0);
 
 
3059 }
3060
--- src/info.c
+++ src/info.c
@@ -267,15 +267,28 @@
267 );
268 if( !parentsOnly ){
269 db_multi_exec(
270 "INSERT OR IGNORE INTO ok SELECT cid FROM plink WHERE pid=%d;", rid
271 );
272 if( db_table_exists("repository","cherrypick") ){
273 db_multi_exec(
274 "INSERT OR IGNORE INTO ok "
275 " SELECT parentid FROM cherrypick WHERE childid=%d;"
276 "INSERT OR IGNORE INTO ok "
277 " SELECT childid FROM cherrypick WHERE parentid=%d;",
278 rid, rid
279 );
280 }
281 }
282 blob_append_sql(&sql, " AND event.objid IN ok ORDER BY mtime DESC");
283 db_prepare(&q, "%s", blob_sql_text(&sql));
284 www_print_timeline(&q,
285 TIMELINE_DISJOINT
286 |TIMELINE_GRAPH
287 |TIMELINE_NOSCROLL
288 |TIMELINE_CHPICK,
289 0, 0, rid, 0);
290 db_finalize(&q);
291 }
292
293 /*
294 ** Show a graph all wiki, tickets, and check-ins that refer to object zUuid.
@@ -612,20 +625,19 @@
625 }
626
627 /*
628 ** WEBPAGE: vinfo
629 ** WEBPAGE: ci
630 ** URL: /ci/ARTIFACTID
631 ** OR: /ci?name=ARTIFACTID
632 **
633 ** Display information about a particular check-in. The exact
634 ** same information is shown on the /info page if the name query
635 ** parameter to /info describes a check-in.
636 **
637 ** The ARTIFACTID can be a unique prefix for the HASH of the check-in,
638 ** or a tag or branch name that identifies the check-in.
 
639 */
640 void ci_page(void){
641 Stmt q1, q2, q3;
642 int rid;
643 int isLeaf;
@@ -677,10 +689,14 @@
689 const char *zUser;
690 const char *zOrigUser;
691 const char *zComment;
692 const char *zDate;
693 const char *zOrigDate;
694 const char *zBrName;
695 int okWiki = 0;
696 Blob wiki_read_links = BLOB_INITIALIZER;
697 Blob wiki_add_links = BLOB_INITIALIZER;
698
699 style_header("Check-in [%S]", zUuid);
700 login_anonymous_available();
701 zEUser = db_text(0,
702 "SELECT value FROM tagxref"
@@ -687,10 +703,13 @@
703 " WHERE tagid=%d AND rid=%d AND tagtype>0",
704 TAG_USER, rid);
705 zEComment = db_text(0,
706 "SELECT value FROM tagxref WHERE tagid=%d AND rid=%d",
707 TAG_COMMENT, rid);
708 zBrName = db_text(0,
709 "SELECT value FROM tagxref WHERE tagid=%d AND rid=%d",
710 TAG_BRANCH, rid);
711 zOrigUser = db_column_text(&q1, 2);
712 zUser = zEUser ? zEUser : zOrigUser;
713 zComment = db_column_text(&q1, 3);
714 zDate = db_column_text(&q1,1);
715 zOrigDate = db_column_text(&q1, 4);
@@ -741,11 +760,29 @@
760 " WHERE rid=%d AND tagtype>0 "
761 " AND tag.tagid=tagxref.tagid "
762 " AND +tag.tagname GLOB 'sym-*'", rid);
763 while( db_step(&q2)==SQLITE_ROW ){
764 const char *zTagName = db_column_text(&q2, 0);
765 if( fossil_strcmp(zTagName,zBrName)==0 ){
766 @ | %z(href("%R/timeline?r=%T&unhide",zTagName))%h(zTagName)</a>
767 if( wiki_tagid2("branch",zTagName)!=0 ){
768 blob_appendf(&wiki_read_links, " | %z%h</a>",
769 href("%R/wiki?name=branch/%h",zTagName), zTagName);
770 }else if( g.perm.Write && g.perm.WrWiki ){
771 blob_appendf(&wiki_add_links, " | %z%h</a>",
772 href("%R/wikiedit?name=branch/%h",zTagName), zTagName);
773 }
774 }else{
775 @ | %z(href("%R/timeline?t=%T&unhide",zTagName))%h(zTagName)</a>
776 if( wiki_tagid2("tag",zTagName)!=0 ){
777 blob_appendf(&wiki_read_links, " | %z%h</a>",
778 href("%R/wiki?name=tag/%h",zTagName), zTagName);
779 }else if( g.perm.Write && g.perm.WrWiki ){
780 blob_appendf(&wiki_add_links, " | %z%h</a>",
781 href("%R/wikiedit?name=tag/%h",zTagName), zTagName);
782 }
783 }
784 }
785 db_finalize(&q2);
786 @ </td></tr>
787
788 @ <tr><th>Files:</th>
@@ -790,10 +827,46 @@
827 @ <tr><th>Received&nbsp;From:</th>
828 @ <td>%h(zUser) @ %h(zIpAddr) on %s(zDate)</td></tr>
829 }
830 db_finalize(&q2);
831 }
832
833 /* Only show links to read wiki pages if the users can read wiki
834 ** and if the wiki pages already exist */
835 if( g.perm.RdWiki
836 && ((okWiki = wiki_tagid2("checkin",zUuid))!=0 ||
837 blob_size(&wiki_read_links)>0)
838 && db_get_boolean("wiki-about",1)
839 ){
840 const char *zLinks = blob_str(&wiki_read_links);
841 @ <tr><th>Wiki:</th><td>\
842 if( okWiki ){
843 @ %z(href("%R/wiki?name=checkin/%s",zUuid))this checkin</a>\
844 }else if( zLinks[0] ){
845 zLinks += 3;
846 }
847 @ %s(zLinks)</td></tr>
848 }
849
850 /* Only show links to create new wiki pages if the users can write wiki
851 ** and if the wiki pages do not already exist */
852 if( g.perm.WrWiki
853 && g.perm.RdWiki
854 && g.perm.Write
855 && (blob_size(&wiki_add_links)>0 || !okWiki)
856 && db_get_boolean("wiki-about",1)
857 ){
858 const char *zLinks = blob_str(&wiki_add_links);
859 @ <tr><th>Add&nbsp;Wiki:</th><td>\
860 if( !okWiki ){
861 @ %z(href("%R/wikiedit?name=checkin/%s",zUuid))this checkin</a>\
862 }else if( zLinks[0] ){
863 zLinks += 3;
864 }
865 @ %s(zLinks)</td></tr>
866 }
867
868 if( g.perm.Hyperlink ){
869 @ <tr><th>Other&nbsp;Links:</th>
870 @ <td>
871 @ %z(href("%R/artifact/%!S",zUuid))manifest</a>
872 @ | %z(href("%R/ci_tags/%!S",zUuid))tags</a>
@@ -805,15 +878,20 @@
878 }
879 @ </td>
880 @ </tr>
881 }
882 @ </table>
883 blob_reset(&wiki_read_links);
884 blob_reset(&wiki_add_links);
885 }else{
886 style_header("Check-in Information");
887 login_anonymous_available();
888 }
889 db_finalize(&q1);
890 if( !PB("nowiki") ){
891 wiki_render_associated("checkin", zUuid, 0);
892 }
893 render_backlink_graph(zUuid, "<div class=\"section\">References</div>\n");
894 @ <div class="section">Context</div>
895 render_checkin_context(rid, 0);
896 @ <div class="section">Changes</div>
897 @ <div class="sectionmenu">
@@ -891,10 +969,12 @@
969 char *zUuid;
970 char *zDate;
971 Blob wiki;
972 int modPending;
973 const char *zModAction;
974 int tagid;
975 int ridNext;
976
977 login_check_credentials();
978 if( !g.perm.RdWiki ){ login_needed(g.anon.RdWiki); return; }
979 rid = name_to_rid_www("name");
980 if( rid==0 || (pWiki = manifest_get(rid, CFTYPE_WIKI, 0))==0 ){
@@ -937,11 +1017,13 @@
1017 if( g.perm.Setup ){
1018 @ (%d(rid))
1019 }
1020 modPending = moderation_pending_www(rid);
1021 @ </td></tr>
1022 @ <tr><th>Page&nbsp;Name:</th>\
1023 @ <td>%z(href("%R/whistory?name=%h",pWiki->zWikiTitle))\
1024 @ %h(pWiki->zWikiTitle)</a></td></tr>
1025 @ <tr><th>Date:</th><td>
1026 hyperlink_to_date(zDate, "</td></tr>");
1027 @ <tr><th>Original&nbsp;User:</th><td>
1028 hyperlink_to_user(pWiki->zUser, zDate, "</td></tr>");
1029 if( pWiki->zMimetype ){
@@ -951,13 +1033,20 @@
1033 int i;
1034 @ <tr><th>Parent%s(pWiki->nParent==1?"":"s"):</th><td>
1035 for(i=0; i<pWiki->nParent; i++){
1036 char *zParent = pWiki->azParent[i];
1037 @ %z(href("info/%!S",zParent))%s(zParent)</a>
1038 @ %z(href("%R/wdiff?id=%!S&pid=%!S",zUuid,zParent))(diff)</a>
1039 }
1040 @ </td></tr>
1041 }
1042 tagid = wiki_tagid(pWiki->zWikiTitle);
1043 if( tagid>0 && (ridNext = wiki_next(tagid, pWiki->rDate))>0 ){
1044 char *zId = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", ridNext);
1045 @ <tr><th>Next</th>
1046 @ <td>%z(href("%R/info/%!S",zId))%s(zId)</a></td>
1047 }
1048 @ </table>
1049
1050 if( g.perm.ModWiki && modPending ){
1051 @ <div class="section">Moderation</div>
1052 @ <blockquote>
@@ -2478,11 +2567,17 @@
2567
2568 /*
2569 ** The apply_newtags method is called after all newtags have been added
2570 ** and the control artifact is completed and then written to the DB.
2571 */
2572 static void apply_newtags(
2573 Blob *ctrl,
2574 int rid,
2575 const char *zUuid,
2576 const char *zUserOvrd, /* The user name on the control artifact */
2577 int fDryRun /* Print control artifact, but make no changes */
2578 ){
2579 Stmt q;
2580 int nChng = 0;
2581
2582 db_prepare(&q, "SELECT tag, prefix, value FROM newtags"
2583 " ORDER BY prefix || tag");
@@ -2499,19 +2594,29 @@
2594 }
2595 db_finalize(&q);
2596 if( nChng>0 ){
2597 int nrid;
2598 Blob cksum;
2599 if( zUserOvrd && zUserOvrd[0] ){
2600 blob_appendf(ctrl, "U %F\n", zUserOvrd);
2601 }else{
2602 blob_appendf(ctrl, "U %F\n", login_name());
2603 }
2604 md5sum_blob(ctrl, &cksum);
2605 blob_appendf(ctrl, "Z %b\n", &cksum);
2606 if( fDryRun ){
2607 assert( g.isHTTP==0 ); /* Only print control artifact in console mode. */
2608 fossil_print("%s", blob_str(ctrl));
2609 blob_reset(ctrl);
2610 }else{
2611 db_begin_transaction();
2612 g.markPrivate = content_is_private(rid);
2613 nrid = content_put(ctrl);
2614 manifest_crosslink(nrid, ctrl, MC_PERMIT_HOOKS);
2615 db_end_transaction(0);
2616 }
2617 assert( blob_is_reset(ctrl) );
 
2618 }
2619 }
2620
2621 /*
2622 ** This method checks that the date can be parsed.
@@ -2647,11 +2752,11 @@
2752 db_finalize(&q);
2753 if( zHideFlag[0] ) hide_branch();
2754 if( zCloseFlag[0] ) close_leaf(rid);
2755 if( zNewTagFlag[0] && zNewTag[0] ) add_tag(zNewTag);
2756 if( zNewBrFlag[0] && zNewBranch[0] ) change_branch(rid,zNewBranch);
2757 apply_newtags(&ctrl, rid, zUuid, 0, 0);
2758 cgi_redirectf("%R/ci/%S", zUuid);
2759 }
2760 blob_zero(&comment);
2761 blob_append(&comment, zNewComment, -1);
2762 zUuid[10] = 0;
@@ -2901,10 +3006,13 @@
3006 ** --tag TAG Add new TAG to this check-in
3007 ** --cancel TAG Cancel TAG from this check-in
3008 ** --branch NAME Make this check-in the start of branch NAME
3009 ** --hide Hide branch starting from this check-in
3010 ** --close Mark this "leaf" as closed
3011 ** -n|--dry-run Print control artifact, but make no changes
3012 ** --date-override DATETIME Set the change time on the control artifact
3013 ** --user-override USER Set the user name on the control artifact
3014 **
3015 ** DATETIME may be "now" or "YYYY-MM-DDTHH:MM:SS.SSS". If in
3016 ** year-month-day form, it may be truncated, the "T" may be replaced by
3017 ** a space, and it may also name a timezone offset from UTC as "-HH:MM"
3018 ** (westward) or "+HH:MM" (eastward). Either no timezone suffix or "Z"
@@ -2930,11 +3038,13 @@
3038 int fPropagateColor; /* True if color propagates before amend */
3039 int fNewPropagateColor = 0; /* True if color propagates after amend */
3040 int fHasHidden = 0; /* True if hidden tag already set */
3041 int fHasClosed = 0; /* True if closed tag already set */
3042 int fEditComment; /* True if editor to be used for comment */
3043 int fDryRun; /* Print control artifact, make no changes */
3044 const char *zChngTime; /* The change time on the control artifact */
3045 const char *zUserOvrd; /* The user name on the control artifact */
3046 const char *zUuid;
3047 Blob ctrl;
3048 Blob comment;
3049 char *zNow;
3050 int nTags, nCancels;
@@ -2956,11 +3066,15 @@
3066 zNewUser = find_option("author",0,1);
3067 pzNewTags = find_repeatable_option("tag",0,&nTags);
3068 pzCancelTags = find_repeatable_option("cancel",0,&nCancels);
3069 fClose = find_option("close",0,0)!=0;
3070 fHide = find_option("hide",0,0)!=0;
3071 fDryRun = find_option("dry-run","n",0)!=0;
3072 if( fDryRun==0 ) fDryRun = find_option("dryrun","n",0)!=0;
3073 zChngTime = find_option("date-override",0,1);
3074 if( zChngTime==0 ) zChngTime = find_option("chngtime",0,1);
3075 zUserOvrd = find_option("user-override",0,1);
3076 db_find_and_open_repository(0,0);
3077 user_select();
3078 verify_all_options();
3079 if( g.argc<3 || g.argc>=4 ) usage(AMEND_USAGE_STMT);
3080 rid = name_to_typed_rid(g.argv[2], "ci");
@@ -3052,8 +3166,10 @@
3166 fossil_free((void *)pzCancelTags);
3167 }
3168 if( fHide && !fHasHidden ) hide_branch();
3169 if( fClose && !fHasClosed ) close_leaf(rid);
3170 if( zNewBranch && zNewBranch[0] ) change_branch(rid,zNewBranch);
3171 apply_newtags(&ctrl, rid, zUuid, zUserOvrd, fDryRun);
3172 if( fDryRun==0 ){
3173 show_common_info(rid, "uuid:", 1, 0);
3174 }
3175 }
3176
--- src/manifest.c
+++ src/manifest.c
@@ -2104,10 +2104,21 @@
21042104
db_begin_transaction();
21052105
if( p->type==CFTYPE_MANIFEST ){
21062106
if( permitHooks ){
21072107
zScript = xfer_commit_code();
21082108
zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid);
2109
+ }
2110
+ if( p->nCherrypick && db_table_exists("repository","cherrypick") ){
2111
+ int i;
2112
+ for(i=0; i<p->nCherrypick; i++){
2113
+ db_multi_exec(
2114
+ "REPLACE INTO cherrypick(parentid,childid,isExclude)"
2115
+ " SELECT rid, %d, %d FROM blob WHERE uuid=%Q",
2116
+ rid, p->aCherrypick[i].zCPTarget[0]=='-',
2117
+ p->aCherrypick[i].zCPTarget+1
2118
+ );
2119
+ }
21092120
}
21102121
if( !db_exists("SELECT 1 FROM mlink WHERE mid=%d", rid) ){
21112122
char *zCom;
21122123
parentid = manifest_add_checkin_linkages(rid,p,p->nParent,p->azParent);
21132124
search_doc_touch('c', rid, 0);
21142125
--- src/manifest.c
+++ src/manifest.c
@@ -2104,10 +2104,21 @@
2104 db_begin_transaction();
2105 if( p->type==CFTYPE_MANIFEST ){
2106 if( permitHooks ){
2107 zScript = xfer_commit_code();
2108 zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid);
 
 
 
 
 
 
 
 
 
 
 
2109 }
2110 if( !db_exists("SELECT 1 FROM mlink WHERE mid=%d", rid) ){
2111 char *zCom;
2112 parentid = manifest_add_checkin_linkages(rid,p,p->nParent,p->azParent);
2113 search_doc_touch('c', rid, 0);
2114
--- src/manifest.c
+++ src/manifest.c
@@ -2104,10 +2104,21 @@
2104 db_begin_transaction();
2105 if( p->type==CFTYPE_MANIFEST ){
2106 if( permitHooks ){
2107 zScript = xfer_commit_code();
2108 zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid);
2109 }
2110 if( p->nCherrypick && db_table_exists("repository","cherrypick") ){
2111 int i;
2112 for(i=0; i<p->nCherrypick; i++){
2113 db_multi_exec(
2114 "REPLACE INTO cherrypick(parentid,childid,isExclude)"
2115 " SELECT rid, %d, %d FROM blob WHERE uuid=%Q",
2116 rid, p->aCherrypick[i].zCPTarget[0]=='-',
2117 p->aCherrypick[i].zCPTarget+1
2118 );
2119 }
2120 }
2121 if( !db_exists("SELECT 1 FROM mlink WHERE mid=%d", rid) ){
2122 char *zCom;
2123 parentid = manifest_add_checkin_linkages(rid,p,p->nParent,p->azParent);
2124 search_doc_touch('c', rid, 0);
2125
+49 -1
--- src/mkbuiltin.c
+++ src/mkbuiltin.c
@@ -25,11 +25,11 @@
2525
** arrays in the resulting executable.
2626
*/
2727
#include <stdio.h>
2828
#include <stdlib.h>
2929
#include <string.h>
30
-
30
+#include <ctype.h>
3131
3232
/*
3333
** Read the entire content of the file named zFilename into memory obtained
3434
** from malloc() and return a pointer to that memory. Write the size of the
3535
** file into *pnByte.
@@ -54,10 +54,49 @@
5454
got = fread(z, 1, nByte, in);
5555
fclose(in);
5656
z[got] = 0;
5757
return z;
5858
}
59
+
60
+/*
61
+** Try to compress a javascript file by removing unnecessary whitespace.
62
+**
63
+** Warning: This compression routine does not necessarily work for any
64
+** arbitrary Javascript source file. But it should work ok for the
65
+** well-behaved source files in this project.
66
+*/
67
+static void compressJavascript(unsigned char *z, int *pn){
68
+ int n = *pn;
69
+ int i, j, k;
70
+ for(i=j=0; i<n; i++){
71
+ unsigned char c = z[i];
72
+ if( c=='/' ){
73
+ if( z[i+1]=='*' ){
74
+ for(k=i+3; k<n && (z[k]!='/' || z[k-1]!='*'); k++){}
75
+ if( k<n ){
76
+ i = k;
77
+ while( i+1<n && isspace(z[i+1]) ) i++;
78
+ continue;
79
+ }
80
+ }else if( z[i+1]=='/' ){
81
+ for(k=i+2; k<n && z[k]!='\n'; k++){}
82
+ i = k;
83
+ while( i+1<n && isspace(z[i+1]) ) i++;
84
+ continue;
85
+ }
86
+ }
87
+ if( c=='\n' ){
88
+ while( j>0 && isspace(z[j-1]) ) j--;
89
+ z[j++] = '\n';
90
+ while( i+1<n && isspace(z[i+1]) ) i++;
91
+ continue;
92
+ }
93
+ z[j++] = c;
94
+ }
95
+ z[j] = 0;
96
+ *pn = j;
97
+}
5998
6099
/*
61100
** There is an instance of the following for each file translated.
62101
*/
63102
typedef struct Resource Resource;
@@ -85,10 +124,11 @@
85124
int nRes;
86125
unsigned char *pData;
87126
int nErr = 0;
88127
int nSkip;
89128
int nPrefix = 0;
129
+ int nName;
90130
91131
if( argc>3 && strcmp(argv[1],"--prefix")==0 ){
92132
nPrefix = (int)strlen(argv[2]);
93133
argc -= 2;
94134
argv += 2;
@@ -119,10 +159,18 @@
119159
nSkip = 0;
120160
while( pData[nSkip]=='#' ){
121161
while( pData[nSkip]!=0 && pData[nSkip]!='\n' ){ nSkip++; }
122162
if( pData[nSkip]=='\n' ) nSkip++;
123163
}
164
+
165
+ /* Compress javascript source files */
166
+ nName = (int)strlen(aRes[i].zName);
167
+ if( nName>3 && strcmp(&aRes[i].zName[nName-3],".js")==0 ){
168
+ int x = sz-nSkip;
169
+ compressJavascript(pData+nSkip, &x);
170
+ sz = x + nSkip;
171
+ }
124172
125173
aRes[i].nByte = sz - nSkip;
126174
aRes[i].idx = i;
127175
printf("/* Content of file %s */\n", aRes[i].zName);
128176
printf("static const unsigned char bidata%d[%d] = {\n ",
129177
--- src/mkbuiltin.c
+++ src/mkbuiltin.c
@@ -25,11 +25,11 @@
25 ** arrays in the resulting executable.
26 */
27 #include <stdio.h>
28 #include <stdlib.h>
29 #include <string.h>
30
31
32 /*
33 ** Read the entire content of the file named zFilename into memory obtained
34 ** from malloc() and return a pointer to that memory. Write the size of the
35 ** file into *pnByte.
@@ -54,10 +54,49 @@
54 got = fread(z, 1, nByte, in);
55 fclose(in);
56 z[got] = 0;
57 return z;
58 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
60 /*
61 ** There is an instance of the following for each file translated.
62 */
63 typedef struct Resource Resource;
@@ -85,10 +124,11 @@
85 int nRes;
86 unsigned char *pData;
87 int nErr = 0;
88 int nSkip;
89 int nPrefix = 0;
 
90
91 if( argc>3 && strcmp(argv[1],"--prefix")==0 ){
92 nPrefix = (int)strlen(argv[2]);
93 argc -= 2;
94 argv += 2;
@@ -119,10 +159,18 @@
119 nSkip = 0;
120 while( pData[nSkip]=='#' ){
121 while( pData[nSkip]!=0 && pData[nSkip]!='\n' ){ nSkip++; }
122 if( pData[nSkip]=='\n' ) nSkip++;
123 }
 
 
 
 
 
 
 
 
124
125 aRes[i].nByte = sz - nSkip;
126 aRes[i].idx = i;
127 printf("/* Content of file %s */\n", aRes[i].zName);
128 printf("static const unsigned char bidata%d[%d] = {\n ",
129
--- src/mkbuiltin.c
+++ src/mkbuiltin.c
@@ -25,11 +25,11 @@
25 ** arrays in the resulting executable.
26 */
27 #include <stdio.h>
28 #include <stdlib.h>
29 #include <string.h>
30 #include <ctype.h>
31
32 /*
33 ** Read the entire content of the file named zFilename into memory obtained
34 ** from malloc() and return a pointer to that memory. Write the size of the
35 ** file into *pnByte.
@@ -54,10 +54,49 @@
54 got = fread(z, 1, nByte, in);
55 fclose(in);
56 z[got] = 0;
57 return z;
58 }
59
60 /*
61 ** Try to compress a javascript file by removing unnecessary whitespace.
62 **
63 ** Warning: This compression routine does not necessarily work for any
64 ** arbitrary Javascript source file. But it should work ok for the
65 ** well-behaved source files in this project.
66 */
67 static void compressJavascript(unsigned char *z, int *pn){
68 int n = *pn;
69 int i, j, k;
70 for(i=j=0; i<n; i++){
71 unsigned char c = z[i];
72 if( c=='/' ){
73 if( z[i+1]=='*' ){
74 for(k=i+3; k<n && (z[k]!='/' || z[k-1]!='*'); k++){}
75 if( k<n ){
76 i = k;
77 while( i+1<n && isspace(z[i+1]) ) i++;
78 continue;
79 }
80 }else if( z[i+1]=='/' ){
81 for(k=i+2; k<n && z[k]!='\n'; k++){}
82 i = k;
83 while( i+1<n && isspace(z[i+1]) ) i++;
84 continue;
85 }
86 }
87 if( c=='\n' ){
88 while( j>0 && isspace(z[j-1]) ) j--;
89 z[j++] = '\n';
90 while( i+1<n && isspace(z[i+1]) ) i++;
91 continue;
92 }
93 z[j++] = c;
94 }
95 z[j] = 0;
96 *pn = j;
97 }
98
99 /*
100 ** There is an instance of the following for each file translated.
101 */
102 typedef struct Resource Resource;
@@ -85,10 +124,11 @@
124 int nRes;
125 unsigned char *pData;
126 int nErr = 0;
127 int nSkip;
128 int nPrefix = 0;
129 int nName;
130
131 if( argc>3 && strcmp(argv[1],"--prefix")==0 ){
132 nPrefix = (int)strlen(argv[2]);
133 argc -= 2;
134 argv += 2;
@@ -119,10 +159,18 @@
159 nSkip = 0;
160 while( pData[nSkip]=='#' ){
161 while( pData[nSkip]!=0 && pData[nSkip]!='\n' ){ nSkip++; }
162 if( pData[nSkip]=='\n' ) nSkip++;
163 }
164
165 /* Compress javascript source files */
166 nName = (int)strlen(aRes[i].zName);
167 if( nName>3 && strcmp(&aRes[i].zName[nName-3],".js")==0 ){
168 int x = sz-nSkip;
169 compressJavascript(pData+nSkip, &x);
170 sz = x + nSkip;
171 }
172
173 aRes[i].nByte = sz - nSkip;
174 aRes[i].idx = i;
175 printf("/* Content of file %s */\n", aRes[i].zName);
176 printf("static const unsigned char bidata%d[%d] = {\n ",
177
--- src/schema.c
+++ src/schema.c
@@ -459,10 +459,19 @@
459459
@ username TEXT,
460460
@ mimetype TEXT,
461461
@ icomment TEXT
462462
@ );
463463
@ CREATE INDEX ticketchng_idx1 ON ticketchng(tkt_id, tkt_mtime);
464
+@
465
+@ -- For tracking cherrypick merges
466
+@ CREATE TABLE cherrypick(
467
+@ parentid INT,
468
+@ childid INT,
469
+@ isExclude BOOLEAN DEFAULT false,
470
+@ PRIMARY KEY(parentid, childid)
471
+@ ) WITHOUT ROWID;
472
+@ CREATE INDEX cherrypick_cid ON cherrypick(childid);
464473
;
465474
466475
/*
467476
** Predefined tagid values
468477
*/
469478
--- src/schema.c
+++ src/schema.c
@@ -459,10 +459,19 @@
459 @ username TEXT,
460 @ mimetype TEXT,
461 @ icomment TEXT
462 @ );
463 @ CREATE INDEX ticketchng_idx1 ON ticketchng(tkt_id, tkt_mtime);
 
 
 
 
 
 
 
 
 
464 ;
465
466 /*
467 ** Predefined tagid values
468 */
469
--- src/schema.c
+++ src/schema.c
@@ -459,10 +459,19 @@
459 @ username TEXT,
460 @ mimetype TEXT,
461 @ icomment TEXT
462 @ );
463 @ CREATE INDEX ticketchng_idx1 ON ticketchng(tkt_id, tkt_mtime);
464 @
465 @ -- For tracking cherrypick merges
466 @ CREATE TABLE cherrypick(
467 @ parentid INT,
468 @ childid INT,
469 @ isExclude BOOLEAN DEFAULT false,
470 @ PRIMARY KEY(parentid, childid)
471 @ ) WITHOUT ROWID;
472 @ CREATE INDEX cherrypick_cid ON cherrypick(childid);
473 ;
474
475 /*
476 ** Predefined tagid values
477 */
478
+47 -7
--- src/setup.c
+++ src/setup.c
@@ -119,10 +119,12 @@
119119
setup_menu_entry("Login-Group", "setup_login_group",
120120
"Manage single sign-on between this repository and others"
121121
" on the same server");
122122
setup_menu_entry("Tickets", "tktsetup",
123123
"Configure the trouble-ticketing system for this repository");
124
+ setup_menu_entry("Wiki", "setup_wiki",
125
+ "Configure the wiki for this repository");
124126
}
125127
setup_menu_entry("Search","srchsetup",
126128
"Configure the built-in search engine");
127129
setup_menu_entry("URL Aliases", "waliassetup",
128130
"Configure URL aliases");
@@ -874,17 +876,10 @@
874876
@ the latest trunk check-in is downloaded. Change this tag to something
875877
@ else (ex: release) to alter the behavior of the /download page.
876878
@ (Property: "download-tag")
877879
@ </p>
878880
@ <hr />
879
- onoff_attribute("Enable WYSIWYG Wiki Editing",
880
- "wysiwyg-wiki", "wysiwyg-wiki", 0, 0);
881
- @ <p>Enable what-you-see-is-what-you-get (WYSIWYG) editing of wiki pages.
882
- @ The WYSIWYG editor generates HTML instead of markup, which makes
883
- @ subsequent manual editing more difficult.
884
- @ (Property: "wysiwyg-wiki")</p>
885
- @ <hr />
886881
entry_attribute("Index Page", 60, "index-page", "idxpg", "/home", 0);
887882
@ <p>Enter the pathname of the page to display when the "Home" menu
888883
@ option is selected and when no pathname is
889884
@ specified in the URL. For example, if you visit the url:</p>
890885
@
@@ -922,10 +917,55 @@
922917
"", 0);
923918
@ (Property: sitemap-license)<br>
924919
entry_attribute("Contact", 40, "sitemap-contact", "smcontact",
925920
"", 0);
926921
@ (Property: sitemap-contact)
922
+ @ <hr />
923
+ @ <p><input type="submit" name="submit" value="Apply Changes" /></p>
924
+ @ </div></form>
925
+ db_end_transaction(0);
926
+ style_footer();
927
+}
928
+
929
+/*
930
+** WEBPAGE: setup_wiki
931
+**
932
+** The "Admin/Wiki" page. Requires Setup privilege.
933
+*/
934
+void setup_wiki(void){
935
+ login_check_credentials();
936
+ if( !g.perm.Setup ){
937
+ login_needed(0);
938
+ return;
939
+ }
940
+
941
+ style_header("Wiki Configuration");
942
+ db_begin_transaction();
943
+ @ <form action="%s(g.zTop)/setup_wiki" method="post"><div>
944
+ login_insert_csrf_secret();
945
+ @ <input type="submit" name="submit" value="Apply Changes" /></p>
946
+ @ <hr />
947
+ onoff_attribute("Associate Wiki Pages With Branches, Tags, or Checkins",
948
+ "wiki-about", "wiki-about", 1, 0);
949
+ @ <p>
950
+ @ Associate wiki pages with branches, tags, or checkins, based on
951
+ @ the wiki page name. Wiki pages that begin with "branch/", "checkin/"
952
+ @ or "tag/" and which continue with the name of an existing branch, checkin
953
+ @ or tag are treated specially when this feature is enabled.
954
+ @ <ul>
955
+ @ <li> <b>branch/</b><i>branch-name</i>
956
+ @ <li> <b>checkin/</b><i>full-checkin-hash</i>
957
+ @ <li> <b>tag/</b><i>tag-name</i>
958
+ @ </ul>
959
+ @ (Property: "wiki-about")</p>
960
+ @ <hr />
961
+ onoff_attribute("Enable WYSIWYG Wiki Editing",
962
+ "wysiwyg-wiki", "wysiwyg-wiki", 0, 0);
963
+ @ <p>Enable what-you-see-is-what-you-get (WYSIWYG) editing of wiki pages.
964
+ @ The WYSIWYG editor generates HTML instead of markup, which makes
965
+ @ subsequent manual editing more difficult.
966
+ @ (Property: "wysiwyg-wiki")</p>
927967
@ <hr />
928968
onoff_attribute("Use HTML as wiki markup language",
929969
"wiki-use-html", "wiki-use-html", 0, 0);
930970
@ <p>Use HTML as the wiki markup language. Wiki links will still be parsed
931971
@ but all other wiki formatting will be ignored. This option is helpful
932972
--- src/setup.c
+++ src/setup.c
@@ -119,10 +119,12 @@
119 setup_menu_entry("Login-Group", "setup_login_group",
120 "Manage single sign-on between this repository and others"
121 " on the same server");
122 setup_menu_entry("Tickets", "tktsetup",
123 "Configure the trouble-ticketing system for this repository");
 
 
124 }
125 setup_menu_entry("Search","srchsetup",
126 "Configure the built-in search engine");
127 setup_menu_entry("URL Aliases", "waliassetup",
128 "Configure URL aliases");
@@ -874,17 +876,10 @@
874 @ the latest trunk check-in is downloaded. Change this tag to something
875 @ else (ex: release) to alter the behavior of the /download page.
876 @ (Property: "download-tag")
877 @ </p>
878 @ <hr />
879 onoff_attribute("Enable WYSIWYG Wiki Editing",
880 "wysiwyg-wiki", "wysiwyg-wiki", 0, 0);
881 @ <p>Enable what-you-see-is-what-you-get (WYSIWYG) editing of wiki pages.
882 @ The WYSIWYG editor generates HTML instead of markup, which makes
883 @ subsequent manual editing more difficult.
884 @ (Property: "wysiwyg-wiki")</p>
885 @ <hr />
886 entry_attribute("Index Page", 60, "index-page", "idxpg", "/home", 0);
887 @ <p>Enter the pathname of the page to display when the "Home" menu
888 @ option is selected and when no pathname is
889 @ specified in the URL. For example, if you visit the url:</p>
890 @
@@ -922,10 +917,55 @@
922 "", 0);
923 @ (Property: sitemap-license)<br>
924 entry_attribute("Contact", 40, "sitemap-contact", "smcontact",
925 "", 0);
926 @ (Property: sitemap-contact)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
927 @ <hr />
928 onoff_attribute("Use HTML as wiki markup language",
929 "wiki-use-html", "wiki-use-html", 0, 0);
930 @ <p>Use HTML as the wiki markup language. Wiki links will still be parsed
931 @ but all other wiki formatting will be ignored. This option is helpful
932
--- src/setup.c
+++ src/setup.c
@@ -119,10 +119,12 @@
119 setup_menu_entry("Login-Group", "setup_login_group",
120 "Manage single sign-on between this repository and others"
121 " on the same server");
122 setup_menu_entry("Tickets", "tktsetup",
123 "Configure the trouble-ticketing system for this repository");
124 setup_menu_entry("Wiki", "setup_wiki",
125 "Configure the wiki for this repository");
126 }
127 setup_menu_entry("Search","srchsetup",
128 "Configure the built-in search engine");
129 setup_menu_entry("URL Aliases", "waliassetup",
130 "Configure URL aliases");
@@ -874,17 +876,10 @@
876 @ the latest trunk check-in is downloaded. Change this tag to something
877 @ else (ex: release) to alter the behavior of the /download page.
878 @ (Property: "download-tag")
879 @ </p>
880 @ <hr />
 
 
 
 
 
 
 
881 entry_attribute("Index Page", 60, "index-page", "idxpg", "/home", 0);
882 @ <p>Enter the pathname of the page to display when the "Home" menu
883 @ option is selected and when no pathname is
884 @ specified in the URL. For example, if you visit the url:</p>
885 @
@@ -922,10 +917,55 @@
917 "", 0);
918 @ (Property: sitemap-license)<br>
919 entry_attribute("Contact", 40, "sitemap-contact", "smcontact",
920 "", 0);
921 @ (Property: sitemap-contact)
922 @ <hr />
923 @ <p><input type="submit" name="submit" value="Apply Changes" /></p>
924 @ </div></form>
925 db_end_transaction(0);
926 style_footer();
927 }
928
929 /*
930 ** WEBPAGE: setup_wiki
931 **
932 ** The "Admin/Wiki" page. Requires Setup privilege.
933 */
934 void setup_wiki(void){
935 login_check_credentials();
936 if( !g.perm.Setup ){
937 login_needed(0);
938 return;
939 }
940
941 style_header("Wiki Configuration");
942 db_begin_transaction();
943 @ <form action="%s(g.zTop)/setup_wiki" method="post"><div>
944 login_insert_csrf_secret();
945 @ <input type="submit" name="submit" value="Apply Changes" /></p>
946 @ <hr />
947 onoff_attribute("Associate Wiki Pages With Branches, Tags, or Checkins",
948 "wiki-about", "wiki-about", 1, 0);
949 @ <p>
950 @ Associate wiki pages with branches, tags, or checkins, based on
951 @ the wiki page name. Wiki pages that begin with "branch/", "checkin/"
952 @ or "tag/" and which continue with the name of an existing branch, checkin
953 @ or tag are treated specially when this feature is enabled.
954 @ <ul>
955 @ <li> <b>branch/</b><i>branch-name</i>
956 @ <li> <b>checkin/</b><i>full-checkin-hash</i>
957 @ <li> <b>tag/</b><i>tag-name</i>
958 @ </ul>
959 @ (Property: "wiki-about")</p>
960 @ <hr />
961 onoff_attribute("Enable WYSIWYG Wiki Editing",
962 "wysiwyg-wiki", "wysiwyg-wiki", 0, 0);
963 @ <p>Enable what-you-see-is-what-you-get (WYSIWYG) editing of wiki pages.
964 @ The WYSIWYG editor generates HTML instead of markup, which makes
965 @ subsequent manual editing more difficult.
966 @ (Property: "wysiwyg-wiki")</p>
967 @ <hr />
968 onoff_attribute("Use HTML as wiki markup language",
969 "wiki-use-html", "wiki-use-html", 0, 0);
970 @ <p>Use HTML as the wiki markup language. Wiki links will still be parsed
971 @ but all other wiki formatting will be ignored. This option is helpful
972
+65 -17
--- src/tag.c
+++ src/tag.c
@@ -394,10 +394,17 @@
394394
**
395395
** Remove the tag TAGNAME from CHECK-IN, and also remove
396396
** the propagation of the tag to any descendants. Use the
397397
** the --dryrun or -n options to see what would have happened.
398398
**
399
+** Options:
400
+** --raw Raw tag name.
401
+** --date-override DATETIME Set date and time deleted.
402
+** --user-override USER Name USER when deleting the tag.
403
+** --dryrun|-n Display the control artifact, but do
404
+** not insert it into the database.
405
+**
399406
** %fossil tag find ?OPTIONS? TAGNAME
400407
**
401408
** List all objects that use TAGNAME. TYPE can be "ci" for
402409
** check-ins or "e" for events. The limit option limits the number
403410
** of results to the given value.
@@ -432,15 +439,10 @@
432439
** will assume that "decaf" is a tag/branch name.
433440
**
434441
*/
435442
void tag_cmd(void){
436443
int n;
437
- int fRaw = find_option("raw","",0)!=0;
438
- int fPropagate = find_option("propagate","",0)!=0;
439
- const char *zPrefix = fRaw ? "" : "sym-";
440
- const char *zFindLimit = find_option("limit","n",1);
441
- const int nFindLimit = zFindLimit ? atoi(zFindLimit) : -2000;
442444
443445
db_find_and_open_repository(0, 0);
444446
if( g.argc<3 ){
445447
goto tag_cmd_usage;
446448
}
@@ -450,10 +452,13 @@
450452
}
451453
452454
if( strncmp(g.argv[2],"add",n)==0 ){
453455
char *zValue;
454456
int dryRun = 0;
457
+ int fRaw = find_option("raw","",0)!=0;
458
+ const char *zPrefix = fRaw ? "" : "sym-";
459
+ int fPropagate = find_option("propagate","",0)!=0;
455460
const char *zDateOvrd = find_option("date-override",0,1);
456461
const char *zUserOvrd = find_option("user-override",0,1);
457462
if( find_option("dryrun","n",0)!=0 ) dryRun = TAG_ADD_DRYRUN;
458463
if( g.argc!=5 && g.argc!=6 ){
459464
usage("add ?options? TAGNAME CHECK-IN ?VALUE?");
@@ -470,21 +475,29 @@
470475
"Use the \"fossil branch new\" command instead.");
471476
}else
472477
473478
if( strncmp(g.argv[2],"cancel",n)==0 ){
474479
int dryRun = 0;
480
+ int fRaw = find_option("raw","",0)!=0;
481
+ const char *zPrefix = fRaw ? "" : "sym-";
482
+ const char *zDateOvrd = find_option("date-override",0,1);
483
+ const char *zUserOvrd = find_option("user-override",0,1);
475484
if( find_option("dryrun","n",0)!=0 ) dryRun = TAG_ADD_DRYRUN;
476485
if( g.argc!=5 ){
477486
usage("cancel ?options? TAGNAME CHECK-IN");
478487
}
479488
db_begin_transaction();
480
- tag_add_artifact(zPrefix, g.argv[3], g.argv[4], 0, dryRun, 0, 0);
489
+ tag_add_artifact(zPrefix, g.argv[3], g.argv[4], 0, dryRun,
490
+ zDateOvrd, zUserOvrd);
481491
db_end_transaction(0);
482492
}else
483493
484494
if( strncmp(g.argv[2],"find",n)==0 ){
485495
Stmt q;
496
+ int fRaw = find_option("raw","",0)!=0;
497
+ const char *zFindLimit = find_option("limit","n",1);
498
+ const int nFindLimit = zFindLimit ? atoi(zFindLimit) : -2000;
486499
const char *zType = find_option("type","t",1);
487500
Blob sql = empty_blob;
488501
if( zType==0 || zType[0]==0 ) zType = "*";
489502
if( g.argc!=4 ){
490503
usage("find ?--raw? ?-t|--type TYPE? ?-n|--limit #? TAGNAME");
@@ -528,10 +541,11 @@
528541
}
529542
}else
530543
531544
if(( strncmp(g.argv[2],"list",n)==0 )||( strncmp(g.argv[2],"ls",n)==0 )){
532545
Stmt q;
546
+ int fRaw = find_option("raw","",0)!=0;
533547
if( g.argc==3 ){
534548
db_prepare(&q,
535549
"SELECT tagname FROM tag"
536550
" WHERE EXISTS(SELECT 1 FROM tagxref"
537551
" WHERE tagid=tag.tagid"
@@ -606,20 +620,26 @@
606620
** --test Make database entries but do not add the tag artifact.
607621
** So the reparent operation will be undone by the next
608622
** "fossil rebuild" command.
609623
** --dryrun | -n Print the tag that would have been created but do not
610624
** actually change the database in any way.
625
+** --date-override DATETIME Set the change time on the control artifact
626
+** --user-override USER Set the user name on the control artifact
611627
*/
612628
void reparent_cmd(void){
613629
int bTest = find_option("test","",0)!=0;
614630
int rid;
615631
int i;
616632
Blob value;
617633
char *zUuid;
618634
int dryRun = 0;
635
+ const char *zDateOvrd; /* The change time on the control artifact */
636
+ const char *zUserOvrd; /* The user name on the control artifact */
619637
620638
if( find_option("dryrun","n",0)!=0 ) dryRun = TAG_ADD_DRYRUN;
639
+ zDateOvrd = find_option("date-override",0,1);
640
+ zUserOvrd = find_option("user-override",0,1);
621641
db_find_and_open_repository(0, 0);
622642
verify_all_options();
623643
if( g.argc<4 ){
624644
usage("[OPTIONS] CHECK-IN PARENT ...");
625645
}
@@ -634,11 +654,12 @@
634654
}
635655
if( bTest && !dryRun ){
636656
tag_insert("parent", 1, blob_str(&value), -1, 0.0, rid);
637657
}else{
638658
zUuid = rid_to_uuid(rid);
639
- tag_add_artifact("","parent",zUuid,blob_str(&value),1|dryRun,0,0);
659
+ tag_add_artifact("","parent",zUuid,blob_str(&value),1|dryRun,
660
+ zDateOvrd,zUserOvrd);
640661
}
641662
}
642663
643664
644665
/*
@@ -669,11 +690,11 @@
669690
);
670691
@ <ul>
671692
while( db_step(&q)==SQLITE_ROW ){
672693
const char *zName = db_column_text(&q, 0);
673694
if( g.perm.Hyperlink ){
674
- @ <li>%z(chref("taglink","%R/timeline?t=%T&n=200",zName))
695
+ @ <li>%z(chref("taglink","%R/timeline?t=%T",zName))
675696
@ %h(zName)</a></li>
676697
}else{
677698
@ <li><span class="tagDsp">%h(zName)</span></li>
678699
}
679700
}
@@ -685,29 +706,56 @@
685706
/*
686707
** WEBPAGE: /tagtimeline
687708
**
688709
** Render a timeline with all check-ins that contain non-propagating
689710
** symbolic tags.
711
+**
712
+** Query parameters:
713
+**
714
+** ng No graph
715
+** nohidden Hide check-ins with "hidden" tag
716
+** onlyhidden Show only check-ins with "hidden" tag
717
+** brbg Background color by branch name
718
+** ubg Background color by user name
690719
*/
691720
void tagtimeline_page(void){
721
+ Blob sql = empty_blob;
692722
Stmt q;
723
+ int tmFlags; /* Timeline display flags */
724
+ int fNoHidden = PB("nohidden")!=0; /* The "nohidden" query parameter */
725
+ int fOnlyHidden = PB("onlyhidden")!=0; /* The "onlyhidden" query parameter */
693726
694727
login_check_credentials();
695728
if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
696729
697730
style_header("Tagged Check-ins");
698731
style_submenu_element("List", "taglist");
699732
login_anonymous_available();
733
+ timeline_ss_submenu();
734
+ cookie_render();
700735
@ <h2>Check-ins with non-propagating tags:</h2>
701
- db_prepare(&q,
702
- "%s AND blob.rid IN (SELECT rid FROM tagxref"
703
- " WHERE tagtype=1 AND srcid>0"
704
- " AND tagid IN (SELECT tagid FROM tag "
705
- " WHERE tagname GLOB 'sym-*'))"
706
- " ORDER BY event.mtime DESC /*sort*/",
707
- timeline_query_for_www()
708
- );
709
- www_print_timeline(&q, 0, 0, 0, 0, 0);
736
+ blob_append(&sql, timeline_query_for_www(), -1);
737
+ blob_append_sql(&sql,
738
+ "AND blob.rid IN (SELECT rid FROM tagxref"
739
+ " WHERE tagtype=1 AND srcid>0"
740
+ " AND tagid IN (SELECT tagid FROM tag "
741
+ " WHERE tagname GLOB 'sym-*'))");
742
+ if( fNoHidden || fOnlyHidden ){
743
+ const char* zUnaryOp = fNoHidden ? "NOT" : "";
744
+ blob_append_sql(&sql,
745
+ " AND %s EXISTS(SELECT 1 FROM tagxref"
746
+ " WHERE tagid=%d AND tagtype>0 AND rid=blob.rid)\n",
747
+ zUnaryOp/*safe-for-%s*/, TAG_HIDDEN);
748
+ }
749
+ db_prepare(&q, "%s ORDER BY event.mtime DESC /*sort*/", blob_sql_text(&sql));
750
+ blob_reset(&sql);
751
+ /* Always specify TIMELINE_DISJOINT, or graph_finish() may fail because of too
752
+ ** many descenders to (off-screen) parents. */
753
+ tmFlags = TIMELINE_DISJOINT | TIMELINE_NOSCROLL;
754
+ if( PB("ng")==0 ) tmFlags |= TIMELINE_GRAPH;
755
+ if( PB("brbg")!=0 ) tmFlags |= TIMELINE_BRCOLOR;
756
+ if( PB("ubg")!=0 ) tmFlags |= TIMELINE_UCOLOR;
757
+ www_print_timeline(&q, tmFlags, 0, 0, 0, 0);
710758
db_finalize(&q);
711759
@ <br />
712760
style_footer();
713761
}
714762
--- src/tag.c
+++ src/tag.c
@@ -394,10 +394,17 @@
394 **
395 ** Remove the tag TAGNAME from CHECK-IN, and also remove
396 ** the propagation of the tag to any descendants. Use the
397 ** the --dryrun or -n options to see what would have happened.
398 **
 
 
 
 
 
 
 
399 ** %fossil tag find ?OPTIONS? TAGNAME
400 **
401 ** List all objects that use TAGNAME. TYPE can be "ci" for
402 ** check-ins or "e" for events. The limit option limits the number
403 ** of results to the given value.
@@ -432,15 +439,10 @@
432 ** will assume that "decaf" is a tag/branch name.
433 **
434 */
435 void tag_cmd(void){
436 int n;
437 int fRaw = find_option("raw","",0)!=0;
438 int fPropagate = find_option("propagate","",0)!=0;
439 const char *zPrefix = fRaw ? "" : "sym-";
440 const char *zFindLimit = find_option("limit","n",1);
441 const int nFindLimit = zFindLimit ? atoi(zFindLimit) : -2000;
442
443 db_find_and_open_repository(0, 0);
444 if( g.argc<3 ){
445 goto tag_cmd_usage;
446 }
@@ -450,10 +452,13 @@
450 }
451
452 if( strncmp(g.argv[2],"add",n)==0 ){
453 char *zValue;
454 int dryRun = 0;
 
 
 
455 const char *zDateOvrd = find_option("date-override",0,1);
456 const char *zUserOvrd = find_option("user-override",0,1);
457 if( find_option("dryrun","n",0)!=0 ) dryRun = TAG_ADD_DRYRUN;
458 if( g.argc!=5 && g.argc!=6 ){
459 usage("add ?options? TAGNAME CHECK-IN ?VALUE?");
@@ -470,21 +475,29 @@
470 "Use the \"fossil branch new\" command instead.");
471 }else
472
473 if( strncmp(g.argv[2],"cancel",n)==0 ){
474 int dryRun = 0;
 
 
 
 
475 if( find_option("dryrun","n",0)!=0 ) dryRun = TAG_ADD_DRYRUN;
476 if( g.argc!=5 ){
477 usage("cancel ?options? TAGNAME CHECK-IN");
478 }
479 db_begin_transaction();
480 tag_add_artifact(zPrefix, g.argv[3], g.argv[4], 0, dryRun, 0, 0);
 
481 db_end_transaction(0);
482 }else
483
484 if( strncmp(g.argv[2],"find",n)==0 ){
485 Stmt q;
 
 
 
486 const char *zType = find_option("type","t",1);
487 Blob sql = empty_blob;
488 if( zType==0 || zType[0]==0 ) zType = "*";
489 if( g.argc!=4 ){
490 usage("find ?--raw? ?-t|--type TYPE? ?-n|--limit #? TAGNAME");
@@ -528,10 +541,11 @@
528 }
529 }else
530
531 if(( strncmp(g.argv[2],"list",n)==0 )||( strncmp(g.argv[2],"ls",n)==0 )){
532 Stmt q;
 
533 if( g.argc==3 ){
534 db_prepare(&q,
535 "SELECT tagname FROM tag"
536 " WHERE EXISTS(SELECT 1 FROM tagxref"
537 " WHERE tagid=tag.tagid"
@@ -606,20 +620,26 @@
606 ** --test Make database entries but do not add the tag artifact.
607 ** So the reparent operation will be undone by the next
608 ** "fossil rebuild" command.
609 ** --dryrun | -n Print the tag that would have been created but do not
610 ** actually change the database in any way.
 
 
611 */
612 void reparent_cmd(void){
613 int bTest = find_option("test","",0)!=0;
614 int rid;
615 int i;
616 Blob value;
617 char *zUuid;
618 int dryRun = 0;
 
 
619
620 if( find_option("dryrun","n",0)!=0 ) dryRun = TAG_ADD_DRYRUN;
 
 
621 db_find_and_open_repository(0, 0);
622 verify_all_options();
623 if( g.argc<4 ){
624 usage("[OPTIONS] CHECK-IN PARENT ...");
625 }
@@ -634,11 +654,12 @@
634 }
635 if( bTest && !dryRun ){
636 tag_insert("parent", 1, blob_str(&value), -1, 0.0, rid);
637 }else{
638 zUuid = rid_to_uuid(rid);
639 tag_add_artifact("","parent",zUuid,blob_str(&value),1|dryRun,0,0);
 
640 }
641 }
642
643
644 /*
@@ -669,11 +690,11 @@
669 );
670 @ <ul>
671 while( db_step(&q)==SQLITE_ROW ){
672 const char *zName = db_column_text(&q, 0);
673 if( g.perm.Hyperlink ){
674 @ <li>%z(chref("taglink","%R/timeline?t=%T&n=200",zName))
675 @ %h(zName)</a></li>
676 }else{
677 @ <li><span class="tagDsp">%h(zName)</span></li>
678 }
679 }
@@ -685,29 +706,56 @@
685 /*
686 ** WEBPAGE: /tagtimeline
687 **
688 ** Render a timeline with all check-ins that contain non-propagating
689 ** symbolic tags.
 
 
 
 
 
 
 
 
690 */
691 void tagtimeline_page(void){
 
692 Stmt q;
 
 
 
693
694 login_check_credentials();
695 if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
696
697 style_header("Tagged Check-ins");
698 style_submenu_element("List", "taglist");
699 login_anonymous_available();
 
 
700 @ <h2>Check-ins with non-propagating tags:</h2>
701 db_prepare(&q,
702 "%s AND blob.rid IN (SELECT rid FROM tagxref"
703 " WHERE tagtype=1 AND srcid>0"
704 " AND tagid IN (SELECT tagid FROM tag "
705 " WHERE tagname GLOB 'sym-*'))"
706 " ORDER BY event.mtime DESC /*sort*/",
707 timeline_query_for_www()
708 );
709 www_print_timeline(&q, 0, 0, 0, 0, 0);
 
 
 
 
 
 
 
 
 
 
 
 
 
710 db_finalize(&q);
711 @ <br />
712 style_footer();
713 }
714
--- src/tag.c
+++ src/tag.c
@@ -394,10 +394,17 @@
394 **
395 ** Remove the tag TAGNAME from CHECK-IN, and also remove
396 ** the propagation of the tag to any descendants. Use the
397 ** the --dryrun or -n options to see what would have happened.
398 **
399 ** Options:
400 ** --raw Raw tag name.
401 ** --date-override DATETIME Set date and time deleted.
402 ** --user-override USER Name USER when deleting the tag.
403 ** --dryrun|-n Display the control artifact, but do
404 ** not insert it into the database.
405 **
406 ** %fossil tag find ?OPTIONS? TAGNAME
407 **
408 ** List all objects that use TAGNAME. TYPE can be "ci" for
409 ** check-ins or "e" for events. The limit option limits the number
410 ** of results to the given value.
@@ -432,15 +439,10 @@
439 ** will assume that "decaf" is a tag/branch name.
440 **
441 */
442 void tag_cmd(void){
443 int n;
 
 
 
 
 
444
445 db_find_and_open_repository(0, 0);
446 if( g.argc<3 ){
447 goto tag_cmd_usage;
448 }
@@ -450,10 +452,13 @@
452 }
453
454 if( strncmp(g.argv[2],"add",n)==0 ){
455 char *zValue;
456 int dryRun = 0;
457 int fRaw = find_option("raw","",0)!=0;
458 const char *zPrefix = fRaw ? "" : "sym-";
459 int fPropagate = find_option("propagate","",0)!=0;
460 const char *zDateOvrd = find_option("date-override",0,1);
461 const char *zUserOvrd = find_option("user-override",0,1);
462 if( find_option("dryrun","n",0)!=0 ) dryRun = TAG_ADD_DRYRUN;
463 if( g.argc!=5 && g.argc!=6 ){
464 usage("add ?options? TAGNAME CHECK-IN ?VALUE?");
@@ -470,21 +475,29 @@
475 "Use the \"fossil branch new\" command instead.");
476 }else
477
478 if( strncmp(g.argv[2],"cancel",n)==0 ){
479 int dryRun = 0;
480 int fRaw = find_option("raw","",0)!=0;
481 const char *zPrefix = fRaw ? "" : "sym-";
482 const char *zDateOvrd = find_option("date-override",0,1);
483 const char *zUserOvrd = find_option("user-override",0,1);
484 if( find_option("dryrun","n",0)!=0 ) dryRun = TAG_ADD_DRYRUN;
485 if( g.argc!=5 ){
486 usage("cancel ?options? TAGNAME CHECK-IN");
487 }
488 db_begin_transaction();
489 tag_add_artifact(zPrefix, g.argv[3], g.argv[4], 0, dryRun,
490 zDateOvrd, zUserOvrd);
491 db_end_transaction(0);
492 }else
493
494 if( strncmp(g.argv[2],"find",n)==0 ){
495 Stmt q;
496 int fRaw = find_option("raw","",0)!=0;
497 const char *zFindLimit = find_option("limit","n",1);
498 const int nFindLimit = zFindLimit ? atoi(zFindLimit) : -2000;
499 const char *zType = find_option("type","t",1);
500 Blob sql = empty_blob;
501 if( zType==0 || zType[0]==0 ) zType = "*";
502 if( g.argc!=4 ){
503 usage("find ?--raw? ?-t|--type TYPE? ?-n|--limit #? TAGNAME");
@@ -528,10 +541,11 @@
541 }
542 }else
543
544 if(( strncmp(g.argv[2],"list",n)==0 )||( strncmp(g.argv[2],"ls",n)==0 )){
545 Stmt q;
546 int fRaw = find_option("raw","",0)!=0;
547 if( g.argc==3 ){
548 db_prepare(&q,
549 "SELECT tagname FROM tag"
550 " WHERE EXISTS(SELECT 1 FROM tagxref"
551 " WHERE tagid=tag.tagid"
@@ -606,20 +620,26 @@
620 ** --test Make database entries but do not add the tag artifact.
621 ** So the reparent operation will be undone by the next
622 ** "fossil rebuild" command.
623 ** --dryrun | -n Print the tag that would have been created but do not
624 ** actually change the database in any way.
625 ** --date-override DATETIME Set the change time on the control artifact
626 ** --user-override USER Set the user name on the control artifact
627 */
628 void reparent_cmd(void){
629 int bTest = find_option("test","",0)!=0;
630 int rid;
631 int i;
632 Blob value;
633 char *zUuid;
634 int dryRun = 0;
635 const char *zDateOvrd; /* The change time on the control artifact */
636 const char *zUserOvrd; /* The user name on the control artifact */
637
638 if( find_option("dryrun","n",0)!=0 ) dryRun = TAG_ADD_DRYRUN;
639 zDateOvrd = find_option("date-override",0,1);
640 zUserOvrd = find_option("user-override",0,1);
641 db_find_and_open_repository(0, 0);
642 verify_all_options();
643 if( g.argc<4 ){
644 usage("[OPTIONS] CHECK-IN PARENT ...");
645 }
@@ -634,11 +654,12 @@
654 }
655 if( bTest && !dryRun ){
656 tag_insert("parent", 1, blob_str(&value), -1, 0.0, rid);
657 }else{
658 zUuid = rid_to_uuid(rid);
659 tag_add_artifact("","parent",zUuid,blob_str(&value),1|dryRun,
660 zDateOvrd,zUserOvrd);
661 }
662 }
663
664
665 /*
@@ -669,11 +690,11 @@
690 );
691 @ <ul>
692 while( db_step(&q)==SQLITE_ROW ){
693 const char *zName = db_column_text(&q, 0);
694 if( g.perm.Hyperlink ){
695 @ <li>%z(chref("taglink","%R/timeline?t=%T",zName))
696 @ %h(zName)</a></li>
697 }else{
698 @ <li><span class="tagDsp">%h(zName)</span></li>
699 }
700 }
@@ -685,29 +706,56 @@
706 /*
707 ** WEBPAGE: /tagtimeline
708 **
709 ** Render a timeline with all check-ins that contain non-propagating
710 ** symbolic tags.
711 **
712 ** Query parameters:
713 **
714 ** ng No graph
715 ** nohidden Hide check-ins with "hidden" tag
716 ** onlyhidden Show only check-ins with "hidden" tag
717 ** brbg Background color by branch name
718 ** ubg Background color by user name
719 */
720 void tagtimeline_page(void){
721 Blob sql = empty_blob;
722 Stmt q;
723 int tmFlags; /* Timeline display flags */
724 int fNoHidden = PB("nohidden")!=0; /* The "nohidden" query parameter */
725 int fOnlyHidden = PB("onlyhidden")!=0; /* The "onlyhidden" query parameter */
726
727 login_check_credentials();
728 if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
729
730 style_header("Tagged Check-ins");
731 style_submenu_element("List", "taglist");
732 login_anonymous_available();
733 timeline_ss_submenu();
734 cookie_render();
735 @ <h2>Check-ins with non-propagating tags:</h2>
736 blob_append(&sql, timeline_query_for_www(), -1);
737 blob_append_sql(&sql,
738 "AND blob.rid IN (SELECT rid FROM tagxref"
739 " WHERE tagtype=1 AND srcid>0"
740 " AND tagid IN (SELECT tagid FROM tag "
741 " WHERE tagname GLOB 'sym-*'))");
742 if( fNoHidden || fOnlyHidden ){
743 const char* zUnaryOp = fNoHidden ? "NOT" : "";
744 blob_append_sql(&sql,
745 " AND %s EXISTS(SELECT 1 FROM tagxref"
746 " WHERE tagid=%d AND tagtype>0 AND rid=blob.rid)\n",
747 zUnaryOp/*safe-for-%s*/, TAG_HIDDEN);
748 }
749 db_prepare(&q, "%s ORDER BY event.mtime DESC /*sort*/", blob_sql_text(&sql));
750 blob_reset(&sql);
751 /* Always specify TIMELINE_DISJOINT, or graph_finish() may fail because of too
752 ** many descenders to (off-screen) parents. */
753 tmFlags = TIMELINE_DISJOINT | TIMELINE_NOSCROLL;
754 if( PB("ng")==0 ) tmFlags |= TIMELINE_GRAPH;
755 if( PB("brbg")!=0 ) tmFlags |= TIMELINE_BRCOLOR;
756 if( PB("ubg")!=0 ) tmFlags |= TIMELINE_UCOLOR;
757 www_print_timeline(&q, tmFlags, 0, 0, 0, 0);
758 db_finalize(&q);
759 @ <br />
760 style_footer();
761 }
762
+263 -83
--- src/timeline.c
+++ src/timeline.c
@@ -91,12 +91,12 @@
9191
9292
/*
9393
** Allowed flags for the tmFlags argument to www_print_timeline
9494
*/
9595
#if INTERFACE
96
-#define TIMELINE_ARTID 0x000001 /* Show artifact IDs on non-check-in lines */
97
-#define TIMELINE_LEAFONLY 0x000002 /* Show "Leaf" but not "Merge", "Fork" etc */
96
+#define TIMELINE_ARTID 0x000001 /* Show artifact IDs on non-check-in lines*/
97
+#define TIMELINE_LEAFONLY 0x000002 /* Show "Leaf" but not "Merge", "Fork" etc*/
9898
#define TIMELINE_BRIEF 0x000004 /* Combine adjacent elements of same obj */
9999
#define TIMELINE_GRAPH 0x000008 /* Compute a graph */
100100
#define TIMELINE_DISJOINT 0x000010 /* Elements are not contiguous */
101101
#define TIMELINE_FCHANGES 0x000020 /* Detail file changes */
102102
#define TIMELINE_BRCOLOR 0x000040 /* Background color by branch name */
@@ -111,10 +111,11 @@
111111
#define TIMELINE_COLUMNAR 0x008000 /* Use the "columns" view style */
112112
#define TIMELINE_CLASSIC 0x010000 /* Use the "classic" view style */
113113
#define TIMELINE_VIEWS 0x01f000 /* Mask for all of the view styles */
114114
#define TIMELINE_NOSCROLL 0x100000 /* Don't scroll to the selection */
115115
#define TIMELINE_FILEDIFF 0x200000 /* Show File differences, not ckin diffs */
116
+#define TIMELINE_CHPICK 0x400000 /* Show cherrypick merges */
116117
#endif
117118
118119
/*
119120
** Hash a string and use the hash to determine a background color.
120121
*/
@@ -290,10 +291,15 @@
290291
}
291292
db_static_prepare(&qbranch,
292293
"SELECT value FROM tagxref WHERE tagid=%d AND tagtype>0 AND rid=:rid",
293294
TAG_BRANCH
294295
);
296
+ if( (tmFlags & TIMELINE_CHPICK)!=0
297
+ && !db_table_exists("repository","cherrypick")
298
+ ){
299
+ tmFlags &= ~TIMELINE_CHPICK;
300
+ }
295301
296302
@ <table id="timelineTable%d(iTableId)" class="timelineTable">
297303
blob_zero(&comment);
298304
while( db_step(pQuery)==SQLITE_ROW ){
299305
int rid = db_column_int(pQuery, 0);
@@ -423,10 +429,11 @@
423429
}
424430
}
425431
}
426432
if( zType[0]=='c' && pGraph ){
427433
int nParent = 0;
434
+ int nCherrypick = 0;
428435
int aParent[GR_MAX_RAIL];
429436
static Stmt qparent;
430437
db_static_prepare(&qparent,
431438
"SELECT pid FROM plink"
432439
" WHERE cid=:rid AND pid NOT IN phantom"
@@ -435,19 +442,32 @@
435442
db_bind_int(&qparent, ":rid", rid);
436443
while( db_step(&qparent)==SQLITE_ROW && nParent<count(aParent) ){
437444
aParent[nParent++] = db_column_int(&qparent, 0);
438445
}
439446
db_reset(&qparent);
440
- gidx = graph_add_row(pGraph, rid, nParent, aParent, zBr, zBgClr,
441
- zUuid, isLeaf);
447
+ if( (tmFlags & TIMELINE_CHPICK)!=0 && nParent>0 ){
448
+ static Stmt qcherrypick;
449
+ db_static_prepare(&qcherrypick,
450
+ "SELECT parentid FROM cherrypick"
451
+ " WHERE childid=:rid AND parentid NOT IN phantom"
452
+ );
453
+ db_bind_int(&qcherrypick, ":rid", rid);
454
+ while( db_step(&qcherrypick)==SQLITE_ROW && nParent<count(aParent) ){
455
+ aParent[nParent++] = db_column_int(&qcherrypick, 0);
456
+ nCherrypick++;
457
+ }
458
+ db_reset(&qcherrypick);
459
+ }
460
+ gidx = graph_add_row(pGraph, rid, nParent, nCherrypick, aParent,
461
+ zBr, zBgClr, zUuid, isLeaf);
442462
db_reset(&qbranch);
443463
@ <div id="m%d(gidx)" class="tl-nodemark"></div>
444464
}else if( zType[0]=='e' && pGraph && zBgClr && zBgClr[0] ){
445465
/* For technotes, make a graph node with nParent==(-1). This will
446466
** not actually draw anything on the graph, but it will set the
447467
** background color of the timeline entry */
448
- gidx = graph_add_row(pGraph, rid, -1, 0, zBr, zBgClr, zUuid, 0);
468
+ gidx = graph_add_row(pGraph, rid, -1, 0, 0, zBr, zBgClr, zUuid, 0);
449469
@ <div id="m%d(gidx)" class="tl-nodemark"></div>
450470
}
451471
@</td>
452472
if( !isSelectedOrCurrent ){
453473
@ <td class="timeline%s(zStyle)Cell" id='mc%d(gidx)'>
@@ -505,11 +525,13 @@
505525
}
506526
}
507527
if( zType[0]!='c' ){
508528
/* Comments for anything other than a check-in are generated by
509529
** "fossil rebuild" and expect to be rendered as text/x-fossil-wiki */
530
+ if( zType[0]=='w' ) wiki_hyperlink_override(zUuid);
510531
wiki_convert(&comment, 0, WIKI_INLINE);
532
+ wiki_hyperlink_override(0);
511533
}else{
512534
if( bCommentGitStyle ){
513535
/* Truncate comment at first blank line */
514536
int ii, jj;
515537
int n = blob_size(&comment);
@@ -841,68 +863,109 @@
841863
** to get an actual id, prepend "m" to the integer. The top node
842864
** is iTopRow and numbers increase moving down the timeline.
843865
** bg: The background color for this row
844866
** r: The "rail" that the node for this row sits on. The left-most
845867
** rail is 0 and the number increases to the right.
846
- ** d: True if there is a "descender" - an arrow coming from the bottom
847
- ** of the page straight up to this node.
848
- ** mo: "merge-out". If non-negative, this is the rail position
849
- ** for the upward portion of a merge arrow. The merge arrow goes up
850
- ** to the row identified by mu:. If this value is negative then
851
- ** node has no merge children and no merge-out line is drawn.
868
+ ** d: If exists and true then there is a "descender" - an arrow
869
+ ** coming from the bottom of the page straight up to this node.
870
+ ** mo: "merge-out". If it exists, this is the rail position
871
+ ** for the upward portion of a merge arrow. The merge arrow goes as
872
+ ** a solid normal merge line up to the row identified by "mu" and
873
+ ** then as a dashed cherrypick merge line up further to "cu".
874
+ ** If this value is omitted if there are no merge children.
852875
** mu: The id of the row which is the top of the merge-out arrow.
876
+ ** Only exists if "mo" exists.
877
+ ** cu: Extend the mu merge arrow up to this row as a cherrypick
878
+ ** merge line, if this value exists.
853879
** u: Draw a thick child-line out of the top of this node and up to
854880
** the node with an id equal to this value. 0 if it is straight to
855881
** the top of the page, -1 if there is no thick-line riser.
856882
** f: 0x01: a leaf node.
857883
** au: An array of integers that define thick-line risers for branches.
858884
** The integers are in pairs. For each pair, the first integer is
859885
** is the rail on which the riser should run and the second integer
860
- ** is the id of the node upto which the riser should run.
886
+ ** is the id of the node upto which the riser should run. If there
887
+ ** are no risers, this array does not exist.
861888
** mi: "merge-in". An array of integer rail positions from which
862889
** merge arrows should be drawn into this node. If the value is
863890
** negative, then the rail position is the absolute value of mi[]
864891
** and a thin merge-arrow descender is drawn to the bottom of
865
- ** the screen.
892
+ ** the screen. This array is omitted if there are no inbound
893
+ ** merges.
894
+ ** ci: "cherrypick-in". Like "mi" except for cherrypick merges.
895
+ ** omitted if there are no cherrypick merges.
866896
** h: The artifact hash of the object being graphed
867897
*/
868898
for(pRow=pGraph->pFirst; pRow; pRow=pRow->pNext){
899
+ int k = 0;
869900
cgi_printf("{\"id\":%d,", pRow->idx);
870901
cgi_printf("\"bg\":\"%s\",", pRow->zBgClr);
871902
cgi_printf("\"r\":%d,", pRow->iRail);
872
- cgi_printf("\"d\":%d,", pRow->bDescender);
873
- cgi_printf("\"mo\":%d,", pRow->mergeOut);
874
- cgi_printf("\"mu\":%d,", pRow->mergeUpto);
903
+ if( pRow->bDescender ){
904
+ cgi_printf("\"d\":%d,", pRow->bDescender);
905
+ }
906
+ if( pRow->mergeOut>=0 ){
907
+ cgi_printf("\"mo\":%d,", pRow->mergeOut);
908
+ if( pRow->mergeUpto==0 ) pRow->mergeUpto = pRow->idx;
909
+ cgi_printf("\"mu\":%d,", pRow->mergeUpto);
910
+ if( pRow->cherrypickUpto>0 && pRow->cherrypickUpto<pRow->mergeUpto ){
911
+ cgi_printf("\"cu\":%d,", pRow->cherrypickUpto);
912
+ }
913
+ }
875914
cgi_printf("\"u\":%d,", pRow->aiRiser[pRow->iRail]);
876
- cgi_printf("\"f\":%d,", pRow->isLeaf ? 1 : 0);
877
- cgi_printf("\"au\":");
878
- cSep = '[';
879
- for(i=0; i<GR_MAX_RAIL; i++){
915
+ k = 0;
916
+ if( pRow->isLeaf ) k |= 1;
917
+ cgi_printf("\"f\":%d,",k);
918
+ for(i=k=0; i<GR_MAX_RAIL; i++){
880919
if( i==pRow->iRail ) continue;
881920
if( pRow->aiRiser[i]>0 ){
921
+ if( k==0 ){
922
+ cgi_printf("\"au\":");
923
+ cSep = '[';
924
+ }
925
+ k++;
882926
cgi_printf("%c%d,%d", cSep, i, pRow->aiRiser[i]);
883927
cSep = ',';
884928
}
885929
}
886
- if( cSep=='[' ) cgi_printf("[");
887
- cgi_printf("],");
930
+ if( k ){
931
+ cgi_printf("],");
932
+ }
888933
if( colorGraph && pRow->zBgClr[0]=='#' ){
889934
cgi_printf("\"fg\":\"%s\",", bg_to_fg(pRow->zBgClr));
890935
}
891936
/* mi */
892
- cgi_printf("\"mi\":");
893
- cSep = '[';
894
- for(i=0; i<GR_MAX_RAIL; i++){
895
- if( pRow->mergeIn[i] ){
937
+ for(i=k=0; i<GR_MAX_RAIL; i++){
938
+ if( pRow->mergeIn[i]==1 ){
896939
int mi = i;
897940
if( (pRow->mergeDown >> i) & 1 ) mi = -mi;
941
+ if( k==0 ){
942
+ cgi_printf("\"mi\":");
943
+ cSep = '[';
944
+ }
945
+ k++;
946
+ cgi_printf("%c%d", cSep, mi);
947
+ cSep = ',';
948
+ }
949
+ }
950
+ if( k ) cgi_printf("],");
951
+ /* ci */
952
+ for(i=k=0; i<GR_MAX_RAIL; i++){
953
+ if( pRow->mergeIn[i]==2 ){
954
+ int mi = i;
955
+ if( (pRow->cherrypickDown >> i) & 1 ) mi = -mi;
956
+ if( k==0 ){
957
+ cgi_printf("\"ci\":");
958
+ cSep = '[';
959
+ }
960
+ k++;
898961
cgi_printf("%c%d", cSep, mi);
899962
cSep = ',';
900963
}
901964
}
902
- if( cSep=='[' ) cgi_printf("[");
903
- cgi_printf("],\"h\":\"%!S\"}%s",
965
+ if( k ) cgi_printf("],");
966
+ cgi_printf("\"h\":\"%!S\"}%s",
904967
pRow->zUuid, pRow->pNext ? ",\n" : "]\n");
905968
}
906969
@ }</script>
907970
style_graph_generator();
908971
graph_free(pGraph);
@@ -1372,29 +1435,33 @@
13721435
** dp=CHECKIN The same as d=CHECKIN&p=CHECKIN
13731436
** t=TAG Show only check-ins with the given TAG
13741437
** r=TAG Show check-ins related to TAG, equivalent to t=TAG&rel
13751438
** rel Show related check-ins as well as those matching t=TAG
13761439
** mionly Limit rel to show ancestors but not descendants
1440
+** nowiki Do not show wiki associated with branch or tag
13771441
** ms=MATCHSTYLE Set tag match style to EXACT, GLOB, LIKE, REGEXP
13781442
** u=USER Only show items associated with USER
13791443
** y=TYPE 'ci', 'w', 't', 'e', 'f', or 'all'.
13801444
** ss=VIEWSTYLE c: "Compact" v: "Verbose" m: "Modern" j: "Columnar"
13811445
** advm Use the "Advanced" or "Busy" menu design.
13821446
** ng No Graph.
1447
+** ncp Omit cherrypick merges
13831448
** nd Do not highlight the focus check-in
13841449
** v Show details of files changed
13851450
** f=CHECKIN Show family (immediate parents and children) of CHECKIN
13861451
** from=CHECKIN Path from...
1387
-** to=CHECKIN ... to this
1388
-** shortest ... show only the shortest path
1452
+** to=CHECKIN ... to this
1453
+** shorest ... show only the shortest path
1454
+** rel ... also show related checkins
13891455
** uf=FILE_HASH Show only check-ins that contain the given file version
13901456
** chng=GLOBLIST Show only check-ins that involve changes to a file whose
13911457
** name matches one of the comma-separate GLOBLIST
13921458
** brbg Background color from branch name
13931459
** ubg Background color from user
13941460
** namechng Show only check-ins that have filename changes
13951461
** forks Show only forks and their children
1462
+** cherrypicks Show all cherrypicks
13961463
** ym=YYYY-MM Show only events for the given year/month
13971464
** yw=YYYY-WW Show only events for the given week of the given year
13981465
** yw=YYYY-MM-DD Show events for the week that includes the given day
13991466
** ymd=YYYY-MM-DD Show only events on the given day
14001467
** days=N Show events over the previous N days
@@ -1444,10 +1511,11 @@
14441511
const char *zChng = P("chng"); /* List of GLOBs for files that changed */
14451512
int useDividers = P("nd")==0; /* Show dividers if "nd" is missing */
14461513
int renameOnly = P("namechng")!=0; /* Show only check-ins that rename files */
14471514
int forkOnly = PB("forks"); /* Show only forks and their children */
14481515
int bisectOnly = PB("bisect"); /* Show the check-ins of the bisect */
1516
+ int cpOnly = PB("cherrypicks"); /* Show all cherrypick checkins */
14491517
int tmFlags = 0; /* Timeline flags */
14501518
const char *zThisTag = 0; /* Suppress links to this tag */
14511519
const char *zThisUser = 0; /* Suppress links to this user */
14521520
HQuery url; /* URL for various branch links */
14531521
int from_rid = name_to_typed_rid(P("from"),"ci"); /* from= for paths */
@@ -1462,10 +1530,11 @@
14621530
char *zNewerButton = 0; /* URL for Newer button at the top */
14631531
int selectedRid = -9999999; /* Show a highlight on this RID */
14641532
int disableY = 0; /* Disable type selector on submenu */
14651533
int advancedMenu = 0; /* Use the advanced menu design */
14661534
char *zPlural; /* Ending for plural forms */
1535
+ int showCherrypicks = 1; /* True to show cherrypick merges */
14671536
void (*xExtra)(int) = NULL;
14681537
14691538
/* Set number of rows to display */
14701539
cookie_read_parameter("n","n");
14711540
z = P("n");
@@ -1487,10 +1556,16 @@
14871556
cgi_replace_query_parameter("n",z);
14881557
cookie_write_parameter("n","n",0);
14891558
tmFlags |= timeline_ss_submenu();
14901559
cookie_link_parameter("advm","advm","0");
14911560
advancedMenu = atoi(PD("advm","0"));
1561
+
1562
+ /* Omit all cherry-pick merge lines if the "ncp" query parameter is
1563
+ ** present or if this repository lacks a "cherrypick" table. */
1564
+ if( PB("ncp") || !db_table_exists("repository","cherrypick") ){
1565
+ showCherrypicks = 0;
1566
+ }
14921567
14931568
/* To view the timeline, must have permission to read project data.
14941569
*/
14951570
pd_rid = name_to_typed_rid(P("dp"),"ci");
14961571
if( pd_rid ){
@@ -1521,19 +1596,21 @@
15211596
cgi_delete_query_parameter("r");
15221597
cgi_set_query_parameter("t", zBrName);
15231598
cgi_set_query_parameter("rel", "1");
15241599
zTagName = zBrName;
15251600
related = 1;
1601
+ zType = "ci";
15261602
}
15271603
15281604
/* Ignore empty tag query strings. */
15291605
if( zTagName && !*zTagName ){
15301606
zTagName = 0;
15311607
}
15321608
15331609
/* Finish preliminary processing of tag match queries. */
15341610
if( zTagName ){
1611
+ zType = "ci";
15351612
/* Interpet the tag style string. */
15361613
if( fossil_stricmp(zMatchStyle, "glob")==0 ){
15371614
matchStyle = MS_GLOB;
15381615
}else if( fossil_stricmp(zMatchStyle, "like")==0 ){
15391616
matchStyle = MS_LIKE;
@@ -1563,16 +1640,19 @@
15631640
){
15641641
nEntry = -1;
15651642
zCirca = 0;
15661643
}
15671644
if( zType[0]=='a' ){
1568
- tmFlags |= TIMELINE_BRIEF | TIMELINE_GRAPH;
1645
+ tmFlags |= TIMELINE_BRIEF | TIMELINE_GRAPH | TIMELINE_CHPICK;
15691646
}else{
1570
- tmFlags |= TIMELINE_GRAPH;
1647
+ tmFlags |= TIMELINE_GRAPH | TIMELINE_CHPICK;
1648
+ }
1649
+ if( PB("ncp") ){
1650
+ tmFlags &= ~TIMELINE_CHPICK;
15711651
}
15721652
if( PB("ng") || zSearch!=0 ){
1573
- tmFlags &= ~TIMELINE_GRAPH;
1653
+ tmFlags &= ~(TIMELINE_GRAPH|TIMELINE_CHPICK);
15741654
}
15751655
if( PB("brbg") ){
15761656
tmFlags |= TIMELINE_BRCOLOR;
15771657
}
15781658
if( PB("unhide") ){
@@ -1658,10 +1738,12 @@
16581738
/* If from= and to= are present, display all nodes on a path connecting
16591739
** the two */
16601740
PathNode *p = 0;
16611741
const char *zFrom = 0;
16621742
const char *zTo = 0;
1743
+ Blob ins;
1744
+ int nNodeOnPath = 0;
16631745
16641746
if( from_rid && to_rid ){
16651747
p = path_shortest(from_rid, to_rid, noMerge, 0);
16661748
zFrom = P("from");
16671749
zTo = P("to");
@@ -1670,28 +1752,72 @@
16701752
p = path_first();
16711753
}
16721754
zFrom = P("me");
16731755
zTo = P("you");
16741756
}
1675
- blob_append(&sql, " AND event.objid IN (0", -1);
1676
- while( p ){
1677
- blob_append_sql(&sql, ",%d", p->rid);
1757
+ blob_init(&ins, 0, 0);
1758
+ db_multi_exec(
1759
+ "CREATE TABLE IF NOT EXISTS temp.pathnode(x INTEGER PRIMARY KEY);"
1760
+ );
1761
+ if( p ){
1762
+ blob_init(&ins, 0, 0);
1763
+ blob_append_sql(&ins, "INSERT INTO pathnode(x) VALUES(%d)", p->rid);
16781764
p = p->u.pTo;
1765
+ nNodeOnPath = 1;
1766
+ while( p ){
1767
+ blob_append_sql(&ins, ",(%d)", p->rid);
1768
+ p = p->u.pTo;
1769
+ nNodeOnPath++;
1770
+ }
16791771
}
1680
- blob_append(&sql, ")", -1);
16811772
path_reset();
1773
+ db_multi_exec("%s", blob_str(&ins)/*safe-for-%s*/);
1774
+ blob_reset(&ins);
1775
+ if( related ){
1776
+ db_multi_exec(
1777
+ "CREATE TABLE IF NOT EXISTS temp.related(x INTEGER PRIMARY KEY);"
1778
+ "INSERT OR IGNORE INTO related(x)"
1779
+ " SELECT pid FROM plink WHERE cid IN pathnode AND NOT isprim;"
1780
+ );
1781
+ if( P("mionly")==0 ){
1782
+ db_multi_exec(
1783
+ "INSERT OR IGNORE INTO related(x)"
1784
+ " SELECT cid FROM plink WHERE pid IN pathnode;"
1785
+ );
1786
+ }
1787
+ if( showCherrypicks ){
1788
+ db_multi_exec(
1789
+ "INSERT OR IGNORE INTO related(x)"
1790
+ " SELECT parentid FROM cherrypick WHERE childid IN pathnode;"
1791
+ );
1792
+ if( P("mionly")==0 ){
1793
+ db_multi_exec(
1794
+ "INSERT OR IGNORE INTO related(x)"
1795
+ " SELECT childid FROM cherrypick WHERE parentid IN pathnode;"
1796
+ );
1797
+ }
1798
+ }
1799
+ db_multi_exec("INSERT OR IGNORE INTO pathnode SELECT x FROM related");
1800
+ }
1801
+ blob_append_sql(&sql, " AND event.objid IN pathnode");
16821802
addFileGlobExclusion(zChng, &sql);
16831803
tmFlags |= TIMELINE_DISJOINT;
16841804
db_multi_exec("%s", blob_sql_text(&sql));
16851805
if( advancedMenu ){
16861806
style_submenu_checkbox("v", "Files", (zType[0]!='a' && zType[0]!='c'),0);
16871807
}
1688
- blob_appendf(&desc, "%d check-ins going from ",
1689
- db_int(0, "SELECT count(*) FROM timeline"));
1808
+ blob_appendf(&desc, "%d check-ins going from ", nNodeOnPath);
16901809
blob_appendf(&desc, "%z[%h]</a>", href("%R/info/%h", zFrom), zFrom);
16911810
blob_append(&desc, " to ", -1);
16921811
blob_appendf(&desc, "%z[%h]</a>", href("%R/info/%h",zTo), zTo);
1812
+ if( related ){
1813
+ int nRelated = db_int(0, "SELECT count(*) FROM timeline") - nNodeOnPath;
1814
+ if( nRelated>0 ){
1815
+ blob_appendf(&desc, " and %d related check-in%s", nRelated,
1816
+ nRelated>1 ? "s" : "");
1817
+ }
1818
+ }
16931819
addFileGlobDescription(zChng, &desc);
16941820
}else if( (p_rid || d_rid) && g.perm.Read && zTagSql==0 ){
16951821
/* If p= or d= is present, ignore all other parameters other than n= */
16961822
char *zUuid;
16971823
int np, nd;
@@ -1707,19 +1833,19 @@
17071833
zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d",
17081834
p_rid ? p_rid : d_rid);
17091835
blob_append_sql(&sql, " AND event.objid IN ok");
17101836
nd = 0;
17111837
if( d_rid ){
1712
- compute_descendants(d_rid, nEntry+1);
1838
+ compute_descendants(d_rid, nEntry==0 ? 0 : nEntry+1);
17131839
nd = db_int(0, "SELECT count(*)-1 FROM ok");
17141840
if( nd>=0 ) db_multi_exec("%s", blob_sql_text(&sql));
17151841
if( nd>0 ) blob_appendf(&desc, "%d descendant%s", nd,(1==nd)?"":"s");
17161842
if( useDividers ) selectedRid = d_rid;
17171843
db_multi_exec("DELETE FROM ok");
17181844
}
17191845
if( p_rid ){
1720
- compute_ancestors(p_rid, nEntry+1, 0);
1846
+ compute_ancestors(p_rid, nEntry==0 ? 0 : nEntry+1, 0);
17211847
np = db_int(0, "SELECT count(*)-1 FROM ok");
17221848
if( np>0 ){
17231849
if( nd>0 ) blob_appendf(&desc, " and ");
17241850
blob_appendf(&desc, "%d ancestors", np);
17251851
db_multi_exec("%s", blob_sql_text(&sql));
@@ -1747,10 +1873,19 @@
17471873
"INSERT INTO ok VALUES(%d);"
17481874
"INSERT OR IGNORE INTO ok SELECT pid FROM plink WHERE cid=%d;"
17491875
"INSERT OR IGNORE INTO ok SELECT cid FROM plink WHERE pid=%d;",
17501876
f_rid, f_rid, f_rid
17511877
);
1878
+ if( showCherrypicks ){
1879
+ db_multi_exec(
1880
+ "INSERT OR IGNORE INTO ok SELECT parentid FROM cherrypick"
1881
+ " WHERE childid=%d;"
1882
+ "INSERT OR IGNORE INTO ok SELECT childid FROM cherrypick"
1883
+ " WHERE parentid=%d;",
1884
+ f_rid, f_rid
1885
+ );
1886
+ }
17521887
blob_append_sql(&sql, " AND event.objid IN ok");
17531888
db_multi_exec("%s", blob_sql_text(&sql));
17541889
if( useDividers ) selectedRid = f_rid;
17551890
blob_appendf(&desc, "Parents and children of check-in ");
17561891
zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d", f_rid);
@@ -1778,10 +1913,18 @@
17781913
blob_append_sql(&cond, " AND event.objid IN rnfile ");
17791914
}
17801915
if( forkOnly ){
17811916
blob_append_sql(&cond, " AND event.objid IN rnfork ");
17821917
}
1918
+ if( cpOnly && showCherrypicks ){
1919
+ db_multi_exec(
1920
+ "CREATE TABLE IF NOT EXISTS cpnodes(rid INTEGER PRIMARY KEY);"
1921
+ "INSERT OR IGNORE INTO cpnodes SELECT childid FROM cherrypick;"
1922
+ "INSERT OR IGNORE INTO cpnodes SELECT parentid FROM cherrypick;"
1923
+ );
1924
+ blob_append_sql(&cond, " AND event.objid IN cpnodes ");
1925
+ }
17831926
if( bisectOnly ){
17841927
blob_append_sql(&cond, " AND event.objid IN (SELECT rid FROM bilog) ");
17851928
}
17861929
if( zYearMonth ){
17871930
blob_append_sql(&cond, " AND %Q=strftime('%%Y-%%m',event.mtime) ",
@@ -1827,49 +1970,65 @@
18271970
blob_append_sql(&cond, " AND event.mtime>=julianday('now','-%d days') ",
18281971
nDays);
18291972
nEntry = -1;
18301973
}
18311974
if( zTagSql ){
1832
- blob_append_sql(&cond,
1833
- " AND (EXISTS(SELECT 1 FROM tagxref NATURAL JOIN tag"
1834
- " WHERE %s AND tagtype>0 AND rid=blob.rid)\n", zTagSql/*safe-for-%s*/);
1835
-
1836
- if( related ){
1837
- /* The next two blob_appendf() calls add SQL that causes check-ins that
1838
- ** are not part of the branch which are parents or children of the
1839
- ** branch to be included in the report. This related check-ins are
1840
- ** useful in helping to visualize what has happened on a quiescent
1841
- ** branch that is infrequently merged with a much more activate branch.
1842
- */
1843
- blob_append_sql(&cond,
1844
- " OR EXISTS(SELECT 1 FROM plink CROSS JOIN tagxref ON rid=cid"
1845
- " NATURAL JOIN tag WHERE %s AND tagtype>0 AND pid=blob.rid)\n",
1846
- zTagSql/*safe-for-%s*/
1847
- );
1848
- if( (tmFlags & TIMELINE_UNHIDE)==0 ){
1849
- blob_append_sql(&cond,
1850
- " AND NOT EXISTS(SELECT 1 FROM plink JOIN tagxref ON rid=cid"
1851
- " WHERE tagid=%d AND tagtype>0 AND pid=blob.rid)\n",
1852
- TAG_HIDDEN
1853
- );
1854
- }
1855
- if( P("mionly")==0 ){
1856
- blob_append_sql(&cond,
1857
- " OR EXISTS(SELECT 1 FROM plink CROSS JOIN tagxref ON rid=pid"
1858
- " NATURAL JOIN tag WHERE %s AND tagtype>0 AND cid=blob.rid)\n",
1859
- zTagSql/*safe-for-%s*/
1860
- );
1861
- if( (tmFlags & TIMELINE_UNHIDE)==0 ){
1862
- blob_append_sql(&cond,
1863
- " AND NOT EXISTS(SELECT 1 FROM plink JOIN tagxref ON rid=pid"
1864
- " WHERE tagid=%d AND tagtype>0 AND cid=blob.rid)\n",
1865
- TAG_HIDDEN
1866
- );
1867
- }
1868
- }
1869
- }
1870
- blob_append_sql(&cond, ")");
1975
+ db_multi_exec(
1976
+ "CREATE TEMP TABLE selected_nodes(rid INTEGER PRIMARY KEY);"
1977
+ "INSERT OR IGNORE INTO selected_nodes"
1978
+ " SELECT tagxref.rid FROM tagxref NATURAL JOIN tag"
1979
+ " WHERE %s AND tagtype>0", zTagSql/*safe-for-%s*/
1980
+ );
1981
+ if( !related ){
1982
+ blob_append_sql(&cond, " AND blob.rid IN selected_nodes");
1983
+ }else{
1984
+ db_multi_exec(
1985
+ "CREATE TEMP TABLE related_nodes(rid INTEGER PRIMARY KEY);"
1986
+ "INSERT INTO related_nodes SELECT rid FROM selected_nodes;"
1987
+ );
1988
+ blob_append_sql(&cond, " AND blob.rid IN related_nodes");
1989
+ /* The next two blob_appendf() calls add SQL that causes check-ins that
1990
+ ** are not part of the branch which are parents or children of the
1991
+ ** branch to be included in the report. These related check-ins are
1992
+ ** useful in helping to visualize what has happened on a quiescent
1993
+ ** branch that is infrequently merged with a much more activate branch.
1994
+ */
1995
+ db_multi_exec(
1996
+ "INSERT OR IGNORE INTO related_nodes"
1997
+ " SELECT pid FROM selected_nodes CROSS JOIN plink"
1998
+ " WHERE selected_nodes.rid=plink.cid;"
1999
+ );
2000
+ if( P("mionly")==0 ){
2001
+ db_multi_exec(
2002
+ "INSERT OR IGNORE INTO related_nodes"
2003
+ " SELECT cid FROM selected_nodes CROSS JOIN plink"
2004
+ " WHERE selected_nodes.rid=plink.pid;"
2005
+ );
2006
+ if( showCherrypicks ){
2007
+ db_multi_exec(
2008
+ "INSERT OR IGNORE INTO related_nodes"
2009
+ " SELECT childid FROM selected_nodes CROSS JOIN cherrypick"
2010
+ " WHERE selected_nodes.rid=cherrypick.parentid;"
2011
+ );
2012
+ }
2013
+ }
2014
+ if( showCherrypicks ){
2015
+ db_multi_exec(
2016
+ "INSERT OR IGNORE INTO related_nodes"
2017
+ " SELECT parentid FROM selected_nodes CROSS JOIN cherrypick"
2018
+ " WHERE selected_nodes.rid=cherrypick.childid;"
2019
+ );
2020
+ }
2021
+ if( (tmFlags & TIMELINE_UNHIDE)==0 ){
2022
+ db_multi_exec(
2023
+ "DELETE FROM related_nodes WHERE rid IN "
2024
+ " (SELECT related_nodes.rid FROM related_nodes, tagxref"
2025
+ " WHERE tagid=%d AND tagtype>0 AND tagxref.rid=related_nodes.rid)",
2026
+ TAG_HIDDEN
2027
+ );
2028
+ }
2029
+ }
18712030
}
18722031
if( (zType[0]=='w' && !g.perm.RdWiki)
18732032
|| (zType[0]=='t' && !g.perm.RdTkt)
18742033
|| (zType[0]=='e' && !g.perm.RdWiki)
18752034
|| (zType[0]=='c' && !g.perm.Read)
@@ -2013,10 +2172,14 @@
20132172
}
20142173
if( bisectOnly ){
20152174
blob_appendf(&desc, " in the most recent bisect");
20162175
tmFlags |= TIMELINE_DISJOINT;
20172176
}
2177
+ if( cpOnly && showCherrypicks ){
2178
+ blob_appendf(&desc, " that participate in a cherrypick merge");
2179
+ tmFlags |= TIMELINE_CHPICK|TIMELINE_DISJOINT;
2180
+ }
20182181
if( zUser ){
20192182
blob_appendf(&desc, " by user %h", zUser);
20202183
tmFlags |= TIMELINE_DISJOINT;
20212184
}
20222185
if( zTagSql ){
@@ -2106,13 +2269,15 @@
21062269
}
21072270
if( search_restrict(SRCH_CKIN)!=0 ){
21082271
style_submenu_element("Search", "%R/search?y=c");
21092272
}
21102273
if( advancedMenu ){
2111
- style_submenu_element("Basic", "%s", url_render(&url, "advm", "0", 0, 0));
2274
+ style_submenu_element("Basic", "%s",
2275
+ url_render(&url, "advm", "0", "udc", "1"));
21122276
}else{
2113
- style_submenu_element("Advanced", "%s", url_render(&url, "advm", "1", 0, 0));
2277
+ style_submenu_element("Advanced", "%s",
2278
+ url_render(&url, "advm", "1", "udc", "1"));
21142279
}
21152280
if( PB("showid") ) tmFlags |= TIMELINE_SHOWRID;
21162281
if( useDividers && zMark && zMark[0] ){
21172282
double r = symbolic_name_to_mtime(zMark);
21182283
if( r>0.0 ) selectedRid = timeline_add_divider(r);
@@ -2120,11 +2285,26 @@
21202285
blob_zero(&sql);
21212286
db_prepare(&q, "SELECT * FROM timeline ORDER BY sortby DESC /*scan*/");
21222287
if( fossil_islower(desc.aData[0]) ){
21232288
desc.aData[0] = fossil_toupper(desc.aData[0]);
21242289
}
2125
- @ <h2>%b(&desc)</h2>
2290
+ if( zBrName
2291
+ && !PB("nowiki")
2292
+ && wiki_render_associated("branch", zBrName, WIKIASSOC_ALL)
2293
+ ){
2294
+ @ <div class="section">%b(&desc)</div>
2295
+ }else
2296
+ if( zTagName
2297
+ && matchStyle==MS_EXACT
2298
+ && zBrName==0
2299
+ && !PB("nowiki")
2300
+ && wiki_render_associated("tag", zTagName, WIKIASSOC_ALL)
2301
+ ){
2302
+ @ <div class="section">%b(&desc)</div>
2303
+ } else{
2304
+ @ <h2>%b(&desc)</h2>
2305
+ }
21262306
blob_reset(&desc);
21272307
21282308
/* Report any errors. */
21292309
if( zError ){
21302310
@ <p class="generalError">%h(zError)</p>
21312311
--- src/timeline.c
+++ src/timeline.c
@@ -91,12 +91,12 @@
91
92 /*
93 ** Allowed flags for the tmFlags argument to www_print_timeline
94 */
95 #if INTERFACE
96 #define TIMELINE_ARTID 0x000001 /* Show artifact IDs on non-check-in lines */
97 #define TIMELINE_LEAFONLY 0x000002 /* Show "Leaf" but not "Merge", "Fork" etc */
98 #define TIMELINE_BRIEF 0x000004 /* Combine adjacent elements of same obj */
99 #define TIMELINE_GRAPH 0x000008 /* Compute a graph */
100 #define TIMELINE_DISJOINT 0x000010 /* Elements are not contiguous */
101 #define TIMELINE_FCHANGES 0x000020 /* Detail file changes */
102 #define TIMELINE_BRCOLOR 0x000040 /* Background color by branch name */
@@ -111,10 +111,11 @@
111 #define TIMELINE_COLUMNAR 0x008000 /* Use the "columns" view style */
112 #define TIMELINE_CLASSIC 0x010000 /* Use the "classic" view style */
113 #define TIMELINE_VIEWS 0x01f000 /* Mask for all of the view styles */
114 #define TIMELINE_NOSCROLL 0x100000 /* Don't scroll to the selection */
115 #define TIMELINE_FILEDIFF 0x200000 /* Show File differences, not ckin diffs */
 
116 #endif
117
118 /*
119 ** Hash a string and use the hash to determine a background color.
120 */
@@ -290,10 +291,15 @@
290 }
291 db_static_prepare(&qbranch,
292 "SELECT value FROM tagxref WHERE tagid=%d AND tagtype>0 AND rid=:rid",
293 TAG_BRANCH
294 );
 
 
 
 
 
295
296 @ <table id="timelineTable%d(iTableId)" class="timelineTable">
297 blob_zero(&comment);
298 while( db_step(pQuery)==SQLITE_ROW ){
299 int rid = db_column_int(pQuery, 0);
@@ -423,10 +429,11 @@
423 }
424 }
425 }
426 if( zType[0]=='c' && pGraph ){
427 int nParent = 0;
 
428 int aParent[GR_MAX_RAIL];
429 static Stmt qparent;
430 db_static_prepare(&qparent,
431 "SELECT pid FROM plink"
432 " WHERE cid=:rid AND pid NOT IN phantom"
@@ -435,19 +442,32 @@
435 db_bind_int(&qparent, ":rid", rid);
436 while( db_step(&qparent)==SQLITE_ROW && nParent<count(aParent) ){
437 aParent[nParent++] = db_column_int(&qparent, 0);
438 }
439 db_reset(&qparent);
440 gidx = graph_add_row(pGraph, rid, nParent, aParent, zBr, zBgClr,
441 zUuid, isLeaf);
 
 
 
 
 
 
 
 
 
 
 
 
 
442 db_reset(&qbranch);
443 @ <div id="m%d(gidx)" class="tl-nodemark"></div>
444 }else if( zType[0]=='e' && pGraph && zBgClr && zBgClr[0] ){
445 /* For technotes, make a graph node with nParent==(-1). This will
446 ** not actually draw anything on the graph, but it will set the
447 ** background color of the timeline entry */
448 gidx = graph_add_row(pGraph, rid, -1, 0, zBr, zBgClr, zUuid, 0);
449 @ <div id="m%d(gidx)" class="tl-nodemark"></div>
450 }
451 @</td>
452 if( !isSelectedOrCurrent ){
453 @ <td class="timeline%s(zStyle)Cell" id='mc%d(gidx)'>
@@ -505,11 +525,13 @@
505 }
506 }
507 if( zType[0]!='c' ){
508 /* Comments for anything other than a check-in are generated by
509 ** "fossil rebuild" and expect to be rendered as text/x-fossil-wiki */
 
510 wiki_convert(&comment, 0, WIKI_INLINE);
 
511 }else{
512 if( bCommentGitStyle ){
513 /* Truncate comment at first blank line */
514 int ii, jj;
515 int n = blob_size(&comment);
@@ -841,68 +863,109 @@
841 ** to get an actual id, prepend "m" to the integer. The top node
842 ** is iTopRow and numbers increase moving down the timeline.
843 ** bg: The background color for this row
844 ** r: The "rail" that the node for this row sits on. The left-most
845 ** rail is 0 and the number increases to the right.
846 ** d: True if there is a "descender" - an arrow coming from the bottom
847 ** of the page straight up to this node.
848 ** mo: "merge-out". If non-negative, this is the rail position
849 ** for the upward portion of a merge arrow. The merge arrow goes up
850 ** to the row identified by mu:. If this value is negative then
851 ** node has no merge children and no merge-out line is drawn.
 
852 ** mu: The id of the row which is the top of the merge-out arrow.
 
 
 
853 ** u: Draw a thick child-line out of the top of this node and up to
854 ** the node with an id equal to this value. 0 if it is straight to
855 ** the top of the page, -1 if there is no thick-line riser.
856 ** f: 0x01: a leaf node.
857 ** au: An array of integers that define thick-line risers for branches.
858 ** The integers are in pairs. For each pair, the first integer is
859 ** is the rail on which the riser should run and the second integer
860 ** is the id of the node upto which the riser should run.
 
861 ** mi: "merge-in". An array of integer rail positions from which
862 ** merge arrows should be drawn into this node. If the value is
863 ** negative, then the rail position is the absolute value of mi[]
864 ** and a thin merge-arrow descender is drawn to the bottom of
865 ** the screen.
 
 
 
866 ** h: The artifact hash of the object being graphed
867 */
868 for(pRow=pGraph->pFirst; pRow; pRow=pRow->pNext){
 
869 cgi_printf("{\"id\":%d,", pRow->idx);
870 cgi_printf("\"bg\":\"%s\",", pRow->zBgClr);
871 cgi_printf("\"r\":%d,", pRow->iRail);
872 cgi_printf("\"d\":%d,", pRow->bDescender);
873 cgi_printf("\"mo\":%d,", pRow->mergeOut);
874 cgi_printf("\"mu\":%d,", pRow->mergeUpto);
 
 
 
 
 
 
 
 
875 cgi_printf("\"u\":%d,", pRow->aiRiser[pRow->iRail]);
876 cgi_printf("\"f\":%d,", pRow->isLeaf ? 1 : 0);
877 cgi_printf("\"au\":");
878 cSep = '[';
879 for(i=0; i<GR_MAX_RAIL; i++){
880 if( i==pRow->iRail ) continue;
881 if( pRow->aiRiser[i]>0 ){
 
 
 
 
 
882 cgi_printf("%c%d,%d", cSep, i, pRow->aiRiser[i]);
883 cSep = ',';
884 }
885 }
886 if( cSep=='[' ) cgi_printf("[");
887 cgi_printf("],");
 
888 if( colorGraph && pRow->zBgClr[0]=='#' ){
889 cgi_printf("\"fg\":\"%s\",", bg_to_fg(pRow->zBgClr));
890 }
891 /* mi */
892 cgi_printf("\"mi\":");
893 cSep = '[';
894 for(i=0; i<GR_MAX_RAIL; i++){
895 if( pRow->mergeIn[i] ){
896 int mi = i;
897 if( (pRow->mergeDown >> i) & 1 ) mi = -mi;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
898 cgi_printf("%c%d", cSep, mi);
899 cSep = ',';
900 }
901 }
902 if( cSep=='[' ) cgi_printf("[");
903 cgi_printf("],\"h\":\"%!S\"}%s",
904 pRow->zUuid, pRow->pNext ? ",\n" : "]\n");
905 }
906 @ }</script>
907 style_graph_generator();
908 graph_free(pGraph);
@@ -1372,29 +1435,33 @@
1372 ** dp=CHECKIN The same as d=CHECKIN&p=CHECKIN
1373 ** t=TAG Show only check-ins with the given TAG
1374 ** r=TAG Show check-ins related to TAG, equivalent to t=TAG&rel
1375 ** rel Show related check-ins as well as those matching t=TAG
1376 ** mionly Limit rel to show ancestors but not descendants
 
1377 ** ms=MATCHSTYLE Set tag match style to EXACT, GLOB, LIKE, REGEXP
1378 ** u=USER Only show items associated with USER
1379 ** y=TYPE 'ci', 'w', 't', 'e', 'f', or 'all'.
1380 ** ss=VIEWSTYLE c: "Compact" v: "Verbose" m: "Modern" j: "Columnar"
1381 ** advm Use the "Advanced" or "Busy" menu design.
1382 ** ng No Graph.
 
1383 ** nd Do not highlight the focus check-in
1384 ** v Show details of files changed
1385 ** f=CHECKIN Show family (immediate parents and children) of CHECKIN
1386 ** from=CHECKIN Path from...
1387 ** to=CHECKIN ... to this
1388 ** shortest ... show only the shortest path
 
1389 ** uf=FILE_HASH Show only check-ins that contain the given file version
1390 ** chng=GLOBLIST Show only check-ins that involve changes to a file whose
1391 ** name matches one of the comma-separate GLOBLIST
1392 ** brbg Background color from branch name
1393 ** ubg Background color from user
1394 ** namechng Show only check-ins that have filename changes
1395 ** forks Show only forks and their children
 
1396 ** ym=YYYY-MM Show only events for the given year/month
1397 ** yw=YYYY-WW Show only events for the given week of the given year
1398 ** yw=YYYY-MM-DD Show events for the week that includes the given day
1399 ** ymd=YYYY-MM-DD Show only events on the given day
1400 ** days=N Show events over the previous N days
@@ -1444,10 +1511,11 @@
1444 const char *zChng = P("chng"); /* List of GLOBs for files that changed */
1445 int useDividers = P("nd")==0; /* Show dividers if "nd" is missing */
1446 int renameOnly = P("namechng")!=0; /* Show only check-ins that rename files */
1447 int forkOnly = PB("forks"); /* Show only forks and their children */
1448 int bisectOnly = PB("bisect"); /* Show the check-ins of the bisect */
 
1449 int tmFlags = 0; /* Timeline flags */
1450 const char *zThisTag = 0; /* Suppress links to this tag */
1451 const char *zThisUser = 0; /* Suppress links to this user */
1452 HQuery url; /* URL for various branch links */
1453 int from_rid = name_to_typed_rid(P("from"),"ci"); /* from= for paths */
@@ -1462,10 +1530,11 @@
1462 char *zNewerButton = 0; /* URL for Newer button at the top */
1463 int selectedRid = -9999999; /* Show a highlight on this RID */
1464 int disableY = 0; /* Disable type selector on submenu */
1465 int advancedMenu = 0; /* Use the advanced menu design */
1466 char *zPlural; /* Ending for plural forms */
 
1467 void (*xExtra)(int) = NULL;
1468
1469 /* Set number of rows to display */
1470 cookie_read_parameter("n","n");
1471 z = P("n");
@@ -1487,10 +1556,16 @@
1487 cgi_replace_query_parameter("n",z);
1488 cookie_write_parameter("n","n",0);
1489 tmFlags |= timeline_ss_submenu();
1490 cookie_link_parameter("advm","advm","0");
1491 advancedMenu = atoi(PD("advm","0"));
 
 
 
 
 
 
1492
1493 /* To view the timeline, must have permission to read project data.
1494 */
1495 pd_rid = name_to_typed_rid(P("dp"),"ci");
1496 if( pd_rid ){
@@ -1521,19 +1596,21 @@
1521 cgi_delete_query_parameter("r");
1522 cgi_set_query_parameter("t", zBrName);
1523 cgi_set_query_parameter("rel", "1");
1524 zTagName = zBrName;
1525 related = 1;
 
1526 }
1527
1528 /* Ignore empty tag query strings. */
1529 if( zTagName && !*zTagName ){
1530 zTagName = 0;
1531 }
1532
1533 /* Finish preliminary processing of tag match queries. */
1534 if( zTagName ){
 
1535 /* Interpet the tag style string. */
1536 if( fossil_stricmp(zMatchStyle, "glob")==0 ){
1537 matchStyle = MS_GLOB;
1538 }else if( fossil_stricmp(zMatchStyle, "like")==0 ){
1539 matchStyle = MS_LIKE;
@@ -1563,16 +1640,19 @@
1563 ){
1564 nEntry = -1;
1565 zCirca = 0;
1566 }
1567 if( zType[0]=='a' ){
1568 tmFlags |= TIMELINE_BRIEF | TIMELINE_GRAPH;
1569 }else{
1570 tmFlags |= TIMELINE_GRAPH;
 
 
 
1571 }
1572 if( PB("ng") || zSearch!=0 ){
1573 tmFlags &= ~TIMELINE_GRAPH;
1574 }
1575 if( PB("brbg") ){
1576 tmFlags |= TIMELINE_BRCOLOR;
1577 }
1578 if( PB("unhide") ){
@@ -1658,10 +1738,12 @@
1658 /* If from= and to= are present, display all nodes on a path connecting
1659 ** the two */
1660 PathNode *p = 0;
1661 const char *zFrom = 0;
1662 const char *zTo = 0;
 
 
1663
1664 if( from_rid && to_rid ){
1665 p = path_shortest(from_rid, to_rid, noMerge, 0);
1666 zFrom = P("from");
1667 zTo = P("to");
@@ -1670,28 +1752,72 @@
1670 p = path_first();
1671 }
1672 zFrom = P("me");
1673 zTo = P("you");
1674 }
1675 blob_append(&sql, " AND event.objid IN (0", -1);
1676 while( p ){
1677 blob_append_sql(&sql, ",%d", p->rid);
 
 
 
 
1678 p = p->u.pTo;
 
 
 
 
 
 
1679 }
1680 blob_append(&sql, ")", -1);
1681 path_reset();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1682 addFileGlobExclusion(zChng, &sql);
1683 tmFlags |= TIMELINE_DISJOINT;
1684 db_multi_exec("%s", blob_sql_text(&sql));
1685 if( advancedMenu ){
1686 style_submenu_checkbox("v", "Files", (zType[0]!='a' && zType[0]!='c'),0);
1687 }
1688 blob_appendf(&desc, "%d check-ins going from ",
1689 db_int(0, "SELECT count(*) FROM timeline"));
1690 blob_appendf(&desc, "%z[%h]</a>", href("%R/info/%h", zFrom), zFrom);
1691 blob_append(&desc, " to ", -1);
1692 blob_appendf(&desc, "%z[%h]</a>", href("%R/info/%h",zTo), zTo);
 
 
 
 
 
 
 
1693 addFileGlobDescription(zChng, &desc);
1694 }else if( (p_rid || d_rid) && g.perm.Read && zTagSql==0 ){
1695 /* If p= or d= is present, ignore all other parameters other than n= */
1696 char *zUuid;
1697 int np, nd;
@@ -1707,19 +1833,19 @@
1707 zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d",
1708 p_rid ? p_rid : d_rid);
1709 blob_append_sql(&sql, " AND event.objid IN ok");
1710 nd = 0;
1711 if( d_rid ){
1712 compute_descendants(d_rid, nEntry+1);
1713 nd = db_int(0, "SELECT count(*)-1 FROM ok");
1714 if( nd>=0 ) db_multi_exec("%s", blob_sql_text(&sql));
1715 if( nd>0 ) blob_appendf(&desc, "%d descendant%s", nd,(1==nd)?"":"s");
1716 if( useDividers ) selectedRid = d_rid;
1717 db_multi_exec("DELETE FROM ok");
1718 }
1719 if( p_rid ){
1720 compute_ancestors(p_rid, nEntry+1, 0);
1721 np = db_int(0, "SELECT count(*)-1 FROM ok");
1722 if( np>0 ){
1723 if( nd>0 ) blob_appendf(&desc, " and ");
1724 blob_appendf(&desc, "%d ancestors", np);
1725 db_multi_exec("%s", blob_sql_text(&sql));
@@ -1747,10 +1873,19 @@
1747 "INSERT INTO ok VALUES(%d);"
1748 "INSERT OR IGNORE INTO ok SELECT pid FROM plink WHERE cid=%d;"
1749 "INSERT OR IGNORE INTO ok SELECT cid FROM plink WHERE pid=%d;",
1750 f_rid, f_rid, f_rid
1751 );
 
 
 
 
 
 
 
 
 
1752 blob_append_sql(&sql, " AND event.objid IN ok");
1753 db_multi_exec("%s", blob_sql_text(&sql));
1754 if( useDividers ) selectedRid = f_rid;
1755 blob_appendf(&desc, "Parents and children of check-in ");
1756 zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d", f_rid);
@@ -1778,10 +1913,18 @@
1778 blob_append_sql(&cond, " AND event.objid IN rnfile ");
1779 }
1780 if( forkOnly ){
1781 blob_append_sql(&cond, " AND event.objid IN rnfork ");
1782 }
 
 
 
 
 
 
 
 
1783 if( bisectOnly ){
1784 blob_append_sql(&cond, " AND event.objid IN (SELECT rid FROM bilog) ");
1785 }
1786 if( zYearMonth ){
1787 blob_append_sql(&cond, " AND %Q=strftime('%%Y-%%m',event.mtime) ",
@@ -1827,49 +1970,65 @@
1827 blob_append_sql(&cond, " AND event.mtime>=julianday('now','-%d days') ",
1828 nDays);
1829 nEntry = -1;
1830 }
1831 if( zTagSql ){
1832 blob_append_sql(&cond,
1833 " AND (EXISTS(SELECT 1 FROM tagxref NATURAL JOIN tag"
1834 " WHERE %s AND tagtype>0 AND rid=blob.rid)\n", zTagSql/*safe-for-%s*/);
1835
1836 if( related ){
1837 /* The next two blob_appendf() calls add SQL that causes check-ins that
1838 ** are not part of the branch which are parents or children of the
1839 ** branch to be included in the report. This related check-ins are
1840 ** useful in helping to visualize what has happened on a quiescent
1841 ** branch that is infrequently merged with a much more activate branch.
1842 */
1843 blob_append_sql(&cond,
1844 " OR EXISTS(SELECT 1 FROM plink CROSS JOIN tagxref ON rid=cid"
1845 " NATURAL JOIN tag WHERE %s AND tagtype>0 AND pid=blob.rid)\n",
1846 zTagSql/*safe-for-%s*/
1847 );
1848 if( (tmFlags & TIMELINE_UNHIDE)==0 ){
1849 blob_append_sql(&cond,
1850 " AND NOT EXISTS(SELECT 1 FROM plink JOIN tagxref ON rid=cid"
1851 " WHERE tagid=%d AND tagtype>0 AND pid=blob.rid)\n",
1852 TAG_HIDDEN
1853 );
1854 }
1855 if( P("mionly")==0 ){
1856 blob_append_sql(&cond,
1857 " OR EXISTS(SELECT 1 FROM plink CROSS JOIN tagxref ON rid=pid"
1858 " NATURAL JOIN tag WHERE %s AND tagtype>0 AND cid=blob.rid)\n",
1859 zTagSql/*safe-for-%s*/
1860 );
1861 if( (tmFlags & TIMELINE_UNHIDE)==0 ){
1862 blob_append_sql(&cond,
1863 " AND NOT EXISTS(SELECT 1 FROM plink JOIN tagxref ON rid=pid"
1864 " WHERE tagid=%d AND tagtype>0 AND cid=blob.rid)\n",
1865 TAG_HIDDEN
1866 );
1867 }
1868 }
1869 }
1870 blob_append_sql(&cond, ")");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1871 }
1872 if( (zType[0]=='w' && !g.perm.RdWiki)
1873 || (zType[0]=='t' && !g.perm.RdTkt)
1874 || (zType[0]=='e' && !g.perm.RdWiki)
1875 || (zType[0]=='c' && !g.perm.Read)
@@ -2013,10 +2172,14 @@
2013 }
2014 if( bisectOnly ){
2015 blob_appendf(&desc, " in the most recent bisect");
2016 tmFlags |= TIMELINE_DISJOINT;
2017 }
 
 
 
 
2018 if( zUser ){
2019 blob_appendf(&desc, " by user %h", zUser);
2020 tmFlags |= TIMELINE_DISJOINT;
2021 }
2022 if( zTagSql ){
@@ -2106,13 +2269,15 @@
2106 }
2107 if( search_restrict(SRCH_CKIN)!=0 ){
2108 style_submenu_element("Search", "%R/search?y=c");
2109 }
2110 if( advancedMenu ){
2111 style_submenu_element("Basic", "%s", url_render(&url, "advm", "0", 0, 0));
 
2112 }else{
2113 style_submenu_element("Advanced", "%s", url_render(&url, "advm", "1", 0, 0));
 
2114 }
2115 if( PB("showid") ) tmFlags |= TIMELINE_SHOWRID;
2116 if( useDividers && zMark && zMark[0] ){
2117 double r = symbolic_name_to_mtime(zMark);
2118 if( r>0.0 ) selectedRid = timeline_add_divider(r);
@@ -2120,11 +2285,26 @@
2120 blob_zero(&sql);
2121 db_prepare(&q, "SELECT * FROM timeline ORDER BY sortby DESC /*scan*/");
2122 if( fossil_islower(desc.aData[0]) ){
2123 desc.aData[0] = fossil_toupper(desc.aData[0]);
2124 }
2125 @ <h2>%b(&desc)</h2>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2126 blob_reset(&desc);
2127
2128 /* Report any errors. */
2129 if( zError ){
2130 @ <p class="generalError">%h(zError)</p>
2131
--- src/timeline.c
+++ src/timeline.c
@@ -91,12 +91,12 @@
91
92 /*
93 ** Allowed flags for the tmFlags argument to www_print_timeline
94 */
95 #if INTERFACE
96 #define TIMELINE_ARTID 0x000001 /* Show artifact IDs on non-check-in lines*/
97 #define TIMELINE_LEAFONLY 0x000002 /* Show "Leaf" but not "Merge", "Fork" etc*/
98 #define TIMELINE_BRIEF 0x000004 /* Combine adjacent elements of same obj */
99 #define TIMELINE_GRAPH 0x000008 /* Compute a graph */
100 #define TIMELINE_DISJOINT 0x000010 /* Elements are not contiguous */
101 #define TIMELINE_FCHANGES 0x000020 /* Detail file changes */
102 #define TIMELINE_BRCOLOR 0x000040 /* Background color by branch name */
@@ -111,10 +111,11 @@
111 #define TIMELINE_COLUMNAR 0x008000 /* Use the "columns" view style */
112 #define TIMELINE_CLASSIC 0x010000 /* Use the "classic" view style */
113 #define TIMELINE_VIEWS 0x01f000 /* Mask for all of the view styles */
114 #define TIMELINE_NOSCROLL 0x100000 /* Don't scroll to the selection */
115 #define TIMELINE_FILEDIFF 0x200000 /* Show File differences, not ckin diffs */
116 #define TIMELINE_CHPICK 0x400000 /* Show cherrypick merges */
117 #endif
118
119 /*
120 ** Hash a string and use the hash to determine a background color.
121 */
@@ -290,10 +291,15 @@
291 }
292 db_static_prepare(&qbranch,
293 "SELECT value FROM tagxref WHERE tagid=%d AND tagtype>0 AND rid=:rid",
294 TAG_BRANCH
295 );
296 if( (tmFlags & TIMELINE_CHPICK)!=0
297 && !db_table_exists("repository","cherrypick")
298 ){
299 tmFlags &= ~TIMELINE_CHPICK;
300 }
301
302 @ <table id="timelineTable%d(iTableId)" class="timelineTable">
303 blob_zero(&comment);
304 while( db_step(pQuery)==SQLITE_ROW ){
305 int rid = db_column_int(pQuery, 0);
@@ -423,10 +429,11 @@
429 }
430 }
431 }
432 if( zType[0]=='c' && pGraph ){
433 int nParent = 0;
434 int nCherrypick = 0;
435 int aParent[GR_MAX_RAIL];
436 static Stmt qparent;
437 db_static_prepare(&qparent,
438 "SELECT pid FROM plink"
439 " WHERE cid=:rid AND pid NOT IN phantom"
@@ -435,19 +442,32 @@
442 db_bind_int(&qparent, ":rid", rid);
443 while( db_step(&qparent)==SQLITE_ROW && nParent<count(aParent) ){
444 aParent[nParent++] = db_column_int(&qparent, 0);
445 }
446 db_reset(&qparent);
447 if( (tmFlags & TIMELINE_CHPICK)!=0 && nParent>0 ){
448 static Stmt qcherrypick;
449 db_static_prepare(&qcherrypick,
450 "SELECT parentid FROM cherrypick"
451 " WHERE childid=:rid AND parentid NOT IN phantom"
452 );
453 db_bind_int(&qcherrypick, ":rid", rid);
454 while( db_step(&qcherrypick)==SQLITE_ROW && nParent<count(aParent) ){
455 aParent[nParent++] = db_column_int(&qcherrypick, 0);
456 nCherrypick++;
457 }
458 db_reset(&qcherrypick);
459 }
460 gidx = graph_add_row(pGraph, rid, nParent, nCherrypick, aParent,
461 zBr, zBgClr, zUuid, isLeaf);
462 db_reset(&qbranch);
463 @ <div id="m%d(gidx)" class="tl-nodemark"></div>
464 }else if( zType[0]=='e' && pGraph && zBgClr && zBgClr[0] ){
465 /* For technotes, make a graph node with nParent==(-1). This will
466 ** not actually draw anything on the graph, but it will set the
467 ** background color of the timeline entry */
468 gidx = graph_add_row(pGraph, rid, -1, 0, 0, zBr, zBgClr, zUuid, 0);
469 @ <div id="m%d(gidx)" class="tl-nodemark"></div>
470 }
471 @</td>
472 if( !isSelectedOrCurrent ){
473 @ <td class="timeline%s(zStyle)Cell" id='mc%d(gidx)'>
@@ -505,11 +525,13 @@
525 }
526 }
527 if( zType[0]!='c' ){
528 /* Comments for anything other than a check-in are generated by
529 ** "fossil rebuild" and expect to be rendered as text/x-fossil-wiki */
530 if( zType[0]=='w' ) wiki_hyperlink_override(zUuid);
531 wiki_convert(&comment, 0, WIKI_INLINE);
532 wiki_hyperlink_override(0);
533 }else{
534 if( bCommentGitStyle ){
535 /* Truncate comment at first blank line */
536 int ii, jj;
537 int n = blob_size(&comment);
@@ -841,68 +863,109 @@
863 ** to get an actual id, prepend "m" to the integer. The top node
864 ** is iTopRow and numbers increase moving down the timeline.
865 ** bg: The background color for this row
866 ** r: The "rail" that the node for this row sits on. The left-most
867 ** rail is 0 and the number increases to the right.
868 ** d: If exists and true then there is a "descender" - an arrow
869 ** coming from the bottom of the page straight up to this node.
870 ** mo: "merge-out". If it exists, this is the rail position
871 ** for the upward portion of a merge arrow. The merge arrow goes as
872 ** a solid normal merge line up to the row identified by "mu" and
873 ** then as a dashed cherrypick merge line up further to "cu".
874 ** If this value is omitted if there are no merge children.
875 ** mu: The id of the row which is the top of the merge-out arrow.
876 ** Only exists if "mo" exists.
877 ** cu: Extend the mu merge arrow up to this row as a cherrypick
878 ** merge line, if this value exists.
879 ** u: Draw a thick child-line out of the top of this node and up to
880 ** the node with an id equal to this value. 0 if it is straight to
881 ** the top of the page, -1 if there is no thick-line riser.
882 ** f: 0x01: a leaf node.
883 ** au: An array of integers that define thick-line risers for branches.
884 ** The integers are in pairs. For each pair, the first integer is
885 ** is the rail on which the riser should run and the second integer
886 ** is the id of the node upto which the riser should run. If there
887 ** are no risers, this array does not exist.
888 ** mi: "merge-in". An array of integer rail positions from which
889 ** merge arrows should be drawn into this node. If the value is
890 ** negative, then the rail position is the absolute value of mi[]
891 ** and a thin merge-arrow descender is drawn to the bottom of
892 ** the screen. This array is omitted if there are no inbound
893 ** merges.
894 ** ci: "cherrypick-in". Like "mi" except for cherrypick merges.
895 ** omitted if there are no cherrypick merges.
896 ** h: The artifact hash of the object being graphed
897 */
898 for(pRow=pGraph->pFirst; pRow; pRow=pRow->pNext){
899 int k = 0;
900 cgi_printf("{\"id\":%d,", pRow->idx);
901 cgi_printf("\"bg\":\"%s\",", pRow->zBgClr);
902 cgi_printf("\"r\":%d,", pRow->iRail);
903 if( pRow->bDescender ){
904 cgi_printf("\"d\":%d,", pRow->bDescender);
905 }
906 if( pRow->mergeOut>=0 ){
907 cgi_printf("\"mo\":%d,", pRow->mergeOut);
908 if( pRow->mergeUpto==0 ) pRow->mergeUpto = pRow->idx;
909 cgi_printf("\"mu\":%d,", pRow->mergeUpto);
910 if( pRow->cherrypickUpto>0 && pRow->cherrypickUpto<pRow->mergeUpto ){
911 cgi_printf("\"cu\":%d,", pRow->cherrypickUpto);
912 }
913 }
914 cgi_printf("\"u\":%d,", pRow->aiRiser[pRow->iRail]);
915 k = 0;
916 if( pRow->isLeaf ) k |= 1;
917 cgi_printf("\"f\":%d,",k);
918 for(i=k=0; i<GR_MAX_RAIL; i++){
919 if( i==pRow->iRail ) continue;
920 if( pRow->aiRiser[i]>0 ){
921 if( k==0 ){
922 cgi_printf("\"au\":");
923 cSep = '[';
924 }
925 k++;
926 cgi_printf("%c%d,%d", cSep, i, pRow->aiRiser[i]);
927 cSep = ',';
928 }
929 }
930 if( k ){
931 cgi_printf("],");
932 }
933 if( colorGraph && pRow->zBgClr[0]=='#' ){
934 cgi_printf("\"fg\":\"%s\",", bg_to_fg(pRow->zBgClr));
935 }
936 /* mi */
937 for(i=k=0; i<GR_MAX_RAIL; i++){
938 if( pRow->mergeIn[i]==1 ){
 
 
939 int mi = i;
940 if( (pRow->mergeDown >> i) & 1 ) mi = -mi;
941 if( k==0 ){
942 cgi_printf("\"mi\":");
943 cSep = '[';
944 }
945 k++;
946 cgi_printf("%c%d", cSep, mi);
947 cSep = ',';
948 }
949 }
950 if( k ) cgi_printf("],");
951 /* ci */
952 for(i=k=0; i<GR_MAX_RAIL; i++){
953 if( pRow->mergeIn[i]==2 ){
954 int mi = i;
955 if( (pRow->cherrypickDown >> i) & 1 ) mi = -mi;
956 if( k==0 ){
957 cgi_printf("\"ci\":");
958 cSep = '[';
959 }
960 k++;
961 cgi_printf("%c%d", cSep, mi);
962 cSep = ',';
963 }
964 }
965 if( k ) cgi_printf("],");
966 cgi_printf("\"h\":\"%!S\"}%s",
967 pRow->zUuid, pRow->pNext ? ",\n" : "]\n");
968 }
969 @ }</script>
970 style_graph_generator();
971 graph_free(pGraph);
@@ -1372,29 +1435,33 @@
1435 ** dp=CHECKIN The same as d=CHECKIN&p=CHECKIN
1436 ** t=TAG Show only check-ins with the given TAG
1437 ** r=TAG Show check-ins related to TAG, equivalent to t=TAG&rel
1438 ** rel Show related check-ins as well as those matching t=TAG
1439 ** mionly Limit rel to show ancestors but not descendants
1440 ** nowiki Do not show wiki associated with branch or tag
1441 ** ms=MATCHSTYLE Set tag match style to EXACT, GLOB, LIKE, REGEXP
1442 ** u=USER Only show items associated with USER
1443 ** y=TYPE 'ci', 'w', 't', 'e', 'f', or 'all'.
1444 ** ss=VIEWSTYLE c: "Compact" v: "Verbose" m: "Modern" j: "Columnar"
1445 ** advm Use the "Advanced" or "Busy" menu design.
1446 ** ng No Graph.
1447 ** ncp Omit cherrypick merges
1448 ** nd Do not highlight the focus check-in
1449 ** v Show details of files changed
1450 ** f=CHECKIN Show family (immediate parents and children) of CHECKIN
1451 ** from=CHECKIN Path from...
1452 ** to=CHECKIN ... to this
1453 ** shorest ... show only the shortest path
1454 ** rel ... also show related checkins
1455 ** uf=FILE_HASH Show only check-ins that contain the given file version
1456 ** chng=GLOBLIST Show only check-ins that involve changes to a file whose
1457 ** name matches one of the comma-separate GLOBLIST
1458 ** brbg Background color from branch name
1459 ** ubg Background color from user
1460 ** namechng Show only check-ins that have filename changes
1461 ** forks Show only forks and their children
1462 ** cherrypicks Show all cherrypicks
1463 ** ym=YYYY-MM Show only events for the given year/month
1464 ** yw=YYYY-WW Show only events for the given week of the given year
1465 ** yw=YYYY-MM-DD Show events for the week that includes the given day
1466 ** ymd=YYYY-MM-DD Show only events on the given day
1467 ** days=N Show events over the previous N days
@@ -1444,10 +1511,11 @@
1511 const char *zChng = P("chng"); /* List of GLOBs for files that changed */
1512 int useDividers = P("nd")==0; /* Show dividers if "nd" is missing */
1513 int renameOnly = P("namechng")!=0; /* Show only check-ins that rename files */
1514 int forkOnly = PB("forks"); /* Show only forks and their children */
1515 int bisectOnly = PB("bisect"); /* Show the check-ins of the bisect */
1516 int cpOnly = PB("cherrypicks"); /* Show all cherrypick checkins */
1517 int tmFlags = 0; /* Timeline flags */
1518 const char *zThisTag = 0; /* Suppress links to this tag */
1519 const char *zThisUser = 0; /* Suppress links to this user */
1520 HQuery url; /* URL for various branch links */
1521 int from_rid = name_to_typed_rid(P("from"),"ci"); /* from= for paths */
@@ -1462,10 +1530,11 @@
1530 char *zNewerButton = 0; /* URL for Newer button at the top */
1531 int selectedRid = -9999999; /* Show a highlight on this RID */
1532 int disableY = 0; /* Disable type selector on submenu */
1533 int advancedMenu = 0; /* Use the advanced menu design */
1534 char *zPlural; /* Ending for plural forms */
1535 int showCherrypicks = 1; /* True to show cherrypick merges */
1536 void (*xExtra)(int) = NULL;
1537
1538 /* Set number of rows to display */
1539 cookie_read_parameter("n","n");
1540 z = P("n");
@@ -1487,10 +1556,16 @@
1556 cgi_replace_query_parameter("n",z);
1557 cookie_write_parameter("n","n",0);
1558 tmFlags |= timeline_ss_submenu();
1559 cookie_link_parameter("advm","advm","0");
1560 advancedMenu = atoi(PD("advm","0"));
1561
1562 /* Omit all cherry-pick merge lines if the "ncp" query parameter is
1563 ** present or if this repository lacks a "cherrypick" table. */
1564 if( PB("ncp") || !db_table_exists("repository","cherrypick") ){
1565 showCherrypicks = 0;
1566 }
1567
1568 /* To view the timeline, must have permission to read project data.
1569 */
1570 pd_rid = name_to_typed_rid(P("dp"),"ci");
1571 if( pd_rid ){
@@ -1521,19 +1596,21 @@
1596 cgi_delete_query_parameter("r");
1597 cgi_set_query_parameter("t", zBrName);
1598 cgi_set_query_parameter("rel", "1");
1599 zTagName = zBrName;
1600 related = 1;
1601 zType = "ci";
1602 }
1603
1604 /* Ignore empty tag query strings. */
1605 if( zTagName && !*zTagName ){
1606 zTagName = 0;
1607 }
1608
1609 /* Finish preliminary processing of tag match queries. */
1610 if( zTagName ){
1611 zType = "ci";
1612 /* Interpet the tag style string. */
1613 if( fossil_stricmp(zMatchStyle, "glob")==0 ){
1614 matchStyle = MS_GLOB;
1615 }else if( fossil_stricmp(zMatchStyle, "like")==0 ){
1616 matchStyle = MS_LIKE;
@@ -1563,16 +1640,19 @@
1640 ){
1641 nEntry = -1;
1642 zCirca = 0;
1643 }
1644 if( zType[0]=='a' ){
1645 tmFlags |= TIMELINE_BRIEF | TIMELINE_GRAPH | TIMELINE_CHPICK;
1646 }else{
1647 tmFlags |= TIMELINE_GRAPH | TIMELINE_CHPICK;
1648 }
1649 if( PB("ncp") ){
1650 tmFlags &= ~TIMELINE_CHPICK;
1651 }
1652 if( PB("ng") || zSearch!=0 ){
1653 tmFlags &= ~(TIMELINE_GRAPH|TIMELINE_CHPICK);
1654 }
1655 if( PB("brbg") ){
1656 tmFlags |= TIMELINE_BRCOLOR;
1657 }
1658 if( PB("unhide") ){
@@ -1658,10 +1738,12 @@
1738 /* If from= and to= are present, display all nodes on a path connecting
1739 ** the two */
1740 PathNode *p = 0;
1741 const char *zFrom = 0;
1742 const char *zTo = 0;
1743 Blob ins;
1744 int nNodeOnPath = 0;
1745
1746 if( from_rid && to_rid ){
1747 p = path_shortest(from_rid, to_rid, noMerge, 0);
1748 zFrom = P("from");
1749 zTo = P("to");
@@ -1670,28 +1752,72 @@
1752 p = path_first();
1753 }
1754 zFrom = P("me");
1755 zTo = P("you");
1756 }
1757 blob_init(&ins, 0, 0);
1758 db_multi_exec(
1759 "CREATE TABLE IF NOT EXISTS temp.pathnode(x INTEGER PRIMARY KEY);"
1760 );
1761 if( p ){
1762 blob_init(&ins, 0, 0);
1763 blob_append_sql(&ins, "INSERT INTO pathnode(x) VALUES(%d)", p->rid);
1764 p = p->u.pTo;
1765 nNodeOnPath = 1;
1766 while( p ){
1767 blob_append_sql(&ins, ",(%d)", p->rid);
1768 p = p->u.pTo;
1769 nNodeOnPath++;
1770 }
1771 }
 
1772 path_reset();
1773 db_multi_exec("%s", blob_str(&ins)/*safe-for-%s*/);
1774 blob_reset(&ins);
1775 if( related ){
1776 db_multi_exec(
1777 "CREATE TABLE IF NOT EXISTS temp.related(x INTEGER PRIMARY KEY);"
1778 "INSERT OR IGNORE INTO related(x)"
1779 " SELECT pid FROM plink WHERE cid IN pathnode AND NOT isprim;"
1780 );
1781 if( P("mionly")==0 ){
1782 db_multi_exec(
1783 "INSERT OR IGNORE INTO related(x)"
1784 " SELECT cid FROM plink WHERE pid IN pathnode;"
1785 );
1786 }
1787 if( showCherrypicks ){
1788 db_multi_exec(
1789 "INSERT OR IGNORE INTO related(x)"
1790 " SELECT parentid FROM cherrypick WHERE childid IN pathnode;"
1791 );
1792 if( P("mionly")==0 ){
1793 db_multi_exec(
1794 "INSERT OR IGNORE INTO related(x)"
1795 " SELECT childid FROM cherrypick WHERE parentid IN pathnode;"
1796 );
1797 }
1798 }
1799 db_multi_exec("INSERT OR IGNORE INTO pathnode SELECT x FROM related");
1800 }
1801 blob_append_sql(&sql, " AND event.objid IN pathnode");
1802 addFileGlobExclusion(zChng, &sql);
1803 tmFlags |= TIMELINE_DISJOINT;
1804 db_multi_exec("%s", blob_sql_text(&sql));
1805 if( advancedMenu ){
1806 style_submenu_checkbox("v", "Files", (zType[0]!='a' && zType[0]!='c'),0);
1807 }
1808 blob_appendf(&desc, "%d check-ins going from ", nNodeOnPath);
 
1809 blob_appendf(&desc, "%z[%h]</a>", href("%R/info/%h", zFrom), zFrom);
1810 blob_append(&desc, " to ", -1);
1811 blob_appendf(&desc, "%z[%h]</a>", href("%R/info/%h",zTo), zTo);
1812 if( related ){
1813 int nRelated = db_int(0, "SELECT count(*) FROM timeline") - nNodeOnPath;
1814 if( nRelated>0 ){
1815 blob_appendf(&desc, " and %d related check-in%s", nRelated,
1816 nRelated>1 ? "s" : "");
1817 }
1818 }
1819 addFileGlobDescription(zChng, &desc);
1820 }else if( (p_rid || d_rid) && g.perm.Read && zTagSql==0 ){
1821 /* If p= or d= is present, ignore all other parameters other than n= */
1822 char *zUuid;
1823 int np, nd;
@@ -1707,19 +1833,19 @@
1833 zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d",
1834 p_rid ? p_rid : d_rid);
1835 blob_append_sql(&sql, " AND event.objid IN ok");
1836 nd = 0;
1837 if( d_rid ){
1838 compute_descendants(d_rid, nEntry==0 ? 0 : nEntry+1);
1839 nd = db_int(0, "SELECT count(*)-1 FROM ok");
1840 if( nd>=0 ) db_multi_exec("%s", blob_sql_text(&sql));
1841 if( nd>0 ) blob_appendf(&desc, "%d descendant%s", nd,(1==nd)?"":"s");
1842 if( useDividers ) selectedRid = d_rid;
1843 db_multi_exec("DELETE FROM ok");
1844 }
1845 if( p_rid ){
1846 compute_ancestors(p_rid, nEntry==0 ? 0 : nEntry+1, 0);
1847 np = db_int(0, "SELECT count(*)-1 FROM ok");
1848 if( np>0 ){
1849 if( nd>0 ) blob_appendf(&desc, " and ");
1850 blob_appendf(&desc, "%d ancestors", np);
1851 db_multi_exec("%s", blob_sql_text(&sql));
@@ -1747,10 +1873,19 @@
1873 "INSERT INTO ok VALUES(%d);"
1874 "INSERT OR IGNORE INTO ok SELECT pid FROM plink WHERE cid=%d;"
1875 "INSERT OR IGNORE INTO ok SELECT cid FROM plink WHERE pid=%d;",
1876 f_rid, f_rid, f_rid
1877 );
1878 if( showCherrypicks ){
1879 db_multi_exec(
1880 "INSERT OR IGNORE INTO ok SELECT parentid FROM cherrypick"
1881 " WHERE childid=%d;"
1882 "INSERT OR IGNORE INTO ok SELECT childid FROM cherrypick"
1883 " WHERE parentid=%d;",
1884 f_rid, f_rid
1885 );
1886 }
1887 blob_append_sql(&sql, " AND event.objid IN ok");
1888 db_multi_exec("%s", blob_sql_text(&sql));
1889 if( useDividers ) selectedRid = f_rid;
1890 blob_appendf(&desc, "Parents and children of check-in ");
1891 zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d", f_rid);
@@ -1778,10 +1913,18 @@
1913 blob_append_sql(&cond, " AND event.objid IN rnfile ");
1914 }
1915 if( forkOnly ){
1916 blob_append_sql(&cond, " AND event.objid IN rnfork ");
1917 }
1918 if( cpOnly && showCherrypicks ){
1919 db_multi_exec(
1920 "CREATE TABLE IF NOT EXISTS cpnodes(rid INTEGER PRIMARY KEY);"
1921 "INSERT OR IGNORE INTO cpnodes SELECT childid FROM cherrypick;"
1922 "INSERT OR IGNORE INTO cpnodes SELECT parentid FROM cherrypick;"
1923 );
1924 blob_append_sql(&cond, " AND event.objid IN cpnodes ");
1925 }
1926 if( bisectOnly ){
1927 blob_append_sql(&cond, " AND event.objid IN (SELECT rid FROM bilog) ");
1928 }
1929 if( zYearMonth ){
1930 blob_append_sql(&cond, " AND %Q=strftime('%%Y-%%m',event.mtime) ",
@@ -1827,49 +1970,65 @@
1970 blob_append_sql(&cond, " AND event.mtime>=julianday('now','-%d days') ",
1971 nDays);
1972 nEntry = -1;
1973 }
1974 if( zTagSql ){
1975 db_multi_exec(
1976 "CREATE TEMP TABLE selected_nodes(rid INTEGER PRIMARY KEY);"
1977 "INSERT OR IGNORE INTO selected_nodes"
1978 " SELECT tagxref.rid FROM tagxref NATURAL JOIN tag"
1979 " WHERE %s AND tagtype>0", zTagSql/*safe-for-%s*/
1980 );
1981 if( !related ){
1982 blob_append_sql(&cond, " AND blob.rid IN selected_nodes");
1983 }else{
1984 db_multi_exec(
1985 "CREATE TEMP TABLE related_nodes(rid INTEGER PRIMARY KEY);"
1986 "INSERT INTO related_nodes SELECT rid FROM selected_nodes;"
1987 );
1988 blob_append_sql(&cond, " AND blob.rid IN related_nodes");
1989 /* The next two blob_appendf() calls add SQL that causes check-ins that
1990 ** are not part of the branch which are parents or children of the
1991 ** branch to be included in the report. These related check-ins are
1992 ** useful in helping to visualize what has happened on a quiescent
1993 ** branch that is infrequently merged with a much more activate branch.
1994 */
1995 db_multi_exec(
1996 "INSERT OR IGNORE INTO related_nodes"
1997 " SELECT pid FROM selected_nodes CROSS JOIN plink"
1998 " WHERE selected_nodes.rid=plink.cid;"
1999 );
2000 if( P("mionly")==0 ){
2001 db_multi_exec(
2002 "INSERT OR IGNORE INTO related_nodes"
2003 " SELECT cid FROM selected_nodes CROSS JOIN plink"
2004 " WHERE selected_nodes.rid=plink.pid;"
2005 );
2006 if( showCherrypicks ){
2007 db_multi_exec(
2008 "INSERT OR IGNORE INTO related_nodes"
2009 " SELECT childid FROM selected_nodes CROSS JOIN cherrypick"
2010 " WHERE selected_nodes.rid=cherrypick.parentid;"
2011 );
2012 }
2013 }
2014 if( showCherrypicks ){
2015 db_multi_exec(
2016 "INSERT OR IGNORE INTO related_nodes"
2017 " SELECT parentid FROM selected_nodes CROSS JOIN cherrypick"
2018 " WHERE selected_nodes.rid=cherrypick.childid;"
2019 );
2020 }
2021 if( (tmFlags & TIMELINE_UNHIDE)==0 ){
2022 db_multi_exec(
2023 "DELETE FROM related_nodes WHERE rid IN "
2024 " (SELECT related_nodes.rid FROM related_nodes, tagxref"
2025 " WHERE tagid=%d AND tagtype>0 AND tagxref.rid=related_nodes.rid)",
2026 TAG_HIDDEN
2027 );
2028 }
2029 }
2030 }
2031 if( (zType[0]=='w' && !g.perm.RdWiki)
2032 || (zType[0]=='t' && !g.perm.RdTkt)
2033 || (zType[0]=='e' && !g.perm.RdWiki)
2034 || (zType[0]=='c' && !g.perm.Read)
@@ -2013,10 +2172,14 @@
2172 }
2173 if( bisectOnly ){
2174 blob_appendf(&desc, " in the most recent bisect");
2175 tmFlags |= TIMELINE_DISJOINT;
2176 }
2177 if( cpOnly && showCherrypicks ){
2178 blob_appendf(&desc, " that participate in a cherrypick merge");
2179 tmFlags |= TIMELINE_CHPICK|TIMELINE_DISJOINT;
2180 }
2181 if( zUser ){
2182 blob_appendf(&desc, " by user %h", zUser);
2183 tmFlags |= TIMELINE_DISJOINT;
2184 }
2185 if( zTagSql ){
@@ -2106,13 +2269,15 @@
2269 }
2270 if( search_restrict(SRCH_CKIN)!=0 ){
2271 style_submenu_element("Search", "%R/search?y=c");
2272 }
2273 if( advancedMenu ){
2274 style_submenu_element("Basic", "%s",
2275 url_render(&url, "advm", "0", "udc", "1"));
2276 }else{
2277 style_submenu_element("Advanced", "%s",
2278 url_render(&url, "advm", "1", "udc", "1"));
2279 }
2280 if( PB("showid") ) tmFlags |= TIMELINE_SHOWRID;
2281 if( useDividers && zMark && zMark[0] ){
2282 double r = symbolic_name_to_mtime(zMark);
2283 if( r>0.0 ) selectedRid = timeline_add_divider(r);
@@ -2120,11 +2285,26 @@
2285 blob_zero(&sql);
2286 db_prepare(&q, "SELECT * FROM timeline ORDER BY sortby DESC /*scan*/");
2287 if( fossil_islower(desc.aData[0]) ){
2288 desc.aData[0] = fossil_toupper(desc.aData[0]);
2289 }
2290 if( zBrName
2291 && !PB("nowiki")
2292 && wiki_render_associated("branch", zBrName, WIKIASSOC_ALL)
2293 ){
2294 @ <div class="section">%b(&desc)</div>
2295 }else
2296 if( zTagName
2297 && matchStyle==MS_EXACT
2298 && zBrName==0
2299 && !PB("nowiki")
2300 && wiki_render_associated("tag", zTagName, WIKIASSOC_ALL)
2301 ){
2302 @ <div class="section">%b(&desc)</div>
2303 } else{
2304 @ <h2>%b(&desc)</h2>
2305 }
2306 blob_reset(&desc);
2307
2308 /* Report any errors. */
2309 if( zError ){
2310 @ <p class="generalError">%h(zError)</p>
2311
+263 -83
--- src/timeline.c
+++ src/timeline.c
@@ -91,12 +91,12 @@
9191
9292
/*
9393
** Allowed flags for the tmFlags argument to www_print_timeline
9494
*/
9595
#if INTERFACE
96
-#define TIMELINE_ARTID 0x000001 /* Show artifact IDs on non-check-in lines */
97
-#define TIMELINE_LEAFONLY 0x000002 /* Show "Leaf" but not "Merge", "Fork" etc */
96
+#define TIMELINE_ARTID 0x000001 /* Show artifact IDs on non-check-in lines*/
97
+#define TIMELINE_LEAFONLY 0x000002 /* Show "Leaf" but not "Merge", "Fork" etc*/
9898
#define TIMELINE_BRIEF 0x000004 /* Combine adjacent elements of same obj */
9999
#define TIMELINE_GRAPH 0x000008 /* Compute a graph */
100100
#define TIMELINE_DISJOINT 0x000010 /* Elements are not contiguous */
101101
#define TIMELINE_FCHANGES 0x000020 /* Detail file changes */
102102
#define TIMELINE_BRCOLOR 0x000040 /* Background color by branch name */
@@ -111,10 +111,11 @@
111111
#define TIMELINE_COLUMNAR 0x008000 /* Use the "columns" view style */
112112
#define TIMELINE_CLASSIC 0x010000 /* Use the "classic" view style */
113113
#define TIMELINE_VIEWS 0x01f000 /* Mask for all of the view styles */
114114
#define TIMELINE_NOSCROLL 0x100000 /* Don't scroll to the selection */
115115
#define TIMELINE_FILEDIFF 0x200000 /* Show File differences, not ckin diffs */
116
+#define TIMELINE_CHPICK 0x400000 /* Show cherrypick merges */
116117
#endif
117118
118119
/*
119120
** Hash a string and use the hash to determine a background color.
120121
*/
@@ -290,10 +291,15 @@
290291
}
291292
db_static_prepare(&qbranch,
292293
"SELECT value FROM tagxref WHERE tagid=%d AND tagtype>0 AND rid=:rid",
293294
TAG_BRANCH
294295
);
296
+ if( (tmFlags & TIMELINE_CHPICK)!=0
297
+ && !db_table_exists("repository","cherrypick")
298
+ ){
299
+ tmFlags &= ~TIMELINE_CHPICK;
300
+ }
295301
296302
@ <table id="timelineTable%d(iTableId)" class="timelineTable">
297303
blob_zero(&comment);
298304
while( db_step(pQuery)==SQLITE_ROW ){
299305
int rid = db_column_int(pQuery, 0);
@@ -423,10 +429,11 @@
423429
}
424430
}
425431
}
426432
if( zType[0]=='c' && pGraph ){
427433
int nParent = 0;
434
+ int nCherrypick = 0;
428435
int aParent[GR_MAX_RAIL];
429436
static Stmt qparent;
430437
db_static_prepare(&qparent,
431438
"SELECT pid FROM plink"
432439
" WHERE cid=:rid AND pid NOT IN phantom"
@@ -435,19 +442,32 @@
435442
db_bind_int(&qparent, ":rid", rid);
436443
while( db_step(&qparent)==SQLITE_ROW && nParent<count(aParent) ){
437444
aParent[nParent++] = db_column_int(&qparent, 0);
438445
}
439446
db_reset(&qparent);
440
- gidx = graph_add_row(pGraph, rid, nParent, aParent, zBr, zBgClr,
441
- zUuid, isLeaf);
447
+ if( (tmFlags & TIMELINE_CHPICK)!=0 && nParent>0 ){
448
+ static Stmt qcherrypick;
449
+ db_static_prepare(&qcherrypick,
450
+ "SELECT parentid FROM cherrypick"
451
+ " WHERE childid=:rid AND parentid NOT IN phantom"
452
+ );
453
+ db_bind_int(&qcherrypick, ":rid", rid);
454
+ while( db_step(&qcherrypick)==SQLITE_ROW && nParent<count(aParent) ){
455
+ aParent[nParent++] = db_column_int(&qcherrypick, 0);
456
+ nCherrypick++;
457
+ }
458
+ db_reset(&qcherrypick);
459
+ }
460
+ gidx = graph_add_row(pGraph, rid, nParent, nCherrypick, aParent,
461
+ zBr, zBgClr, zUuid, isLeaf);
442462
db_reset(&qbranch);
443463
@ <div id="m%d(gidx)" class="tl-nodemark"></div>
444464
}else if( zType[0]=='e' && pGraph && zBgClr && zBgClr[0] ){
445465
/* For technotes, make a graph node with nParent==(-1). This will
446466
** not actually draw anything on the graph, but it will set the
447467
** background color of the timeline entry */
448
- gidx = graph_add_row(pGraph, rid, -1, 0, zBr, zBgClr, zUuid, 0);
468
+ gidx = graph_add_row(pGraph, rid, -1, 0, 0, zBr, zBgClr, zUuid, 0);
449469
@ <div id="m%d(gidx)" class="tl-nodemark"></div>
450470
}
451471
@</td>
452472
if( !isSelectedOrCurrent ){
453473
@ <td class="timeline%s(zStyle)Cell" id='mc%d(gidx)'>
@@ -505,11 +525,13 @@
505525
}
506526
}
507527
if( zType[0]!='c' ){
508528
/* Comments for anything other than a check-in are generated by
509529
** "fossil rebuild" and expect to be rendered as text/x-fossil-wiki */
530
+ if( zType[0]=='w' ) wiki_hyperlink_override(zUuid);
510531
wiki_convert(&comment, 0, WIKI_INLINE);
532
+ wiki_hyperlink_override(0);
511533
}else{
512534
if( bCommentGitStyle ){
513535
/* Truncate comment at first blank line */
514536
int ii, jj;
515537
int n = blob_size(&comment);
@@ -841,68 +863,109 @@
841863
** to get an actual id, prepend "m" to the integer. The top node
842864
** is iTopRow and numbers increase moving down the timeline.
843865
** bg: The background color for this row
844866
** r: The "rail" that the node for this row sits on. The left-most
845867
** rail is 0 and the number increases to the right.
846
- ** d: True if there is a "descender" - an arrow coming from the bottom
847
- ** of the page straight up to this node.
848
- ** mo: "merge-out". If non-negative, this is the rail position
849
- ** for the upward portion of a merge arrow. The merge arrow goes up
850
- ** to the row identified by mu:. If this value is negative then
851
- ** node has no merge children and no merge-out line is drawn.
868
+ ** d: If exists and true then there is a "descender" - an arrow
869
+ ** coming from the bottom of the page straight up to this node.
870
+ ** mo: "merge-out". If it exists, this is the rail position
871
+ ** for the upward portion of a merge arrow. The merge arrow goes as
872
+ ** a solid normal merge line up to the row identified by "mu" and
873
+ ** then as a dashed cherrypick merge line up further to "cu".
874
+ ** If this value is omitted if there are no merge children.
852875
** mu: The id of the row which is the top of the merge-out arrow.
876
+ ** Only exists if "mo" exists.
877
+ ** cu: Extend the mu merge arrow up to this row as a cherrypick
878
+ ** merge line, if this value exists.
853879
** u: Draw a thick child-line out of the top of this node and up to
854880
** the node with an id equal to this value. 0 if it is straight to
855881
** the top of the page, -1 if there is no thick-line riser.
856882
** f: 0x01: a leaf node.
857883
** au: An array of integers that define thick-line risers for branches.
858884
** The integers are in pairs. For each pair, the first integer is
859885
** is the rail on which the riser should run and the second integer
860
- ** is the id of the node upto which the riser should run.
886
+ ** is the id of the node upto which the riser should run. If there
887
+ ** are no risers, this array does not exist.
861888
** mi: "merge-in". An array of integer rail positions from which
862889
** merge arrows should be drawn into this node. If the value is
863890
** negative, then the rail position is the absolute value of mi[]
864891
** and a thin merge-arrow descender is drawn to the bottom of
865
- ** the screen.
892
+ ** the screen. This array is omitted if there are no inbound
893
+ ** merges.
894
+ ** ci: "cherrypick-in". Like "mi" except for cherrypick merges.
895
+ ** omitted if there are no cherrypick merges.
866896
** h: The artifact hash of the object being graphed
867897
*/
868898
for(pRow=pGraph->pFirst; pRow; pRow=pRow->pNext){
899
+ int k = 0;
869900
cgi_printf("{\"id\":%d,", pRow->idx);
870901
cgi_printf("\"bg\":\"%s\",", pRow->zBgClr);
871902
cgi_printf("\"r\":%d,", pRow->iRail);
872
- cgi_printf("\"d\":%d,", pRow->bDescender);
873
- cgi_printf("\"mo\":%d,", pRow->mergeOut);
874
- cgi_printf("\"mu\":%d,", pRow->mergeUpto);
903
+ if( pRow->bDescender ){
904
+ cgi_printf("\"d\":%d,", pRow->bDescender);
905
+ }
906
+ if( pRow->mergeOut>=0 ){
907
+ cgi_printf("\"mo\":%d,", pRow->mergeOut);
908
+ if( pRow->mergeUpto==0 ) pRow->mergeUpto = pRow->idx;
909
+ cgi_printf("\"mu\":%d,", pRow->mergeUpto);
910
+ if( pRow->cherrypickUpto>0 && pRow->cherrypickUpto<pRow->mergeUpto ){
911
+ cgi_printf("\"cu\":%d,", pRow->cherrypickUpto);
912
+ }
913
+ }
875914
cgi_printf("\"u\":%d,", pRow->aiRiser[pRow->iRail]);
876
- cgi_printf("\"f\":%d,", pRow->isLeaf ? 1 : 0);
877
- cgi_printf("\"au\":");
878
- cSep = '[';
879
- for(i=0; i<GR_MAX_RAIL; i++){
915
+ k = 0;
916
+ if( pRow->isLeaf ) k |= 1;
917
+ cgi_printf("\"f\":%d,",k);
918
+ for(i=k=0; i<GR_MAX_RAIL; i++){
880919
if( i==pRow->iRail ) continue;
881920
if( pRow->aiRiser[i]>0 ){
921
+ if( k==0 ){
922
+ cgi_printf("\"au\":");
923
+ cSep = '[';
924
+ }
925
+ k++;
882926
cgi_printf("%c%d,%d", cSep, i, pRow->aiRiser[i]);
883927
cSep = ',';
884928
}
885929
}
886
- if( cSep=='[' ) cgi_printf("[");
887
- cgi_printf("],");
930
+ if( k ){
931
+ cgi_printf("],");
932
+ }
888933
if( colorGraph && pRow->zBgClr[0]=='#' ){
889934
cgi_printf("\"fg\":\"%s\",", bg_to_fg(pRow->zBgClr));
890935
}
891936
/* mi */
892
- cgi_printf("\"mi\":");
893
- cSep = '[';
894
- for(i=0; i<GR_MAX_RAIL; i++){
895
- if( pRow->mergeIn[i] ){
937
+ for(i=k=0; i<GR_MAX_RAIL; i++){
938
+ if( pRow->mergeIn[i]==1 ){
896939
int mi = i;
897940
if( (pRow->mergeDown >> i) & 1 ) mi = -mi;
941
+ if( k==0 ){
942
+ cgi_printf("\"mi\":");
943
+ cSep = '[';
944
+ }
945
+ k++;
946
+ cgi_printf("%c%d", cSep, mi);
947
+ cSep = ',';
948
+ }
949
+ }
950
+ if( k ) cgi_printf("],");
951
+ /* ci */
952
+ for(i=k=0; i<GR_MAX_RAIL; i++){
953
+ if( pRow->mergeIn[i]==2 ){
954
+ int mi = i;
955
+ if( (pRow->cherrypickDown >> i) & 1 ) mi = -mi;
956
+ if( k==0 ){
957
+ cgi_printf("\"ci\":");
958
+ cSep = '[';
959
+ }
960
+ k++;
898961
cgi_printf("%c%d", cSep, mi);
899962
cSep = ',';
900963
}
901964
}
902
- if( cSep=='[' ) cgi_printf("[");
903
- cgi_printf("],\"h\":\"%!S\"}%s",
965
+ if( k ) cgi_printf("],");
966
+ cgi_printf("\"h\":\"%!S\"}%s",
904967
pRow->zUuid, pRow->pNext ? ",\n" : "]\n");
905968
}
906969
@ }</script>
907970
style_graph_generator();
908971
graph_free(pGraph);
@@ -1372,29 +1435,33 @@
13721435
** dp=CHECKIN The same as d=CHECKIN&p=CHECKIN
13731436
** t=TAG Show only check-ins with the given TAG
13741437
** r=TAG Show check-ins related to TAG, equivalent to t=TAG&rel
13751438
** rel Show related check-ins as well as those matching t=TAG
13761439
** mionly Limit rel to show ancestors but not descendants
1440
+** nowiki Do not show wiki associated with branch or tag
13771441
** ms=MATCHSTYLE Set tag match style to EXACT, GLOB, LIKE, REGEXP
13781442
** u=USER Only show items associated with USER
13791443
** y=TYPE 'ci', 'w', 't', 'e', 'f', or 'all'.
13801444
** ss=VIEWSTYLE c: "Compact" v: "Verbose" m: "Modern" j: "Columnar"
13811445
** advm Use the "Advanced" or "Busy" menu design.
13821446
** ng No Graph.
1447
+** ncp Omit cherrypick merges
13831448
** nd Do not highlight the focus check-in
13841449
** v Show details of files changed
13851450
** f=CHECKIN Show family (immediate parents and children) of CHECKIN
13861451
** from=CHECKIN Path from...
1387
-** to=CHECKIN ... to this
1388
-** shortest ... show only the shortest path
1452
+** to=CHECKIN ... to this
1453
+** shorest ... show only the shortest path
1454
+** rel ... also show related checkins
13891455
** uf=FILE_HASH Show only check-ins that contain the given file version
13901456
** chng=GLOBLIST Show only check-ins that involve changes to a file whose
13911457
** name matches one of the comma-separate GLOBLIST
13921458
** brbg Background color from branch name
13931459
** ubg Background color from user
13941460
** namechng Show only check-ins that have filename changes
13951461
** forks Show only forks and their children
1462
+** cherrypicks Show all cherrypicks
13961463
** ym=YYYY-MM Show only events for the given year/month
13971464
** yw=YYYY-WW Show only events for the given week of the given year
13981465
** yw=YYYY-MM-DD Show events for the week that includes the given day
13991466
** ymd=YYYY-MM-DD Show only events on the given day
14001467
** days=N Show events over the previous N days
@@ -1444,10 +1511,11 @@
14441511
const char *zChng = P("chng"); /* List of GLOBs for files that changed */
14451512
int useDividers = P("nd")==0; /* Show dividers if "nd" is missing */
14461513
int renameOnly = P("namechng")!=0; /* Show only check-ins that rename files */
14471514
int forkOnly = PB("forks"); /* Show only forks and their children */
14481515
int bisectOnly = PB("bisect"); /* Show the check-ins of the bisect */
1516
+ int cpOnly = PB("cherrypicks"); /* Show all cherrypick checkins */
14491517
int tmFlags = 0; /* Timeline flags */
14501518
const char *zThisTag = 0; /* Suppress links to this tag */
14511519
const char *zThisUser = 0; /* Suppress links to this user */
14521520
HQuery url; /* URL for various branch links */
14531521
int from_rid = name_to_typed_rid(P("from"),"ci"); /* from= for paths */
@@ -1462,10 +1530,11 @@
14621530
char *zNewerButton = 0; /* URL for Newer button at the top */
14631531
int selectedRid = -9999999; /* Show a highlight on this RID */
14641532
int disableY = 0; /* Disable type selector on submenu */
14651533
int advancedMenu = 0; /* Use the advanced menu design */
14661534
char *zPlural; /* Ending for plural forms */
1535
+ int showCherrypicks = 1; /* True to show cherrypick merges */
14671536
void (*xExtra)(int) = NULL;
14681537
14691538
/* Set number of rows to display */
14701539
cookie_read_parameter("n","n");
14711540
z = P("n");
@@ -1487,10 +1556,16 @@
14871556
cgi_replace_query_parameter("n",z);
14881557
cookie_write_parameter("n","n",0);
14891558
tmFlags |= timeline_ss_submenu();
14901559
cookie_link_parameter("advm","advm","0");
14911560
advancedMenu = atoi(PD("advm","0"));
1561
+
1562
+ /* Omit all cherry-pick merge lines if the "ncp" query parameter is
1563
+ ** present or if this repository lacks a "cherrypick" table. */
1564
+ if( PB("ncp") || !db_table_exists("repository","cherrypick") ){
1565
+ showCherrypicks = 0;
1566
+ }
14921567
14931568
/* To view the timeline, must have permission to read project data.
14941569
*/
14951570
pd_rid = name_to_typed_rid(P("dp"),"ci");
14961571
if( pd_rid ){
@@ -1521,19 +1596,21 @@
15211596
cgi_delete_query_parameter("r");
15221597
cgi_set_query_parameter("t", zBrName);
15231598
cgi_set_query_parameter("rel", "1");
15241599
zTagName = zBrName;
15251600
related = 1;
1601
+ zType = "ci";
15261602
}
15271603
15281604
/* Ignore empty tag query strings. */
15291605
if( zTagName && !*zTagName ){
15301606
zTagName = 0;
15311607
}
15321608
15331609
/* Finish preliminary processing of tag match queries. */
15341610
if( zTagName ){
1611
+ zType = "ci";
15351612
/* Interpet the tag style string. */
15361613
if( fossil_stricmp(zMatchStyle, "glob")==0 ){
15371614
matchStyle = MS_GLOB;
15381615
}else if( fossil_stricmp(zMatchStyle, "like")==0 ){
15391616
matchStyle = MS_LIKE;
@@ -1563,16 +1640,19 @@
15631640
){
15641641
nEntry = -1;
15651642
zCirca = 0;
15661643
}
15671644
if( zType[0]=='a' ){
1568
- tmFlags |= TIMELINE_BRIEF | TIMELINE_GRAPH;
1645
+ tmFlags |= TIMELINE_BRIEF | TIMELINE_GRAPH | TIMELINE_CHPICK;
15691646
}else{
1570
- tmFlags |= TIMELINE_GRAPH;
1647
+ tmFlags |= TIMELINE_GRAPH | TIMELINE_CHPICK;
1648
+ }
1649
+ if( PB("ncp") ){
1650
+ tmFlags &= ~TIMELINE_CHPICK;
15711651
}
15721652
if( PB("ng") || zSearch!=0 ){
1573
- tmFlags &= ~TIMELINE_GRAPH;
1653
+ tmFlags &= ~(TIMELINE_GRAPH|TIMELINE_CHPICK);
15741654
}
15751655
if( PB("brbg") ){
15761656
tmFlags |= TIMELINE_BRCOLOR;
15771657
}
15781658
if( PB("unhide") ){
@@ -1658,10 +1738,12 @@
16581738
/* If from= and to= are present, display all nodes on a path connecting
16591739
** the two */
16601740
PathNode *p = 0;
16611741
const char *zFrom = 0;
16621742
const char *zTo = 0;
1743
+ Blob ins;
1744
+ int nNodeOnPath = 0;
16631745
16641746
if( from_rid && to_rid ){
16651747
p = path_shortest(from_rid, to_rid, noMerge, 0);
16661748
zFrom = P("from");
16671749
zTo = P("to");
@@ -1670,28 +1752,72 @@
16701752
p = path_first();
16711753
}
16721754
zFrom = P("me");
16731755
zTo = P("you");
16741756
}
1675
- blob_append(&sql, " AND event.objid IN (0", -1);
1676
- while( p ){
1677
- blob_append_sql(&sql, ",%d", p->rid);
1757
+ blob_init(&ins, 0, 0);
1758
+ db_multi_exec(
1759
+ "CREATE TABLE IF NOT EXISTS temp.pathnode(x INTEGER PRIMARY KEY);"
1760
+ );
1761
+ if( p ){
1762
+ blob_init(&ins, 0, 0);
1763
+ blob_append_sql(&ins, "INSERT INTO pathnode(x) VALUES(%d)", p->rid);
16781764
p = p->u.pTo;
1765
+ nNodeOnPath = 1;
1766
+ while( p ){
1767
+ blob_append_sql(&ins, ",(%d)", p->rid);
1768
+ p = p->u.pTo;
1769
+ nNodeOnPath++;
1770
+ }
16791771
}
1680
- blob_append(&sql, ")", -1);
16811772
path_reset();
1773
+ db_multi_exec("%s", blob_str(&ins)/*safe-for-%s*/);
1774
+ blob_reset(&ins);
1775
+ if( related ){
1776
+ db_multi_exec(
1777
+ "CREATE TABLE IF NOT EXISTS temp.related(x INTEGER PRIMARY KEY);"
1778
+ "INSERT OR IGNORE INTO related(x)"
1779
+ " SELECT pid FROM plink WHERE cid IN pathnode AND NOT isprim;"
1780
+ );
1781
+ if( P("mionly")==0 ){
1782
+ db_multi_exec(
1783
+ "INSERT OR IGNORE INTO related(x)"
1784
+ " SELECT cid FROM plink WHERE pid IN pathnode;"
1785
+ );
1786
+ }
1787
+ if( showCherrypicks ){
1788
+ db_multi_exec(
1789
+ "INSERT OR IGNORE INTO related(x)"
1790
+ " SELECT parentid FROM cherrypick WHERE childid IN pathnode;"
1791
+ );
1792
+ if( P("mionly")==0 ){
1793
+ db_multi_exec(
1794
+ "INSERT OR IGNORE INTO related(x)"
1795
+ " SELECT childid FROM cherrypick WHERE parentid IN pathnode;"
1796
+ );
1797
+ }
1798
+ }
1799
+ db_multi_exec("INSERT OR IGNORE INTO pathnode SELECT x FROM related");
1800
+ }
1801
+ blob_append_sql(&sql, " AND event.objid IN pathnode");
16821802
addFileGlobExclusion(zChng, &sql);
16831803
tmFlags |= TIMELINE_DISJOINT;
16841804
db_multi_exec("%s", blob_sql_text(&sql));
16851805
if( advancedMenu ){
16861806
style_submenu_checkbox("v", "Files", (zType[0]!='a' && zType[0]!='c'),0);
16871807
}
1688
- blob_appendf(&desc, "%d check-ins going from ",
1689
- db_int(0, "SELECT count(*) FROM timeline"));
1808
+ blob_appendf(&desc, "%d check-ins going from ", nNodeOnPath);
16901809
blob_appendf(&desc, "%z[%h]</a>", href("%R/info/%h", zFrom), zFrom);
16911810
blob_append(&desc, " to ", -1);
16921811
blob_appendf(&desc, "%z[%h]</a>", href("%R/info/%h",zTo), zTo);
1812
+ if( related ){
1813
+ int nRelated = db_int(0, "SELECT count(*) FROM timeline") - nNodeOnPath;
1814
+ if( nRelated>0 ){
1815
+ blob_appendf(&desc, " and %d related check-in%s", nRelated,
1816
+ nRelated>1 ? "s" : "");
1817
+ }
1818
+ }
16931819
addFileGlobDescription(zChng, &desc);
16941820
}else if( (p_rid || d_rid) && g.perm.Read && zTagSql==0 ){
16951821
/* If p= or d= is present, ignore all other parameters other than n= */
16961822
char *zUuid;
16971823
int np, nd;
@@ -1707,19 +1833,19 @@
17071833
zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d",
17081834
p_rid ? p_rid : d_rid);
17091835
blob_append_sql(&sql, " AND event.objid IN ok");
17101836
nd = 0;
17111837
if( d_rid ){
1712
- compute_descendants(d_rid, nEntry+1);
1838
+ compute_descendants(d_rid, nEntry==0 ? 0 : nEntry+1);
17131839
nd = db_int(0, "SELECT count(*)-1 FROM ok");
17141840
if( nd>=0 ) db_multi_exec("%s", blob_sql_text(&sql));
17151841
if( nd>0 ) blob_appendf(&desc, "%d descendant%s", nd,(1==nd)?"":"s");
17161842
if( useDividers ) selectedRid = d_rid;
17171843
db_multi_exec("DELETE FROM ok");
17181844
}
17191845
if( p_rid ){
1720
- compute_ancestors(p_rid, nEntry+1, 0);
1846
+ compute_ancestors(p_rid, nEntry==0 ? 0 : nEntry+1, 0);
17211847
np = db_int(0, "SELECT count(*)-1 FROM ok");
17221848
if( np>0 ){
17231849
if( nd>0 ) blob_appendf(&desc, " and ");
17241850
blob_appendf(&desc, "%d ancestors", np);
17251851
db_multi_exec("%s", blob_sql_text(&sql));
@@ -1747,10 +1873,19 @@
17471873
"INSERT INTO ok VALUES(%d);"
17481874
"INSERT OR IGNORE INTO ok SELECT pid FROM plink WHERE cid=%d;"
17491875
"INSERT OR IGNORE INTO ok SELECT cid FROM plink WHERE pid=%d;",
17501876
f_rid, f_rid, f_rid
17511877
);
1878
+ if( showCherrypicks ){
1879
+ db_multi_exec(
1880
+ "INSERT OR IGNORE INTO ok SELECT parentid FROM cherrypick"
1881
+ " WHERE childid=%d;"
1882
+ "INSERT OR IGNORE INTO ok SELECT childid FROM cherrypick"
1883
+ " WHERE parentid=%d;",
1884
+ f_rid, f_rid
1885
+ );
1886
+ }
17521887
blob_append_sql(&sql, " AND event.objid IN ok");
17531888
db_multi_exec("%s", blob_sql_text(&sql));
17541889
if( useDividers ) selectedRid = f_rid;
17551890
blob_appendf(&desc, "Parents and children of check-in ");
17561891
zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d", f_rid);
@@ -1778,10 +1913,18 @@
17781913
blob_append_sql(&cond, " AND event.objid IN rnfile ");
17791914
}
17801915
if( forkOnly ){
17811916
blob_append_sql(&cond, " AND event.objid IN rnfork ");
17821917
}
1918
+ if( cpOnly && showCherrypicks ){
1919
+ db_multi_exec(
1920
+ "CREATE TABLE IF NOT EXISTS cpnodes(rid INTEGER PRIMARY KEY);"
1921
+ "INSERT OR IGNORE INTO cpnodes SELECT childid FROM cherrypick;"
1922
+ "INSERT OR IGNORE INTO cpnodes SELECT parentid FROM cherrypick;"
1923
+ );
1924
+ blob_append_sql(&cond, " AND event.objid IN cpnodes ");
1925
+ }
17831926
if( bisectOnly ){
17841927
blob_append_sql(&cond, " AND event.objid IN (SELECT rid FROM bilog) ");
17851928
}
17861929
if( zYearMonth ){
17871930
blob_append_sql(&cond, " AND %Q=strftime('%%Y-%%m',event.mtime) ",
@@ -1827,49 +1970,65 @@
18271970
blob_append_sql(&cond, " AND event.mtime>=julianday('now','-%d days') ",
18281971
nDays);
18291972
nEntry = -1;
18301973
}
18311974
if( zTagSql ){
1832
- blob_append_sql(&cond,
1833
- " AND (EXISTS(SELECT 1 FROM tagxref NATURAL JOIN tag"
1834
- " WHERE %s AND tagtype>0 AND rid=blob.rid)\n", zTagSql/*safe-for-%s*/);
1835
-
1836
- if( related ){
1837
- /* The next two blob_appendf() calls add SQL that causes check-ins that
1838
- ** are not part of the branch which are parents or children of the
1839
- ** branch to be included in the report. This related check-ins are
1840
- ** useful in helping to visualize what has happened on a quiescent
1841
- ** branch that is infrequently merged with a much more activate branch.
1842
- */
1843
- blob_append_sql(&cond,
1844
- " OR EXISTS(SELECT 1 FROM plink CROSS JOIN tagxref ON rid=cid"
1845
- " NATURAL JOIN tag WHERE %s AND tagtype>0 AND pid=blob.rid)\n",
1846
- zTagSql/*safe-for-%s*/
1847
- );
1848
- if( (tmFlags & TIMELINE_UNHIDE)==0 ){
1849
- blob_append_sql(&cond,
1850
- " AND NOT EXISTS(SELECT 1 FROM plink JOIN tagxref ON rid=cid"
1851
- " WHERE tagid=%d AND tagtype>0 AND pid=blob.rid)\n",
1852
- TAG_HIDDEN
1853
- );
1854
- }
1855
- if( P("mionly")==0 ){
1856
- blob_append_sql(&cond,
1857
- " OR EXISTS(SELECT 1 FROM plink CROSS JOIN tagxref ON rid=pid"
1858
- " NATURAL JOIN tag WHERE %s AND tagtype>0 AND cid=blob.rid)\n",
1859
- zTagSql/*safe-for-%s*/
1860
- );
1861
- if( (tmFlags & TIMELINE_UNHIDE)==0 ){
1862
- blob_append_sql(&cond,
1863
- " AND NOT EXISTS(SELECT 1 FROM plink JOIN tagxref ON rid=pid"
1864
- " WHERE tagid=%d AND tagtype>0 AND cid=blob.rid)\n",
1865
- TAG_HIDDEN
1866
- );
1867
- }
1868
- }
1869
- }
1870
- blob_append_sql(&cond, ")");
1975
+ db_multi_exec(
1976
+ "CREATE TEMP TABLE selected_nodes(rid INTEGER PRIMARY KEY);"
1977
+ "INSERT OR IGNORE INTO selected_nodes"
1978
+ " SELECT tagxref.rid FROM tagxref NATURAL JOIN tag"
1979
+ " WHERE %s AND tagtype>0", zTagSql/*safe-for-%s*/
1980
+ );
1981
+ if( !related ){
1982
+ blob_append_sql(&cond, " AND blob.rid IN selected_nodes");
1983
+ }else{
1984
+ db_multi_exec(
1985
+ "CREATE TEMP TABLE related_nodes(rid INTEGER PRIMARY KEY);"
1986
+ "INSERT INTO related_nodes SELECT rid FROM selected_nodes;"
1987
+ );
1988
+ blob_append_sql(&cond, " AND blob.rid IN related_nodes");
1989
+ /* The next two blob_appendf() calls add SQL that causes check-ins that
1990
+ ** are not part of the branch which are parents or children of the
1991
+ ** branch to be included in the report. These related check-ins are
1992
+ ** useful in helping to visualize what has happened on a quiescent
1993
+ ** branch that is infrequently merged with a much more activate branch.
1994
+ */
1995
+ db_multi_exec(
1996
+ "INSERT OR IGNORE INTO related_nodes"
1997
+ " SELECT pid FROM selected_nodes CROSS JOIN plink"
1998
+ " WHERE selected_nodes.rid=plink.cid;"
1999
+ );
2000
+ if( P("mionly")==0 ){
2001
+ db_multi_exec(
2002
+ "INSERT OR IGNORE INTO related_nodes"
2003
+ " SELECT cid FROM selected_nodes CROSS JOIN plink"
2004
+ " WHERE selected_nodes.rid=plink.pid;"
2005
+ );
2006
+ if( showCherrypicks ){
2007
+ db_multi_exec(
2008
+ "INSERT OR IGNORE INTO related_nodes"
2009
+ " SELECT childid FROM selected_nodes CROSS JOIN cherrypick"
2010
+ " WHERE selected_nodes.rid=cherrypick.parentid;"
2011
+ );
2012
+ }
2013
+ }
2014
+ if( showCherrypicks ){
2015
+ db_multi_exec(
2016
+ "INSERT OR IGNORE INTO related_nodes"
2017
+ " SELECT parentid FROM selected_nodes CROSS JOIN cherrypick"
2018
+ " WHERE selected_nodes.rid=cherrypick.childid;"
2019
+ );
2020
+ }
2021
+ if( (tmFlags & TIMELINE_UNHIDE)==0 ){
2022
+ db_multi_exec(
2023
+ "DELETE FROM related_nodes WHERE rid IN "
2024
+ " (SELECT related_nodes.rid FROM related_nodes, tagxref"
2025
+ " WHERE tagid=%d AND tagtype>0 AND tagxref.rid=related_nodes.rid)",
2026
+ TAG_HIDDEN
2027
+ );
2028
+ }
2029
+ }
18712030
}
18722031
if( (zType[0]=='w' && !g.perm.RdWiki)
18732032
|| (zType[0]=='t' && !g.perm.RdTkt)
18742033
|| (zType[0]=='e' && !g.perm.RdWiki)
18752034
|| (zType[0]=='c' && !g.perm.Read)
@@ -2013,10 +2172,14 @@
20132172
}
20142173
if( bisectOnly ){
20152174
blob_appendf(&desc, " in the most recent bisect");
20162175
tmFlags |= TIMELINE_DISJOINT;
20172176
}
2177
+ if( cpOnly && showCherrypicks ){
2178
+ blob_appendf(&desc, " that participate in a cherrypick merge");
2179
+ tmFlags |= TIMELINE_CHPICK|TIMELINE_DISJOINT;
2180
+ }
20182181
if( zUser ){
20192182
blob_appendf(&desc, " by user %h", zUser);
20202183
tmFlags |= TIMELINE_DISJOINT;
20212184
}
20222185
if( zTagSql ){
@@ -2106,13 +2269,15 @@
21062269
}
21072270
if( search_restrict(SRCH_CKIN)!=0 ){
21082271
style_submenu_element("Search", "%R/search?y=c");
21092272
}
21102273
if( advancedMenu ){
2111
- style_submenu_element("Basic", "%s", url_render(&url, "advm", "0", 0, 0));
2274
+ style_submenu_element("Basic", "%s",
2275
+ url_render(&url, "advm", "0", "udc", "1"));
21122276
}else{
2113
- style_submenu_element("Advanced", "%s", url_render(&url, "advm", "1", 0, 0));
2277
+ style_submenu_element("Advanced", "%s",
2278
+ url_render(&url, "advm", "1", "udc", "1"));
21142279
}
21152280
if( PB("showid") ) tmFlags |= TIMELINE_SHOWRID;
21162281
if( useDividers && zMark && zMark[0] ){
21172282
double r = symbolic_name_to_mtime(zMark);
21182283
if( r>0.0 ) selectedRid = timeline_add_divider(r);
@@ -2120,11 +2285,26 @@
21202285
blob_zero(&sql);
21212286
db_prepare(&q, "SELECT * FROM timeline ORDER BY sortby DESC /*scan*/");
21222287
if( fossil_islower(desc.aData[0]) ){
21232288
desc.aData[0] = fossil_toupper(desc.aData[0]);
21242289
}
2125
- @ <h2>%b(&desc)</h2>
2290
+ if( zBrName
2291
+ && !PB("nowiki")
2292
+ && wiki_render_associated("branch", zBrName, WIKIASSOC_ALL)
2293
+ ){
2294
+ @ <div class="section">%b(&desc)</div>
2295
+ }else
2296
+ if( zTagName
2297
+ && matchStyle==MS_EXACT
2298
+ && zBrName==0
2299
+ && !PB("nowiki")
2300
+ && wiki_render_associated("tag", zTagName, WIKIASSOC_ALL)
2301
+ ){
2302
+ @ <div class="section">%b(&desc)</div>
2303
+ } else{
2304
+ @ <h2>%b(&desc)</h2>
2305
+ }
21262306
blob_reset(&desc);
21272307
21282308
/* Report any errors. */
21292309
if( zError ){
21302310
@ <p class="generalError">%h(zError)</p>
21312311
--- src/timeline.c
+++ src/timeline.c
@@ -91,12 +91,12 @@
91
92 /*
93 ** Allowed flags for the tmFlags argument to www_print_timeline
94 */
95 #if INTERFACE
96 #define TIMELINE_ARTID 0x000001 /* Show artifact IDs on non-check-in lines */
97 #define TIMELINE_LEAFONLY 0x000002 /* Show "Leaf" but not "Merge", "Fork" etc */
98 #define TIMELINE_BRIEF 0x000004 /* Combine adjacent elements of same obj */
99 #define TIMELINE_GRAPH 0x000008 /* Compute a graph */
100 #define TIMELINE_DISJOINT 0x000010 /* Elements are not contiguous */
101 #define TIMELINE_FCHANGES 0x000020 /* Detail file changes */
102 #define TIMELINE_BRCOLOR 0x000040 /* Background color by branch name */
@@ -111,10 +111,11 @@
111 #define TIMELINE_COLUMNAR 0x008000 /* Use the "columns" view style */
112 #define TIMELINE_CLASSIC 0x010000 /* Use the "classic" view style */
113 #define TIMELINE_VIEWS 0x01f000 /* Mask for all of the view styles */
114 #define TIMELINE_NOSCROLL 0x100000 /* Don't scroll to the selection */
115 #define TIMELINE_FILEDIFF 0x200000 /* Show File differences, not ckin diffs */
 
116 #endif
117
118 /*
119 ** Hash a string and use the hash to determine a background color.
120 */
@@ -290,10 +291,15 @@
290 }
291 db_static_prepare(&qbranch,
292 "SELECT value FROM tagxref WHERE tagid=%d AND tagtype>0 AND rid=:rid",
293 TAG_BRANCH
294 );
 
 
 
 
 
295
296 @ <table id="timelineTable%d(iTableId)" class="timelineTable">
297 blob_zero(&comment);
298 while( db_step(pQuery)==SQLITE_ROW ){
299 int rid = db_column_int(pQuery, 0);
@@ -423,10 +429,11 @@
423 }
424 }
425 }
426 if( zType[0]=='c' && pGraph ){
427 int nParent = 0;
 
428 int aParent[GR_MAX_RAIL];
429 static Stmt qparent;
430 db_static_prepare(&qparent,
431 "SELECT pid FROM plink"
432 " WHERE cid=:rid AND pid NOT IN phantom"
@@ -435,19 +442,32 @@
435 db_bind_int(&qparent, ":rid", rid);
436 while( db_step(&qparent)==SQLITE_ROW && nParent<count(aParent) ){
437 aParent[nParent++] = db_column_int(&qparent, 0);
438 }
439 db_reset(&qparent);
440 gidx = graph_add_row(pGraph, rid, nParent, aParent, zBr, zBgClr,
441 zUuid, isLeaf);
 
 
 
 
 
 
 
 
 
 
 
 
 
442 db_reset(&qbranch);
443 @ <div id="m%d(gidx)" class="tl-nodemark"></div>
444 }else if( zType[0]=='e' && pGraph && zBgClr && zBgClr[0] ){
445 /* For technotes, make a graph node with nParent==(-1). This will
446 ** not actually draw anything on the graph, but it will set the
447 ** background color of the timeline entry */
448 gidx = graph_add_row(pGraph, rid, -1, 0, zBr, zBgClr, zUuid, 0);
449 @ <div id="m%d(gidx)" class="tl-nodemark"></div>
450 }
451 @</td>
452 if( !isSelectedOrCurrent ){
453 @ <td class="timeline%s(zStyle)Cell" id='mc%d(gidx)'>
@@ -505,11 +525,13 @@
505 }
506 }
507 if( zType[0]!='c' ){
508 /* Comments for anything other than a check-in are generated by
509 ** "fossil rebuild" and expect to be rendered as text/x-fossil-wiki */
 
510 wiki_convert(&comment, 0, WIKI_INLINE);
 
511 }else{
512 if( bCommentGitStyle ){
513 /* Truncate comment at first blank line */
514 int ii, jj;
515 int n = blob_size(&comment);
@@ -841,68 +863,109 @@
841 ** to get an actual id, prepend "m" to the integer. The top node
842 ** is iTopRow and numbers increase moving down the timeline.
843 ** bg: The background color for this row
844 ** r: The "rail" that the node for this row sits on. The left-most
845 ** rail is 0 and the number increases to the right.
846 ** d: True if there is a "descender" - an arrow coming from the bottom
847 ** of the page straight up to this node.
848 ** mo: "merge-out". If non-negative, this is the rail position
849 ** for the upward portion of a merge arrow. The merge arrow goes up
850 ** to the row identified by mu:. If this value is negative then
851 ** node has no merge children and no merge-out line is drawn.
 
852 ** mu: The id of the row which is the top of the merge-out arrow.
 
 
 
853 ** u: Draw a thick child-line out of the top of this node and up to
854 ** the node with an id equal to this value. 0 if it is straight to
855 ** the top of the page, -1 if there is no thick-line riser.
856 ** f: 0x01: a leaf node.
857 ** au: An array of integers that define thick-line risers for branches.
858 ** The integers are in pairs. For each pair, the first integer is
859 ** is the rail on which the riser should run and the second integer
860 ** is the id of the node upto which the riser should run.
 
861 ** mi: "merge-in". An array of integer rail positions from which
862 ** merge arrows should be drawn into this node. If the value is
863 ** negative, then the rail position is the absolute value of mi[]
864 ** and a thin merge-arrow descender is drawn to the bottom of
865 ** the screen.
 
 
 
866 ** h: The artifact hash of the object being graphed
867 */
868 for(pRow=pGraph->pFirst; pRow; pRow=pRow->pNext){
 
869 cgi_printf("{\"id\":%d,", pRow->idx);
870 cgi_printf("\"bg\":\"%s\",", pRow->zBgClr);
871 cgi_printf("\"r\":%d,", pRow->iRail);
872 cgi_printf("\"d\":%d,", pRow->bDescender);
873 cgi_printf("\"mo\":%d,", pRow->mergeOut);
874 cgi_printf("\"mu\":%d,", pRow->mergeUpto);
 
 
 
 
 
 
 
 
875 cgi_printf("\"u\":%d,", pRow->aiRiser[pRow->iRail]);
876 cgi_printf("\"f\":%d,", pRow->isLeaf ? 1 : 0);
877 cgi_printf("\"au\":");
878 cSep = '[';
879 for(i=0; i<GR_MAX_RAIL; i++){
880 if( i==pRow->iRail ) continue;
881 if( pRow->aiRiser[i]>0 ){
 
 
 
 
 
882 cgi_printf("%c%d,%d", cSep, i, pRow->aiRiser[i]);
883 cSep = ',';
884 }
885 }
886 if( cSep=='[' ) cgi_printf("[");
887 cgi_printf("],");
 
888 if( colorGraph && pRow->zBgClr[0]=='#' ){
889 cgi_printf("\"fg\":\"%s\",", bg_to_fg(pRow->zBgClr));
890 }
891 /* mi */
892 cgi_printf("\"mi\":");
893 cSep = '[';
894 for(i=0; i<GR_MAX_RAIL; i++){
895 if( pRow->mergeIn[i] ){
896 int mi = i;
897 if( (pRow->mergeDown >> i) & 1 ) mi = -mi;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
898 cgi_printf("%c%d", cSep, mi);
899 cSep = ',';
900 }
901 }
902 if( cSep=='[' ) cgi_printf("[");
903 cgi_printf("],\"h\":\"%!S\"}%s",
904 pRow->zUuid, pRow->pNext ? ",\n" : "]\n");
905 }
906 @ }</script>
907 style_graph_generator();
908 graph_free(pGraph);
@@ -1372,29 +1435,33 @@
1372 ** dp=CHECKIN The same as d=CHECKIN&p=CHECKIN
1373 ** t=TAG Show only check-ins with the given TAG
1374 ** r=TAG Show check-ins related to TAG, equivalent to t=TAG&rel
1375 ** rel Show related check-ins as well as those matching t=TAG
1376 ** mionly Limit rel to show ancestors but not descendants
 
1377 ** ms=MATCHSTYLE Set tag match style to EXACT, GLOB, LIKE, REGEXP
1378 ** u=USER Only show items associated with USER
1379 ** y=TYPE 'ci', 'w', 't', 'e', 'f', or 'all'.
1380 ** ss=VIEWSTYLE c: "Compact" v: "Verbose" m: "Modern" j: "Columnar"
1381 ** advm Use the "Advanced" or "Busy" menu design.
1382 ** ng No Graph.
 
1383 ** nd Do not highlight the focus check-in
1384 ** v Show details of files changed
1385 ** f=CHECKIN Show family (immediate parents and children) of CHECKIN
1386 ** from=CHECKIN Path from...
1387 ** to=CHECKIN ... to this
1388 ** shortest ... show only the shortest path
 
1389 ** uf=FILE_HASH Show only check-ins that contain the given file version
1390 ** chng=GLOBLIST Show only check-ins that involve changes to a file whose
1391 ** name matches one of the comma-separate GLOBLIST
1392 ** brbg Background color from branch name
1393 ** ubg Background color from user
1394 ** namechng Show only check-ins that have filename changes
1395 ** forks Show only forks and their children
 
1396 ** ym=YYYY-MM Show only events for the given year/month
1397 ** yw=YYYY-WW Show only events for the given week of the given year
1398 ** yw=YYYY-MM-DD Show events for the week that includes the given day
1399 ** ymd=YYYY-MM-DD Show only events on the given day
1400 ** days=N Show events over the previous N days
@@ -1444,10 +1511,11 @@
1444 const char *zChng = P("chng"); /* List of GLOBs for files that changed */
1445 int useDividers = P("nd")==0; /* Show dividers if "nd" is missing */
1446 int renameOnly = P("namechng")!=0; /* Show only check-ins that rename files */
1447 int forkOnly = PB("forks"); /* Show only forks and their children */
1448 int bisectOnly = PB("bisect"); /* Show the check-ins of the bisect */
 
1449 int tmFlags = 0; /* Timeline flags */
1450 const char *zThisTag = 0; /* Suppress links to this tag */
1451 const char *zThisUser = 0; /* Suppress links to this user */
1452 HQuery url; /* URL for various branch links */
1453 int from_rid = name_to_typed_rid(P("from"),"ci"); /* from= for paths */
@@ -1462,10 +1530,11 @@
1462 char *zNewerButton = 0; /* URL for Newer button at the top */
1463 int selectedRid = -9999999; /* Show a highlight on this RID */
1464 int disableY = 0; /* Disable type selector on submenu */
1465 int advancedMenu = 0; /* Use the advanced menu design */
1466 char *zPlural; /* Ending for plural forms */
 
1467 void (*xExtra)(int) = NULL;
1468
1469 /* Set number of rows to display */
1470 cookie_read_parameter("n","n");
1471 z = P("n");
@@ -1487,10 +1556,16 @@
1487 cgi_replace_query_parameter("n",z);
1488 cookie_write_parameter("n","n",0);
1489 tmFlags |= timeline_ss_submenu();
1490 cookie_link_parameter("advm","advm","0");
1491 advancedMenu = atoi(PD("advm","0"));
 
 
 
 
 
 
1492
1493 /* To view the timeline, must have permission to read project data.
1494 */
1495 pd_rid = name_to_typed_rid(P("dp"),"ci");
1496 if( pd_rid ){
@@ -1521,19 +1596,21 @@
1521 cgi_delete_query_parameter("r");
1522 cgi_set_query_parameter("t", zBrName);
1523 cgi_set_query_parameter("rel", "1");
1524 zTagName = zBrName;
1525 related = 1;
 
1526 }
1527
1528 /* Ignore empty tag query strings. */
1529 if( zTagName && !*zTagName ){
1530 zTagName = 0;
1531 }
1532
1533 /* Finish preliminary processing of tag match queries. */
1534 if( zTagName ){
 
1535 /* Interpet the tag style string. */
1536 if( fossil_stricmp(zMatchStyle, "glob")==0 ){
1537 matchStyle = MS_GLOB;
1538 }else if( fossil_stricmp(zMatchStyle, "like")==0 ){
1539 matchStyle = MS_LIKE;
@@ -1563,16 +1640,19 @@
1563 ){
1564 nEntry = -1;
1565 zCirca = 0;
1566 }
1567 if( zType[0]=='a' ){
1568 tmFlags |= TIMELINE_BRIEF | TIMELINE_GRAPH;
1569 }else{
1570 tmFlags |= TIMELINE_GRAPH;
 
 
 
1571 }
1572 if( PB("ng") || zSearch!=0 ){
1573 tmFlags &= ~TIMELINE_GRAPH;
1574 }
1575 if( PB("brbg") ){
1576 tmFlags |= TIMELINE_BRCOLOR;
1577 }
1578 if( PB("unhide") ){
@@ -1658,10 +1738,12 @@
1658 /* If from= and to= are present, display all nodes on a path connecting
1659 ** the two */
1660 PathNode *p = 0;
1661 const char *zFrom = 0;
1662 const char *zTo = 0;
 
 
1663
1664 if( from_rid && to_rid ){
1665 p = path_shortest(from_rid, to_rid, noMerge, 0);
1666 zFrom = P("from");
1667 zTo = P("to");
@@ -1670,28 +1752,72 @@
1670 p = path_first();
1671 }
1672 zFrom = P("me");
1673 zTo = P("you");
1674 }
1675 blob_append(&sql, " AND event.objid IN (0", -1);
1676 while( p ){
1677 blob_append_sql(&sql, ",%d", p->rid);
 
 
 
 
1678 p = p->u.pTo;
 
 
 
 
 
 
1679 }
1680 blob_append(&sql, ")", -1);
1681 path_reset();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1682 addFileGlobExclusion(zChng, &sql);
1683 tmFlags |= TIMELINE_DISJOINT;
1684 db_multi_exec("%s", blob_sql_text(&sql));
1685 if( advancedMenu ){
1686 style_submenu_checkbox("v", "Files", (zType[0]!='a' && zType[0]!='c'),0);
1687 }
1688 blob_appendf(&desc, "%d check-ins going from ",
1689 db_int(0, "SELECT count(*) FROM timeline"));
1690 blob_appendf(&desc, "%z[%h]</a>", href("%R/info/%h", zFrom), zFrom);
1691 blob_append(&desc, " to ", -1);
1692 blob_appendf(&desc, "%z[%h]</a>", href("%R/info/%h",zTo), zTo);
 
 
 
 
 
 
 
1693 addFileGlobDescription(zChng, &desc);
1694 }else if( (p_rid || d_rid) && g.perm.Read && zTagSql==0 ){
1695 /* If p= or d= is present, ignore all other parameters other than n= */
1696 char *zUuid;
1697 int np, nd;
@@ -1707,19 +1833,19 @@
1707 zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d",
1708 p_rid ? p_rid : d_rid);
1709 blob_append_sql(&sql, " AND event.objid IN ok");
1710 nd = 0;
1711 if( d_rid ){
1712 compute_descendants(d_rid, nEntry+1);
1713 nd = db_int(0, "SELECT count(*)-1 FROM ok");
1714 if( nd>=0 ) db_multi_exec("%s", blob_sql_text(&sql));
1715 if( nd>0 ) blob_appendf(&desc, "%d descendant%s", nd,(1==nd)?"":"s");
1716 if( useDividers ) selectedRid = d_rid;
1717 db_multi_exec("DELETE FROM ok");
1718 }
1719 if( p_rid ){
1720 compute_ancestors(p_rid, nEntry+1, 0);
1721 np = db_int(0, "SELECT count(*)-1 FROM ok");
1722 if( np>0 ){
1723 if( nd>0 ) blob_appendf(&desc, " and ");
1724 blob_appendf(&desc, "%d ancestors", np);
1725 db_multi_exec("%s", blob_sql_text(&sql));
@@ -1747,10 +1873,19 @@
1747 "INSERT INTO ok VALUES(%d);"
1748 "INSERT OR IGNORE INTO ok SELECT pid FROM plink WHERE cid=%d;"
1749 "INSERT OR IGNORE INTO ok SELECT cid FROM plink WHERE pid=%d;",
1750 f_rid, f_rid, f_rid
1751 );
 
 
 
 
 
 
 
 
 
1752 blob_append_sql(&sql, " AND event.objid IN ok");
1753 db_multi_exec("%s", blob_sql_text(&sql));
1754 if( useDividers ) selectedRid = f_rid;
1755 blob_appendf(&desc, "Parents and children of check-in ");
1756 zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d", f_rid);
@@ -1778,10 +1913,18 @@
1778 blob_append_sql(&cond, " AND event.objid IN rnfile ");
1779 }
1780 if( forkOnly ){
1781 blob_append_sql(&cond, " AND event.objid IN rnfork ");
1782 }
 
 
 
 
 
 
 
 
1783 if( bisectOnly ){
1784 blob_append_sql(&cond, " AND event.objid IN (SELECT rid FROM bilog) ");
1785 }
1786 if( zYearMonth ){
1787 blob_append_sql(&cond, " AND %Q=strftime('%%Y-%%m',event.mtime) ",
@@ -1827,49 +1970,65 @@
1827 blob_append_sql(&cond, " AND event.mtime>=julianday('now','-%d days') ",
1828 nDays);
1829 nEntry = -1;
1830 }
1831 if( zTagSql ){
1832 blob_append_sql(&cond,
1833 " AND (EXISTS(SELECT 1 FROM tagxref NATURAL JOIN tag"
1834 " WHERE %s AND tagtype>0 AND rid=blob.rid)\n", zTagSql/*safe-for-%s*/);
1835
1836 if( related ){
1837 /* The next two blob_appendf() calls add SQL that causes check-ins that
1838 ** are not part of the branch which are parents or children of the
1839 ** branch to be included in the report. This related check-ins are
1840 ** useful in helping to visualize what has happened on a quiescent
1841 ** branch that is infrequently merged with a much more activate branch.
1842 */
1843 blob_append_sql(&cond,
1844 " OR EXISTS(SELECT 1 FROM plink CROSS JOIN tagxref ON rid=cid"
1845 " NATURAL JOIN tag WHERE %s AND tagtype>0 AND pid=blob.rid)\n",
1846 zTagSql/*safe-for-%s*/
1847 );
1848 if( (tmFlags & TIMELINE_UNHIDE)==0 ){
1849 blob_append_sql(&cond,
1850 " AND NOT EXISTS(SELECT 1 FROM plink JOIN tagxref ON rid=cid"
1851 " WHERE tagid=%d AND tagtype>0 AND pid=blob.rid)\n",
1852 TAG_HIDDEN
1853 );
1854 }
1855 if( P("mionly")==0 ){
1856 blob_append_sql(&cond,
1857 " OR EXISTS(SELECT 1 FROM plink CROSS JOIN tagxref ON rid=pid"
1858 " NATURAL JOIN tag WHERE %s AND tagtype>0 AND cid=blob.rid)\n",
1859 zTagSql/*safe-for-%s*/
1860 );
1861 if( (tmFlags & TIMELINE_UNHIDE)==0 ){
1862 blob_append_sql(&cond,
1863 " AND NOT EXISTS(SELECT 1 FROM plink JOIN tagxref ON rid=pid"
1864 " WHERE tagid=%d AND tagtype>0 AND cid=blob.rid)\n",
1865 TAG_HIDDEN
1866 );
1867 }
1868 }
1869 }
1870 blob_append_sql(&cond, ")");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1871 }
1872 if( (zType[0]=='w' && !g.perm.RdWiki)
1873 || (zType[0]=='t' && !g.perm.RdTkt)
1874 || (zType[0]=='e' && !g.perm.RdWiki)
1875 || (zType[0]=='c' && !g.perm.Read)
@@ -2013,10 +2172,14 @@
2013 }
2014 if( bisectOnly ){
2015 blob_appendf(&desc, " in the most recent bisect");
2016 tmFlags |= TIMELINE_DISJOINT;
2017 }
 
 
 
 
2018 if( zUser ){
2019 blob_appendf(&desc, " by user %h", zUser);
2020 tmFlags |= TIMELINE_DISJOINT;
2021 }
2022 if( zTagSql ){
@@ -2106,13 +2269,15 @@
2106 }
2107 if( search_restrict(SRCH_CKIN)!=0 ){
2108 style_submenu_element("Search", "%R/search?y=c");
2109 }
2110 if( advancedMenu ){
2111 style_submenu_element("Basic", "%s", url_render(&url, "advm", "0", 0, 0));
 
2112 }else{
2113 style_submenu_element("Advanced", "%s", url_render(&url, "advm", "1", 0, 0));
 
2114 }
2115 if( PB("showid") ) tmFlags |= TIMELINE_SHOWRID;
2116 if( useDividers && zMark && zMark[0] ){
2117 double r = symbolic_name_to_mtime(zMark);
2118 if( r>0.0 ) selectedRid = timeline_add_divider(r);
@@ -2120,11 +2285,26 @@
2120 blob_zero(&sql);
2121 db_prepare(&q, "SELECT * FROM timeline ORDER BY sortby DESC /*scan*/");
2122 if( fossil_islower(desc.aData[0]) ){
2123 desc.aData[0] = fossil_toupper(desc.aData[0]);
2124 }
2125 @ <h2>%b(&desc)</h2>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2126 blob_reset(&desc);
2127
2128 /* Report any errors. */
2129 if( zError ){
2130 @ <p class="generalError">%h(zError)</p>
2131
--- src/timeline.c
+++ src/timeline.c
@@ -91,12 +91,12 @@
91
92 /*
93 ** Allowed flags for the tmFlags argument to www_print_timeline
94 */
95 #if INTERFACE
96 #define TIMELINE_ARTID 0x000001 /* Show artifact IDs on non-check-in lines*/
97 #define TIMELINE_LEAFONLY 0x000002 /* Show "Leaf" but not "Merge", "Fork" etc*/
98 #define TIMELINE_BRIEF 0x000004 /* Combine adjacent elements of same obj */
99 #define TIMELINE_GRAPH 0x000008 /* Compute a graph */
100 #define TIMELINE_DISJOINT 0x000010 /* Elements are not contiguous */
101 #define TIMELINE_FCHANGES 0x000020 /* Detail file changes */
102 #define TIMELINE_BRCOLOR 0x000040 /* Background color by branch name */
@@ -111,10 +111,11 @@
111 #define TIMELINE_COLUMNAR 0x008000 /* Use the "columns" view style */
112 #define TIMELINE_CLASSIC 0x010000 /* Use the "classic" view style */
113 #define TIMELINE_VIEWS 0x01f000 /* Mask for all of the view styles */
114 #define TIMELINE_NOSCROLL 0x100000 /* Don't scroll to the selection */
115 #define TIMELINE_FILEDIFF 0x200000 /* Show File differences, not ckin diffs */
116 #define TIMELINE_CHPICK 0x400000 /* Show cherrypick merges */
117 #endif
118
119 /*
120 ** Hash a string and use the hash to determine a background color.
121 */
@@ -290,10 +291,15 @@
291 }
292 db_static_prepare(&qbranch,
293 "SELECT value FROM tagxref WHERE tagid=%d AND tagtype>0 AND rid=:rid",
294 TAG_BRANCH
295 );
296 if( (tmFlags & TIMELINE_CHPICK)!=0
297 && !db_table_exists("repository","cherrypick")
298 ){
299 tmFlags &= ~TIMELINE_CHPICK;
300 }
301
302 @ <table id="timelineTable%d(iTableId)" class="timelineTable">
303 blob_zero(&comment);
304 while( db_step(pQuery)==SQLITE_ROW ){
305 int rid = db_column_int(pQuery, 0);
@@ -423,10 +429,11 @@
429 }
430 }
431 }
432 if( zType[0]=='c' && pGraph ){
433 int nParent = 0;
434 int nCherrypick = 0;
435 int aParent[GR_MAX_RAIL];
436 static Stmt qparent;
437 db_static_prepare(&qparent,
438 "SELECT pid FROM plink"
439 " WHERE cid=:rid AND pid NOT IN phantom"
@@ -435,19 +442,32 @@
442 db_bind_int(&qparent, ":rid", rid);
443 while( db_step(&qparent)==SQLITE_ROW && nParent<count(aParent) ){
444 aParent[nParent++] = db_column_int(&qparent, 0);
445 }
446 db_reset(&qparent);
447 if( (tmFlags & TIMELINE_CHPICK)!=0 && nParent>0 ){
448 static Stmt qcherrypick;
449 db_static_prepare(&qcherrypick,
450 "SELECT parentid FROM cherrypick"
451 " WHERE childid=:rid AND parentid NOT IN phantom"
452 );
453 db_bind_int(&qcherrypick, ":rid", rid);
454 while( db_step(&qcherrypick)==SQLITE_ROW && nParent<count(aParent) ){
455 aParent[nParent++] = db_column_int(&qcherrypick, 0);
456 nCherrypick++;
457 }
458 db_reset(&qcherrypick);
459 }
460 gidx = graph_add_row(pGraph, rid, nParent, nCherrypick, aParent,
461 zBr, zBgClr, zUuid, isLeaf);
462 db_reset(&qbranch);
463 @ <div id="m%d(gidx)" class="tl-nodemark"></div>
464 }else if( zType[0]=='e' && pGraph && zBgClr && zBgClr[0] ){
465 /* For technotes, make a graph node with nParent==(-1). This will
466 ** not actually draw anything on the graph, but it will set the
467 ** background color of the timeline entry */
468 gidx = graph_add_row(pGraph, rid, -1, 0, 0, zBr, zBgClr, zUuid, 0);
469 @ <div id="m%d(gidx)" class="tl-nodemark"></div>
470 }
471 @</td>
472 if( !isSelectedOrCurrent ){
473 @ <td class="timeline%s(zStyle)Cell" id='mc%d(gidx)'>
@@ -505,11 +525,13 @@
525 }
526 }
527 if( zType[0]!='c' ){
528 /* Comments for anything other than a check-in are generated by
529 ** "fossil rebuild" and expect to be rendered as text/x-fossil-wiki */
530 if( zType[0]=='w' ) wiki_hyperlink_override(zUuid);
531 wiki_convert(&comment, 0, WIKI_INLINE);
532 wiki_hyperlink_override(0);
533 }else{
534 if( bCommentGitStyle ){
535 /* Truncate comment at first blank line */
536 int ii, jj;
537 int n = blob_size(&comment);
@@ -841,68 +863,109 @@
863 ** to get an actual id, prepend "m" to the integer. The top node
864 ** is iTopRow and numbers increase moving down the timeline.
865 ** bg: The background color for this row
866 ** r: The "rail" that the node for this row sits on. The left-most
867 ** rail is 0 and the number increases to the right.
868 ** d: If exists and true then there is a "descender" - an arrow
869 ** coming from the bottom of the page straight up to this node.
870 ** mo: "merge-out". If it exists, this is the rail position
871 ** for the upward portion of a merge arrow. The merge arrow goes as
872 ** a solid normal merge line up to the row identified by "mu" and
873 ** then as a dashed cherrypick merge line up further to "cu".
874 ** If this value is omitted if there are no merge children.
875 ** mu: The id of the row which is the top of the merge-out arrow.
876 ** Only exists if "mo" exists.
877 ** cu: Extend the mu merge arrow up to this row as a cherrypick
878 ** merge line, if this value exists.
879 ** u: Draw a thick child-line out of the top of this node and up to
880 ** the node with an id equal to this value. 0 if it is straight to
881 ** the top of the page, -1 if there is no thick-line riser.
882 ** f: 0x01: a leaf node.
883 ** au: An array of integers that define thick-line risers for branches.
884 ** The integers are in pairs. For each pair, the first integer is
885 ** is the rail on which the riser should run and the second integer
886 ** is the id of the node upto which the riser should run. If there
887 ** are no risers, this array does not exist.
888 ** mi: "merge-in". An array of integer rail positions from which
889 ** merge arrows should be drawn into this node. If the value is
890 ** negative, then the rail position is the absolute value of mi[]
891 ** and a thin merge-arrow descender is drawn to the bottom of
892 ** the screen. This array is omitted if there are no inbound
893 ** merges.
894 ** ci: "cherrypick-in". Like "mi" except for cherrypick merges.
895 ** omitted if there are no cherrypick merges.
896 ** h: The artifact hash of the object being graphed
897 */
898 for(pRow=pGraph->pFirst; pRow; pRow=pRow->pNext){
899 int k = 0;
900 cgi_printf("{\"id\":%d,", pRow->idx);
901 cgi_printf("\"bg\":\"%s\",", pRow->zBgClr);
902 cgi_printf("\"r\":%d,", pRow->iRail);
903 if( pRow->bDescender ){
904 cgi_printf("\"d\":%d,", pRow->bDescender);
905 }
906 if( pRow->mergeOut>=0 ){
907 cgi_printf("\"mo\":%d,", pRow->mergeOut);
908 if( pRow->mergeUpto==0 ) pRow->mergeUpto = pRow->idx;
909 cgi_printf("\"mu\":%d,", pRow->mergeUpto);
910 if( pRow->cherrypickUpto>0 && pRow->cherrypickUpto<pRow->mergeUpto ){
911 cgi_printf("\"cu\":%d,", pRow->cherrypickUpto);
912 }
913 }
914 cgi_printf("\"u\":%d,", pRow->aiRiser[pRow->iRail]);
915 k = 0;
916 if( pRow->isLeaf ) k |= 1;
917 cgi_printf("\"f\":%d,",k);
918 for(i=k=0; i<GR_MAX_RAIL; i++){
919 if( i==pRow->iRail ) continue;
920 if( pRow->aiRiser[i]>0 ){
921 if( k==0 ){
922 cgi_printf("\"au\":");
923 cSep = '[';
924 }
925 k++;
926 cgi_printf("%c%d,%d", cSep, i, pRow->aiRiser[i]);
927 cSep = ',';
928 }
929 }
930 if( k ){
931 cgi_printf("],");
932 }
933 if( colorGraph && pRow->zBgClr[0]=='#' ){
934 cgi_printf("\"fg\":\"%s\",", bg_to_fg(pRow->zBgClr));
935 }
936 /* mi */
937 for(i=k=0; i<GR_MAX_RAIL; i++){
938 if( pRow->mergeIn[i]==1 ){
 
 
939 int mi = i;
940 if( (pRow->mergeDown >> i) & 1 ) mi = -mi;
941 if( k==0 ){
942 cgi_printf("\"mi\":");
943 cSep = '[';
944 }
945 k++;
946 cgi_printf("%c%d", cSep, mi);
947 cSep = ',';
948 }
949 }
950 if( k ) cgi_printf("],");
951 /* ci */
952 for(i=k=0; i<GR_MAX_RAIL; i++){
953 if( pRow->mergeIn[i]==2 ){
954 int mi = i;
955 if( (pRow->cherrypickDown >> i) & 1 ) mi = -mi;
956 if( k==0 ){
957 cgi_printf("\"ci\":");
958 cSep = '[';
959 }
960 k++;
961 cgi_printf("%c%d", cSep, mi);
962 cSep = ',';
963 }
964 }
965 if( k ) cgi_printf("],");
966 cgi_printf("\"h\":\"%!S\"}%s",
967 pRow->zUuid, pRow->pNext ? ",\n" : "]\n");
968 }
969 @ }</script>
970 style_graph_generator();
971 graph_free(pGraph);
@@ -1372,29 +1435,33 @@
1435 ** dp=CHECKIN The same as d=CHECKIN&p=CHECKIN
1436 ** t=TAG Show only check-ins with the given TAG
1437 ** r=TAG Show check-ins related to TAG, equivalent to t=TAG&rel
1438 ** rel Show related check-ins as well as those matching t=TAG
1439 ** mionly Limit rel to show ancestors but not descendants
1440 ** nowiki Do not show wiki associated with branch or tag
1441 ** ms=MATCHSTYLE Set tag match style to EXACT, GLOB, LIKE, REGEXP
1442 ** u=USER Only show items associated with USER
1443 ** y=TYPE 'ci', 'w', 't', 'e', 'f', or 'all'.
1444 ** ss=VIEWSTYLE c: "Compact" v: "Verbose" m: "Modern" j: "Columnar"
1445 ** advm Use the "Advanced" or "Busy" menu design.
1446 ** ng No Graph.
1447 ** ncp Omit cherrypick merges
1448 ** nd Do not highlight the focus check-in
1449 ** v Show details of files changed
1450 ** f=CHECKIN Show family (immediate parents and children) of CHECKIN
1451 ** from=CHECKIN Path from...
1452 ** to=CHECKIN ... to this
1453 ** shorest ... show only the shortest path
1454 ** rel ... also show related checkins
1455 ** uf=FILE_HASH Show only check-ins that contain the given file version
1456 ** chng=GLOBLIST Show only check-ins that involve changes to a file whose
1457 ** name matches one of the comma-separate GLOBLIST
1458 ** brbg Background color from branch name
1459 ** ubg Background color from user
1460 ** namechng Show only check-ins that have filename changes
1461 ** forks Show only forks and their children
1462 ** cherrypicks Show all cherrypicks
1463 ** ym=YYYY-MM Show only events for the given year/month
1464 ** yw=YYYY-WW Show only events for the given week of the given year
1465 ** yw=YYYY-MM-DD Show events for the week that includes the given day
1466 ** ymd=YYYY-MM-DD Show only events on the given day
1467 ** days=N Show events over the previous N days
@@ -1444,10 +1511,11 @@
1511 const char *zChng = P("chng"); /* List of GLOBs for files that changed */
1512 int useDividers = P("nd")==0; /* Show dividers if "nd" is missing */
1513 int renameOnly = P("namechng")!=0; /* Show only check-ins that rename files */
1514 int forkOnly = PB("forks"); /* Show only forks and their children */
1515 int bisectOnly = PB("bisect"); /* Show the check-ins of the bisect */
1516 int cpOnly = PB("cherrypicks"); /* Show all cherrypick checkins */
1517 int tmFlags = 0; /* Timeline flags */
1518 const char *zThisTag = 0; /* Suppress links to this tag */
1519 const char *zThisUser = 0; /* Suppress links to this user */
1520 HQuery url; /* URL for various branch links */
1521 int from_rid = name_to_typed_rid(P("from"),"ci"); /* from= for paths */
@@ -1462,10 +1530,11 @@
1530 char *zNewerButton = 0; /* URL for Newer button at the top */
1531 int selectedRid = -9999999; /* Show a highlight on this RID */
1532 int disableY = 0; /* Disable type selector on submenu */
1533 int advancedMenu = 0; /* Use the advanced menu design */
1534 char *zPlural; /* Ending for plural forms */
1535 int showCherrypicks = 1; /* True to show cherrypick merges */
1536 void (*xExtra)(int) = NULL;
1537
1538 /* Set number of rows to display */
1539 cookie_read_parameter("n","n");
1540 z = P("n");
@@ -1487,10 +1556,16 @@
1556 cgi_replace_query_parameter("n",z);
1557 cookie_write_parameter("n","n",0);
1558 tmFlags |= timeline_ss_submenu();
1559 cookie_link_parameter("advm","advm","0");
1560 advancedMenu = atoi(PD("advm","0"));
1561
1562 /* Omit all cherry-pick merge lines if the "ncp" query parameter is
1563 ** present or if this repository lacks a "cherrypick" table. */
1564 if( PB("ncp") || !db_table_exists("repository","cherrypick") ){
1565 showCherrypicks = 0;
1566 }
1567
1568 /* To view the timeline, must have permission to read project data.
1569 */
1570 pd_rid = name_to_typed_rid(P("dp"),"ci");
1571 if( pd_rid ){
@@ -1521,19 +1596,21 @@
1596 cgi_delete_query_parameter("r");
1597 cgi_set_query_parameter("t", zBrName);
1598 cgi_set_query_parameter("rel", "1");
1599 zTagName = zBrName;
1600 related = 1;
1601 zType = "ci";
1602 }
1603
1604 /* Ignore empty tag query strings. */
1605 if( zTagName && !*zTagName ){
1606 zTagName = 0;
1607 }
1608
1609 /* Finish preliminary processing of tag match queries. */
1610 if( zTagName ){
1611 zType = "ci";
1612 /* Interpet the tag style string. */
1613 if( fossil_stricmp(zMatchStyle, "glob")==0 ){
1614 matchStyle = MS_GLOB;
1615 }else if( fossil_stricmp(zMatchStyle, "like")==0 ){
1616 matchStyle = MS_LIKE;
@@ -1563,16 +1640,19 @@
1640 ){
1641 nEntry = -1;
1642 zCirca = 0;
1643 }
1644 if( zType[0]=='a' ){
1645 tmFlags |= TIMELINE_BRIEF | TIMELINE_GRAPH | TIMELINE_CHPICK;
1646 }else{
1647 tmFlags |= TIMELINE_GRAPH | TIMELINE_CHPICK;
1648 }
1649 if( PB("ncp") ){
1650 tmFlags &= ~TIMELINE_CHPICK;
1651 }
1652 if( PB("ng") || zSearch!=0 ){
1653 tmFlags &= ~(TIMELINE_GRAPH|TIMELINE_CHPICK);
1654 }
1655 if( PB("brbg") ){
1656 tmFlags |= TIMELINE_BRCOLOR;
1657 }
1658 if( PB("unhide") ){
@@ -1658,10 +1738,12 @@
1738 /* If from= and to= are present, display all nodes on a path connecting
1739 ** the two */
1740 PathNode *p = 0;
1741 const char *zFrom = 0;
1742 const char *zTo = 0;
1743 Blob ins;
1744 int nNodeOnPath = 0;
1745
1746 if( from_rid && to_rid ){
1747 p = path_shortest(from_rid, to_rid, noMerge, 0);
1748 zFrom = P("from");
1749 zTo = P("to");
@@ -1670,28 +1752,72 @@
1752 p = path_first();
1753 }
1754 zFrom = P("me");
1755 zTo = P("you");
1756 }
1757 blob_init(&ins, 0, 0);
1758 db_multi_exec(
1759 "CREATE TABLE IF NOT EXISTS temp.pathnode(x INTEGER PRIMARY KEY);"
1760 );
1761 if( p ){
1762 blob_init(&ins, 0, 0);
1763 blob_append_sql(&ins, "INSERT INTO pathnode(x) VALUES(%d)", p->rid);
1764 p = p->u.pTo;
1765 nNodeOnPath = 1;
1766 while( p ){
1767 blob_append_sql(&ins, ",(%d)", p->rid);
1768 p = p->u.pTo;
1769 nNodeOnPath++;
1770 }
1771 }
 
1772 path_reset();
1773 db_multi_exec("%s", blob_str(&ins)/*safe-for-%s*/);
1774 blob_reset(&ins);
1775 if( related ){
1776 db_multi_exec(
1777 "CREATE TABLE IF NOT EXISTS temp.related(x INTEGER PRIMARY KEY);"
1778 "INSERT OR IGNORE INTO related(x)"
1779 " SELECT pid FROM plink WHERE cid IN pathnode AND NOT isprim;"
1780 );
1781 if( P("mionly")==0 ){
1782 db_multi_exec(
1783 "INSERT OR IGNORE INTO related(x)"
1784 " SELECT cid FROM plink WHERE pid IN pathnode;"
1785 );
1786 }
1787 if( showCherrypicks ){
1788 db_multi_exec(
1789 "INSERT OR IGNORE INTO related(x)"
1790 " SELECT parentid FROM cherrypick WHERE childid IN pathnode;"
1791 );
1792 if( P("mionly")==0 ){
1793 db_multi_exec(
1794 "INSERT OR IGNORE INTO related(x)"
1795 " SELECT childid FROM cherrypick WHERE parentid IN pathnode;"
1796 );
1797 }
1798 }
1799 db_multi_exec("INSERT OR IGNORE INTO pathnode SELECT x FROM related");
1800 }
1801 blob_append_sql(&sql, " AND event.objid IN pathnode");
1802 addFileGlobExclusion(zChng, &sql);
1803 tmFlags |= TIMELINE_DISJOINT;
1804 db_multi_exec("%s", blob_sql_text(&sql));
1805 if( advancedMenu ){
1806 style_submenu_checkbox("v", "Files", (zType[0]!='a' && zType[0]!='c'),0);
1807 }
1808 blob_appendf(&desc, "%d check-ins going from ", nNodeOnPath);
 
1809 blob_appendf(&desc, "%z[%h]</a>", href("%R/info/%h", zFrom), zFrom);
1810 blob_append(&desc, " to ", -1);
1811 blob_appendf(&desc, "%z[%h]</a>", href("%R/info/%h",zTo), zTo);
1812 if( related ){
1813 int nRelated = db_int(0, "SELECT count(*) FROM timeline") - nNodeOnPath;
1814 if( nRelated>0 ){
1815 blob_appendf(&desc, " and %d related check-in%s", nRelated,
1816 nRelated>1 ? "s" : "");
1817 }
1818 }
1819 addFileGlobDescription(zChng, &desc);
1820 }else if( (p_rid || d_rid) && g.perm.Read && zTagSql==0 ){
1821 /* If p= or d= is present, ignore all other parameters other than n= */
1822 char *zUuid;
1823 int np, nd;
@@ -1707,19 +1833,19 @@
1833 zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d",
1834 p_rid ? p_rid : d_rid);
1835 blob_append_sql(&sql, " AND event.objid IN ok");
1836 nd = 0;
1837 if( d_rid ){
1838 compute_descendants(d_rid, nEntry==0 ? 0 : nEntry+1);
1839 nd = db_int(0, "SELECT count(*)-1 FROM ok");
1840 if( nd>=0 ) db_multi_exec("%s", blob_sql_text(&sql));
1841 if( nd>0 ) blob_appendf(&desc, "%d descendant%s", nd,(1==nd)?"":"s");
1842 if( useDividers ) selectedRid = d_rid;
1843 db_multi_exec("DELETE FROM ok");
1844 }
1845 if( p_rid ){
1846 compute_ancestors(p_rid, nEntry==0 ? 0 : nEntry+1, 0);
1847 np = db_int(0, "SELECT count(*)-1 FROM ok");
1848 if( np>0 ){
1849 if( nd>0 ) blob_appendf(&desc, " and ");
1850 blob_appendf(&desc, "%d ancestors", np);
1851 db_multi_exec("%s", blob_sql_text(&sql));
@@ -1747,10 +1873,19 @@
1873 "INSERT INTO ok VALUES(%d);"
1874 "INSERT OR IGNORE INTO ok SELECT pid FROM plink WHERE cid=%d;"
1875 "INSERT OR IGNORE INTO ok SELECT cid FROM plink WHERE pid=%d;",
1876 f_rid, f_rid, f_rid
1877 );
1878 if( showCherrypicks ){
1879 db_multi_exec(
1880 "INSERT OR IGNORE INTO ok SELECT parentid FROM cherrypick"
1881 " WHERE childid=%d;"
1882 "INSERT OR IGNORE INTO ok SELECT childid FROM cherrypick"
1883 " WHERE parentid=%d;",
1884 f_rid, f_rid
1885 );
1886 }
1887 blob_append_sql(&sql, " AND event.objid IN ok");
1888 db_multi_exec("%s", blob_sql_text(&sql));
1889 if( useDividers ) selectedRid = f_rid;
1890 blob_appendf(&desc, "Parents and children of check-in ");
1891 zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d", f_rid);
@@ -1778,10 +1913,18 @@
1913 blob_append_sql(&cond, " AND event.objid IN rnfile ");
1914 }
1915 if( forkOnly ){
1916 blob_append_sql(&cond, " AND event.objid IN rnfork ");
1917 }
1918 if( cpOnly && showCherrypicks ){
1919 db_multi_exec(
1920 "CREATE TABLE IF NOT EXISTS cpnodes(rid INTEGER PRIMARY KEY);"
1921 "INSERT OR IGNORE INTO cpnodes SELECT childid FROM cherrypick;"
1922 "INSERT OR IGNORE INTO cpnodes SELECT parentid FROM cherrypick;"
1923 );
1924 blob_append_sql(&cond, " AND event.objid IN cpnodes ");
1925 }
1926 if( bisectOnly ){
1927 blob_append_sql(&cond, " AND event.objid IN (SELECT rid FROM bilog) ");
1928 }
1929 if( zYearMonth ){
1930 blob_append_sql(&cond, " AND %Q=strftime('%%Y-%%m',event.mtime) ",
@@ -1827,49 +1970,65 @@
1970 blob_append_sql(&cond, " AND event.mtime>=julianday('now','-%d days') ",
1971 nDays);
1972 nEntry = -1;
1973 }
1974 if( zTagSql ){
1975 db_multi_exec(
1976 "CREATE TEMP TABLE selected_nodes(rid INTEGER PRIMARY KEY);"
1977 "INSERT OR IGNORE INTO selected_nodes"
1978 " SELECT tagxref.rid FROM tagxref NATURAL JOIN tag"
1979 " WHERE %s AND tagtype>0", zTagSql/*safe-for-%s*/
1980 );
1981 if( !related ){
1982 blob_append_sql(&cond, " AND blob.rid IN selected_nodes");
1983 }else{
1984 db_multi_exec(
1985 "CREATE TEMP TABLE related_nodes(rid INTEGER PRIMARY KEY);"
1986 "INSERT INTO related_nodes SELECT rid FROM selected_nodes;"
1987 );
1988 blob_append_sql(&cond, " AND blob.rid IN related_nodes");
1989 /* The next two blob_appendf() calls add SQL that causes check-ins that
1990 ** are not part of the branch which are parents or children of the
1991 ** branch to be included in the report. These related check-ins are
1992 ** useful in helping to visualize what has happened on a quiescent
1993 ** branch that is infrequently merged with a much more activate branch.
1994 */
1995 db_multi_exec(
1996 "INSERT OR IGNORE INTO related_nodes"
1997 " SELECT pid FROM selected_nodes CROSS JOIN plink"
1998 " WHERE selected_nodes.rid=plink.cid;"
1999 );
2000 if( P("mionly")==0 ){
2001 db_multi_exec(
2002 "INSERT OR IGNORE INTO related_nodes"
2003 " SELECT cid FROM selected_nodes CROSS JOIN plink"
2004 " WHERE selected_nodes.rid=plink.pid;"
2005 );
2006 if( showCherrypicks ){
2007 db_multi_exec(
2008 "INSERT OR IGNORE INTO related_nodes"
2009 " SELECT childid FROM selected_nodes CROSS JOIN cherrypick"
2010 " WHERE selected_nodes.rid=cherrypick.parentid;"
2011 );
2012 }
2013 }
2014 if( showCherrypicks ){
2015 db_multi_exec(
2016 "INSERT OR IGNORE INTO related_nodes"
2017 " SELECT parentid FROM selected_nodes CROSS JOIN cherrypick"
2018 " WHERE selected_nodes.rid=cherrypick.childid;"
2019 );
2020 }
2021 if( (tmFlags & TIMELINE_UNHIDE)==0 ){
2022 db_multi_exec(
2023 "DELETE FROM related_nodes WHERE rid IN "
2024 " (SELECT related_nodes.rid FROM related_nodes, tagxref"
2025 " WHERE tagid=%d AND tagtype>0 AND tagxref.rid=related_nodes.rid)",
2026 TAG_HIDDEN
2027 );
2028 }
2029 }
2030 }
2031 if( (zType[0]=='w' && !g.perm.RdWiki)
2032 || (zType[0]=='t' && !g.perm.RdTkt)
2033 || (zType[0]=='e' && !g.perm.RdWiki)
2034 || (zType[0]=='c' && !g.perm.Read)
@@ -2013,10 +2172,14 @@
2172 }
2173 if( bisectOnly ){
2174 blob_appendf(&desc, " in the most recent bisect");
2175 tmFlags |= TIMELINE_DISJOINT;
2176 }
2177 if( cpOnly && showCherrypicks ){
2178 blob_appendf(&desc, " that participate in a cherrypick merge");
2179 tmFlags |= TIMELINE_CHPICK|TIMELINE_DISJOINT;
2180 }
2181 if( zUser ){
2182 blob_appendf(&desc, " by user %h", zUser);
2183 tmFlags |= TIMELINE_DISJOINT;
2184 }
2185 if( zTagSql ){
@@ -2106,13 +2269,15 @@
2269 }
2270 if( search_restrict(SRCH_CKIN)!=0 ){
2271 style_submenu_element("Search", "%R/search?y=c");
2272 }
2273 if( advancedMenu ){
2274 style_submenu_element("Basic", "%s",
2275 url_render(&url, "advm", "0", "udc", "1"));
2276 }else{
2277 style_submenu_element("Advanced", "%s",
2278 url_render(&url, "advm", "1", "udc", "1"));
2279 }
2280 if( PB("showid") ) tmFlags |= TIMELINE_SHOWRID;
2281 if( useDividers && zMark && zMark[0] ){
2282 double r = symbolic_name_to_mtime(zMark);
2283 if( r>0.0 ) selectedRid = timeline_add_divider(r);
@@ -2120,11 +2285,26 @@
2285 blob_zero(&sql);
2286 db_prepare(&q, "SELECT * FROM timeline ORDER BY sortby DESC /*scan*/");
2287 if( fossil_islower(desc.aData[0]) ){
2288 desc.aData[0] = fossil_toupper(desc.aData[0]);
2289 }
2290 if( zBrName
2291 && !PB("nowiki")
2292 && wiki_render_associated("branch", zBrName, WIKIASSOC_ALL)
2293 ){
2294 @ <div class="section">%b(&desc)</div>
2295 }else
2296 if( zTagName
2297 && matchStyle==MS_EXACT
2298 && zBrName==0
2299 && !PB("nowiki")
2300 && wiki_render_associated("tag", zTagName, WIKIASSOC_ALL)
2301 ){
2302 @ <div class="section">%b(&desc)</div>
2303 } else{
2304 @ <h2>%b(&desc)</h2>
2305 }
2306 blob_reset(&desc);
2307
2308 /* Report any errors. */
2309 if( zError ){
2310 @ <p class="generalError">%h(zError)</p>
2311
+467 -117
--- src/wiki.c
+++ src/wiki.c
@@ -71,10 +71,41 @@
7171
style_footer();
7272
return 1;
7373
}
7474
return 0;
7575
}
76
+
77
+/*
78
+** Return the tagid associated with a particular wiki page.
79
+*/
80
+int wiki_tagid(const char *zPageName){
81
+ return db_int(0, "SELECT tagid FROM tag WHERE tagname='wiki-%q'",zPageName);
82
+}
83
+int wiki_tagid2(const char *zPrefix, const char *zPageName){
84
+ return db_int(0, "SELECT tagid FROM tag WHERE tagname='wiki-%q/%q'",
85
+ zPrefix, zPageName);
86
+}
87
+
88
+/*
89
+** Return the RID of the next or previous version of a wiki page.
90
+** Return 0 if rid is the last/first version.
91
+*/
92
+int wiki_next(int tagid, double mtime){
93
+ return db_int(0,
94
+ "SELECT srcid FROM tagxref"
95
+ " WHERE tagid=%d AND mtime>%.16g"
96
+ " ORDER BY mtime ASC LIMIT 1",
97
+ tagid, mtime);
98
+}
99
+int wiki_prev(int tagid, double mtime){
100
+ return db_int(0,
101
+ "SELECT srcid FROM tagxref"
102
+ " WHERE tagid=%d AND mtime<%.16g"
103
+ " ORDER BY mtime DESC LIMIT 1",
104
+ tagid, mtime);
105
+}
106
+
76107
77108
/*
78109
** WEBPAGE: home
79110
** WEBPAGE: index
80111
** WEBPAGE: not_found
@@ -262,13 +293,10 @@
262293
style_submenu_element("Help", "%R/wikihelp");
263294
}
264295
if( (ok & W_NEW)!=0 && g.anon.NewWiki ){
265296
style_submenu_element("New", "%R/wikinew");
266297
}
267
-#if 0
268
- if( (ok & W_BLOG)!=0
269
-#endif
270298
if( (ok & W_SANDBOX)!=0 ){
271299
style_submenu_element("Sandbox", "%R/wiki?name=Sandbox");
272300
}
273301
}
274302
@@ -281,36 +309,24 @@
281309
if( !g.perm.RdWiki ){ login_needed(g.anon.RdWiki); return; }
282310
style_header("Wiki Help");
283311
wiki_standard_submenu(W_ALL_BUT(W_HELP));
284312
@ <h2>Wiki Links</h2>
285313
@ <ul>
286
- { char *zWikiHomePageName = db_get("index-page",0);
287
- if( zWikiHomePageName ){
288
- @ <li> %z(href("%R%s",zWikiHomePageName))
289
- @ %h(zWikiHomePageName)</a> wiki home page.</li>
290
- }
291
- }
292
- { char *zHomePageName = db_get("project-name",0);
293
- if( zHomePageName ){
294
- @ <li> %z(href("%R/wiki?name=%t",zHomePageName))
295
- @ %h(zHomePageName)</a> project home page.</li>
296
- }
297
- }
298314
@ <li> %z(href("%R/timeline?y=w"))Recent changes</a> to wiki pages.</li>
299315
@ <li> Formatting rules for %z(href("%R/wiki_rules"))Fossil Wiki</a> and for
300316
@ %z(href("%R/md_rules"))Markdown Wiki</a>.</li>
301317
@ <li> Use the %z(href("%R/wiki?name=Sandbox"))Sandbox</a>
302318
@ to experiment.</li>
303
- if( g.anon.NewWiki ){
319
+ if( g.perm.NewWiki ){
304320
@ <li> Create a %z(href("%R/wikinew"))new wiki page</a>.</li>
305
- if( g.anon.Write ){
321
+ if( g.perm.Write ){
306322
@ <li> Create a %z(href("%R/technoteedit"))new tech-note</a>.</li>
307323
}
308324
}
309325
@ <li> %z(href("%R/wcontent"))List of All Wiki Pages</a>
310326
@ available on this server.</li>
311
- if( g.anon.ModWiki ){
327
+ if( g.perm.ModWiki ){
312328
@ <li> %z(href("%R/modreq"))Tend to pending moderation requests</a></li>
313329
}
314330
if( search_restrict(SRCH_WIKI)!=0 ){
315331
@ <li> %z(href("%R/wikisrch"))Search</a> for wiki pages containing key
316332
@ words</li>
@@ -331,21 +347,106 @@
331347
style_header("Wiki Search");
332348
wiki_standard_submenu(W_HELP|W_LIST|W_SANDBOX);
333349
search_screen(SRCH_WIKI, 0);
334350
style_footer();
335351
}
352
+
353
+/* Return values from wiki_page_type() */
354
+#define WIKITYPE_UNKNOWN (-1)
355
+#define WIKITYPE_NORMAL 0
356
+#define WIKITYPE_BRANCH 1
357
+#define WIKITYPE_CHECKIN 2
358
+#define WIKITYPE_TAG 3
359
+
360
+/*
361
+** Figure out what type of wiki page we are dealing with.
362
+*/
363
+static int wiki_page_type(const char *zPageName){
364
+ if( db_get_boolean("wiki-about",1)==0 ){
365
+ return WIKITYPE_NORMAL;
366
+ }else
367
+ if( sqlite3_strglob("checkin/*", zPageName)==0
368
+ && db_exists("SELECT 1 FROM blob WHERE uuid=%Q",zPageName+8)
369
+ ){
370
+ return WIKITYPE_CHECKIN;
371
+ }else
372
+ if( sqlite3_strglob("branch/*", zPageName)==0 ){
373
+ return WIKITYPE_BRANCH;
374
+ }else
375
+ if( sqlite3_strglob("tag/*", zPageName)==0 ){
376
+ return WIKITYPE_TAG;
377
+ }
378
+ return WIKITYPE_NORMAL;
379
+}
380
+
381
+/*
382
+** Add an appropriate style_header() for either the /wiki or /wikiedit page
383
+** for zPageName.
384
+*/
385
+static int wiki_page_header(
386
+ int eType, /* Page type. -1 for unknown */
387
+ const char *zPageName, /* Name of the page */
388
+ const char *zExtra /* Extra prefix text on the page header */
389
+){
390
+ if( eType<0 ) eType = wiki_page_type(zPageName);
391
+ switch( eType ){
392
+ case WIKITYPE_NORMAL: {
393
+ style_header("%s%s", zExtra, zPageName);
394
+ break;
395
+ }
396
+ case WIKITYPE_CHECKIN: {
397
+ zPageName += 8;
398
+ style_header("Notes About Checkin %S", zPageName);
399
+ style_submenu_element("Checkin Timeline","%R/timeline?f=%s", zPageName);
400
+ style_submenu_element("Checkin Info","%R/info/%s", zPageName);
401
+ break;
402
+ }
403
+ case WIKITYPE_BRANCH: {
404
+ zPageName += 7;
405
+ style_header("Notes About Branch %h", zPageName);
406
+ style_submenu_element("Branch Timeline","%R/timeline?r=%t", zPageName);
407
+ break;
408
+ }
409
+ case WIKITYPE_TAG: {
410
+ zPageName += 4;
411
+ style_header("Notes About Tag %h", zPageName);
412
+ style_submenu_element("Tag Timeline","%R/timeline?t=%t",zPageName);
413
+ break;
414
+ }
415
+ }
416
+ return eType;
417
+}
418
+
419
+/*
420
+** Wiki pages with special names "branch/...", "checkin/...", and "tag/..."
421
+** requires perm.Write privilege in addition to perm.WrWiki in order
422
+** to write. This function determines whether the extra perm.Write
423
+** is required and available. Return true if writing to the wiki page
424
+** may proceed, and return false if permission is lacking.
425
+*/
426
+static int wiki_special_permission(const char *zPageName){
427
+ if( strncmp(zPageName,"branch/",7)!=0
428
+ && strncmp(zPageName,"checkin/",8)!=0
429
+ && strncmp(zPageName,"tag/",4)!=0
430
+ ){
431
+ return 1;
432
+ }
433
+ if( db_get_boolean("wiki-about",1)==0 ){
434
+ return 1;
435
+ }
436
+ return g.perm.Write;
437
+}
336438
337439
/*
338440
** WEBPAGE: wiki
339441
** URL: /wiki?name=PAGENAME
340442
*/
341443
void wiki_page(void){
342444
char *zTag;
343445
int rid = 0;
344446
int isSandbox;
345
- char *zUuid;
346
- unsigned submenuFlags = W_ALL;
447
+ unsigned submenuFlags = W_HELP;
347448
Blob wiki;
348449
Manifest *pWiki = 0;
349450
const char *zPageName;
350451
const char *zMimetype = 0;
351452
char *zBody = mprintf("%s","<i>Empty Page</i>");
@@ -385,43 +486,35 @@
385486
zMimetype = pWiki->zMimetype;
386487
}
387488
}
388489
zMimetype = wiki_filter_mimetypes(zMimetype);
389490
if( !g.isHome ){
390
- if( rid ){
391
- style_submenu_element("Diff", "%R/wdiff?name=%T&a=%d", zPageName, rid);
392
- zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid);
393
- style_submenu_element("Details", "%R/info/%s", zUuid);
394
- }
395
- if( (rid && g.anon.WrWiki) || (!rid && g.anon.NewWiki) ){
491
+ if( ((rid && g.perm.WrWiki) || (!rid && g.perm.NewWiki))
492
+ && wiki_special_permission(zPageName)
493
+ ){
396494
if( db_get_boolean("wysiwyg-wiki", 0) ){
397495
style_submenu_element("Edit", "%s/wikiedit?name=%T&wysiwyg=1",
398496
g.zTop, zPageName);
399497
}else{
400498
style_submenu_element("Edit", "%s/wikiedit?name=%T", g.zTop, zPageName);
401499
}
402500
}
403
- if( rid && g.anon.ApndWiki && g.anon.Attach ){
404
- style_submenu_element("Attach",
405
- "%s/attachadd?page=%T&from=%s/wiki%%3fname=%T",
406
- g.zTop, zPageName, g.zTop, zPageName);
407
- }
408
- if( rid && g.anon.ApndWiki ){
409
- style_submenu_element("Append", "%s/wikiappend?name=%T&mimetype=%s",
410
- g.zTop, zPageName, zMimetype);
411
- }
412501
if( g.perm.Hyperlink ){
413502
style_submenu_element("History", "%s/whistory?name=%T",
414503
g.zTop, zPageName);
415504
}
416505
}
417506
style_set_current_page("%T?name=%T", g.zPath, zPageName);
418
- style_header("%s", zPageName);
507
+ wiki_page_header(WIKITYPE_UNKNOWN, zPageName, "");
419508
wiki_standard_submenu(submenuFlags);
420
- blob_init(&wiki, zBody, -1);
421
- wiki_render_by_mimetype(&wiki, zMimetype);
422
- blob_reset(&wiki);
509
+ if( zBody[0]==0 ){
510
+ @ <i>This page has been deleted</i>
511
+ }else{
512
+ blob_init(&wiki, zBody, -1);
513
+ wiki_render_by_mimetype(&wiki, zMimetype);
514
+ blob_reset(&wiki);
515
+ }
423516
attachment_list(zPageName, "<hr /><h2>Attachments:</h2><ul>");
424517
manifest_destroy(pWiki);
425518
style_footer();
426519
}
427520
@@ -491,10 +584,12 @@
491584
const char *z;
492585
char *zBody = (char*)P("w");
493586
const char *zMimetype = wiki_filter_mimetypes(P("mimetype"));
494587
int isWysiwyg = P("wysiwyg")!=0;
495588
int goodCaptcha = 1;
589
+ int eType = WIKITYPE_UNKNOWN;
590
+ int havePreview = 0;
496591
497592
if( P("edit-wysiwyg")!=0 ){ isWysiwyg = 1; zBody = 0; }
498593
if( P("edit-markup")!=0 ){ isWysiwyg = 0; zBody = 0; }
499594
if( zBody ){
500595
if( isWysiwyg ){
@@ -525,10 +620,14 @@
525620
"SELECT rid FROM tagxref"
526621
" WHERE tagid=(SELECT tagid FROM tag WHERE tagname=%Q)"
527622
" ORDER BY mtime DESC", zTag
528623
);
529624
free(zTag);
625
+ if( !wiki_special_permission(zPageName) ){
626
+ login_needed(0);
627
+ return;
628
+ }
530629
if( (rid && !g.perm.WrWiki) || (!rid && !g.perm.NewWiki) ){
531630
login_needed(rid ? g.anon.WrWiki : g.anon.NewWiki);
532631
return;
533632
}
534633
if( zBody==0 && (pWiki = manifest_get(rid, CFTYPE_WIKI, 0))!=0 ){
@@ -575,20 +674,30 @@
575674
if( P("cancel")!=0 ){
576675
cgi_redirectf("wiki?name=%T", zPageName);
577676
return;
578677
}
579678
if( zBody==0 ){
580
- zBody = mprintf("<i>Empty Page</i>");
679
+ zBody = mprintf("");
581680
}
582681
style_set_current_page("%T?name=%T", g.zPath, zPageName);
583
- style_header("Edit: %s", zPageName);
682
+ eType = wiki_page_header(WIKITYPE_UNKNOWN, zPageName, "Edit: ");
683
+ if( rid && !isSandbox && g.perm.ApndWiki ){
684
+ if( g.perm.Attach ){
685
+ style_submenu_element("Attach",
686
+ "%s/attachadd?page=%T&from=%s/wiki%%3fname=%T",
687
+ g.zTop, zPageName, g.zTop, zPageName);
688
+ }
689
+ style_submenu_element("Append", "%s/wikiappend?name=%T&mimetype=%s",
690
+ g.zTop, zPageName, zMimetype);
691
+ }
584692
if( !goodCaptcha ){
585693
@ <p class="generalError">Error: Incorrect security code.</p>
586694
}
587695
blob_zero(&wiki);
588696
blob_append(&wiki, zBody, -1);
589
- if( P("preview")!=0 ){
697
+ if( P("preview")!=0 && zBody[0] ){
698
+ havePreview = 1;
590699
@ Preview:<hr />
591700
wiki_render_by_mimetype(&wiki, zMimetype);
592701
@ <hr />
593702
blob_reset(&wiki);
594703
}
@@ -597,23 +706,45 @@
597706
}
598707
if( n<20 ) n = 20;
599708
if( n>30 ) n = 30;
600709
if( !isWysiwyg ){
601710
/* Traditional markup-only editing */
711
+ char *zPlaceholder = 0;
712
+ switch( eType ){
713
+ case WIKITYPE_NORMAL: {
714
+ zPlaceholder = mprintf("Enter text for wiki page %s", zPageName);
715
+ break;
716
+ }
717
+ case WIKITYPE_BRANCH: {
718
+ zPlaceholder = mprintf("Enter notes about branch %s", zPageName+7);
719
+ break;
720
+ }
721
+ case WIKITYPE_CHECKIN: {
722
+ zPlaceholder = mprintf("Enter notes about check-in %.20s", zPageName+8);
723
+ break;
724
+ }
725
+ case WIKITYPE_TAG: {
726
+ zPlaceholder = mprintf("Enter notes about tag %s", zPageName+4);
727
+ break;
728
+ }
729
+ }
602730
form_begin(0, "%R/wikiedit");
603731
@ <div>Markup style:
604732
mimetype_option_menu(zMimetype);
605
- @ <br /><textarea name="w" class="wikiedit" cols="80"
606
- @ rows="%d(n)" wrap="virtual">%h(zBody)</textarea>
733
+ @ <br /><textarea name="w" class="wikiedit" cols="80" \
734
+ @ rows="%d(n)" wrap="virtual" placeholder="%h(zPlaceholder)">\
735
+ @ %h(zBody)</textarea>
607736
@ <br />
737
+ fossil_free(zPlaceholder);
608738
if( db_get_boolean("wysiwyg-wiki", 0) ){
609739
@ <input type="submit" name="edit-wysiwyg" value="Wysiwyg Editor" />
610740
}
611741
@ <input type="submit" name="preview" value="Preview Your Changes" />
612742
}else{
613743
/* Wysiwyg editing */
614744
Blob html, temp;
745
+ havePreview = 1;
615746
form_begin("", "%R/wikiedit");
616747
@ <div>
617748
@ <input type="hidden" name="wysiwyg" value="1" />
618749
blob_zero(&temp);
619750
wiki_convert(&wiki, &temp, 0);
@@ -624,11 +755,13 @@
624755
blob_reset(&html);
625756
@ <br />
626757
@ <input type="submit" name="edit-markup" value="Markup Editor" />
627758
}
628759
login_insert_csrf_secret();
629
- @ <input type="submit" name="submit" value="Apply These Changes" />
760
+ if( havePreview ){
761
+ @ <input type="submit" name="submit" value="Apply These Changes" />
762
+ }
630763
@ <input type="hidden" name="name" value="%h(zPageName)" />
631764
@ <input type="submit" name="cancel" value="Cancel" />
632765
@ </div>
633766
@ <script nonce="%h(style_nonce())">
634767
@ confirmOnClick("edit-wysiwyg", "Switching to WYSIWYG-mode\nwill erase your markup edits.\n\nContinue?");
@@ -851,90 +984,143 @@
851984
captcha_generate(0);
852985
@ </form>
853986
style_footer();
854987
}
855988
856
-/*
857
-** Name of the wiki history page being generated
858
-*/
859
-static const char *zWikiPageName;
860
-
861
-/*
862
-** Function called to output extra text at the end of each line in
863
-** a wiki history listing.
864
-*/
865
-static void wiki_history_extra(int rid){
866
- if( g.perm.Hyperlink && db_exists("SELECT 1 FROM tagxref WHERE rid=%d", rid) ){
867
- @ %z(href("%R/wdiff?name=%t&a=%d",zWikiPageName,rid))[diff]</a>
868
- }
869
-}
870
-
871989
/*
872990
** WEBPAGE: whistory
873991
** URL: /whistory?name=PAGENAME
874992
**
993
+** Additional parameters:
994
+**
995
+** showid Show RID values
996
+**
875997
** Show the complete change history for a single wiki page.
876998
*/
877999
void whistory_page(void){
8781000
Stmt q;
8791001
const char *zPageName;
1002
+ double rNow;
1003
+ int showRid;
8801004
login_check_credentials();
8811005
if( !g.perm.Hyperlink ){ login_needed(g.anon.Hyperlink); return; }
8821006
zPageName = PD("name","");
8831007
style_header("History Of %s", zPageName);
884
-
885
- db_prepare(&q, "%s AND event.objid IN "
886
- " (SELECT rid FROM tagxref WHERE tagid="
887
- "(SELECT tagid FROM tag WHERE tagname='wiki-%q')"
888
- " UNION SELECT attachid FROM attachment"
889
- " WHERE target=%Q)"
890
- "ORDER BY mtime DESC",
891
- timeline_query_for_www(), zPageName, zPageName);
892
- zWikiPageName = zPageName;
893
- www_print_timeline(&q, TIMELINE_ARTID, 0, 0, 0, wiki_history_extra);
1008
+ showRid = P("showid")!=0;
1009
+ db_prepare(&q,
1010
+ "SELECT"
1011
+ " event.mtime,"
1012
+ " blob.uuid,"
1013
+ " coalesce(event.euser,event.user),"
1014
+ " event.objid"
1015
+ " FROM event, blob, tag, tagxref"
1016
+ " WHERE event.type='w' AND blob.rid=event.objid"
1017
+ " AND tag.tagname='wiki-%q'"
1018
+ " AND tagxref.tagid=tag.tagid AND tagxref.srcid=event.objid"
1019
+ " ORDER BY event.mtime DESC",
1020
+ zPageName
1021
+ );
1022
+ @ <h2>History of <a href="%R/wiki?name=%T(zPageName)">%h(zPageName)</a></h2>
1023
+ @ <div class="brlist">
1024
+ @ <table>
1025
+ @ <thead><tr>
1026
+ @ <th>Age</th>
1027
+ @ <th>Hash</th>
1028
+ @ <th>User</th>
1029
+ if( showRid ){
1030
+ @ <th>RID</th>
1031
+ }
1032
+ @ <th>&nbsp;</th>
1033
+ @ </tr></thead><tbody>
1034
+ rNow = db_double(0.0, "SELECT julianday('now')");
1035
+ while( db_step(&q)==SQLITE_ROW ){
1036
+ double rMtime = db_column_double(&q, 0);
1037
+ const char *zUuid = db_column_text(&q, 1);
1038
+ const char *zUser = db_column_text(&q, 2);
1039
+ int wrid = db_column_int(&q, 3);
1040
+ /* sqlite3_int64 iMtime = (sqlite3_int64)(rMtime*86400.0); */
1041
+ char *zAge = human_readable_age(rNow - rMtime);
1042
+ @ <tr>
1043
+ /* @ <td data-sortkey="%016llx(iMtime)">%s(zAge)</td> */
1044
+ @ <td>%s(zAge)</td>
1045
+ fossil_free(zAge);
1046
+ @ <td>%z(href("%R/info/%s",zUuid))%S(zUuid)</a></td>
1047
+ @ <td>%h(zUser)</td>
1048
+ if( showRid ){
1049
+ @ <td>%z(href("%R/artifact/%S",zUuid))%d(wrid)</a></td>
1050
+ }
1051
+ @ <td>%z(href("%R/wdiff?id=%S",zUuid))diff</a></td>
1052
+ @ </tr>
1053
+ }
1054
+ @ </tbody></table></div>
8941055
db_finalize(&q);
1056
+ /* style_table_sorter(); */
8951057
style_footer();
8961058
}
8971059
8981060
/*
8991061
** WEBPAGE: wdiff
900
-** URL: /wdiff?name=PAGENAME&a=RID1&b=RID2&diff=DIFFTYPE
1062
+**
1063
+** Show the changes to a wiki page.
1064
+**
1065
+** Query parameters:
1066
+**
1067
+** id=HASH Hash prefix for the child version to be diffed.
1068
+** rid=INTEGER RecordID for the child version
1069
+** pid=HASH Hash prefix for the parent.
9011070
**
902
-** Show the difference between two wiki pages.
1071
+** The "id" query parameter is required. "pid" is optional. If "pid"
1072
+** is omitted, then the diff is against the first parent of the child.
9031073
*/
9041074
void wdiff_page(void){
905
- int rid1, rid2;
906
- const char *zPageName;
1075
+ const char *zId;
1076
+ const char *zPid;
9071077
Manifest *pW1, *pW2 = 0;
1078
+ int rid1, rid2, nextRid;
9081079
Blob w1, w2, d;
9091080
u64 diffFlags;
9101081
int diffType; /* 0: none, 1: unified, 2: side-by-side */
9111082
9121083
login_check_credentials();
913
- rid1 = atoi(PD("a","0"));
914
- if( !g.perm.Hyperlink ){ login_needed(g.anon.Hyperlink); return; }
915
- if( rid1==0 ) fossil_redirect_home();
916
- rid2 = atoi(PD("b","0"));
917
- zPageName = PD("name","");
918
- style_header("Changes To %s", zPageName);
919
-
920
- if( rid2==0 ){
921
- rid2 = db_int(0,
922
- "SELECT objid FROM event JOIN tagxref ON objid=rid AND tagxref.tagid="
923
- "(SELECT tagid FROM tag WHERE tagname='wiki-%q')"
924
- " WHERE event.mtime<(SELECT mtime FROM event WHERE objid=%d)"
925
- " ORDER BY event.mtime DESC LIMIT 1",
926
- zPageName, rid1
927
- );
928
- }
1084
+ if( !g.perm.RdWiki ){ login_needed(g.anon.RdWiki); return; }
1085
+ zId = P("id");
1086
+ if( zId==0 ){
1087
+ rid1 = atoi(PD("rid","0"));
1088
+ }else{
1089
+ rid1 = name_to_typed_rid(zId, "w");
1090
+ }
1091
+ zId = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid1);
9291092
pW1 = manifest_get(rid1, CFTYPE_WIKI, 0);
9301093
if( pW1==0 ) fossil_redirect_home();
9311094
blob_init(&w1, pW1->zWiki, -1);
932
- blob_zero(&w2);
933
- if( rid2 && (pW2 = manifest_get(rid2, CFTYPE_WIKI, 0))!=0 ){
1095
+ zPid = P("pid");
1096
+ if( zPid==0 && pW1->nParent ){
1097
+ zPid = pW1->azParent[0];
1098
+ }
1099
+ if( zPid ){
1100
+ char *zDate;
1101
+ rid2 = name_to_typed_rid(zPid, "w");
1102
+ pW2 = manifest_get(rid2, CFTYPE_WIKI, 0);
9341103
blob_init(&w2, pW2->zWiki, -1);
1104
+ @ <h2>Changes to \
1105
+ @ "%z(href("%R/whistory?name=%s",pW1->zWikiTitle))%h(pW1->zWikiTitle)</a>" \
1106
+ zDate = db_text(0, "SELECT datetime(%.16g)",pW2->rDate);
1107
+ @ between %z(href("%R/info/%s",zPid))%z(zDate)</a> \
1108
+ zDate = db_text(0, "SELECT datetime(%.16g)",pW1->rDate);
1109
+ @ and %z(href("%R/info/%s",zId))%z(zDate)</a></h2>
1110
+ style_submenu_element("Previous", "%R/wdiff?id=%S", zPid);
1111
+ }else{
1112
+ blob_zero(&w2);
1113
+ @ <h2>Initial version of \
1114
+ @ "%z(href("%R/whistory?name=%s",pW1->zWikiTitle))%h(pW1->zWikiTitle)</a>"\
1115
+ @ </h2>
1116
+ }
1117
+ nextRid = wiki_next(wiki_tagid(pW1->zWikiTitle),pW1->rDate);
1118
+ if( nextRid ){
1119
+ style_submenu_element("Next", "%R/wdiff?rid=%d", nextRid);
9351120
}
1121
+ style_header("Changes To %s", pW1->zWikiTitle);
9361122
blob_zero(&d);
9371123
diffFlags = construct_diff_flags(1);
9381124
diffType = atoi( PD("diff", "2") );
9391125
if( diffType==2 ){
9401126
diffFlags |= DIFF_SIDEBYSIDE;
@@ -959,37 +1145,50 @@
9591145
manifest_destroy(pW2);
9601146
style_footer();
9611147
}
9621148
9631149
/*
964
-** prepare()s pStmt with a query requesting:
1150
+** A query that returns information about all wiki pages.
9651151
**
966
-** - wiki page name
967
-** - tagxref (whatever that really is!)
1152
+** wname Name of the wiki page
1153
+** wsort Sort names by this label
1154
+** wrid rid of the most recent version of the page
1155
+** wmtime time most recent version was created
1156
+** wcnt Number of versions of this wiki page
9681157
**
969
-** Used by wcontent_page() and the JSON wiki code.
1158
+** The wrid value is zero for deleted wiki pages.
9701159
*/
971
-void wiki_prepare_page_list( Stmt * pStmt ){
972
- db_prepare(pStmt,
973
- "SELECT"
974
- " substr(tagname, 6) as name,"
975
- " (SELECT value FROM tagxref WHERE tagid=tag.tagid"
976
- " ORDER BY mtime DESC) as tagXref"
977
- " FROM tag WHERE tagname GLOB 'wiki-*'"
978
- " ORDER BY lower(tagname) /*sort*/"
979
- );
980
-}
1160
+static const char listAllWikiPages[] =
1161
+@ SELECT
1162
+@ substr(tag.tagname, 6) AS wname,
1163
+@ lower(substr(tag.tagname, 6)) AS sortname,
1164
+@ tagxref.value+0 AS wrid,
1165
+@ max(tagxref.mtime) AS wmtime,
1166
+@ count(*) AS wcnt
1167
+@ FROM
1168
+@ tag,
1169
+@ tagxref
1170
+@ WHERE
1171
+@ tag.tagname GLOB 'wiki-*'
1172
+@ AND tagxref.tagid=tag.tagid
1173
+@ GROUP BY 1
1174
+@ ORDER BY 2;
1175
+;
1176
+
9811177
/*
9821178
** WEBPAGE: wcontent
9831179
**
9841180
** all=1 Show deleted pages
1181
+** showid Show rid values for each page.
9851182
**
9861183
** List all available wiki pages with date created and last modified.
9871184
*/
9881185
void wcontent_page(void){
9891186
Stmt q;
1187
+ double rNow;
9901188
int showAll = P("all")!=0;
1189
+ int showRid = P("showid")!=0;
9911190
9921191
login_check_credentials();
9931192
if( !g.perm.RdWiki ){ login_needed(g.anon.RdWiki); return; }
9941193
style_header("Available Wiki Pages");
9951194
if( showAll ){
@@ -996,23 +1195,58 @@
9961195
style_submenu_element("Active", "%s/wcontent", g.zTop);
9971196
}else{
9981197
style_submenu_element("All", "%s/wcontent?all=1", g.zTop);
9991198
}
10001199
wiki_standard_submenu(W_ALL_BUT(W_LIST));
1001
- @ <ul>
1002
- wiki_prepare_page_list(&q);
1200
+ db_prepare(&q, listAllWikiPages/*works-like:""*/);
1201
+ @ <div class="brlist">
1202
+ @ <table class='sortable' data-column-types='tKN' data-init-sort='1'>
1203
+ @ <thead><tr>
1204
+ @ <th>Name</th>
1205
+ @ <th>Last Change</th>
1206
+ @ <th>Versions</th>
1207
+ if( showRid ){
1208
+ @ <th>RID</th>
1209
+ }
1210
+ @ </tr></thead><tbody>
1211
+ rNow = db_double(0.0, "SELECT julianday('now')");
10031212
while( db_step(&q)==SQLITE_ROW ){
1004
- const char *zName = db_column_text(&q, 0);
1005
- int size = db_column_int(&q, 1);
1006
- if( size>0 ){
1007
- @ <li>%z(href("%R/wiki?name=%T",zName))%h(zName)</a></li>
1008
- }else if( showAll ){
1009
- @ <li>%z(href("%R/wiki?name=%T",zName))<s>%h(zName)</s></a></li>
1010
- }
1011
- }
1213
+ const char *zWName = db_column_text(&q, 0);
1214
+ const char *zSort = db_column_text(&q, 1);
1215
+ int wrid = db_column_int(&q, 2);
1216
+ double rWmtime = db_column_double(&q, 3);
1217
+ sqlite3_int64 iMtime = (sqlite3_int64)(rWmtime*86400.0);
1218
+ char *zAge;
1219
+ int wcnt = db_column_int(&q, 4);
1220
+ char *zWDisplayName;
1221
+
1222
+ if( sqlite3_strglob("checkin/*", zWName)==0 ){
1223
+ zWDisplayName = mprintf("%.25s...", zWName);
1224
+ }else{
1225
+ zWDisplayName = mprintf("%s", zWName);
1226
+ }
1227
+ if( wrid==0 ){
1228
+ if( !showAll ) continue;
1229
+ @ <tr><td data-sortkey="%h(zSort)">\
1230
+ @ %z(href("%R/whistory?name=%T",zWName))<s>%h(zWDisplayName)</s></a></td>
1231
+ }else{
1232
+ @ <tr><td data=sortkey='%h(zSort)">\
1233
+ @ %z(href("%R/wiki?name=%T",zWName))%h(zWDisplayName)</a></td>
1234
+ }
1235
+ zAge = human_readable_age(rNow - rWmtime);
1236
+ @ <td data-sortkey="%016llx(iMtime)">%s(zAge)</td>
1237
+ fossil_free(zAge);
1238
+ @ <td>%z(href("%R/whistory?name=%T",zWName))%d(wcnt)</a></td>
1239
+ if( showRid ){
1240
+ @ <td>%d(wrid)</td>
1241
+ }
1242
+ @ </tr>
1243
+ fossil_free(zWDisplayName);
1244
+ }
1245
+ @ </tbody></table></div>
10121246
db_finalize(&q);
1013
- @ </ul>
1247
+ style_table_sorter();
10141248
style_footer();
10151249
}
10161250
10171251
/*
10181252
** WEBPAGE: wfind
@@ -1405,5 +1639,121 @@
14051639
blob_zero(&out);
14061640
blob_read_from_file(&in, g.argv[2], ExtFILE);
14071641
markdown_to_html(&in, 0, &out);
14081642
blob_write_to_file(&out, "-");
14091643
}
1644
+
1645
+/*
1646
+** Allowed flags for wiki_render_associated
1647
+*/
1648
+#if INTERFACE
1649
+#define WIKIASSOC_FULL_TITLE 0x00001 /* Full title */
1650
+#define WIKIASSOC_MENU_READ 0x00002 /* Add submenu link to read wiki */
1651
+#define WIKIASSOC_MENU_WRITE 0x00004 /* Add submenu link to add wiki */
1652
+#define WIKIASSOC_ALL 0x00007 /* All of the above */
1653
+#endif
1654
+
1655
+/*
1656
+** Show the default Section label for an associated wiki page.
1657
+*/
1658
+static void wiki_section_label(
1659
+ const char *zPrefix, /* "branch", "tag", or "checkin" */
1660
+ const char *zName, /* Name of the object */
1661
+ unsigned int mFlags /* Zero or more WIKIASSOC_* flags */
1662
+){
1663
+ if( (mFlags & WIKIASSOC_FULL_TITLE)==0 ){
1664
+ @ <div class="section">About</div>
1665
+ }else if( zPrefix[0]=='c' ){ /* checkin/... */
1666
+ @ <div class="section">About checkin %.20h(zName)</div>
1667
+ }else{
1668
+ @ <div class="section">About %s(zPrefix) %h(zName)</div>
1669
+ }
1670
+}
1671
+
1672
+/*
1673
+** Add an "Wiki" button in a submenu that links to the read-wiki page.
1674
+*/
1675
+static void wiki_submenu_to_read_wiki(
1676
+ const char *zPrefix, /* "branch", "tag", or "checkin" */
1677
+ const char *zName, /* Name of the object */
1678
+ unsigned int mFlags /* Zero or more WIKIASSOC_* flags */
1679
+){
1680
+ if( g.perm.RdWiki && (mFlags & WIKIASSOC_MENU_READ)!=0 ){
1681
+ style_submenu_element("Wiki", "%R/wiki?name=%s/%t", zPrefix, zName);
1682
+ }
1683
+}
1684
+
1685
+/*
1686
+** Check to see if there exists a wiki page with a name zPrefix/zName.
1687
+** If there is, then render a <div class='section'>..</div> and
1688
+** return true.
1689
+**
1690
+** If there is no such wiki page, return false.
1691
+*/
1692
+int wiki_render_associated(
1693
+ const char *zPrefix, /* "branch", "tag", or "checkin" */
1694
+ const char *zName, /* Name of the object */
1695
+ unsigned int mFlags /* Zero or more WIKIASSOC_* flags */
1696
+){
1697
+ int rid;
1698
+ Manifest *pWiki;
1699
+ if( !db_get_boolean("wiki-about",1) ) return 0;
1700
+ rid = db_int(0,
1701
+ "SELECT rid FROM tagxref"
1702
+ " WHERE tagid=(SELECT tagid FROM tag WHERE tagname='wiki-%q/%q')"
1703
+ " ORDER BY mtime DESC LIMIT 1",
1704
+ zPrefix, zName
1705
+ );
1706
+ if( rid==0 ){
1707
+ if( g.perm.WrWiki && g.perm.Write && (mFlags & WIKIASSOC_MENU_WRITE)!=0 ){
1708
+ style_submenu_element("Add Wiki", "%R/wikiedit?name=%s/%t",
1709
+ zPrefix, zName);
1710
+ }
1711
+ }
1712
+ pWiki = manifest_get(rid, CFTYPE_WIKI, 0);
1713
+ if( pWiki==0 ) return 0;
1714
+ if( fossil_strcmp(pWiki->zMimetype, "text/x-markdown")==0 ){
1715
+ Blob tail = BLOB_INITIALIZER;
1716
+ Blob title = BLOB_INITIALIZER;
1717
+ Blob markdown;
1718
+ blob_init(&markdown, pWiki->zWiki, -1);
1719
+ markdown_to_html(&markdown, &title, &tail);
1720
+ if( blob_size(&title) ){
1721
+ @ <div class="section">%h(blob_str(&title))</div>
1722
+ }else{
1723
+ wiki_section_label(zPrefix, zName, mFlags);
1724
+ }
1725
+ wiki_submenu_to_read_wiki(zPrefix, zName, mFlags);
1726
+ convert_href_and_output(&tail);
1727
+ blob_reset(&tail);
1728
+ blob_reset(&title);
1729
+ blob_reset(&markdown);
1730
+ }else if( fossil_strcmp(pWiki->zMimetype, "text/plain")==0 ){
1731
+ wiki_section_label(zPrefix, zName, mFlags);
1732
+ wiki_submenu_to_read_wiki(zPrefix, zName, mFlags);
1733
+ @ <pre>
1734
+ @ %h(pWiki->zWiki)
1735
+ @ </pre>
1736
+ }else{
1737
+ Blob tail = BLOB_INITIALIZER;
1738
+ Blob title = BLOB_INITIALIZER;
1739
+ Blob wiki;
1740
+ Blob *pBody;
1741
+ blob_init(&wiki, pWiki->zWiki, -1);
1742
+ if( wiki_find_title(&wiki, &title, &tail) ){
1743
+ @ <div class="section">%h(blob_str(&title))</div>
1744
+ pBody = &tail;
1745
+ }else{
1746
+ wiki_section_label(zPrefix, zName, mFlags);
1747
+ pBody = &wiki;
1748
+ }
1749
+ wiki_submenu_to_read_wiki(zPrefix, zName, mFlags);
1750
+ @ <div class="wiki">
1751
+ wiki_convert(pBody, 0, WIKI_BUTTONS);
1752
+ @ </div>
1753
+ blob_reset(&tail);
1754
+ blob_reset(&title);
1755
+ blob_reset(&wiki);
1756
+ }
1757
+ manifest_destroy(pWiki);
1758
+ return 1;
1759
+}
14101760
--- src/wiki.c
+++ src/wiki.c
@@ -71,10 +71,41 @@
71 style_footer();
72 return 1;
73 }
74 return 0;
75 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
77 /*
78 ** WEBPAGE: home
79 ** WEBPAGE: index
80 ** WEBPAGE: not_found
@@ -262,13 +293,10 @@
262 style_submenu_element("Help", "%R/wikihelp");
263 }
264 if( (ok & W_NEW)!=0 && g.anon.NewWiki ){
265 style_submenu_element("New", "%R/wikinew");
266 }
267 #if 0
268 if( (ok & W_BLOG)!=0
269 #endif
270 if( (ok & W_SANDBOX)!=0 ){
271 style_submenu_element("Sandbox", "%R/wiki?name=Sandbox");
272 }
273 }
274
@@ -281,36 +309,24 @@
281 if( !g.perm.RdWiki ){ login_needed(g.anon.RdWiki); return; }
282 style_header("Wiki Help");
283 wiki_standard_submenu(W_ALL_BUT(W_HELP));
284 @ <h2>Wiki Links</h2>
285 @ <ul>
286 { char *zWikiHomePageName = db_get("index-page",0);
287 if( zWikiHomePageName ){
288 @ <li> %z(href("%R%s",zWikiHomePageName))
289 @ %h(zWikiHomePageName)</a> wiki home page.</li>
290 }
291 }
292 { char *zHomePageName = db_get("project-name",0);
293 if( zHomePageName ){
294 @ <li> %z(href("%R/wiki?name=%t",zHomePageName))
295 @ %h(zHomePageName)</a> project home page.</li>
296 }
297 }
298 @ <li> %z(href("%R/timeline?y=w"))Recent changes</a> to wiki pages.</li>
299 @ <li> Formatting rules for %z(href("%R/wiki_rules"))Fossil Wiki</a> and for
300 @ %z(href("%R/md_rules"))Markdown Wiki</a>.</li>
301 @ <li> Use the %z(href("%R/wiki?name=Sandbox"))Sandbox</a>
302 @ to experiment.</li>
303 if( g.anon.NewWiki ){
304 @ <li> Create a %z(href("%R/wikinew"))new wiki page</a>.</li>
305 if( g.anon.Write ){
306 @ <li> Create a %z(href("%R/technoteedit"))new tech-note</a>.</li>
307 }
308 }
309 @ <li> %z(href("%R/wcontent"))List of All Wiki Pages</a>
310 @ available on this server.</li>
311 if( g.anon.ModWiki ){
312 @ <li> %z(href("%R/modreq"))Tend to pending moderation requests</a></li>
313 }
314 if( search_restrict(SRCH_WIKI)!=0 ){
315 @ <li> %z(href("%R/wikisrch"))Search</a> for wiki pages containing key
316 @ words</li>
@@ -331,21 +347,106 @@
331 style_header("Wiki Search");
332 wiki_standard_submenu(W_HELP|W_LIST|W_SANDBOX);
333 search_screen(SRCH_WIKI, 0);
334 style_footer();
335 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
336
337 /*
338 ** WEBPAGE: wiki
339 ** URL: /wiki?name=PAGENAME
340 */
341 void wiki_page(void){
342 char *zTag;
343 int rid = 0;
344 int isSandbox;
345 char *zUuid;
346 unsigned submenuFlags = W_ALL;
347 Blob wiki;
348 Manifest *pWiki = 0;
349 const char *zPageName;
350 const char *zMimetype = 0;
351 char *zBody = mprintf("%s","<i>Empty Page</i>");
@@ -385,43 +486,35 @@
385 zMimetype = pWiki->zMimetype;
386 }
387 }
388 zMimetype = wiki_filter_mimetypes(zMimetype);
389 if( !g.isHome ){
390 if( rid ){
391 style_submenu_element("Diff", "%R/wdiff?name=%T&a=%d", zPageName, rid);
392 zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid);
393 style_submenu_element("Details", "%R/info/%s", zUuid);
394 }
395 if( (rid && g.anon.WrWiki) || (!rid && g.anon.NewWiki) ){
396 if( db_get_boolean("wysiwyg-wiki", 0) ){
397 style_submenu_element("Edit", "%s/wikiedit?name=%T&wysiwyg=1",
398 g.zTop, zPageName);
399 }else{
400 style_submenu_element("Edit", "%s/wikiedit?name=%T", g.zTop, zPageName);
401 }
402 }
403 if( rid && g.anon.ApndWiki && g.anon.Attach ){
404 style_submenu_element("Attach",
405 "%s/attachadd?page=%T&from=%s/wiki%%3fname=%T",
406 g.zTop, zPageName, g.zTop, zPageName);
407 }
408 if( rid && g.anon.ApndWiki ){
409 style_submenu_element("Append", "%s/wikiappend?name=%T&mimetype=%s",
410 g.zTop, zPageName, zMimetype);
411 }
412 if( g.perm.Hyperlink ){
413 style_submenu_element("History", "%s/whistory?name=%T",
414 g.zTop, zPageName);
415 }
416 }
417 style_set_current_page("%T?name=%T", g.zPath, zPageName);
418 style_header("%s", zPageName);
419 wiki_standard_submenu(submenuFlags);
420 blob_init(&wiki, zBody, -1);
421 wiki_render_by_mimetype(&wiki, zMimetype);
422 blob_reset(&wiki);
 
 
 
 
423 attachment_list(zPageName, "<hr /><h2>Attachments:</h2><ul>");
424 manifest_destroy(pWiki);
425 style_footer();
426 }
427
@@ -491,10 +584,12 @@
491 const char *z;
492 char *zBody = (char*)P("w");
493 const char *zMimetype = wiki_filter_mimetypes(P("mimetype"));
494 int isWysiwyg = P("wysiwyg")!=0;
495 int goodCaptcha = 1;
 
 
496
497 if( P("edit-wysiwyg")!=0 ){ isWysiwyg = 1; zBody = 0; }
498 if( P("edit-markup")!=0 ){ isWysiwyg = 0; zBody = 0; }
499 if( zBody ){
500 if( isWysiwyg ){
@@ -525,10 +620,14 @@
525 "SELECT rid FROM tagxref"
526 " WHERE tagid=(SELECT tagid FROM tag WHERE tagname=%Q)"
527 " ORDER BY mtime DESC", zTag
528 );
529 free(zTag);
 
 
 
 
530 if( (rid && !g.perm.WrWiki) || (!rid && !g.perm.NewWiki) ){
531 login_needed(rid ? g.anon.WrWiki : g.anon.NewWiki);
532 return;
533 }
534 if( zBody==0 && (pWiki = manifest_get(rid, CFTYPE_WIKI, 0))!=0 ){
@@ -575,20 +674,30 @@
575 if( P("cancel")!=0 ){
576 cgi_redirectf("wiki?name=%T", zPageName);
577 return;
578 }
579 if( zBody==0 ){
580 zBody = mprintf("<i>Empty Page</i>");
581 }
582 style_set_current_page("%T?name=%T", g.zPath, zPageName);
583 style_header("Edit: %s", zPageName);
 
 
 
 
 
 
 
 
 
584 if( !goodCaptcha ){
585 @ <p class="generalError">Error: Incorrect security code.</p>
586 }
587 blob_zero(&wiki);
588 blob_append(&wiki, zBody, -1);
589 if( P("preview")!=0 ){
 
590 @ Preview:<hr />
591 wiki_render_by_mimetype(&wiki, zMimetype);
592 @ <hr />
593 blob_reset(&wiki);
594 }
@@ -597,23 +706,45 @@
597 }
598 if( n<20 ) n = 20;
599 if( n>30 ) n = 30;
600 if( !isWysiwyg ){
601 /* Traditional markup-only editing */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
602 form_begin(0, "%R/wikiedit");
603 @ <div>Markup style:
604 mimetype_option_menu(zMimetype);
605 @ <br /><textarea name="w" class="wikiedit" cols="80"
606 @ rows="%d(n)" wrap="virtual">%h(zBody)</textarea>
 
607 @ <br />
 
608 if( db_get_boolean("wysiwyg-wiki", 0) ){
609 @ <input type="submit" name="edit-wysiwyg" value="Wysiwyg Editor" />
610 }
611 @ <input type="submit" name="preview" value="Preview Your Changes" />
612 }else{
613 /* Wysiwyg editing */
614 Blob html, temp;
 
615 form_begin("", "%R/wikiedit");
616 @ <div>
617 @ <input type="hidden" name="wysiwyg" value="1" />
618 blob_zero(&temp);
619 wiki_convert(&wiki, &temp, 0);
@@ -624,11 +755,13 @@
624 blob_reset(&html);
625 @ <br />
626 @ <input type="submit" name="edit-markup" value="Markup Editor" />
627 }
628 login_insert_csrf_secret();
629 @ <input type="submit" name="submit" value="Apply These Changes" />
 
 
630 @ <input type="hidden" name="name" value="%h(zPageName)" />
631 @ <input type="submit" name="cancel" value="Cancel" />
632 @ </div>
633 @ <script nonce="%h(style_nonce())">
634 @ confirmOnClick("edit-wysiwyg", "Switching to WYSIWYG-mode\nwill erase your markup edits.\n\nContinue?");
@@ -851,90 +984,143 @@
851 captcha_generate(0);
852 @ </form>
853 style_footer();
854 }
855
856 /*
857 ** Name of the wiki history page being generated
858 */
859 static const char *zWikiPageName;
860
861 /*
862 ** Function called to output extra text at the end of each line in
863 ** a wiki history listing.
864 */
865 static void wiki_history_extra(int rid){
866 if( g.perm.Hyperlink && db_exists("SELECT 1 FROM tagxref WHERE rid=%d", rid) ){
867 @ %z(href("%R/wdiff?name=%t&a=%d",zWikiPageName,rid))[diff]</a>
868 }
869 }
870
871 /*
872 ** WEBPAGE: whistory
873 ** URL: /whistory?name=PAGENAME
874 **
 
 
 
 
875 ** Show the complete change history for a single wiki page.
876 */
877 void whistory_page(void){
878 Stmt q;
879 const char *zPageName;
 
 
880 login_check_credentials();
881 if( !g.perm.Hyperlink ){ login_needed(g.anon.Hyperlink); return; }
882 zPageName = PD("name","");
883 style_header("History Of %s", zPageName);
884
885 db_prepare(&q, "%s AND event.objid IN "
886 " (SELECT rid FROM tagxref WHERE tagid="
887 "(SELECT tagid FROM tag WHERE tagname='wiki-%q')"
888 " UNION SELECT attachid FROM attachment"
889 " WHERE target=%Q)"
890 "ORDER BY mtime DESC",
891 timeline_query_for_www(), zPageName, zPageName);
892 zWikiPageName = zPageName;
893 www_print_timeline(&q, TIMELINE_ARTID, 0, 0, 0, wiki_history_extra);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
894 db_finalize(&q);
 
895 style_footer();
896 }
897
898 /*
899 ** WEBPAGE: wdiff
900 ** URL: /wdiff?name=PAGENAME&a=RID1&b=RID2&diff=DIFFTYPE
 
 
 
 
 
 
 
901 **
902 ** Show the difference between two wiki pages.
 
903 */
904 void wdiff_page(void){
905 int rid1, rid2;
906 const char *zPageName;
907 Manifest *pW1, *pW2 = 0;
 
908 Blob w1, w2, d;
909 u64 diffFlags;
910 int diffType; /* 0: none, 1: unified, 2: side-by-side */
911
912 login_check_credentials();
913 rid1 = atoi(PD("a","0"));
914 if( !g.perm.Hyperlink ){ login_needed(g.anon.Hyperlink); return; }
915 if( rid1==0 ) fossil_redirect_home();
916 rid2 = atoi(PD("b","0"));
917 zPageName = PD("name","");
918 style_header("Changes To %s", zPageName);
919
920 if( rid2==0 ){
921 rid2 = db_int(0,
922 "SELECT objid FROM event JOIN tagxref ON objid=rid AND tagxref.tagid="
923 "(SELECT tagid FROM tag WHERE tagname='wiki-%q')"
924 " WHERE event.mtime<(SELECT mtime FROM event WHERE objid=%d)"
925 " ORDER BY event.mtime DESC LIMIT 1",
926 zPageName, rid1
927 );
928 }
929 pW1 = manifest_get(rid1, CFTYPE_WIKI, 0);
930 if( pW1==0 ) fossil_redirect_home();
931 blob_init(&w1, pW1->zWiki, -1);
932 blob_zero(&w2);
933 if( rid2 && (pW2 = manifest_get(rid2, CFTYPE_WIKI, 0))!=0 ){
 
 
 
 
 
 
934 blob_init(&w2, pW2->zWiki, -1);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
935 }
 
936 blob_zero(&d);
937 diffFlags = construct_diff_flags(1);
938 diffType = atoi( PD("diff", "2") );
939 if( diffType==2 ){
940 diffFlags |= DIFF_SIDEBYSIDE;
@@ -959,37 +1145,50 @@
959 manifest_destroy(pW2);
960 style_footer();
961 }
962
963 /*
964 ** prepare()s pStmt with a query requesting:
965 **
966 ** - wiki page name
967 ** - tagxref (whatever that really is!)
 
 
 
968 **
969 ** Used by wcontent_page() and the JSON wiki code.
970 */
971 void wiki_prepare_page_list( Stmt * pStmt ){
972 db_prepare(pStmt,
973 "SELECT"
974 " substr(tagname, 6) as name,"
975 " (SELECT value FROM tagxref WHERE tagid=tag.tagid"
976 " ORDER BY mtime DESC) as tagXref"
977 " FROM tag WHERE tagname GLOB 'wiki-*'"
978 " ORDER BY lower(tagname) /*sort*/"
979 );
980 }
 
 
 
 
 
 
 
981 /*
982 ** WEBPAGE: wcontent
983 **
984 ** all=1 Show deleted pages
 
985 **
986 ** List all available wiki pages with date created and last modified.
987 */
988 void wcontent_page(void){
989 Stmt q;
 
990 int showAll = P("all")!=0;
 
991
992 login_check_credentials();
993 if( !g.perm.RdWiki ){ login_needed(g.anon.RdWiki); return; }
994 style_header("Available Wiki Pages");
995 if( showAll ){
@@ -996,23 +1195,58 @@
996 style_submenu_element("Active", "%s/wcontent", g.zTop);
997 }else{
998 style_submenu_element("All", "%s/wcontent?all=1", g.zTop);
999 }
1000 wiki_standard_submenu(W_ALL_BUT(W_LIST));
1001 @ <ul>
1002 wiki_prepare_page_list(&q);
 
 
 
 
 
 
 
 
 
 
1003 while( db_step(&q)==SQLITE_ROW ){
1004 const char *zName = db_column_text(&q, 0);
1005 int size = db_column_int(&q, 1);
1006 if( size>0 ){
1007 @ <li>%z(href("%R/wiki?name=%T",zName))%h(zName)</a></li>
1008 }else if( showAll ){
1009 @ <li>%z(href("%R/wiki?name=%T",zName))<s>%h(zName)</s></a></li>
1010 }
1011 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1012 db_finalize(&q);
1013 @ </ul>
1014 style_footer();
1015 }
1016
1017 /*
1018 ** WEBPAGE: wfind
@@ -1405,5 +1639,121 @@
1405 blob_zero(&out);
1406 blob_read_from_file(&in, g.argv[2], ExtFILE);
1407 markdown_to_html(&in, 0, &out);
1408 blob_write_to_file(&out, "-");
1409 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1410
--- src/wiki.c
+++ src/wiki.c
@@ -71,10 +71,41 @@
71 style_footer();
72 return 1;
73 }
74 return 0;
75 }
76
77 /*
78 ** Return the tagid associated with a particular wiki page.
79 */
80 int wiki_tagid(const char *zPageName){
81 return db_int(0, "SELECT tagid FROM tag WHERE tagname='wiki-%q'",zPageName);
82 }
83 int wiki_tagid2(const char *zPrefix, const char *zPageName){
84 return db_int(0, "SELECT tagid FROM tag WHERE tagname='wiki-%q/%q'",
85 zPrefix, zPageName);
86 }
87
88 /*
89 ** Return the RID of the next or previous version of a wiki page.
90 ** Return 0 if rid is the last/first version.
91 */
92 int wiki_next(int tagid, double mtime){
93 return db_int(0,
94 "SELECT srcid FROM tagxref"
95 " WHERE tagid=%d AND mtime>%.16g"
96 " ORDER BY mtime ASC LIMIT 1",
97 tagid, mtime);
98 }
99 int wiki_prev(int tagid, double mtime){
100 return db_int(0,
101 "SELECT srcid FROM tagxref"
102 " WHERE tagid=%d AND mtime<%.16g"
103 " ORDER BY mtime DESC LIMIT 1",
104 tagid, mtime);
105 }
106
107
108 /*
109 ** WEBPAGE: home
110 ** WEBPAGE: index
111 ** WEBPAGE: not_found
@@ -262,13 +293,10 @@
293 style_submenu_element("Help", "%R/wikihelp");
294 }
295 if( (ok & W_NEW)!=0 && g.anon.NewWiki ){
296 style_submenu_element("New", "%R/wikinew");
297 }
 
 
 
298 if( (ok & W_SANDBOX)!=0 ){
299 style_submenu_element("Sandbox", "%R/wiki?name=Sandbox");
300 }
301 }
302
@@ -281,36 +309,24 @@
309 if( !g.perm.RdWiki ){ login_needed(g.anon.RdWiki); return; }
310 style_header("Wiki Help");
311 wiki_standard_submenu(W_ALL_BUT(W_HELP));
312 @ <h2>Wiki Links</h2>
313 @ <ul>
 
 
 
 
 
 
 
 
 
 
 
 
314 @ <li> %z(href("%R/timeline?y=w"))Recent changes</a> to wiki pages.</li>
315 @ <li> Formatting rules for %z(href("%R/wiki_rules"))Fossil Wiki</a> and for
316 @ %z(href("%R/md_rules"))Markdown Wiki</a>.</li>
317 @ <li> Use the %z(href("%R/wiki?name=Sandbox"))Sandbox</a>
318 @ to experiment.</li>
319 if( g.perm.NewWiki ){
320 @ <li> Create a %z(href("%R/wikinew"))new wiki page</a>.</li>
321 if( g.perm.Write ){
322 @ <li> Create a %z(href("%R/technoteedit"))new tech-note</a>.</li>
323 }
324 }
325 @ <li> %z(href("%R/wcontent"))List of All Wiki Pages</a>
326 @ available on this server.</li>
327 if( g.perm.ModWiki ){
328 @ <li> %z(href("%R/modreq"))Tend to pending moderation requests</a></li>
329 }
330 if( search_restrict(SRCH_WIKI)!=0 ){
331 @ <li> %z(href("%R/wikisrch"))Search</a> for wiki pages containing key
332 @ words</li>
@@ -331,21 +347,106 @@
347 style_header("Wiki Search");
348 wiki_standard_submenu(W_HELP|W_LIST|W_SANDBOX);
349 search_screen(SRCH_WIKI, 0);
350 style_footer();
351 }
352
353 /* Return values from wiki_page_type() */
354 #define WIKITYPE_UNKNOWN (-1)
355 #define WIKITYPE_NORMAL 0
356 #define WIKITYPE_BRANCH 1
357 #define WIKITYPE_CHECKIN 2
358 #define WIKITYPE_TAG 3
359
360 /*
361 ** Figure out what type of wiki page we are dealing with.
362 */
363 static int wiki_page_type(const char *zPageName){
364 if( db_get_boolean("wiki-about",1)==0 ){
365 return WIKITYPE_NORMAL;
366 }else
367 if( sqlite3_strglob("checkin/*", zPageName)==0
368 && db_exists("SELECT 1 FROM blob WHERE uuid=%Q",zPageName+8)
369 ){
370 return WIKITYPE_CHECKIN;
371 }else
372 if( sqlite3_strglob("branch/*", zPageName)==0 ){
373 return WIKITYPE_BRANCH;
374 }else
375 if( sqlite3_strglob("tag/*", zPageName)==0 ){
376 return WIKITYPE_TAG;
377 }
378 return WIKITYPE_NORMAL;
379 }
380
381 /*
382 ** Add an appropriate style_header() for either the /wiki or /wikiedit page
383 ** for zPageName.
384 */
385 static int wiki_page_header(
386 int eType, /* Page type. -1 for unknown */
387 const char *zPageName, /* Name of the page */
388 const char *zExtra /* Extra prefix text on the page header */
389 ){
390 if( eType<0 ) eType = wiki_page_type(zPageName);
391 switch( eType ){
392 case WIKITYPE_NORMAL: {
393 style_header("%s%s", zExtra, zPageName);
394 break;
395 }
396 case WIKITYPE_CHECKIN: {
397 zPageName += 8;
398 style_header("Notes About Checkin %S", zPageName);
399 style_submenu_element("Checkin Timeline","%R/timeline?f=%s", zPageName);
400 style_submenu_element("Checkin Info","%R/info/%s", zPageName);
401 break;
402 }
403 case WIKITYPE_BRANCH: {
404 zPageName += 7;
405 style_header("Notes About Branch %h", zPageName);
406 style_submenu_element("Branch Timeline","%R/timeline?r=%t", zPageName);
407 break;
408 }
409 case WIKITYPE_TAG: {
410 zPageName += 4;
411 style_header("Notes About Tag %h", zPageName);
412 style_submenu_element("Tag Timeline","%R/timeline?t=%t",zPageName);
413 break;
414 }
415 }
416 return eType;
417 }
418
419 /*
420 ** Wiki pages with special names "branch/...", "checkin/...", and "tag/..."
421 ** requires perm.Write privilege in addition to perm.WrWiki in order
422 ** to write. This function determines whether the extra perm.Write
423 ** is required and available. Return true if writing to the wiki page
424 ** may proceed, and return false if permission is lacking.
425 */
426 static int wiki_special_permission(const char *zPageName){
427 if( strncmp(zPageName,"branch/",7)!=0
428 && strncmp(zPageName,"checkin/",8)!=0
429 && strncmp(zPageName,"tag/",4)!=0
430 ){
431 return 1;
432 }
433 if( db_get_boolean("wiki-about",1)==0 ){
434 return 1;
435 }
436 return g.perm.Write;
437 }
438
439 /*
440 ** WEBPAGE: wiki
441 ** URL: /wiki?name=PAGENAME
442 */
443 void wiki_page(void){
444 char *zTag;
445 int rid = 0;
446 int isSandbox;
447 unsigned submenuFlags = W_HELP;
 
448 Blob wiki;
449 Manifest *pWiki = 0;
450 const char *zPageName;
451 const char *zMimetype = 0;
452 char *zBody = mprintf("%s","<i>Empty Page</i>");
@@ -385,43 +486,35 @@
486 zMimetype = pWiki->zMimetype;
487 }
488 }
489 zMimetype = wiki_filter_mimetypes(zMimetype);
490 if( !g.isHome ){
491 if( ((rid && g.perm.WrWiki) || (!rid && g.perm.NewWiki))
492 && wiki_special_permission(zPageName)
493 ){
 
 
 
494 if( db_get_boolean("wysiwyg-wiki", 0) ){
495 style_submenu_element("Edit", "%s/wikiedit?name=%T&wysiwyg=1",
496 g.zTop, zPageName);
497 }else{
498 style_submenu_element("Edit", "%s/wikiedit?name=%T", g.zTop, zPageName);
499 }
500 }
 
 
 
 
 
 
 
 
 
501 if( g.perm.Hyperlink ){
502 style_submenu_element("History", "%s/whistory?name=%T",
503 g.zTop, zPageName);
504 }
505 }
506 style_set_current_page("%T?name=%T", g.zPath, zPageName);
507 wiki_page_header(WIKITYPE_UNKNOWN, zPageName, "");
508 wiki_standard_submenu(submenuFlags);
509 if( zBody[0]==0 ){
510 @ <i>This page has been deleted</i>
511 }else{
512 blob_init(&wiki, zBody, -1);
513 wiki_render_by_mimetype(&wiki, zMimetype);
514 blob_reset(&wiki);
515 }
516 attachment_list(zPageName, "<hr /><h2>Attachments:</h2><ul>");
517 manifest_destroy(pWiki);
518 style_footer();
519 }
520
@@ -491,10 +584,12 @@
584 const char *z;
585 char *zBody = (char*)P("w");
586 const char *zMimetype = wiki_filter_mimetypes(P("mimetype"));
587 int isWysiwyg = P("wysiwyg")!=0;
588 int goodCaptcha = 1;
589 int eType = WIKITYPE_UNKNOWN;
590 int havePreview = 0;
591
592 if( P("edit-wysiwyg")!=0 ){ isWysiwyg = 1; zBody = 0; }
593 if( P("edit-markup")!=0 ){ isWysiwyg = 0; zBody = 0; }
594 if( zBody ){
595 if( isWysiwyg ){
@@ -525,10 +620,14 @@
620 "SELECT rid FROM tagxref"
621 " WHERE tagid=(SELECT tagid FROM tag WHERE tagname=%Q)"
622 " ORDER BY mtime DESC", zTag
623 );
624 free(zTag);
625 if( !wiki_special_permission(zPageName) ){
626 login_needed(0);
627 return;
628 }
629 if( (rid && !g.perm.WrWiki) || (!rid && !g.perm.NewWiki) ){
630 login_needed(rid ? g.anon.WrWiki : g.anon.NewWiki);
631 return;
632 }
633 if( zBody==0 && (pWiki = manifest_get(rid, CFTYPE_WIKI, 0))!=0 ){
@@ -575,20 +674,30 @@
674 if( P("cancel")!=0 ){
675 cgi_redirectf("wiki?name=%T", zPageName);
676 return;
677 }
678 if( zBody==0 ){
679 zBody = mprintf("");
680 }
681 style_set_current_page("%T?name=%T", g.zPath, zPageName);
682 eType = wiki_page_header(WIKITYPE_UNKNOWN, zPageName, "Edit: ");
683 if( rid && !isSandbox && g.perm.ApndWiki ){
684 if( g.perm.Attach ){
685 style_submenu_element("Attach",
686 "%s/attachadd?page=%T&from=%s/wiki%%3fname=%T",
687 g.zTop, zPageName, g.zTop, zPageName);
688 }
689 style_submenu_element("Append", "%s/wikiappend?name=%T&mimetype=%s",
690 g.zTop, zPageName, zMimetype);
691 }
692 if( !goodCaptcha ){
693 @ <p class="generalError">Error: Incorrect security code.</p>
694 }
695 blob_zero(&wiki);
696 blob_append(&wiki, zBody, -1);
697 if( P("preview")!=0 && zBody[0] ){
698 havePreview = 1;
699 @ Preview:<hr />
700 wiki_render_by_mimetype(&wiki, zMimetype);
701 @ <hr />
702 blob_reset(&wiki);
703 }
@@ -597,23 +706,45 @@
706 }
707 if( n<20 ) n = 20;
708 if( n>30 ) n = 30;
709 if( !isWysiwyg ){
710 /* Traditional markup-only editing */
711 char *zPlaceholder = 0;
712 switch( eType ){
713 case WIKITYPE_NORMAL: {
714 zPlaceholder = mprintf("Enter text for wiki page %s", zPageName);
715 break;
716 }
717 case WIKITYPE_BRANCH: {
718 zPlaceholder = mprintf("Enter notes about branch %s", zPageName+7);
719 break;
720 }
721 case WIKITYPE_CHECKIN: {
722 zPlaceholder = mprintf("Enter notes about check-in %.20s", zPageName+8);
723 break;
724 }
725 case WIKITYPE_TAG: {
726 zPlaceholder = mprintf("Enter notes about tag %s", zPageName+4);
727 break;
728 }
729 }
730 form_begin(0, "%R/wikiedit");
731 @ <div>Markup style:
732 mimetype_option_menu(zMimetype);
733 @ <br /><textarea name="w" class="wikiedit" cols="80" \
734 @ rows="%d(n)" wrap="virtual" placeholder="%h(zPlaceholder)">\
735 @ %h(zBody)</textarea>
736 @ <br />
737 fossil_free(zPlaceholder);
738 if( db_get_boolean("wysiwyg-wiki", 0) ){
739 @ <input type="submit" name="edit-wysiwyg" value="Wysiwyg Editor" />
740 }
741 @ <input type="submit" name="preview" value="Preview Your Changes" />
742 }else{
743 /* Wysiwyg editing */
744 Blob html, temp;
745 havePreview = 1;
746 form_begin("", "%R/wikiedit");
747 @ <div>
748 @ <input type="hidden" name="wysiwyg" value="1" />
749 blob_zero(&temp);
750 wiki_convert(&wiki, &temp, 0);
@@ -624,11 +755,13 @@
755 blob_reset(&html);
756 @ <br />
757 @ <input type="submit" name="edit-markup" value="Markup Editor" />
758 }
759 login_insert_csrf_secret();
760 if( havePreview ){
761 @ <input type="submit" name="submit" value="Apply These Changes" />
762 }
763 @ <input type="hidden" name="name" value="%h(zPageName)" />
764 @ <input type="submit" name="cancel" value="Cancel" />
765 @ </div>
766 @ <script nonce="%h(style_nonce())">
767 @ confirmOnClick("edit-wysiwyg", "Switching to WYSIWYG-mode\nwill erase your markup edits.\n\nContinue?");
@@ -851,90 +984,143 @@
984 captcha_generate(0);
985 @ </form>
986 style_footer();
987 }
988
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
989 /*
990 ** WEBPAGE: whistory
991 ** URL: /whistory?name=PAGENAME
992 **
993 ** Additional parameters:
994 **
995 ** showid Show RID values
996 **
997 ** Show the complete change history for a single wiki page.
998 */
999 void whistory_page(void){
1000 Stmt q;
1001 const char *zPageName;
1002 double rNow;
1003 int showRid;
1004 login_check_credentials();
1005 if( !g.perm.Hyperlink ){ login_needed(g.anon.Hyperlink); return; }
1006 zPageName = PD("name","");
1007 style_header("History Of %s", zPageName);
1008 showRid = P("showid")!=0;
1009 db_prepare(&q,
1010 "SELECT"
1011 " event.mtime,"
1012 " blob.uuid,"
1013 " coalesce(event.euser,event.user),"
1014 " event.objid"
1015 " FROM event, blob, tag, tagxref"
1016 " WHERE event.type='w' AND blob.rid=event.objid"
1017 " AND tag.tagname='wiki-%q'"
1018 " AND tagxref.tagid=tag.tagid AND tagxref.srcid=event.objid"
1019 " ORDER BY event.mtime DESC",
1020 zPageName
1021 );
1022 @ <h2>History of <a href="%R/wiki?name=%T(zPageName)">%h(zPageName)</a></h2>
1023 @ <div class="brlist">
1024 @ <table>
1025 @ <thead><tr>
1026 @ <th>Age</th>
1027 @ <th>Hash</th>
1028 @ <th>User</th>
1029 if( showRid ){
1030 @ <th>RID</th>
1031 }
1032 @ <th>&nbsp;</th>
1033 @ </tr></thead><tbody>
1034 rNow = db_double(0.0, "SELECT julianday('now')");
1035 while( db_step(&q)==SQLITE_ROW ){
1036 double rMtime = db_column_double(&q, 0);
1037 const char *zUuid = db_column_text(&q, 1);
1038 const char *zUser = db_column_text(&q, 2);
1039 int wrid = db_column_int(&q, 3);
1040 /* sqlite3_int64 iMtime = (sqlite3_int64)(rMtime*86400.0); */
1041 char *zAge = human_readable_age(rNow - rMtime);
1042 @ <tr>
1043 /* @ <td data-sortkey="%016llx(iMtime)">%s(zAge)</td> */
1044 @ <td>%s(zAge)</td>
1045 fossil_free(zAge);
1046 @ <td>%z(href("%R/info/%s",zUuid))%S(zUuid)</a></td>
1047 @ <td>%h(zUser)</td>
1048 if( showRid ){
1049 @ <td>%z(href("%R/artifact/%S",zUuid))%d(wrid)</a></td>
1050 }
1051 @ <td>%z(href("%R/wdiff?id=%S",zUuid))diff</a></td>
1052 @ </tr>
1053 }
1054 @ </tbody></table></div>
1055 db_finalize(&q);
1056 /* style_table_sorter(); */
1057 style_footer();
1058 }
1059
1060 /*
1061 ** WEBPAGE: wdiff
1062 **
1063 ** Show the changes to a wiki page.
1064 **
1065 ** Query parameters:
1066 **
1067 ** id=HASH Hash prefix for the child version to be diffed.
1068 ** rid=INTEGER RecordID for the child version
1069 ** pid=HASH Hash prefix for the parent.
1070 **
1071 ** The "id" query parameter is required. "pid" is optional. If "pid"
1072 ** is omitted, then the diff is against the first parent of the child.
1073 */
1074 void wdiff_page(void){
1075 const char *zId;
1076 const char *zPid;
1077 Manifest *pW1, *pW2 = 0;
1078 int rid1, rid2, nextRid;
1079 Blob w1, w2, d;
1080 u64 diffFlags;
1081 int diffType; /* 0: none, 1: unified, 2: side-by-side */
1082
1083 login_check_credentials();
1084 if( !g.perm.RdWiki ){ login_needed(g.anon.RdWiki); return; }
1085 zId = P("id");
1086 if( zId==0 ){
1087 rid1 = atoi(PD("rid","0"));
1088 }else{
1089 rid1 = name_to_typed_rid(zId, "w");
1090 }
1091 zId = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid1);
 
 
 
 
 
 
 
 
1092 pW1 = manifest_get(rid1, CFTYPE_WIKI, 0);
1093 if( pW1==0 ) fossil_redirect_home();
1094 blob_init(&w1, pW1->zWiki, -1);
1095 zPid = P("pid");
1096 if( zPid==0 && pW1->nParent ){
1097 zPid = pW1->azParent[0];
1098 }
1099 if( zPid ){
1100 char *zDate;
1101 rid2 = name_to_typed_rid(zPid, "w");
1102 pW2 = manifest_get(rid2, CFTYPE_WIKI, 0);
1103 blob_init(&w2, pW2->zWiki, -1);
1104 @ <h2>Changes to \
1105 @ "%z(href("%R/whistory?name=%s",pW1->zWikiTitle))%h(pW1->zWikiTitle)</a>" \
1106 zDate = db_text(0, "SELECT datetime(%.16g)",pW2->rDate);
1107 @ between %z(href("%R/info/%s",zPid))%z(zDate)</a> \
1108 zDate = db_text(0, "SELECT datetime(%.16g)",pW1->rDate);
1109 @ and %z(href("%R/info/%s",zId))%z(zDate)</a></h2>
1110 style_submenu_element("Previous", "%R/wdiff?id=%S", zPid);
1111 }else{
1112 blob_zero(&w2);
1113 @ <h2>Initial version of \
1114 @ "%z(href("%R/whistory?name=%s",pW1->zWikiTitle))%h(pW1->zWikiTitle)</a>"\
1115 @ </h2>
1116 }
1117 nextRid = wiki_next(wiki_tagid(pW1->zWikiTitle),pW1->rDate);
1118 if( nextRid ){
1119 style_submenu_element("Next", "%R/wdiff?rid=%d", nextRid);
1120 }
1121 style_header("Changes To %s", pW1->zWikiTitle);
1122 blob_zero(&d);
1123 diffFlags = construct_diff_flags(1);
1124 diffType = atoi( PD("diff", "2") );
1125 if( diffType==2 ){
1126 diffFlags |= DIFF_SIDEBYSIDE;
@@ -959,37 +1145,50 @@
1145 manifest_destroy(pW2);
1146 style_footer();
1147 }
1148
1149 /*
1150 ** A query that returns information about all wiki pages.
1151 **
1152 ** wname Name of the wiki page
1153 ** wsort Sort names by this label
1154 ** wrid rid of the most recent version of the page
1155 ** wmtime time most recent version was created
1156 ** wcnt Number of versions of this wiki page
1157 **
1158 ** The wrid value is zero for deleted wiki pages.
1159 */
1160 static const char listAllWikiPages[] =
1161 @ SELECT
1162 @ substr(tag.tagname, 6) AS wname,
1163 @ lower(substr(tag.tagname, 6)) AS sortname,
1164 @ tagxref.value+0 AS wrid,
1165 @ max(tagxref.mtime) AS wmtime,
1166 @ count(*) AS wcnt
1167 @ FROM
1168 @ tag,
1169 @ tagxref
1170 @ WHERE
1171 @ tag.tagname GLOB 'wiki-*'
1172 @ AND tagxref.tagid=tag.tagid
1173 @ GROUP BY 1
1174 @ ORDER BY 2;
1175 ;
1176
1177 /*
1178 ** WEBPAGE: wcontent
1179 **
1180 ** all=1 Show deleted pages
1181 ** showid Show rid values for each page.
1182 **
1183 ** List all available wiki pages with date created and last modified.
1184 */
1185 void wcontent_page(void){
1186 Stmt q;
1187 double rNow;
1188 int showAll = P("all")!=0;
1189 int showRid = P("showid")!=0;
1190
1191 login_check_credentials();
1192 if( !g.perm.RdWiki ){ login_needed(g.anon.RdWiki); return; }
1193 style_header("Available Wiki Pages");
1194 if( showAll ){
@@ -996,23 +1195,58 @@
1195 style_submenu_element("Active", "%s/wcontent", g.zTop);
1196 }else{
1197 style_submenu_element("All", "%s/wcontent?all=1", g.zTop);
1198 }
1199 wiki_standard_submenu(W_ALL_BUT(W_LIST));
1200 db_prepare(&q, listAllWikiPages/*works-like:""*/);
1201 @ <div class="brlist">
1202 @ <table class='sortable' data-column-types='tKN' data-init-sort='1'>
1203 @ <thead><tr>
1204 @ <th>Name</th>
1205 @ <th>Last Change</th>
1206 @ <th>Versions</th>
1207 if( showRid ){
1208 @ <th>RID</th>
1209 }
1210 @ </tr></thead><tbody>
1211 rNow = db_double(0.0, "SELECT julianday('now')");
1212 while( db_step(&q)==SQLITE_ROW ){
1213 const char *zWName = db_column_text(&q, 0);
1214 const char *zSort = db_column_text(&q, 1);
1215 int wrid = db_column_int(&q, 2);
1216 double rWmtime = db_column_double(&q, 3);
1217 sqlite3_int64 iMtime = (sqlite3_int64)(rWmtime*86400.0);
1218 char *zAge;
1219 int wcnt = db_column_int(&q, 4);
1220 char *zWDisplayName;
1221
1222 if( sqlite3_strglob("checkin/*", zWName)==0 ){
1223 zWDisplayName = mprintf("%.25s...", zWName);
1224 }else{
1225 zWDisplayName = mprintf("%s", zWName);
1226 }
1227 if( wrid==0 ){
1228 if( !showAll ) continue;
1229 @ <tr><td data-sortkey="%h(zSort)">\
1230 @ %z(href("%R/whistory?name=%T",zWName))<s>%h(zWDisplayName)</s></a></td>
1231 }else{
1232 @ <tr><td data=sortkey='%h(zSort)">\
1233 @ %z(href("%R/wiki?name=%T",zWName))%h(zWDisplayName)</a></td>
1234 }
1235 zAge = human_readable_age(rNow - rWmtime);
1236 @ <td data-sortkey="%016llx(iMtime)">%s(zAge)</td>
1237 fossil_free(zAge);
1238 @ <td>%z(href("%R/whistory?name=%T",zWName))%d(wcnt)</a></td>
1239 if( showRid ){
1240 @ <td>%d(wrid)</td>
1241 }
1242 @ </tr>
1243 fossil_free(zWDisplayName);
1244 }
1245 @ </tbody></table></div>
1246 db_finalize(&q);
1247 style_table_sorter();
1248 style_footer();
1249 }
1250
1251 /*
1252 ** WEBPAGE: wfind
@@ -1405,5 +1639,121 @@
1639 blob_zero(&out);
1640 blob_read_from_file(&in, g.argv[2], ExtFILE);
1641 markdown_to_html(&in, 0, &out);
1642 blob_write_to_file(&out, "-");
1643 }
1644
1645 /*
1646 ** Allowed flags for wiki_render_associated
1647 */
1648 #if INTERFACE
1649 #define WIKIASSOC_FULL_TITLE 0x00001 /* Full title */
1650 #define WIKIASSOC_MENU_READ 0x00002 /* Add submenu link to read wiki */
1651 #define WIKIASSOC_MENU_WRITE 0x00004 /* Add submenu link to add wiki */
1652 #define WIKIASSOC_ALL 0x00007 /* All of the above */
1653 #endif
1654
1655 /*
1656 ** Show the default Section label for an associated wiki page.
1657 */
1658 static void wiki_section_label(
1659 const char *zPrefix, /* "branch", "tag", or "checkin" */
1660 const char *zName, /* Name of the object */
1661 unsigned int mFlags /* Zero or more WIKIASSOC_* flags */
1662 ){
1663 if( (mFlags & WIKIASSOC_FULL_TITLE)==0 ){
1664 @ <div class="section">About</div>
1665 }else if( zPrefix[0]=='c' ){ /* checkin/... */
1666 @ <div class="section">About checkin %.20h(zName)</div>
1667 }else{
1668 @ <div class="section">About %s(zPrefix) %h(zName)</div>
1669 }
1670 }
1671
1672 /*
1673 ** Add an "Wiki" button in a submenu that links to the read-wiki page.
1674 */
1675 static void wiki_submenu_to_read_wiki(
1676 const char *zPrefix, /* "branch", "tag", or "checkin" */
1677 const char *zName, /* Name of the object */
1678 unsigned int mFlags /* Zero or more WIKIASSOC_* flags */
1679 ){
1680 if( g.perm.RdWiki && (mFlags & WIKIASSOC_MENU_READ)!=0 ){
1681 style_submenu_element("Wiki", "%R/wiki?name=%s/%t", zPrefix, zName);
1682 }
1683 }
1684
1685 /*
1686 ** Check to see if there exists a wiki page with a name zPrefix/zName.
1687 ** If there is, then render a <div class='section'>..</div> and
1688 ** return true.
1689 **
1690 ** If there is no such wiki page, return false.
1691 */
1692 int wiki_render_associated(
1693 const char *zPrefix, /* "branch", "tag", or "checkin" */
1694 const char *zName, /* Name of the object */
1695 unsigned int mFlags /* Zero or more WIKIASSOC_* flags */
1696 ){
1697 int rid;
1698 Manifest *pWiki;
1699 if( !db_get_boolean("wiki-about",1) ) return 0;
1700 rid = db_int(0,
1701 "SELECT rid FROM tagxref"
1702 " WHERE tagid=(SELECT tagid FROM tag WHERE tagname='wiki-%q/%q')"
1703 " ORDER BY mtime DESC LIMIT 1",
1704 zPrefix, zName
1705 );
1706 if( rid==0 ){
1707 if( g.perm.WrWiki && g.perm.Write && (mFlags & WIKIASSOC_MENU_WRITE)!=0 ){
1708 style_submenu_element("Add Wiki", "%R/wikiedit?name=%s/%t",
1709 zPrefix, zName);
1710 }
1711 }
1712 pWiki = manifest_get(rid, CFTYPE_WIKI, 0);
1713 if( pWiki==0 ) return 0;
1714 if( fossil_strcmp(pWiki->zMimetype, "text/x-markdown")==0 ){
1715 Blob tail = BLOB_INITIALIZER;
1716 Blob title = BLOB_INITIALIZER;
1717 Blob markdown;
1718 blob_init(&markdown, pWiki->zWiki, -1);
1719 markdown_to_html(&markdown, &title, &tail);
1720 if( blob_size(&title) ){
1721 @ <div class="section">%h(blob_str(&title))</div>
1722 }else{
1723 wiki_section_label(zPrefix, zName, mFlags);
1724 }
1725 wiki_submenu_to_read_wiki(zPrefix, zName, mFlags);
1726 convert_href_and_output(&tail);
1727 blob_reset(&tail);
1728 blob_reset(&title);
1729 blob_reset(&markdown);
1730 }else if( fossil_strcmp(pWiki->zMimetype, "text/plain")==0 ){
1731 wiki_section_label(zPrefix, zName, mFlags);
1732 wiki_submenu_to_read_wiki(zPrefix, zName, mFlags);
1733 @ <pre>
1734 @ %h(pWiki->zWiki)
1735 @ </pre>
1736 }else{
1737 Blob tail = BLOB_INITIALIZER;
1738 Blob title = BLOB_INITIALIZER;
1739 Blob wiki;
1740 Blob *pBody;
1741 blob_init(&wiki, pWiki->zWiki, -1);
1742 if( wiki_find_title(&wiki, &title, &tail) ){
1743 @ <div class="section">%h(blob_str(&title))</div>
1744 pBody = &tail;
1745 }else{
1746 wiki_section_label(zPrefix, zName, mFlags);
1747 pBody = &wiki;
1748 }
1749 wiki_submenu_to_read_wiki(zPrefix, zName, mFlags);
1750 @ <div class="wiki">
1751 wiki_convert(pBody, 0, WIKI_BUTTONS);
1752 @ </div>
1753 blob_reset(&tail);
1754 blob_reset(&title);
1755 blob_reset(&wiki);
1756 }
1757 manifest_destroy(pWiki);
1758 return 1;
1759 }
1760
+467 -117
--- src/wiki.c
+++ src/wiki.c
@@ -71,10 +71,41 @@
7171
style_footer();
7272
return 1;
7373
}
7474
return 0;
7575
}
76
+
77
+/*
78
+** Return the tagid associated with a particular wiki page.
79
+*/
80
+int wiki_tagid(const char *zPageName){
81
+ return db_int(0, "SELECT tagid FROM tag WHERE tagname='wiki-%q'",zPageName);
82
+}
83
+int wiki_tagid2(const char *zPrefix, const char *zPageName){
84
+ return db_int(0, "SELECT tagid FROM tag WHERE tagname='wiki-%q/%q'",
85
+ zPrefix, zPageName);
86
+}
87
+
88
+/*
89
+** Return the RID of the next or previous version of a wiki page.
90
+** Return 0 if rid is the last/first version.
91
+*/
92
+int wiki_next(int tagid, double mtime){
93
+ return db_int(0,
94
+ "SELECT srcid FROM tagxref"
95
+ " WHERE tagid=%d AND mtime>%.16g"
96
+ " ORDER BY mtime ASC LIMIT 1",
97
+ tagid, mtime);
98
+}
99
+int wiki_prev(int tagid, double mtime){
100
+ return db_int(0,
101
+ "SELECT srcid FROM tagxref"
102
+ " WHERE tagid=%d AND mtime<%.16g"
103
+ " ORDER BY mtime DESC LIMIT 1",
104
+ tagid, mtime);
105
+}
106
+
76107
77108
/*
78109
** WEBPAGE: home
79110
** WEBPAGE: index
80111
** WEBPAGE: not_found
@@ -262,13 +293,10 @@
262293
style_submenu_element("Help", "%R/wikihelp");
263294
}
264295
if( (ok & W_NEW)!=0 && g.anon.NewWiki ){
265296
style_submenu_element("New", "%R/wikinew");
266297
}
267
-#if 0
268
- if( (ok & W_BLOG)!=0
269
-#endif
270298
if( (ok & W_SANDBOX)!=0 ){
271299
style_submenu_element("Sandbox", "%R/wiki?name=Sandbox");
272300
}
273301
}
274302
@@ -281,36 +309,24 @@
281309
if( !g.perm.RdWiki ){ login_needed(g.anon.RdWiki); return; }
282310
style_header("Wiki Help");
283311
wiki_standard_submenu(W_ALL_BUT(W_HELP));
284312
@ <h2>Wiki Links</h2>
285313
@ <ul>
286
- { char *zWikiHomePageName = db_get("index-page",0);
287
- if( zWikiHomePageName ){
288
- @ <li> %z(href("%R%s",zWikiHomePageName))
289
- @ %h(zWikiHomePageName)</a> wiki home page.</li>
290
- }
291
- }
292
- { char *zHomePageName = db_get("project-name",0);
293
- if( zHomePageName ){
294
- @ <li> %z(href("%R/wiki?name=%t",zHomePageName))
295
- @ %h(zHomePageName)</a> project home page.</li>
296
- }
297
- }
298314
@ <li> %z(href("%R/timeline?y=w"))Recent changes</a> to wiki pages.</li>
299315
@ <li> Formatting rules for %z(href("%R/wiki_rules"))Fossil Wiki</a> and for
300316
@ %z(href("%R/md_rules"))Markdown Wiki</a>.</li>
301317
@ <li> Use the %z(href("%R/wiki?name=Sandbox"))Sandbox</a>
302318
@ to experiment.</li>
303
- if( g.anon.NewWiki ){
319
+ if( g.perm.NewWiki ){
304320
@ <li> Create a %z(href("%R/wikinew"))new wiki page</a>.</li>
305
- if( g.anon.Write ){
321
+ if( g.perm.Write ){
306322
@ <li> Create a %z(href("%R/technoteedit"))new tech-note</a>.</li>
307323
}
308324
}
309325
@ <li> %z(href("%R/wcontent"))List of All Wiki Pages</a>
310326
@ available on this server.</li>
311
- if( g.anon.ModWiki ){
327
+ if( g.perm.ModWiki ){
312328
@ <li> %z(href("%R/modreq"))Tend to pending moderation requests</a></li>
313329
}
314330
if( search_restrict(SRCH_WIKI)!=0 ){
315331
@ <li> %z(href("%R/wikisrch"))Search</a> for wiki pages containing key
316332
@ words</li>
@@ -331,21 +347,106 @@
331347
style_header("Wiki Search");
332348
wiki_standard_submenu(W_HELP|W_LIST|W_SANDBOX);
333349
search_screen(SRCH_WIKI, 0);
334350
style_footer();
335351
}
352
+
353
+/* Return values from wiki_page_type() */
354
+#define WIKITYPE_UNKNOWN (-1)
355
+#define WIKITYPE_NORMAL 0
356
+#define WIKITYPE_BRANCH 1
357
+#define WIKITYPE_CHECKIN 2
358
+#define WIKITYPE_TAG 3
359
+
360
+/*
361
+** Figure out what type of wiki page we are dealing with.
362
+*/
363
+static int wiki_page_type(const char *zPageName){
364
+ if( db_get_boolean("wiki-about",1)==0 ){
365
+ return WIKITYPE_NORMAL;
366
+ }else
367
+ if( sqlite3_strglob("checkin/*", zPageName)==0
368
+ && db_exists("SELECT 1 FROM blob WHERE uuid=%Q",zPageName+8)
369
+ ){
370
+ return WIKITYPE_CHECKIN;
371
+ }else
372
+ if( sqlite3_strglob("branch/*", zPageName)==0 ){
373
+ return WIKITYPE_BRANCH;
374
+ }else
375
+ if( sqlite3_strglob("tag/*", zPageName)==0 ){
376
+ return WIKITYPE_TAG;
377
+ }
378
+ return WIKITYPE_NORMAL;
379
+}
380
+
381
+/*
382
+** Add an appropriate style_header() for either the /wiki or /wikiedit page
383
+** for zPageName.
384
+*/
385
+static int wiki_page_header(
386
+ int eType, /* Page type. -1 for unknown */
387
+ const char *zPageName, /* Name of the page */
388
+ const char *zExtra /* Extra prefix text on the page header */
389
+){
390
+ if( eType<0 ) eType = wiki_page_type(zPageName);
391
+ switch( eType ){
392
+ case WIKITYPE_NORMAL: {
393
+ style_header("%s%s", zExtra, zPageName);
394
+ break;
395
+ }
396
+ case WIKITYPE_CHECKIN: {
397
+ zPageName += 8;
398
+ style_header("Notes About Checkin %S", zPageName);
399
+ style_submenu_element("Checkin Timeline","%R/timeline?f=%s", zPageName);
400
+ style_submenu_element("Checkin Info","%R/info/%s", zPageName);
401
+ break;
402
+ }
403
+ case WIKITYPE_BRANCH: {
404
+ zPageName += 7;
405
+ style_header("Notes About Branch %h", zPageName);
406
+ style_submenu_element("Branch Timeline","%R/timeline?r=%t", zPageName);
407
+ break;
408
+ }
409
+ case WIKITYPE_TAG: {
410
+ zPageName += 4;
411
+ style_header("Notes About Tag %h", zPageName);
412
+ style_submenu_element("Tag Timeline","%R/timeline?t=%t",zPageName);
413
+ break;
414
+ }
415
+ }
416
+ return eType;
417
+}
418
+
419
+/*
420
+** Wiki pages with special names "branch/...", "checkin/...", and "tag/..."
421
+** requires perm.Write privilege in addition to perm.WrWiki in order
422
+** to write. This function determines whether the extra perm.Write
423
+** is required and available. Return true if writing to the wiki page
424
+** may proceed, and return false if permission is lacking.
425
+*/
426
+static int wiki_special_permission(const char *zPageName){
427
+ if( strncmp(zPageName,"branch/",7)!=0
428
+ && strncmp(zPageName,"checkin/",8)!=0
429
+ && strncmp(zPageName,"tag/",4)!=0
430
+ ){
431
+ return 1;
432
+ }
433
+ if( db_get_boolean("wiki-about",1)==0 ){
434
+ return 1;
435
+ }
436
+ return g.perm.Write;
437
+}
336438
337439
/*
338440
** WEBPAGE: wiki
339441
** URL: /wiki?name=PAGENAME
340442
*/
341443
void wiki_page(void){
342444
char *zTag;
343445
int rid = 0;
344446
int isSandbox;
345
- char *zUuid;
346
- unsigned submenuFlags = W_ALL;
447
+ unsigned submenuFlags = W_HELP;
347448
Blob wiki;
348449
Manifest *pWiki = 0;
349450
const char *zPageName;
350451
const char *zMimetype = 0;
351452
char *zBody = mprintf("%s","<i>Empty Page</i>");
@@ -385,43 +486,35 @@
385486
zMimetype = pWiki->zMimetype;
386487
}
387488
}
388489
zMimetype = wiki_filter_mimetypes(zMimetype);
389490
if( !g.isHome ){
390
- if( rid ){
391
- style_submenu_element("Diff", "%R/wdiff?name=%T&a=%d", zPageName, rid);
392
- zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid);
393
- style_submenu_element("Details", "%R/info/%s", zUuid);
394
- }
395
- if( (rid && g.anon.WrWiki) || (!rid && g.anon.NewWiki) ){
491
+ if( ((rid && g.perm.WrWiki) || (!rid && g.perm.NewWiki))
492
+ && wiki_special_permission(zPageName)
493
+ ){
396494
if( db_get_boolean("wysiwyg-wiki", 0) ){
397495
style_submenu_element("Edit", "%s/wikiedit?name=%T&wysiwyg=1",
398496
g.zTop, zPageName);
399497
}else{
400498
style_submenu_element("Edit", "%s/wikiedit?name=%T", g.zTop, zPageName);
401499
}
402500
}
403
- if( rid && g.anon.ApndWiki && g.anon.Attach ){
404
- style_submenu_element("Attach",
405
- "%s/attachadd?page=%T&from=%s/wiki%%3fname=%T",
406
- g.zTop, zPageName, g.zTop, zPageName);
407
- }
408
- if( rid && g.anon.ApndWiki ){
409
- style_submenu_element("Append", "%s/wikiappend?name=%T&mimetype=%s",
410
- g.zTop, zPageName, zMimetype);
411
- }
412501
if( g.perm.Hyperlink ){
413502
style_submenu_element("History", "%s/whistory?name=%T",
414503
g.zTop, zPageName);
415504
}
416505
}
417506
style_set_current_page("%T?name=%T", g.zPath, zPageName);
418
- style_header("%s", zPageName);
507
+ wiki_page_header(WIKITYPE_UNKNOWN, zPageName, "");
419508
wiki_standard_submenu(submenuFlags);
420
- blob_init(&wiki, zBody, -1);
421
- wiki_render_by_mimetype(&wiki, zMimetype);
422
- blob_reset(&wiki);
509
+ if( zBody[0]==0 ){
510
+ @ <i>This page has been deleted</i>
511
+ }else{
512
+ blob_init(&wiki, zBody, -1);
513
+ wiki_render_by_mimetype(&wiki, zMimetype);
514
+ blob_reset(&wiki);
515
+ }
423516
attachment_list(zPageName, "<hr /><h2>Attachments:</h2><ul>");
424517
manifest_destroy(pWiki);
425518
style_footer();
426519
}
427520
@@ -491,10 +584,12 @@
491584
const char *z;
492585
char *zBody = (char*)P("w");
493586
const char *zMimetype = wiki_filter_mimetypes(P("mimetype"));
494587
int isWysiwyg = P("wysiwyg")!=0;
495588
int goodCaptcha = 1;
589
+ int eType = WIKITYPE_UNKNOWN;
590
+ int havePreview = 0;
496591
497592
if( P("edit-wysiwyg")!=0 ){ isWysiwyg = 1; zBody = 0; }
498593
if( P("edit-markup")!=0 ){ isWysiwyg = 0; zBody = 0; }
499594
if( zBody ){
500595
if( isWysiwyg ){
@@ -525,10 +620,14 @@
525620
"SELECT rid FROM tagxref"
526621
" WHERE tagid=(SELECT tagid FROM tag WHERE tagname=%Q)"
527622
" ORDER BY mtime DESC", zTag
528623
);
529624
free(zTag);
625
+ if( !wiki_special_permission(zPageName) ){
626
+ login_needed(0);
627
+ return;
628
+ }
530629
if( (rid && !g.perm.WrWiki) || (!rid && !g.perm.NewWiki) ){
531630
login_needed(rid ? g.anon.WrWiki : g.anon.NewWiki);
532631
return;
533632
}
534633
if( zBody==0 && (pWiki = manifest_get(rid, CFTYPE_WIKI, 0))!=0 ){
@@ -575,20 +674,30 @@
575674
if( P("cancel")!=0 ){
576675
cgi_redirectf("wiki?name=%T", zPageName);
577676
return;
578677
}
579678
if( zBody==0 ){
580
- zBody = mprintf("<i>Empty Page</i>");
679
+ zBody = mprintf("");
581680
}
582681
style_set_current_page("%T?name=%T", g.zPath, zPageName);
583
- style_header("Edit: %s", zPageName);
682
+ eType = wiki_page_header(WIKITYPE_UNKNOWN, zPageName, "Edit: ");
683
+ if( rid && !isSandbox && g.perm.ApndWiki ){
684
+ if( g.perm.Attach ){
685
+ style_submenu_element("Attach",
686
+ "%s/attachadd?page=%T&from=%s/wiki%%3fname=%T",
687
+ g.zTop, zPageName, g.zTop, zPageName);
688
+ }
689
+ style_submenu_element("Append", "%s/wikiappend?name=%T&mimetype=%s",
690
+ g.zTop, zPageName, zMimetype);
691
+ }
584692
if( !goodCaptcha ){
585693
@ <p class="generalError">Error: Incorrect security code.</p>
586694
}
587695
blob_zero(&wiki);
588696
blob_append(&wiki, zBody, -1);
589
- if( P("preview")!=0 ){
697
+ if( P("preview")!=0 && zBody[0] ){
698
+ havePreview = 1;
590699
@ Preview:<hr />
591700
wiki_render_by_mimetype(&wiki, zMimetype);
592701
@ <hr />
593702
blob_reset(&wiki);
594703
}
@@ -597,23 +706,45 @@
597706
}
598707
if( n<20 ) n = 20;
599708
if( n>30 ) n = 30;
600709
if( !isWysiwyg ){
601710
/* Traditional markup-only editing */
711
+ char *zPlaceholder = 0;
712
+ switch( eType ){
713
+ case WIKITYPE_NORMAL: {
714
+ zPlaceholder = mprintf("Enter text for wiki page %s", zPageName);
715
+ break;
716
+ }
717
+ case WIKITYPE_BRANCH: {
718
+ zPlaceholder = mprintf("Enter notes about branch %s", zPageName+7);
719
+ break;
720
+ }
721
+ case WIKITYPE_CHECKIN: {
722
+ zPlaceholder = mprintf("Enter notes about check-in %.20s", zPageName+8);
723
+ break;
724
+ }
725
+ case WIKITYPE_TAG: {
726
+ zPlaceholder = mprintf("Enter notes about tag %s", zPageName+4);
727
+ break;
728
+ }
729
+ }
602730
form_begin(0, "%R/wikiedit");
603731
@ <div>Markup style:
604732
mimetype_option_menu(zMimetype);
605
- @ <br /><textarea name="w" class="wikiedit" cols="80"
606
- @ rows="%d(n)" wrap="virtual">%h(zBody)</textarea>
733
+ @ <br /><textarea name="w" class="wikiedit" cols="80" \
734
+ @ rows="%d(n)" wrap="virtual" placeholder="%h(zPlaceholder)">\
735
+ @ %h(zBody)</textarea>
607736
@ <br />
737
+ fossil_free(zPlaceholder);
608738
if( db_get_boolean("wysiwyg-wiki", 0) ){
609739
@ <input type="submit" name="edit-wysiwyg" value="Wysiwyg Editor" />
610740
}
611741
@ <input type="submit" name="preview" value="Preview Your Changes" />
612742
}else{
613743
/* Wysiwyg editing */
614744
Blob html, temp;
745
+ havePreview = 1;
615746
form_begin("", "%R/wikiedit");
616747
@ <div>
617748
@ <input type="hidden" name="wysiwyg" value="1" />
618749
blob_zero(&temp);
619750
wiki_convert(&wiki, &temp, 0);
@@ -624,11 +755,13 @@
624755
blob_reset(&html);
625756
@ <br />
626757
@ <input type="submit" name="edit-markup" value="Markup Editor" />
627758
}
628759
login_insert_csrf_secret();
629
- @ <input type="submit" name="submit" value="Apply These Changes" />
760
+ if( havePreview ){
761
+ @ <input type="submit" name="submit" value="Apply These Changes" />
762
+ }
630763
@ <input type="hidden" name="name" value="%h(zPageName)" />
631764
@ <input type="submit" name="cancel" value="Cancel" />
632765
@ </div>
633766
@ <script nonce="%h(style_nonce())">
634767
@ confirmOnClick("edit-wysiwyg", "Switching to WYSIWYG-mode\nwill erase your markup edits.\n\nContinue?");
@@ -851,90 +984,143 @@
851984
captcha_generate(0);
852985
@ </form>
853986
style_footer();
854987
}
855988
856
-/*
857
-** Name of the wiki history page being generated
858
-*/
859
-static const char *zWikiPageName;
860
-
861
-/*
862
-** Function called to output extra text at the end of each line in
863
-** a wiki history listing.
864
-*/
865
-static void wiki_history_extra(int rid){
866
- if( g.perm.Hyperlink && db_exists("SELECT 1 FROM tagxref WHERE rid=%d", rid) ){
867
- @ %z(href("%R/wdiff?name=%t&a=%d",zWikiPageName,rid))[diff]</a>
868
- }
869
-}
870
-
871989
/*
872990
** WEBPAGE: whistory
873991
** URL: /whistory?name=PAGENAME
874992
**
993
+** Additional parameters:
994
+**
995
+** showid Show RID values
996
+**
875997
** Show the complete change history for a single wiki page.
876998
*/
877999
void whistory_page(void){
8781000
Stmt q;
8791001
const char *zPageName;
1002
+ double rNow;
1003
+ int showRid;
8801004
login_check_credentials();
8811005
if( !g.perm.Hyperlink ){ login_needed(g.anon.Hyperlink); return; }
8821006
zPageName = PD("name","");
8831007
style_header("History Of %s", zPageName);
884
-
885
- db_prepare(&q, "%s AND event.objid IN "
886
- " (SELECT rid FROM tagxref WHERE tagid="
887
- "(SELECT tagid FROM tag WHERE tagname='wiki-%q')"
888
- " UNION SELECT attachid FROM attachment"
889
- " WHERE target=%Q)"
890
- "ORDER BY mtime DESC",
891
- timeline_query_for_www(), zPageName, zPageName);
892
- zWikiPageName = zPageName;
893
- www_print_timeline(&q, TIMELINE_ARTID, 0, 0, 0, wiki_history_extra);
1008
+ showRid = P("showid")!=0;
1009
+ db_prepare(&q,
1010
+ "SELECT"
1011
+ " event.mtime,"
1012
+ " blob.uuid,"
1013
+ " coalesce(event.euser,event.user),"
1014
+ " event.objid"
1015
+ " FROM event, blob, tag, tagxref"
1016
+ " WHERE event.type='w' AND blob.rid=event.objid"
1017
+ " AND tag.tagname='wiki-%q'"
1018
+ " AND tagxref.tagid=tag.tagid AND tagxref.srcid=event.objid"
1019
+ " ORDER BY event.mtime DESC",
1020
+ zPageName
1021
+ );
1022
+ @ <h2>History of <a href="%R/wiki?name=%T(zPageName)">%h(zPageName)</a></h2>
1023
+ @ <div class="brlist">
1024
+ @ <table>
1025
+ @ <thead><tr>
1026
+ @ <th>Age</th>
1027
+ @ <th>Hash</th>
1028
+ @ <th>User</th>
1029
+ if( showRid ){
1030
+ @ <th>RID</th>
1031
+ }
1032
+ @ <th>&nbsp;</th>
1033
+ @ </tr></thead><tbody>
1034
+ rNow = db_double(0.0, "SELECT julianday('now')");
1035
+ while( db_step(&q)==SQLITE_ROW ){
1036
+ double rMtime = db_column_double(&q, 0);
1037
+ const char *zUuid = db_column_text(&q, 1);
1038
+ const char *zUser = db_column_text(&q, 2);
1039
+ int wrid = db_column_int(&q, 3);
1040
+ /* sqlite3_int64 iMtime = (sqlite3_int64)(rMtime*86400.0); */
1041
+ char *zAge = human_readable_age(rNow - rMtime);
1042
+ @ <tr>
1043
+ /* @ <td data-sortkey="%016llx(iMtime)">%s(zAge)</td> */
1044
+ @ <td>%s(zAge)</td>
1045
+ fossil_free(zAge);
1046
+ @ <td>%z(href("%R/info/%s",zUuid))%S(zUuid)</a></td>
1047
+ @ <td>%h(zUser)</td>
1048
+ if( showRid ){
1049
+ @ <td>%z(href("%R/artifact/%S",zUuid))%d(wrid)</a></td>
1050
+ }
1051
+ @ <td>%z(href("%R/wdiff?id=%S",zUuid))diff</a></td>
1052
+ @ </tr>
1053
+ }
1054
+ @ </tbody></table></div>
8941055
db_finalize(&q);
1056
+ /* style_table_sorter(); */
8951057
style_footer();
8961058
}
8971059
8981060
/*
8991061
** WEBPAGE: wdiff
900
-** URL: /wdiff?name=PAGENAME&a=RID1&b=RID2&diff=DIFFTYPE
1062
+**
1063
+** Show the changes to a wiki page.
1064
+**
1065
+** Query parameters:
1066
+**
1067
+** id=HASH Hash prefix for the child version to be diffed.
1068
+** rid=INTEGER RecordID for the child version
1069
+** pid=HASH Hash prefix for the parent.
9011070
**
902
-** Show the difference between two wiki pages.
1071
+** The "id" query parameter is required. "pid" is optional. If "pid"
1072
+** is omitted, then the diff is against the first parent of the child.
9031073
*/
9041074
void wdiff_page(void){
905
- int rid1, rid2;
906
- const char *zPageName;
1075
+ const char *zId;
1076
+ const char *zPid;
9071077
Manifest *pW1, *pW2 = 0;
1078
+ int rid1, rid2, nextRid;
9081079
Blob w1, w2, d;
9091080
u64 diffFlags;
9101081
int diffType; /* 0: none, 1: unified, 2: side-by-side */
9111082
9121083
login_check_credentials();
913
- rid1 = atoi(PD("a","0"));
914
- if( !g.perm.Hyperlink ){ login_needed(g.anon.Hyperlink); return; }
915
- if( rid1==0 ) fossil_redirect_home();
916
- rid2 = atoi(PD("b","0"));
917
- zPageName = PD("name","");
918
- style_header("Changes To %s", zPageName);
919
-
920
- if( rid2==0 ){
921
- rid2 = db_int(0,
922
- "SELECT objid FROM event JOIN tagxref ON objid=rid AND tagxref.tagid="
923
- "(SELECT tagid FROM tag WHERE tagname='wiki-%q')"
924
- " WHERE event.mtime<(SELECT mtime FROM event WHERE objid=%d)"
925
- " ORDER BY event.mtime DESC LIMIT 1",
926
- zPageName, rid1
927
- );
928
- }
1084
+ if( !g.perm.RdWiki ){ login_needed(g.anon.RdWiki); return; }
1085
+ zId = P("id");
1086
+ if( zId==0 ){
1087
+ rid1 = atoi(PD("rid","0"));
1088
+ }else{
1089
+ rid1 = name_to_typed_rid(zId, "w");
1090
+ }
1091
+ zId = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid1);
9291092
pW1 = manifest_get(rid1, CFTYPE_WIKI, 0);
9301093
if( pW1==0 ) fossil_redirect_home();
9311094
blob_init(&w1, pW1->zWiki, -1);
932
- blob_zero(&w2);
933
- if( rid2 && (pW2 = manifest_get(rid2, CFTYPE_WIKI, 0))!=0 ){
1095
+ zPid = P("pid");
1096
+ if( zPid==0 && pW1->nParent ){
1097
+ zPid = pW1->azParent[0];
1098
+ }
1099
+ if( zPid ){
1100
+ char *zDate;
1101
+ rid2 = name_to_typed_rid(zPid, "w");
1102
+ pW2 = manifest_get(rid2, CFTYPE_WIKI, 0);
9341103
blob_init(&w2, pW2->zWiki, -1);
1104
+ @ <h2>Changes to \
1105
+ @ "%z(href("%R/whistory?name=%s",pW1->zWikiTitle))%h(pW1->zWikiTitle)</a>" \
1106
+ zDate = db_text(0, "SELECT datetime(%.16g)",pW2->rDate);
1107
+ @ between %z(href("%R/info/%s",zPid))%z(zDate)</a> \
1108
+ zDate = db_text(0, "SELECT datetime(%.16g)",pW1->rDate);
1109
+ @ and %z(href("%R/info/%s",zId))%z(zDate)</a></h2>
1110
+ style_submenu_element("Previous", "%R/wdiff?id=%S", zPid);
1111
+ }else{
1112
+ blob_zero(&w2);
1113
+ @ <h2>Initial version of \
1114
+ @ "%z(href("%R/whistory?name=%s",pW1->zWikiTitle))%h(pW1->zWikiTitle)</a>"\
1115
+ @ </h2>
1116
+ }
1117
+ nextRid = wiki_next(wiki_tagid(pW1->zWikiTitle),pW1->rDate);
1118
+ if( nextRid ){
1119
+ style_submenu_element("Next", "%R/wdiff?rid=%d", nextRid);
9351120
}
1121
+ style_header("Changes To %s", pW1->zWikiTitle);
9361122
blob_zero(&d);
9371123
diffFlags = construct_diff_flags(1);
9381124
diffType = atoi( PD("diff", "2") );
9391125
if( diffType==2 ){
9401126
diffFlags |= DIFF_SIDEBYSIDE;
@@ -959,37 +1145,50 @@
9591145
manifest_destroy(pW2);
9601146
style_footer();
9611147
}
9621148
9631149
/*
964
-** prepare()s pStmt with a query requesting:
1150
+** A query that returns information about all wiki pages.
9651151
**
966
-** - wiki page name
967
-** - tagxref (whatever that really is!)
1152
+** wname Name of the wiki page
1153
+** wsort Sort names by this label
1154
+** wrid rid of the most recent version of the page
1155
+** wmtime time most recent version was created
1156
+** wcnt Number of versions of this wiki page
9681157
**
969
-** Used by wcontent_page() and the JSON wiki code.
1158
+** The wrid value is zero for deleted wiki pages.
9701159
*/
971
-void wiki_prepare_page_list( Stmt * pStmt ){
972
- db_prepare(pStmt,
973
- "SELECT"
974
- " substr(tagname, 6) as name,"
975
- " (SELECT value FROM tagxref WHERE tagid=tag.tagid"
976
- " ORDER BY mtime DESC) as tagXref"
977
- " FROM tag WHERE tagname GLOB 'wiki-*'"
978
- " ORDER BY lower(tagname) /*sort*/"
979
- );
980
-}
1160
+static const char listAllWikiPages[] =
1161
+@ SELECT
1162
+@ substr(tag.tagname, 6) AS wname,
1163
+@ lower(substr(tag.tagname, 6)) AS sortname,
1164
+@ tagxref.value+0 AS wrid,
1165
+@ max(tagxref.mtime) AS wmtime,
1166
+@ count(*) AS wcnt
1167
+@ FROM
1168
+@ tag,
1169
+@ tagxref
1170
+@ WHERE
1171
+@ tag.tagname GLOB 'wiki-*'
1172
+@ AND tagxref.tagid=tag.tagid
1173
+@ GROUP BY 1
1174
+@ ORDER BY 2;
1175
+;
1176
+
9811177
/*
9821178
** WEBPAGE: wcontent
9831179
**
9841180
** all=1 Show deleted pages
1181
+** showid Show rid values for each page.
9851182
**
9861183
** List all available wiki pages with date created and last modified.
9871184
*/
9881185
void wcontent_page(void){
9891186
Stmt q;
1187
+ double rNow;
9901188
int showAll = P("all")!=0;
1189
+ int showRid = P("showid")!=0;
9911190
9921191
login_check_credentials();
9931192
if( !g.perm.RdWiki ){ login_needed(g.anon.RdWiki); return; }
9941193
style_header("Available Wiki Pages");
9951194
if( showAll ){
@@ -996,23 +1195,58 @@
9961195
style_submenu_element("Active", "%s/wcontent", g.zTop);
9971196
}else{
9981197
style_submenu_element("All", "%s/wcontent?all=1", g.zTop);
9991198
}
10001199
wiki_standard_submenu(W_ALL_BUT(W_LIST));
1001
- @ <ul>
1002
- wiki_prepare_page_list(&q);
1200
+ db_prepare(&q, listAllWikiPages/*works-like:""*/);
1201
+ @ <div class="brlist">
1202
+ @ <table class='sortable' data-column-types='tKN' data-init-sort='1'>
1203
+ @ <thead><tr>
1204
+ @ <th>Name</th>
1205
+ @ <th>Last Change</th>
1206
+ @ <th>Versions</th>
1207
+ if( showRid ){
1208
+ @ <th>RID</th>
1209
+ }
1210
+ @ </tr></thead><tbody>
1211
+ rNow = db_double(0.0, "SELECT julianday('now')");
10031212
while( db_step(&q)==SQLITE_ROW ){
1004
- const char *zName = db_column_text(&q, 0);
1005
- int size = db_column_int(&q, 1);
1006
- if( size>0 ){
1007
- @ <li>%z(href("%R/wiki?name=%T",zName))%h(zName)</a></li>
1008
- }else if( showAll ){
1009
- @ <li>%z(href("%R/wiki?name=%T",zName))<s>%h(zName)</s></a></li>
1010
- }
1011
- }
1213
+ const char *zWName = db_column_text(&q, 0);
1214
+ const char *zSort = db_column_text(&q, 1);
1215
+ int wrid = db_column_int(&q, 2);
1216
+ double rWmtime = db_column_double(&q, 3);
1217
+ sqlite3_int64 iMtime = (sqlite3_int64)(rWmtime*86400.0);
1218
+ char *zAge;
1219
+ int wcnt = db_column_int(&q, 4);
1220
+ char *zWDisplayName;
1221
+
1222
+ if( sqlite3_strglob("checkin/*", zWName)==0 ){
1223
+ zWDisplayName = mprintf("%.25s...", zWName);
1224
+ }else{
1225
+ zWDisplayName = mprintf("%s", zWName);
1226
+ }
1227
+ if( wrid==0 ){
1228
+ if( !showAll ) continue;
1229
+ @ <tr><td data-sortkey="%h(zSort)">\
1230
+ @ %z(href("%R/whistory?name=%T",zWName))<s>%h(zWDisplayName)</s></a></td>
1231
+ }else{
1232
+ @ <tr><td data=sortkey='%h(zSort)">\
1233
+ @ %z(href("%R/wiki?name=%T",zWName))%h(zWDisplayName)</a></td>
1234
+ }
1235
+ zAge = human_readable_age(rNow - rWmtime);
1236
+ @ <td data-sortkey="%016llx(iMtime)">%s(zAge)</td>
1237
+ fossil_free(zAge);
1238
+ @ <td>%z(href("%R/whistory?name=%T",zWName))%d(wcnt)</a></td>
1239
+ if( showRid ){
1240
+ @ <td>%d(wrid)</td>
1241
+ }
1242
+ @ </tr>
1243
+ fossil_free(zWDisplayName);
1244
+ }
1245
+ @ </tbody></table></div>
10121246
db_finalize(&q);
1013
- @ </ul>
1247
+ style_table_sorter();
10141248
style_footer();
10151249
}
10161250
10171251
/*
10181252
** WEBPAGE: wfind
@@ -1405,5 +1639,121 @@
14051639
blob_zero(&out);
14061640
blob_read_from_file(&in, g.argv[2], ExtFILE);
14071641
markdown_to_html(&in, 0, &out);
14081642
blob_write_to_file(&out, "-");
14091643
}
1644
+
1645
+/*
1646
+** Allowed flags for wiki_render_associated
1647
+*/
1648
+#if INTERFACE
1649
+#define WIKIASSOC_FULL_TITLE 0x00001 /* Full title */
1650
+#define WIKIASSOC_MENU_READ 0x00002 /* Add submenu link to read wiki */
1651
+#define WIKIASSOC_MENU_WRITE 0x00004 /* Add submenu link to add wiki */
1652
+#define WIKIASSOC_ALL 0x00007 /* All of the above */
1653
+#endif
1654
+
1655
+/*
1656
+** Show the default Section label for an associated wiki page.
1657
+*/
1658
+static void wiki_section_label(
1659
+ const char *zPrefix, /* "branch", "tag", or "checkin" */
1660
+ const char *zName, /* Name of the object */
1661
+ unsigned int mFlags /* Zero or more WIKIASSOC_* flags */
1662
+){
1663
+ if( (mFlags & WIKIASSOC_FULL_TITLE)==0 ){
1664
+ @ <div class="section">About</div>
1665
+ }else if( zPrefix[0]=='c' ){ /* checkin/... */
1666
+ @ <div class="section">About checkin %.20h(zName)</div>
1667
+ }else{
1668
+ @ <div class="section">About %s(zPrefix) %h(zName)</div>
1669
+ }
1670
+}
1671
+
1672
+/*
1673
+** Add an "Wiki" button in a submenu that links to the read-wiki page.
1674
+*/
1675
+static void wiki_submenu_to_read_wiki(
1676
+ const char *zPrefix, /* "branch", "tag", or "checkin" */
1677
+ const char *zName, /* Name of the object */
1678
+ unsigned int mFlags /* Zero or more WIKIASSOC_* flags */
1679
+){
1680
+ if( g.perm.RdWiki && (mFlags & WIKIASSOC_MENU_READ)!=0 ){
1681
+ style_submenu_element("Wiki", "%R/wiki?name=%s/%t", zPrefix, zName);
1682
+ }
1683
+}
1684
+
1685
+/*
1686
+** Check to see if there exists a wiki page with a name zPrefix/zName.
1687
+** If there is, then render a <div class='section'>..</div> and
1688
+** return true.
1689
+**
1690
+** If there is no such wiki page, return false.
1691
+*/
1692
+int wiki_render_associated(
1693
+ const char *zPrefix, /* "branch", "tag", or "checkin" */
1694
+ const char *zName, /* Name of the object */
1695
+ unsigned int mFlags /* Zero or more WIKIASSOC_* flags */
1696
+){
1697
+ int rid;
1698
+ Manifest *pWiki;
1699
+ if( !db_get_boolean("wiki-about",1) ) return 0;
1700
+ rid = db_int(0,
1701
+ "SELECT rid FROM tagxref"
1702
+ " WHERE tagid=(SELECT tagid FROM tag WHERE tagname='wiki-%q/%q')"
1703
+ " ORDER BY mtime DESC LIMIT 1",
1704
+ zPrefix, zName
1705
+ );
1706
+ if( rid==0 ){
1707
+ if( g.perm.WrWiki && g.perm.Write && (mFlags & WIKIASSOC_MENU_WRITE)!=0 ){
1708
+ style_submenu_element("Add Wiki", "%R/wikiedit?name=%s/%t",
1709
+ zPrefix, zName);
1710
+ }
1711
+ }
1712
+ pWiki = manifest_get(rid, CFTYPE_WIKI, 0);
1713
+ if( pWiki==0 ) return 0;
1714
+ if( fossil_strcmp(pWiki->zMimetype, "text/x-markdown")==0 ){
1715
+ Blob tail = BLOB_INITIALIZER;
1716
+ Blob title = BLOB_INITIALIZER;
1717
+ Blob markdown;
1718
+ blob_init(&markdown, pWiki->zWiki, -1);
1719
+ markdown_to_html(&markdown, &title, &tail);
1720
+ if( blob_size(&title) ){
1721
+ @ <div class="section">%h(blob_str(&title))</div>
1722
+ }else{
1723
+ wiki_section_label(zPrefix, zName, mFlags);
1724
+ }
1725
+ wiki_submenu_to_read_wiki(zPrefix, zName, mFlags);
1726
+ convert_href_and_output(&tail);
1727
+ blob_reset(&tail);
1728
+ blob_reset(&title);
1729
+ blob_reset(&markdown);
1730
+ }else if( fossil_strcmp(pWiki->zMimetype, "text/plain")==0 ){
1731
+ wiki_section_label(zPrefix, zName, mFlags);
1732
+ wiki_submenu_to_read_wiki(zPrefix, zName, mFlags);
1733
+ @ <pre>
1734
+ @ %h(pWiki->zWiki)
1735
+ @ </pre>
1736
+ }else{
1737
+ Blob tail = BLOB_INITIALIZER;
1738
+ Blob title = BLOB_INITIALIZER;
1739
+ Blob wiki;
1740
+ Blob *pBody;
1741
+ blob_init(&wiki, pWiki->zWiki, -1);
1742
+ if( wiki_find_title(&wiki, &title, &tail) ){
1743
+ @ <div class="section">%h(blob_str(&title))</div>
1744
+ pBody = &tail;
1745
+ }else{
1746
+ wiki_section_label(zPrefix, zName, mFlags);
1747
+ pBody = &wiki;
1748
+ }
1749
+ wiki_submenu_to_read_wiki(zPrefix, zName, mFlags);
1750
+ @ <div class="wiki">
1751
+ wiki_convert(pBody, 0, WIKI_BUTTONS);
1752
+ @ </div>
1753
+ blob_reset(&tail);
1754
+ blob_reset(&title);
1755
+ blob_reset(&wiki);
1756
+ }
1757
+ manifest_destroy(pWiki);
1758
+ return 1;
1759
+}
14101760
--- src/wiki.c
+++ src/wiki.c
@@ -71,10 +71,41 @@
71 style_footer();
72 return 1;
73 }
74 return 0;
75 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
77 /*
78 ** WEBPAGE: home
79 ** WEBPAGE: index
80 ** WEBPAGE: not_found
@@ -262,13 +293,10 @@
262 style_submenu_element("Help", "%R/wikihelp");
263 }
264 if( (ok & W_NEW)!=0 && g.anon.NewWiki ){
265 style_submenu_element("New", "%R/wikinew");
266 }
267 #if 0
268 if( (ok & W_BLOG)!=0
269 #endif
270 if( (ok & W_SANDBOX)!=0 ){
271 style_submenu_element("Sandbox", "%R/wiki?name=Sandbox");
272 }
273 }
274
@@ -281,36 +309,24 @@
281 if( !g.perm.RdWiki ){ login_needed(g.anon.RdWiki); return; }
282 style_header("Wiki Help");
283 wiki_standard_submenu(W_ALL_BUT(W_HELP));
284 @ <h2>Wiki Links</h2>
285 @ <ul>
286 { char *zWikiHomePageName = db_get("index-page",0);
287 if( zWikiHomePageName ){
288 @ <li> %z(href("%R%s",zWikiHomePageName))
289 @ %h(zWikiHomePageName)</a> wiki home page.</li>
290 }
291 }
292 { char *zHomePageName = db_get("project-name",0);
293 if( zHomePageName ){
294 @ <li> %z(href("%R/wiki?name=%t",zHomePageName))
295 @ %h(zHomePageName)</a> project home page.</li>
296 }
297 }
298 @ <li> %z(href("%R/timeline?y=w"))Recent changes</a> to wiki pages.</li>
299 @ <li> Formatting rules for %z(href("%R/wiki_rules"))Fossil Wiki</a> and for
300 @ %z(href("%R/md_rules"))Markdown Wiki</a>.</li>
301 @ <li> Use the %z(href("%R/wiki?name=Sandbox"))Sandbox</a>
302 @ to experiment.</li>
303 if( g.anon.NewWiki ){
304 @ <li> Create a %z(href("%R/wikinew"))new wiki page</a>.</li>
305 if( g.anon.Write ){
306 @ <li> Create a %z(href("%R/technoteedit"))new tech-note</a>.</li>
307 }
308 }
309 @ <li> %z(href("%R/wcontent"))List of All Wiki Pages</a>
310 @ available on this server.</li>
311 if( g.anon.ModWiki ){
312 @ <li> %z(href("%R/modreq"))Tend to pending moderation requests</a></li>
313 }
314 if( search_restrict(SRCH_WIKI)!=0 ){
315 @ <li> %z(href("%R/wikisrch"))Search</a> for wiki pages containing key
316 @ words</li>
@@ -331,21 +347,106 @@
331 style_header("Wiki Search");
332 wiki_standard_submenu(W_HELP|W_LIST|W_SANDBOX);
333 search_screen(SRCH_WIKI, 0);
334 style_footer();
335 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
336
337 /*
338 ** WEBPAGE: wiki
339 ** URL: /wiki?name=PAGENAME
340 */
341 void wiki_page(void){
342 char *zTag;
343 int rid = 0;
344 int isSandbox;
345 char *zUuid;
346 unsigned submenuFlags = W_ALL;
347 Blob wiki;
348 Manifest *pWiki = 0;
349 const char *zPageName;
350 const char *zMimetype = 0;
351 char *zBody = mprintf("%s","<i>Empty Page</i>");
@@ -385,43 +486,35 @@
385 zMimetype = pWiki->zMimetype;
386 }
387 }
388 zMimetype = wiki_filter_mimetypes(zMimetype);
389 if( !g.isHome ){
390 if( rid ){
391 style_submenu_element("Diff", "%R/wdiff?name=%T&a=%d", zPageName, rid);
392 zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid);
393 style_submenu_element("Details", "%R/info/%s", zUuid);
394 }
395 if( (rid && g.anon.WrWiki) || (!rid && g.anon.NewWiki) ){
396 if( db_get_boolean("wysiwyg-wiki", 0) ){
397 style_submenu_element("Edit", "%s/wikiedit?name=%T&wysiwyg=1",
398 g.zTop, zPageName);
399 }else{
400 style_submenu_element("Edit", "%s/wikiedit?name=%T", g.zTop, zPageName);
401 }
402 }
403 if( rid && g.anon.ApndWiki && g.anon.Attach ){
404 style_submenu_element("Attach",
405 "%s/attachadd?page=%T&from=%s/wiki%%3fname=%T",
406 g.zTop, zPageName, g.zTop, zPageName);
407 }
408 if( rid && g.anon.ApndWiki ){
409 style_submenu_element("Append", "%s/wikiappend?name=%T&mimetype=%s",
410 g.zTop, zPageName, zMimetype);
411 }
412 if( g.perm.Hyperlink ){
413 style_submenu_element("History", "%s/whistory?name=%T",
414 g.zTop, zPageName);
415 }
416 }
417 style_set_current_page("%T?name=%T", g.zPath, zPageName);
418 style_header("%s", zPageName);
419 wiki_standard_submenu(submenuFlags);
420 blob_init(&wiki, zBody, -1);
421 wiki_render_by_mimetype(&wiki, zMimetype);
422 blob_reset(&wiki);
 
 
 
 
423 attachment_list(zPageName, "<hr /><h2>Attachments:</h2><ul>");
424 manifest_destroy(pWiki);
425 style_footer();
426 }
427
@@ -491,10 +584,12 @@
491 const char *z;
492 char *zBody = (char*)P("w");
493 const char *zMimetype = wiki_filter_mimetypes(P("mimetype"));
494 int isWysiwyg = P("wysiwyg")!=0;
495 int goodCaptcha = 1;
 
 
496
497 if( P("edit-wysiwyg")!=0 ){ isWysiwyg = 1; zBody = 0; }
498 if( P("edit-markup")!=0 ){ isWysiwyg = 0; zBody = 0; }
499 if( zBody ){
500 if( isWysiwyg ){
@@ -525,10 +620,14 @@
525 "SELECT rid FROM tagxref"
526 " WHERE tagid=(SELECT tagid FROM tag WHERE tagname=%Q)"
527 " ORDER BY mtime DESC", zTag
528 );
529 free(zTag);
 
 
 
 
530 if( (rid && !g.perm.WrWiki) || (!rid && !g.perm.NewWiki) ){
531 login_needed(rid ? g.anon.WrWiki : g.anon.NewWiki);
532 return;
533 }
534 if( zBody==0 && (pWiki = manifest_get(rid, CFTYPE_WIKI, 0))!=0 ){
@@ -575,20 +674,30 @@
575 if( P("cancel")!=0 ){
576 cgi_redirectf("wiki?name=%T", zPageName);
577 return;
578 }
579 if( zBody==0 ){
580 zBody = mprintf("<i>Empty Page</i>");
581 }
582 style_set_current_page("%T?name=%T", g.zPath, zPageName);
583 style_header("Edit: %s", zPageName);
 
 
 
 
 
 
 
 
 
584 if( !goodCaptcha ){
585 @ <p class="generalError">Error: Incorrect security code.</p>
586 }
587 blob_zero(&wiki);
588 blob_append(&wiki, zBody, -1);
589 if( P("preview")!=0 ){
 
590 @ Preview:<hr />
591 wiki_render_by_mimetype(&wiki, zMimetype);
592 @ <hr />
593 blob_reset(&wiki);
594 }
@@ -597,23 +706,45 @@
597 }
598 if( n<20 ) n = 20;
599 if( n>30 ) n = 30;
600 if( !isWysiwyg ){
601 /* Traditional markup-only editing */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
602 form_begin(0, "%R/wikiedit");
603 @ <div>Markup style:
604 mimetype_option_menu(zMimetype);
605 @ <br /><textarea name="w" class="wikiedit" cols="80"
606 @ rows="%d(n)" wrap="virtual">%h(zBody)</textarea>
 
607 @ <br />
 
608 if( db_get_boolean("wysiwyg-wiki", 0) ){
609 @ <input type="submit" name="edit-wysiwyg" value="Wysiwyg Editor" />
610 }
611 @ <input type="submit" name="preview" value="Preview Your Changes" />
612 }else{
613 /* Wysiwyg editing */
614 Blob html, temp;
 
615 form_begin("", "%R/wikiedit");
616 @ <div>
617 @ <input type="hidden" name="wysiwyg" value="1" />
618 blob_zero(&temp);
619 wiki_convert(&wiki, &temp, 0);
@@ -624,11 +755,13 @@
624 blob_reset(&html);
625 @ <br />
626 @ <input type="submit" name="edit-markup" value="Markup Editor" />
627 }
628 login_insert_csrf_secret();
629 @ <input type="submit" name="submit" value="Apply These Changes" />
 
 
630 @ <input type="hidden" name="name" value="%h(zPageName)" />
631 @ <input type="submit" name="cancel" value="Cancel" />
632 @ </div>
633 @ <script nonce="%h(style_nonce())">
634 @ confirmOnClick("edit-wysiwyg", "Switching to WYSIWYG-mode\nwill erase your markup edits.\n\nContinue?");
@@ -851,90 +984,143 @@
851 captcha_generate(0);
852 @ </form>
853 style_footer();
854 }
855
856 /*
857 ** Name of the wiki history page being generated
858 */
859 static const char *zWikiPageName;
860
861 /*
862 ** Function called to output extra text at the end of each line in
863 ** a wiki history listing.
864 */
865 static void wiki_history_extra(int rid){
866 if( g.perm.Hyperlink && db_exists("SELECT 1 FROM tagxref WHERE rid=%d", rid) ){
867 @ %z(href("%R/wdiff?name=%t&a=%d",zWikiPageName,rid))[diff]</a>
868 }
869 }
870
871 /*
872 ** WEBPAGE: whistory
873 ** URL: /whistory?name=PAGENAME
874 **
 
 
 
 
875 ** Show the complete change history for a single wiki page.
876 */
877 void whistory_page(void){
878 Stmt q;
879 const char *zPageName;
 
 
880 login_check_credentials();
881 if( !g.perm.Hyperlink ){ login_needed(g.anon.Hyperlink); return; }
882 zPageName = PD("name","");
883 style_header("History Of %s", zPageName);
884
885 db_prepare(&q, "%s AND event.objid IN "
886 " (SELECT rid FROM tagxref WHERE tagid="
887 "(SELECT tagid FROM tag WHERE tagname='wiki-%q')"
888 " UNION SELECT attachid FROM attachment"
889 " WHERE target=%Q)"
890 "ORDER BY mtime DESC",
891 timeline_query_for_www(), zPageName, zPageName);
892 zWikiPageName = zPageName;
893 www_print_timeline(&q, TIMELINE_ARTID, 0, 0, 0, wiki_history_extra);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
894 db_finalize(&q);
 
895 style_footer();
896 }
897
898 /*
899 ** WEBPAGE: wdiff
900 ** URL: /wdiff?name=PAGENAME&a=RID1&b=RID2&diff=DIFFTYPE
 
 
 
 
 
 
 
901 **
902 ** Show the difference between two wiki pages.
 
903 */
904 void wdiff_page(void){
905 int rid1, rid2;
906 const char *zPageName;
907 Manifest *pW1, *pW2 = 0;
 
908 Blob w1, w2, d;
909 u64 diffFlags;
910 int diffType; /* 0: none, 1: unified, 2: side-by-side */
911
912 login_check_credentials();
913 rid1 = atoi(PD("a","0"));
914 if( !g.perm.Hyperlink ){ login_needed(g.anon.Hyperlink); return; }
915 if( rid1==0 ) fossil_redirect_home();
916 rid2 = atoi(PD("b","0"));
917 zPageName = PD("name","");
918 style_header("Changes To %s", zPageName);
919
920 if( rid2==0 ){
921 rid2 = db_int(0,
922 "SELECT objid FROM event JOIN tagxref ON objid=rid AND tagxref.tagid="
923 "(SELECT tagid FROM tag WHERE tagname='wiki-%q')"
924 " WHERE event.mtime<(SELECT mtime FROM event WHERE objid=%d)"
925 " ORDER BY event.mtime DESC LIMIT 1",
926 zPageName, rid1
927 );
928 }
929 pW1 = manifest_get(rid1, CFTYPE_WIKI, 0);
930 if( pW1==0 ) fossil_redirect_home();
931 blob_init(&w1, pW1->zWiki, -1);
932 blob_zero(&w2);
933 if( rid2 && (pW2 = manifest_get(rid2, CFTYPE_WIKI, 0))!=0 ){
 
 
 
 
 
 
934 blob_init(&w2, pW2->zWiki, -1);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
935 }
 
936 blob_zero(&d);
937 diffFlags = construct_diff_flags(1);
938 diffType = atoi( PD("diff", "2") );
939 if( diffType==2 ){
940 diffFlags |= DIFF_SIDEBYSIDE;
@@ -959,37 +1145,50 @@
959 manifest_destroy(pW2);
960 style_footer();
961 }
962
963 /*
964 ** prepare()s pStmt with a query requesting:
965 **
966 ** - wiki page name
967 ** - tagxref (whatever that really is!)
 
 
 
968 **
969 ** Used by wcontent_page() and the JSON wiki code.
970 */
971 void wiki_prepare_page_list( Stmt * pStmt ){
972 db_prepare(pStmt,
973 "SELECT"
974 " substr(tagname, 6) as name,"
975 " (SELECT value FROM tagxref WHERE tagid=tag.tagid"
976 " ORDER BY mtime DESC) as tagXref"
977 " FROM tag WHERE tagname GLOB 'wiki-*'"
978 " ORDER BY lower(tagname) /*sort*/"
979 );
980 }
 
 
 
 
 
 
 
981 /*
982 ** WEBPAGE: wcontent
983 **
984 ** all=1 Show deleted pages
 
985 **
986 ** List all available wiki pages with date created and last modified.
987 */
988 void wcontent_page(void){
989 Stmt q;
 
990 int showAll = P("all")!=0;
 
991
992 login_check_credentials();
993 if( !g.perm.RdWiki ){ login_needed(g.anon.RdWiki); return; }
994 style_header("Available Wiki Pages");
995 if( showAll ){
@@ -996,23 +1195,58 @@
996 style_submenu_element("Active", "%s/wcontent", g.zTop);
997 }else{
998 style_submenu_element("All", "%s/wcontent?all=1", g.zTop);
999 }
1000 wiki_standard_submenu(W_ALL_BUT(W_LIST));
1001 @ <ul>
1002 wiki_prepare_page_list(&q);
 
 
 
 
 
 
 
 
 
 
1003 while( db_step(&q)==SQLITE_ROW ){
1004 const char *zName = db_column_text(&q, 0);
1005 int size = db_column_int(&q, 1);
1006 if( size>0 ){
1007 @ <li>%z(href("%R/wiki?name=%T",zName))%h(zName)</a></li>
1008 }else if( showAll ){
1009 @ <li>%z(href("%R/wiki?name=%T",zName))<s>%h(zName)</s></a></li>
1010 }
1011 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1012 db_finalize(&q);
1013 @ </ul>
1014 style_footer();
1015 }
1016
1017 /*
1018 ** WEBPAGE: wfind
@@ -1405,5 +1639,121 @@
1405 blob_zero(&out);
1406 blob_read_from_file(&in, g.argv[2], ExtFILE);
1407 markdown_to_html(&in, 0, &out);
1408 blob_write_to_file(&out, "-");
1409 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1410
--- src/wiki.c
+++ src/wiki.c
@@ -71,10 +71,41 @@
71 style_footer();
72 return 1;
73 }
74 return 0;
75 }
76
77 /*
78 ** Return the tagid associated with a particular wiki page.
79 */
80 int wiki_tagid(const char *zPageName){
81 return db_int(0, "SELECT tagid FROM tag WHERE tagname='wiki-%q'",zPageName);
82 }
83 int wiki_tagid2(const char *zPrefix, const char *zPageName){
84 return db_int(0, "SELECT tagid FROM tag WHERE tagname='wiki-%q/%q'",
85 zPrefix, zPageName);
86 }
87
88 /*
89 ** Return the RID of the next or previous version of a wiki page.
90 ** Return 0 if rid is the last/first version.
91 */
92 int wiki_next(int tagid, double mtime){
93 return db_int(0,
94 "SELECT srcid FROM tagxref"
95 " WHERE tagid=%d AND mtime>%.16g"
96 " ORDER BY mtime ASC LIMIT 1",
97 tagid, mtime);
98 }
99 int wiki_prev(int tagid, double mtime){
100 return db_int(0,
101 "SELECT srcid FROM tagxref"
102 " WHERE tagid=%d AND mtime<%.16g"
103 " ORDER BY mtime DESC LIMIT 1",
104 tagid, mtime);
105 }
106
107
108 /*
109 ** WEBPAGE: home
110 ** WEBPAGE: index
111 ** WEBPAGE: not_found
@@ -262,13 +293,10 @@
293 style_submenu_element("Help", "%R/wikihelp");
294 }
295 if( (ok & W_NEW)!=0 && g.anon.NewWiki ){
296 style_submenu_element("New", "%R/wikinew");
297 }
 
 
 
298 if( (ok & W_SANDBOX)!=0 ){
299 style_submenu_element("Sandbox", "%R/wiki?name=Sandbox");
300 }
301 }
302
@@ -281,36 +309,24 @@
309 if( !g.perm.RdWiki ){ login_needed(g.anon.RdWiki); return; }
310 style_header("Wiki Help");
311 wiki_standard_submenu(W_ALL_BUT(W_HELP));
312 @ <h2>Wiki Links</h2>
313 @ <ul>
 
 
 
 
 
 
 
 
 
 
 
 
314 @ <li> %z(href("%R/timeline?y=w"))Recent changes</a> to wiki pages.</li>
315 @ <li> Formatting rules for %z(href("%R/wiki_rules"))Fossil Wiki</a> and for
316 @ %z(href("%R/md_rules"))Markdown Wiki</a>.</li>
317 @ <li> Use the %z(href("%R/wiki?name=Sandbox"))Sandbox</a>
318 @ to experiment.</li>
319 if( g.perm.NewWiki ){
320 @ <li> Create a %z(href("%R/wikinew"))new wiki page</a>.</li>
321 if( g.perm.Write ){
322 @ <li> Create a %z(href("%R/technoteedit"))new tech-note</a>.</li>
323 }
324 }
325 @ <li> %z(href("%R/wcontent"))List of All Wiki Pages</a>
326 @ available on this server.</li>
327 if( g.perm.ModWiki ){
328 @ <li> %z(href("%R/modreq"))Tend to pending moderation requests</a></li>
329 }
330 if( search_restrict(SRCH_WIKI)!=0 ){
331 @ <li> %z(href("%R/wikisrch"))Search</a> for wiki pages containing key
332 @ words</li>
@@ -331,21 +347,106 @@
347 style_header("Wiki Search");
348 wiki_standard_submenu(W_HELP|W_LIST|W_SANDBOX);
349 search_screen(SRCH_WIKI, 0);
350 style_footer();
351 }
352
353 /* Return values from wiki_page_type() */
354 #define WIKITYPE_UNKNOWN (-1)
355 #define WIKITYPE_NORMAL 0
356 #define WIKITYPE_BRANCH 1
357 #define WIKITYPE_CHECKIN 2
358 #define WIKITYPE_TAG 3
359
360 /*
361 ** Figure out what type of wiki page we are dealing with.
362 */
363 static int wiki_page_type(const char *zPageName){
364 if( db_get_boolean("wiki-about",1)==0 ){
365 return WIKITYPE_NORMAL;
366 }else
367 if( sqlite3_strglob("checkin/*", zPageName)==0
368 && db_exists("SELECT 1 FROM blob WHERE uuid=%Q",zPageName+8)
369 ){
370 return WIKITYPE_CHECKIN;
371 }else
372 if( sqlite3_strglob("branch/*", zPageName)==0 ){
373 return WIKITYPE_BRANCH;
374 }else
375 if( sqlite3_strglob("tag/*", zPageName)==0 ){
376 return WIKITYPE_TAG;
377 }
378 return WIKITYPE_NORMAL;
379 }
380
381 /*
382 ** Add an appropriate style_header() for either the /wiki or /wikiedit page
383 ** for zPageName.
384 */
385 static int wiki_page_header(
386 int eType, /* Page type. -1 for unknown */
387 const char *zPageName, /* Name of the page */
388 const char *zExtra /* Extra prefix text on the page header */
389 ){
390 if( eType<0 ) eType = wiki_page_type(zPageName);
391 switch( eType ){
392 case WIKITYPE_NORMAL: {
393 style_header("%s%s", zExtra, zPageName);
394 break;
395 }
396 case WIKITYPE_CHECKIN: {
397 zPageName += 8;
398 style_header("Notes About Checkin %S", zPageName);
399 style_submenu_element("Checkin Timeline","%R/timeline?f=%s", zPageName);
400 style_submenu_element("Checkin Info","%R/info/%s", zPageName);
401 break;
402 }
403 case WIKITYPE_BRANCH: {
404 zPageName += 7;
405 style_header("Notes About Branch %h", zPageName);
406 style_submenu_element("Branch Timeline","%R/timeline?r=%t", zPageName);
407 break;
408 }
409 case WIKITYPE_TAG: {
410 zPageName += 4;
411 style_header("Notes About Tag %h", zPageName);
412 style_submenu_element("Tag Timeline","%R/timeline?t=%t",zPageName);
413 break;
414 }
415 }
416 return eType;
417 }
418
419 /*
420 ** Wiki pages with special names "branch/...", "checkin/...", and "tag/..."
421 ** requires perm.Write privilege in addition to perm.WrWiki in order
422 ** to write. This function determines whether the extra perm.Write
423 ** is required and available. Return true if writing to the wiki page
424 ** may proceed, and return false if permission is lacking.
425 */
426 static int wiki_special_permission(const char *zPageName){
427 if( strncmp(zPageName,"branch/",7)!=0
428 && strncmp(zPageName,"checkin/",8)!=0
429 && strncmp(zPageName,"tag/",4)!=0
430 ){
431 return 1;
432 }
433 if( db_get_boolean("wiki-about",1)==0 ){
434 return 1;
435 }
436 return g.perm.Write;
437 }
438
439 /*
440 ** WEBPAGE: wiki
441 ** URL: /wiki?name=PAGENAME
442 */
443 void wiki_page(void){
444 char *zTag;
445 int rid = 0;
446 int isSandbox;
447 unsigned submenuFlags = W_HELP;
 
448 Blob wiki;
449 Manifest *pWiki = 0;
450 const char *zPageName;
451 const char *zMimetype = 0;
452 char *zBody = mprintf("%s","<i>Empty Page</i>");
@@ -385,43 +486,35 @@
486 zMimetype = pWiki->zMimetype;
487 }
488 }
489 zMimetype = wiki_filter_mimetypes(zMimetype);
490 if( !g.isHome ){
491 if( ((rid && g.perm.WrWiki) || (!rid && g.perm.NewWiki))
492 && wiki_special_permission(zPageName)
493 ){
 
 
 
494 if( db_get_boolean("wysiwyg-wiki", 0) ){
495 style_submenu_element("Edit", "%s/wikiedit?name=%T&wysiwyg=1",
496 g.zTop, zPageName);
497 }else{
498 style_submenu_element("Edit", "%s/wikiedit?name=%T", g.zTop, zPageName);
499 }
500 }
 
 
 
 
 
 
 
 
 
501 if( g.perm.Hyperlink ){
502 style_submenu_element("History", "%s/whistory?name=%T",
503 g.zTop, zPageName);
504 }
505 }
506 style_set_current_page("%T?name=%T", g.zPath, zPageName);
507 wiki_page_header(WIKITYPE_UNKNOWN, zPageName, "");
508 wiki_standard_submenu(submenuFlags);
509 if( zBody[0]==0 ){
510 @ <i>This page has been deleted</i>
511 }else{
512 blob_init(&wiki, zBody, -1);
513 wiki_render_by_mimetype(&wiki, zMimetype);
514 blob_reset(&wiki);
515 }
516 attachment_list(zPageName, "<hr /><h2>Attachments:</h2><ul>");
517 manifest_destroy(pWiki);
518 style_footer();
519 }
520
@@ -491,10 +584,12 @@
584 const char *z;
585 char *zBody = (char*)P("w");
586 const char *zMimetype = wiki_filter_mimetypes(P("mimetype"));
587 int isWysiwyg = P("wysiwyg")!=0;
588 int goodCaptcha = 1;
589 int eType = WIKITYPE_UNKNOWN;
590 int havePreview = 0;
591
592 if( P("edit-wysiwyg")!=0 ){ isWysiwyg = 1; zBody = 0; }
593 if( P("edit-markup")!=0 ){ isWysiwyg = 0; zBody = 0; }
594 if( zBody ){
595 if( isWysiwyg ){
@@ -525,10 +620,14 @@
620 "SELECT rid FROM tagxref"
621 " WHERE tagid=(SELECT tagid FROM tag WHERE tagname=%Q)"
622 " ORDER BY mtime DESC", zTag
623 );
624 free(zTag);
625 if( !wiki_special_permission(zPageName) ){
626 login_needed(0);
627 return;
628 }
629 if( (rid && !g.perm.WrWiki) || (!rid && !g.perm.NewWiki) ){
630 login_needed(rid ? g.anon.WrWiki : g.anon.NewWiki);
631 return;
632 }
633 if( zBody==0 && (pWiki = manifest_get(rid, CFTYPE_WIKI, 0))!=0 ){
@@ -575,20 +674,30 @@
674 if( P("cancel")!=0 ){
675 cgi_redirectf("wiki?name=%T", zPageName);
676 return;
677 }
678 if( zBody==0 ){
679 zBody = mprintf("");
680 }
681 style_set_current_page("%T?name=%T", g.zPath, zPageName);
682 eType = wiki_page_header(WIKITYPE_UNKNOWN, zPageName, "Edit: ");
683 if( rid && !isSandbox && g.perm.ApndWiki ){
684 if( g.perm.Attach ){
685 style_submenu_element("Attach",
686 "%s/attachadd?page=%T&from=%s/wiki%%3fname=%T",
687 g.zTop, zPageName, g.zTop, zPageName);
688 }
689 style_submenu_element("Append", "%s/wikiappend?name=%T&mimetype=%s",
690 g.zTop, zPageName, zMimetype);
691 }
692 if( !goodCaptcha ){
693 @ <p class="generalError">Error: Incorrect security code.</p>
694 }
695 blob_zero(&wiki);
696 blob_append(&wiki, zBody, -1);
697 if( P("preview")!=0 && zBody[0] ){
698 havePreview = 1;
699 @ Preview:<hr />
700 wiki_render_by_mimetype(&wiki, zMimetype);
701 @ <hr />
702 blob_reset(&wiki);
703 }
@@ -597,23 +706,45 @@
706 }
707 if( n<20 ) n = 20;
708 if( n>30 ) n = 30;
709 if( !isWysiwyg ){
710 /* Traditional markup-only editing */
711 char *zPlaceholder = 0;
712 switch( eType ){
713 case WIKITYPE_NORMAL: {
714 zPlaceholder = mprintf("Enter text for wiki page %s", zPageName);
715 break;
716 }
717 case WIKITYPE_BRANCH: {
718 zPlaceholder = mprintf("Enter notes about branch %s", zPageName+7);
719 break;
720 }
721 case WIKITYPE_CHECKIN: {
722 zPlaceholder = mprintf("Enter notes about check-in %.20s", zPageName+8);
723 break;
724 }
725 case WIKITYPE_TAG: {
726 zPlaceholder = mprintf("Enter notes about tag %s", zPageName+4);
727 break;
728 }
729 }
730 form_begin(0, "%R/wikiedit");
731 @ <div>Markup style:
732 mimetype_option_menu(zMimetype);
733 @ <br /><textarea name="w" class="wikiedit" cols="80" \
734 @ rows="%d(n)" wrap="virtual" placeholder="%h(zPlaceholder)">\
735 @ %h(zBody)</textarea>
736 @ <br />
737 fossil_free(zPlaceholder);
738 if( db_get_boolean("wysiwyg-wiki", 0) ){
739 @ <input type="submit" name="edit-wysiwyg" value="Wysiwyg Editor" />
740 }
741 @ <input type="submit" name="preview" value="Preview Your Changes" />
742 }else{
743 /* Wysiwyg editing */
744 Blob html, temp;
745 havePreview = 1;
746 form_begin("", "%R/wikiedit");
747 @ <div>
748 @ <input type="hidden" name="wysiwyg" value="1" />
749 blob_zero(&temp);
750 wiki_convert(&wiki, &temp, 0);
@@ -624,11 +755,13 @@
755 blob_reset(&html);
756 @ <br />
757 @ <input type="submit" name="edit-markup" value="Markup Editor" />
758 }
759 login_insert_csrf_secret();
760 if( havePreview ){
761 @ <input type="submit" name="submit" value="Apply These Changes" />
762 }
763 @ <input type="hidden" name="name" value="%h(zPageName)" />
764 @ <input type="submit" name="cancel" value="Cancel" />
765 @ </div>
766 @ <script nonce="%h(style_nonce())">
767 @ confirmOnClick("edit-wysiwyg", "Switching to WYSIWYG-mode\nwill erase your markup edits.\n\nContinue?");
@@ -851,90 +984,143 @@
984 captcha_generate(0);
985 @ </form>
986 style_footer();
987 }
988
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
989 /*
990 ** WEBPAGE: whistory
991 ** URL: /whistory?name=PAGENAME
992 **
993 ** Additional parameters:
994 **
995 ** showid Show RID values
996 **
997 ** Show the complete change history for a single wiki page.
998 */
999 void whistory_page(void){
1000 Stmt q;
1001 const char *zPageName;
1002 double rNow;
1003 int showRid;
1004 login_check_credentials();
1005 if( !g.perm.Hyperlink ){ login_needed(g.anon.Hyperlink); return; }
1006 zPageName = PD("name","");
1007 style_header("History Of %s", zPageName);
1008 showRid = P("showid")!=0;
1009 db_prepare(&q,
1010 "SELECT"
1011 " event.mtime,"
1012 " blob.uuid,"
1013 " coalesce(event.euser,event.user),"
1014 " event.objid"
1015 " FROM event, blob, tag, tagxref"
1016 " WHERE event.type='w' AND blob.rid=event.objid"
1017 " AND tag.tagname='wiki-%q'"
1018 " AND tagxref.tagid=tag.tagid AND tagxref.srcid=event.objid"
1019 " ORDER BY event.mtime DESC",
1020 zPageName
1021 );
1022 @ <h2>History of <a href="%R/wiki?name=%T(zPageName)">%h(zPageName)</a></h2>
1023 @ <div class="brlist">
1024 @ <table>
1025 @ <thead><tr>
1026 @ <th>Age</th>
1027 @ <th>Hash</th>
1028 @ <th>User</th>
1029 if( showRid ){
1030 @ <th>RID</th>
1031 }
1032 @ <th>&nbsp;</th>
1033 @ </tr></thead><tbody>
1034 rNow = db_double(0.0, "SELECT julianday('now')");
1035 while( db_step(&q)==SQLITE_ROW ){
1036 double rMtime = db_column_double(&q, 0);
1037 const char *zUuid = db_column_text(&q, 1);
1038 const char *zUser = db_column_text(&q, 2);
1039 int wrid = db_column_int(&q, 3);
1040 /* sqlite3_int64 iMtime = (sqlite3_int64)(rMtime*86400.0); */
1041 char *zAge = human_readable_age(rNow - rMtime);
1042 @ <tr>
1043 /* @ <td data-sortkey="%016llx(iMtime)">%s(zAge)</td> */
1044 @ <td>%s(zAge)</td>
1045 fossil_free(zAge);
1046 @ <td>%z(href("%R/info/%s",zUuid))%S(zUuid)</a></td>
1047 @ <td>%h(zUser)</td>
1048 if( showRid ){
1049 @ <td>%z(href("%R/artifact/%S",zUuid))%d(wrid)</a></td>
1050 }
1051 @ <td>%z(href("%R/wdiff?id=%S",zUuid))diff</a></td>
1052 @ </tr>
1053 }
1054 @ </tbody></table></div>
1055 db_finalize(&q);
1056 /* style_table_sorter(); */
1057 style_footer();
1058 }
1059
1060 /*
1061 ** WEBPAGE: wdiff
1062 **
1063 ** Show the changes to a wiki page.
1064 **
1065 ** Query parameters:
1066 **
1067 ** id=HASH Hash prefix for the child version to be diffed.
1068 ** rid=INTEGER RecordID for the child version
1069 ** pid=HASH Hash prefix for the parent.
1070 **
1071 ** The "id" query parameter is required. "pid" is optional. If "pid"
1072 ** is omitted, then the diff is against the first parent of the child.
1073 */
1074 void wdiff_page(void){
1075 const char *zId;
1076 const char *zPid;
1077 Manifest *pW1, *pW2 = 0;
1078 int rid1, rid2, nextRid;
1079 Blob w1, w2, d;
1080 u64 diffFlags;
1081 int diffType; /* 0: none, 1: unified, 2: side-by-side */
1082
1083 login_check_credentials();
1084 if( !g.perm.RdWiki ){ login_needed(g.anon.RdWiki); return; }
1085 zId = P("id");
1086 if( zId==0 ){
1087 rid1 = atoi(PD("rid","0"));
1088 }else{
1089 rid1 = name_to_typed_rid(zId, "w");
1090 }
1091 zId = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid1);
 
 
 
 
 
 
 
 
1092 pW1 = manifest_get(rid1, CFTYPE_WIKI, 0);
1093 if( pW1==0 ) fossil_redirect_home();
1094 blob_init(&w1, pW1->zWiki, -1);
1095 zPid = P("pid");
1096 if( zPid==0 && pW1->nParent ){
1097 zPid = pW1->azParent[0];
1098 }
1099 if( zPid ){
1100 char *zDate;
1101 rid2 = name_to_typed_rid(zPid, "w");
1102 pW2 = manifest_get(rid2, CFTYPE_WIKI, 0);
1103 blob_init(&w2, pW2->zWiki, -1);
1104 @ <h2>Changes to \
1105 @ "%z(href("%R/whistory?name=%s",pW1->zWikiTitle))%h(pW1->zWikiTitle)</a>" \
1106 zDate = db_text(0, "SELECT datetime(%.16g)",pW2->rDate);
1107 @ between %z(href("%R/info/%s",zPid))%z(zDate)</a> \
1108 zDate = db_text(0, "SELECT datetime(%.16g)",pW1->rDate);
1109 @ and %z(href("%R/info/%s",zId))%z(zDate)</a></h2>
1110 style_submenu_element("Previous", "%R/wdiff?id=%S", zPid);
1111 }else{
1112 blob_zero(&w2);
1113 @ <h2>Initial version of \
1114 @ "%z(href("%R/whistory?name=%s",pW1->zWikiTitle))%h(pW1->zWikiTitle)</a>"\
1115 @ </h2>
1116 }
1117 nextRid = wiki_next(wiki_tagid(pW1->zWikiTitle),pW1->rDate);
1118 if( nextRid ){
1119 style_submenu_element("Next", "%R/wdiff?rid=%d", nextRid);
1120 }
1121 style_header("Changes To %s", pW1->zWikiTitle);
1122 blob_zero(&d);
1123 diffFlags = construct_diff_flags(1);
1124 diffType = atoi( PD("diff", "2") );
1125 if( diffType==2 ){
1126 diffFlags |= DIFF_SIDEBYSIDE;
@@ -959,37 +1145,50 @@
1145 manifest_destroy(pW2);
1146 style_footer();
1147 }
1148
1149 /*
1150 ** A query that returns information about all wiki pages.
1151 **
1152 ** wname Name of the wiki page
1153 ** wsort Sort names by this label
1154 ** wrid rid of the most recent version of the page
1155 ** wmtime time most recent version was created
1156 ** wcnt Number of versions of this wiki page
1157 **
1158 ** The wrid value is zero for deleted wiki pages.
1159 */
1160 static const char listAllWikiPages[] =
1161 @ SELECT
1162 @ substr(tag.tagname, 6) AS wname,
1163 @ lower(substr(tag.tagname, 6)) AS sortname,
1164 @ tagxref.value+0 AS wrid,
1165 @ max(tagxref.mtime) AS wmtime,
1166 @ count(*) AS wcnt
1167 @ FROM
1168 @ tag,
1169 @ tagxref
1170 @ WHERE
1171 @ tag.tagname GLOB 'wiki-*'
1172 @ AND tagxref.tagid=tag.tagid
1173 @ GROUP BY 1
1174 @ ORDER BY 2;
1175 ;
1176
1177 /*
1178 ** WEBPAGE: wcontent
1179 **
1180 ** all=1 Show deleted pages
1181 ** showid Show rid values for each page.
1182 **
1183 ** List all available wiki pages with date created and last modified.
1184 */
1185 void wcontent_page(void){
1186 Stmt q;
1187 double rNow;
1188 int showAll = P("all")!=0;
1189 int showRid = P("showid")!=0;
1190
1191 login_check_credentials();
1192 if( !g.perm.RdWiki ){ login_needed(g.anon.RdWiki); return; }
1193 style_header("Available Wiki Pages");
1194 if( showAll ){
@@ -996,23 +1195,58 @@
1195 style_submenu_element("Active", "%s/wcontent", g.zTop);
1196 }else{
1197 style_submenu_element("All", "%s/wcontent?all=1", g.zTop);
1198 }
1199 wiki_standard_submenu(W_ALL_BUT(W_LIST));
1200 db_prepare(&q, listAllWikiPages/*works-like:""*/);
1201 @ <div class="brlist">
1202 @ <table class='sortable' data-column-types='tKN' data-init-sort='1'>
1203 @ <thead><tr>
1204 @ <th>Name</th>
1205 @ <th>Last Change</th>
1206 @ <th>Versions</th>
1207 if( showRid ){
1208 @ <th>RID</th>
1209 }
1210 @ </tr></thead><tbody>
1211 rNow = db_double(0.0, "SELECT julianday('now')");
1212 while( db_step(&q)==SQLITE_ROW ){
1213 const char *zWName = db_column_text(&q, 0);
1214 const char *zSort = db_column_text(&q, 1);
1215 int wrid = db_column_int(&q, 2);
1216 double rWmtime = db_column_double(&q, 3);
1217 sqlite3_int64 iMtime = (sqlite3_int64)(rWmtime*86400.0);
1218 char *zAge;
1219 int wcnt = db_column_int(&q, 4);
1220 char *zWDisplayName;
1221
1222 if( sqlite3_strglob("checkin/*", zWName)==0 ){
1223 zWDisplayName = mprintf("%.25s...", zWName);
1224 }else{
1225 zWDisplayName = mprintf("%s", zWName);
1226 }
1227 if( wrid==0 ){
1228 if( !showAll ) continue;
1229 @ <tr><td data-sortkey="%h(zSort)">\
1230 @ %z(href("%R/whistory?name=%T",zWName))<s>%h(zWDisplayName)</s></a></td>
1231 }else{
1232 @ <tr><td data=sortkey='%h(zSort)">\
1233 @ %z(href("%R/wiki?name=%T",zWName))%h(zWDisplayName)</a></td>
1234 }
1235 zAge = human_readable_age(rNow - rWmtime);
1236 @ <td data-sortkey="%016llx(iMtime)">%s(zAge)</td>
1237 fossil_free(zAge);
1238 @ <td>%z(href("%R/whistory?name=%T",zWName))%d(wcnt)</a></td>
1239 if( showRid ){
1240 @ <td>%d(wrid)</td>
1241 }
1242 @ </tr>
1243 fossil_free(zWDisplayName);
1244 }
1245 @ </tbody></table></div>
1246 db_finalize(&q);
1247 style_table_sorter();
1248 style_footer();
1249 }
1250
1251 /*
1252 ** WEBPAGE: wfind
@@ -1405,5 +1639,121 @@
1639 blob_zero(&out);
1640 blob_read_from_file(&in, g.argv[2], ExtFILE);
1641 markdown_to_html(&in, 0, &out);
1642 blob_write_to_file(&out, "-");
1643 }
1644
1645 /*
1646 ** Allowed flags for wiki_render_associated
1647 */
1648 #if INTERFACE
1649 #define WIKIASSOC_FULL_TITLE 0x00001 /* Full title */
1650 #define WIKIASSOC_MENU_READ 0x00002 /* Add submenu link to read wiki */
1651 #define WIKIASSOC_MENU_WRITE 0x00004 /* Add submenu link to add wiki */
1652 #define WIKIASSOC_ALL 0x00007 /* All of the above */
1653 #endif
1654
1655 /*
1656 ** Show the default Section label for an associated wiki page.
1657 */
1658 static void wiki_section_label(
1659 const char *zPrefix, /* "branch", "tag", or "checkin" */
1660 const char *zName, /* Name of the object */
1661 unsigned int mFlags /* Zero or more WIKIASSOC_* flags */
1662 ){
1663 if( (mFlags & WIKIASSOC_FULL_TITLE)==0 ){
1664 @ <div class="section">About</div>
1665 }else if( zPrefix[0]=='c' ){ /* checkin/... */
1666 @ <div class="section">About checkin %.20h(zName)</div>
1667 }else{
1668 @ <div class="section">About %s(zPrefix) %h(zName)</div>
1669 }
1670 }
1671
1672 /*
1673 ** Add an "Wiki" button in a submenu that links to the read-wiki page.
1674 */
1675 static void wiki_submenu_to_read_wiki(
1676 const char *zPrefix, /* "branch", "tag", or "checkin" */
1677 const char *zName, /* Name of the object */
1678 unsigned int mFlags /* Zero or more WIKIASSOC_* flags */
1679 ){
1680 if( g.perm.RdWiki && (mFlags & WIKIASSOC_MENU_READ)!=0 ){
1681 style_submenu_element("Wiki", "%R/wiki?name=%s/%t", zPrefix, zName);
1682 }
1683 }
1684
1685 /*
1686 ** Check to see if there exists a wiki page with a name zPrefix/zName.
1687 ** If there is, then render a <div class='section'>..</div> and
1688 ** return true.
1689 **
1690 ** If there is no such wiki page, return false.
1691 */
1692 int wiki_render_associated(
1693 const char *zPrefix, /* "branch", "tag", or "checkin" */
1694 const char *zName, /* Name of the object */
1695 unsigned int mFlags /* Zero or more WIKIASSOC_* flags */
1696 ){
1697 int rid;
1698 Manifest *pWiki;
1699 if( !db_get_boolean("wiki-about",1) ) return 0;
1700 rid = db_int(0,
1701 "SELECT rid FROM tagxref"
1702 " WHERE tagid=(SELECT tagid FROM tag WHERE tagname='wiki-%q/%q')"
1703 " ORDER BY mtime DESC LIMIT 1",
1704 zPrefix, zName
1705 );
1706 if( rid==0 ){
1707 if( g.perm.WrWiki && g.perm.Write && (mFlags & WIKIASSOC_MENU_WRITE)!=0 ){
1708 style_submenu_element("Add Wiki", "%R/wikiedit?name=%s/%t",
1709 zPrefix, zName);
1710 }
1711 }
1712 pWiki = manifest_get(rid, CFTYPE_WIKI, 0);
1713 if( pWiki==0 ) return 0;
1714 if( fossil_strcmp(pWiki->zMimetype, "text/x-markdown")==0 ){
1715 Blob tail = BLOB_INITIALIZER;
1716 Blob title = BLOB_INITIALIZER;
1717 Blob markdown;
1718 blob_init(&markdown, pWiki->zWiki, -1);
1719 markdown_to_html(&markdown, &title, &tail);
1720 if( blob_size(&title) ){
1721 @ <div class="section">%h(blob_str(&title))</div>
1722 }else{
1723 wiki_section_label(zPrefix, zName, mFlags);
1724 }
1725 wiki_submenu_to_read_wiki(zPrefix, zName, mFlags);
1726 convert_href_and_output(&tail);
1727 blob_reset(&tail);
1728 blob_reset(&title);
1729 blob_reset(&markdown);
1730 }else if( fossil_strcmp(pWiki->zMimetype, "text/plain")==0 ){
1731 wiki_section_label(zPrefix, zName, mFlags);
1732 wiki_submenu_to_read_wiki(zPrefix, zName, mFlags);
1733 @ <pre>
1734 @ %h(pWiki->zWiki)
1735 @ </pre>
1736 }else{
1737 Blob tail = BLOB_INITIALIZER;
1738 Blob title = BLOB_INITIALIZER;
1739 Blob wiki;
1740 Blob *pBody;
1741 blob_init(&wiki, pWiki->zWiki, -1);
1742 if( wiki_find_title(&wiki, &title, &tail) ){
1743 @ <div class="section">%h(blob_str(&title))</div>
1744 pBody = &tail;
1745 }else{
1746 wiki_section_label(zPrefix, zName, mFlags);
1747 pBody = &wiki;
1748 }
1749 wiki_submenu_to_read_wiki(zPrefix, zName, mFlags);
1750 @ <div class="wiki">
1751 wiki_convert(pBody, 0, WIKI_BUTTONS);
1752 @ </div>
1753 blob_reset(&tail);
1754 blob_reset(&title);
1755 blob_reset(&wiki);
1756 }
1757 manifest_destroy(pWiki);
1758 return 1;
1759 }
1760
+40 -1
--- src/wikiformat.c
+++ src/wikiformat.c
@@ -1159,10 +1159,44 @@
11591159
){
11601160
return zTarget;
11611161
}
11621162
return 0;
11631163
}
1164
+
1165
+static const char *wikiOverrideHash = 0;
1166
+
1167
+/*
1168
+** Fossil-wiki hyperlinks to wiki pages should be overridden to the
1169
+** hash value supplied. If the value is NULL, then override is cancelled
1170
+** and all overwrites operate normally.
1171
+*/
1172
+void wiki_hyperlink_override(const char *zUuid){
1173
+ wikiOverrideHash = zUuid;
1174
+}
1175
+
1176
+
1177
+/*
1178
+** If links to wiki page zTarget should be redirected to some historical
1179
+** version of that page, then return the hash of the historical version.
1180
+** If no override is required, return NULL.
1181
+*/
1182
+static const char *wiki_is_overridden(const char *zTarget){
1183
+ if( wikiOverrideHash==0 ) return 0;
1184
+ /* The override should only happen if the override version is not the
1185
+ ** latest version of the wiki page. */
1186
+ if( !db_exists(
1187
+ "SELECT 1 FROM tag, blob, tagxref AS xA, tagxref AS xB "
1188
+ " WHERE tag.tagname GLOB 'wiki-%q*'"
1189
+ " AND blob.uuid GLOB '%q'"
1190
+ " AND xA.tagid=tag.tagid AND xA.rid=blob.rid"
1191
+ " AND xB.tagid=tag.tagid AND xB.mtime>xA.mtime",
1192
+ zTarget, wikiOverrideHash
1193
+ ) ){
1194
+ return 0;
1195
+ }
1196
+ return wikiOverrideHash;
1197
+}
11641198
11651199
/*
11661200
** Resolve a hyperlink. The zTarget argument is the content of the [...]
11671201
** in the wiki. Append to the output string whatever text is appropriate
11681202
** for opening the hyperlink. Write into zClose[0...nClose-1] text that will
@@ -1259,11 +1293,16 @@
12591293
}
12601294
}else if( strlen(zTarget)>=10 && fossil_isdigit(zTarget[0]) && zTarget[4]=='-'
12611295
&& db_int(0, "SELECT datetime(%Q) NOT NULL", zTarget) ){
12621296
blob_appendf(p->pOut, "<a href=\"%R/timeline?c=%T\">", zTarget);
12631297
}else if( (z = validWikiPageName(p, zTarget))!=0 ){
1264
- blob_appendf(p->pOut, "<a href=\"wiki?name=%T\">", z);
1298
+ const char *zOverride = wiki_is_overridden(zTarget);
1299
+ if( zOverride ){
1300
+ blob_appendf(p->pOut, "<a href=\"%R/info/%S\">", zOverride);
1301
+ }else{
1302
+ blob_appendf(p->pOut, "<a href=\"%R/wiki?name=%T\">", z);
1303
+ }
12651304
}else if( zTarget>=&zOrig[2] && !fossil_isspace(zTarget[-2]) ){
12661305
/* Probably an array subscript in code */
12671306
zTerm = "";
12681307
}else if( (p->state & (WIKI_NOBADLINKS|WIKI_LINKSONLY))!=0 ){
12691308
zTerm = "";
12701309
--- src/wikiformat.c
+++ src/wikiformat.c
@@ -1159,10 +1159,44 @@
1159 ){
1160 return zTarget;
1161 }
1162 return 0;
1163 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1164
1165 /*
1166 ** Resolve a hyperlink. The zTarget argument is the content of the [...]
1167 ** in the wiki. Append to the output string whatever text is appropriate
1168 ** for opening the hyperlink. Write into zClose[0...nClose-1] text that will
@@ -1259,11 +1293,16 @@
1259 }
1260 }else if( strlen(zTarget)>=10 && fossil_isdigit(zTarget[0]) && zTarget[4]=='-'
1261 && db_int(0, "SELECT datetime(%Q) NOT NULL", zTarget) ){
1262 blob_appendf(p->pOut, "<a href=\"%R/timeline?c=%T\">", zTarget);
1263 }else if( (z = validWikiPageName(p, zTarget))!=0 ){
1264 blob_appendf(p->pOut, "<a href=\"wiki?name=%T\">", z);
 
 
 
 
 
1265 }else if( zTarget>=&zOrig[2] && !fossil_isspace(zTarget[-2]) ){
1266 /* Probably an array subscript in code */
1267 zTerm = "";
1268 }else if( (p->state & (WIKI_NOBADLINKS|WIKI_LINKSONLY))!=0 ){
1269 zTerm = "";
1270
--- src/wikiformat.c
+++ src/wikiformat.c
@@ -1159,10 +1159,44 @@
1159 ){
1160 return zTarget;
1161 }
1162 return 0;
1163 }
1164
1165 static const char *wikiOverrideHash = 0;
1166
1167 /*
1168 ** Fossil-wiki hyperlinks to wiki pages should be overridden to the
1169 ** hash value supplied. If the value is NULL, then override is cancelled
1170 ** and all overwrites operate normally.
1171 */
1172 void wiki_hyperlink_override(const char *zUuid){
1173 wikiOverrideHash = zUuid;
1174 }
1175
1176
1177 /*
1178 ** If links to wiki page zTarget should be redirected to some historical
1179 ** version of that page, then return the hash of the historical version.
1180 ** If no override is required, return NULL.
1181 */
1182 static const char *wiki_is_overridden(const char *zTarget){
1183 if( wikiOverrideHash==0 ) return 0;
1184 /* The override should only happen if the override version is not the
1185 ** latest version of the wiki page. */
1186 if( !db_exists(
1187 "SELECT 1 FROM tag, blob, tagxref AS xA, tagxref AS xB "
1188 " WHERE tag.tagname GLOB 'wiki-%q*'"
1189 " AND blob.uuid GLOB '%q'"
1190 " AND xA.tagid=tag.tagid AND xA.rid=blob.rid"
1191 " AND xB.tagid=tag.tagid AND xB.mtime>xA.mtime",
1192 zTarget, wikiOverrideHash
1193 ) ){
1194 return 0;
1195 }
1196 return wikiOverrideHash;
1197 }
1198
1199 /*
1200 ** Resolve a hyperlink. The zTarget argument is the content of the [...]
1201 ** in the wiki. Append to the output string whatever text is appropriate
1202 ** for opening the hyperlink. Write into zClose[0...nClose-1] text that will
@@ -1259,11 +1293,16 @@
1293 }
1294 }else if( strlen(zTarget)>=10 && fossil_isdigit(zTarget[0]) && zTarget[4]=='-'
1295 && db_int(0, "SELECT datetime(%Q) NOT NULL", zTarget) ){
1296 blob_appendf(p->pOut, "<a href=\"%R/timeline?c=%T\">", zTarget);
1297 }else if( (z = validWikiPageName(p, zTarget))!=0 ){
1298 const char *zOverride = wiki_is_overridden(zTarget);
1299 if( zOverride ){
1300 blob_appendf(p->pOut, "<a href=\"%R/info/%S\">", zOverride);
1301 }else{
1302 blob_appendf(p->pOut, "<a href=\"%R/wiki?name=%T\">", z);
1303 }
1304 }else if( zTarget>=&zOrig[2] && !fossil_isspace(zTarget[-2]) ){
1305 /* Probably an array subscript in code */
1306 zTerm = "";
1307 }else if( (p->state & (WIKI_NOBADLINKS|WIKI_LINKSONLY))!=0 ){
1308 zTerm = "";
1309
+40 -1
--- src/wikiformat.c
+++ src/wikiformat.c
@@ -1159,10 +1159,44 @@
11591159
){
11601160
return zTarget;
11611161
}
11621162
return 0;
11631163
}
1164
+
1165
+static const char *wikiOverrideHash = 0;
1166
+
1167
+/*
1168
+** Fossil-wiki hyperlinks to wiki pages should be overridden to the
1169
+** hash value supplied. If the value is NULL, then override is cancelled
1170
+** and all overwrites operate normally.
1171
+*/
1172
+void wiki_hyperlink_override(const char *zUuid){
1173
+ wikiOverrideHash = zUuid;
1174
+}
1175
+
1176
+
1177
+/*
1178
+** If links to wiki page zTarget should be redirected to some historical
1179
+** version of that page, then return the hash of the historical version.
1180
+** If no override is required, return NULL.
1181
+*/
1182
+static const char *wiki_is_overridden(const char *zTarget){
1183
+ if( wikiOverrideHash==0 ) return 0;
1184
+ /* The override should only happen if the override version is not the
1185
+ ** latest version of the wiki page. */
1186
+ if( !db_exists(
1187
+ "SELECT 1 FROM tag, blob, tagxref AS xA, tagxref AS xB "
1188
+ " WHERE tag.tagname GLOB 'wiki-%q*'"
1189
+ " AND blob.uuid GLOB '%q'"
1190
+ " AND xA.tagid=tag.tagid AND xA.rid=blob.rid"
1191
+ " AND xB.tagid=tag.tagid AND xB.mtime>xA.mtime",
1192
+ zTarget, wikiOverrideHash
1193
+ ) ){
1194
+ return 0;
1195
+ }
1196
+ return wikiOverrideHash;
1197
+}
11641198
11651199
/*
11661200
** Resolve a hyperlink. The zTarget argument is the content of the [...]
11671201
** in the wiki. Append to the output string whatever text is appropriate
11681202
** for opening the hyperlink. Write into zClose[0...nClose-1] text that will
@@ -1259,11 +1293,16 @@
12591293
}
12601294
}else if( strlen(zTarget)>=10 && fossil_isdigit(zTarget[0]) && zTarget[4]=='-'
12611295
&& db_int(0, "SELECT datetime(%Q) NOT NULL", zTarget) ){
12621296
blob_appendf(p->pOut, "<a href=\"%R/timeline?c=%T\">", zTarget);
12631297
}else if( (z = validWikiPageName(p, zTarget))!=0 ){
1264
- blob_appendf(p->pOut, "<a href=\"wiki?name=%T\">", z);
1298
+ const char *zOverride = wiki_is_overridden(zTarget);
1299
+ if( zOverride ){
1300
+ blob_appendf(p->pOut, "<a href=\"%R/info/%S\">", zOverride);
1301
+ }else{
1302
+ blob_appendf(p->pOut, "<a href=\"%R/wiki?name=%T\">", z);
1303
+ }
12651304
}else if( zTarget>=&zOrig[2] && !fossil_isspace(zTarget[-2]) ){
12661305
/* Probably an array subscript in code */
12671306
zTerm = "";
12681307
}else if( (p->state & (WIKI_NOBADLINKS|WIKI_LINKSONLY))!=0 ){
12691308
zTerm = "";
12701309
--- src/wikiformat.c
+++ src/wikiformat.c
@@ -1159,10 +1159,44 @@
1159 ){
1160 return zTarget;
1161 }
1162 return 0;
1163 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1164
1165 /*
1166 ** Resolve a hyperlink. The zTarget argument is the content of the [...]
1167 ** in the wiki. Append to the output string whatever text is appropriate
1168 ** for opening the hyperlink. Write into zClose[0...nClose-1] text that will
@@ -1259,11 +1293,16 @@
1259 }
1260 }else if( strlen(zTarget)>=10 && fossil_isdigit(zTarget[0]) && zTarget[4]=='-'
1261 && db_int(0, "SELECT datetime(%Q) NOT NULL", zTarget) ){
1262 blob_appendf(p->pOut, "<a href=\"%R/timeline?c=%T\">", zTarget);
1263 }else if( (z = validWikiPageName(p, zTarget))!=0 ){
1264 blob_appendf(p->pOut, "<a href=\"wiki?name=%T\">", z);
 
 
 
 
 
1265 }else if( zTarget>=&zOrig[2] && !fossil_isspace(zTarget[-2]) ){
1266 /* Probably an array subscript in code */
1267 zTerm = "";
1268 }else if( (p->state & (WIKI_NOBADLINKS|WIKI_LINKSONLY))!=0 ){
1269 zTerm = "";
1270
--- src/wikiformat.c
+++ src/wikiformat.c
@@ -1159,10 +1159,44 @@
1159 ){
1160 return zTarget;
1161 }
1162 return 0;
1163 }
1164
1165 static const char *wikiOverrideHash = 0;
1166
1167 /*
1168 ** Fossil-wiki hyperlinks to wiki pages should be overridden to the
1169 ** hash value supplied. If the value is NULL, then override is cancelled
1170 ** and all overwrites operate normally.
1171 */
1172 void wiki_hyperlink_override(const char *zUuid){
1173 wikiOverrideHash = zUuid;
1174 }
1175
1176
1177 /*
1178 ** If links to wiki page zTarget should be redirected to some historical
1179 ** version of that page, then return the hash of the historical version.
1180 ** If no override is required, return NULL.
1181 */
1182 static const char *wiki_is_overridden(const char *zTarget){
1183 if( wikiOverrideHash==0 ) return 0;
1184 /* The override should only happen if the override version is not the
1185 ** latest version of the wiki page. */
1186 if( !db_exists(
1187 "SELECT 1 FROM tag, blob, tagxref AS xA, tagxref AS xB "
1188 " WHERE tag.tagname GLOB 'wiki-%q*'"
1189 " AND blob.uuid GLOB '%q'"
1190 " AND xA.tagid=tag.tagid AND xA.rid=blob.rid"
1191 " AND xB.tagid=tag.tagid AND xB.mtime>xA.mtime",
1192 zTarget, wikiOverrideHash
1193 ) ){
1194 return 0;
1195 }
1196 return wikiOverrideHash;
1197 }
1198
1199 /*
1200 ** Resolve a hyperlink. The zTarget argument is the content of the [...]
1201 ** in the wiki. Append to the output string whatever text is appropriate
1202 ** for opening the hyperlink. Write into zClose[0...nClose-1] text that will
@@ -1259,11 +1293,16 @@
1293 }
1294 }else if( strlen(zTarget)>=10 && fossil_isdigit(zTarget[0]) && zTarget[4]=='-'
1295 && db_int(0, "SELECT datetime(%Q) NOT NULL", zTarget) ){
1296 blob_appendf(p->pOut, "<a href=\"%R/timeline?c=%T\">", zTarget);
1297 }else if( (z = validWikiPageName(p, zTarget))!=0 ){
1298 const char *zOverride = wiki_is_overridden(zTarget);
1299 if( zOverride ){
1300 blob_appendf(p->pOut, "<a href=\"%R/info/%S\">", zOverride);
1301 }else{
1302 blob_appendf(p->pOut, "<a href=\"%R/wiki?name=%T\">", z);
1303 }
1304 }else if( zTarget>=&zOrig[2] && !fossil_isspace(zTarget[-2]) ){
1305 /* Probably an array subscript in code */
1306 zTerm = "";
1307 }else if( (p->state & (WIKI_NOBADLINKS|WIKI_LINKSONLY))!=0 ){
1308 zTerm = "";
1309
--- test/graph-test-1.wiki
+++ test/graph-test-1.wiki
@@ -78,10 +78,20 @@
7878
screen, not from the andygoth-crlf branch.</a>
7979
* <a href="../../../timeline?a=b8c7af5b&n=12"
8080
target="testwindow">Check-in 2de15c8e has merge arrows from two
8181
different trunk check-ins. One of the merge risers also branches
8282
to check-in ea7f3297</a>
83
+ * <a href="../../../timeline?b=ae8709e2&n=25" target="testwindow">
84
+ Cherrypick merge arrows</a>
85
+ * <a href="../../../timeline?r=branch-1.37" target="testwindow">Branch
86
+ 1.37 with cherry-pick merges from trunk.</a>
87
+ * <a href="../../../timeline?f=68bd2e7bedb8d05a" target="testwindow">
88
+ Single check-in takes both a full merge and a cherrypick merge</a>
89
+ * <a href="../../../timeline?b=?b=dc81ac70&n=14" target="testwindow">
90
+ Mixed merge arrow, partly fully and partly cherrypick</a>
91
+ * <a href="../../../timeline?b=?b=dc81ac70&n=13" target="testwindow">
92
+ Mixed merge arrow to bottom of screen.</a>
8393
8494
External:
8595
8696
* <a href="http://www.sqlite.org/src/timeline?c=2010-09-29&nd"
8797
target="testwindow">Timewarp due to a mis-configured system clock.</a>
8898
--- test/graph-test-1.wiki
+++ test/graph-test-1.wiki
@@ -78,10 +78,20 @@
78 screen, not from the andygoth-crlf branch.</a>
79 * <a href="../../../timeline?a=b8c7af5b&n=12"
80 target="testwindow">Check-in 2de15c8e has merge arrows from two
81 different trunk check-ins. One of the merge risers also branches
82 to check-in ea7f3297</a>
 
 
 
 
 
 
 
 
 
 
83
84 External:
85
86 * <a href="http://www.sqlite.org/src/timeline?c=2010-09-29&nd"
87 target="testwindow">Timewarp due to a mis-configured system clock.</a>
88
--- test/graph-test-1.wiki
+++ test/graph-test-1.wiki
@@ -78,10 +78,20 @@
78 screen, not from the andygoth-crlf branch.</a>
79 * <a href="../../../timeline?a=b8c7af5b&n=12"
80 target="testwindow">Check-in 2de15c8e has merge arrows from two
81 different trunk check-ins. One of the merge risers also branches
82 to check-in ea7f3297</a>
83 * <a href="../../../timeline?b=ae8709e2&n=25" target="testwindow">
84 Cherrypick merge arrows</a>
85 * <a href="../../../timeline?r=branch-1.37" target="testwindow">Branch
86 1.37 with cherry-pick merges from trunk.</a>
87 * <a href="../../../timeline?f=68bd2e7bedb8d05a" target="testwindow">
88 Single check-in takes both a full merge and a cherrypick merge</a>
89 * <a href="../../../timeline?b=?b=dc81ac70&n=14" target="testwindow">
90 Mixed merge arrow, partly fully and partly cherrypick</a>
91 * <a href="../../../timeline?b=?b=dc81ac70&n=13" target="testwindow">
92 Mixed merge arrow to bottom of screen.</a>
93
94 External:
95
96 * <a href="http://www.sqlite.org/src/timeline?c=2010-09-29&nd"
97 target="testwindow">Timewarp due to a mis-configured system clock.</a>
98
--- www/customskin.md
+++ www/customskin.md
@@ -1,16 +1,16 @@
11
Theming
22
=======
33
44
Every HTML page generated by Fossil has the following basic structure:
55
6
-
76
<blockquote><table border=1 cellpadding=10><tbody>
87
<tr><td style='background-color:lightblue;text-align:center;'>Header</td></tr>
98
<tr><td style='background-color:lightgreen;text-align:center;'>
109
Fossil-Generated Content</td></tr>
1110
<tr><td style='background-color:lightblue;text-align:center;'>Footer</td></tr>
11
+<tr><td style='background-color:lightyellow;text-align:center;'>Javascript (optional)</td></tr>
1212
</tbody></table></blockquote>
1313
1414
The header and footer control the "look" of Fossil pages. Those
1515
two sections can be customized separately for each repository to
1616
develop a new theme.
@@ -134,10 +134,69 @@
134134
135135
The same TH1 interpreter is used for both the header and the footer
136136
and for all scripts contained within them both. Hence, any global
137137
TH1 variables that are set by the header are available to the footer.
138138
139
+Customizing the ≡ Hamburger Menu
140
+--------------------------------
141
+
142
+The menu bar of the default skin has an entry to open a drop-down menu with
143
+additional navigation links, represented by the ≡ button (hence the name
144
+"hamburger menu"). The Javascript logic to open and close the hamburger menu
145
+when the button is clicked is contained in the optional Javascript part (js.txt)
146
+of the default skin. Out of the box, the drop-down menu shows the [Site
147
+Map](../../../sitemap), loaded by an AJAX request prior to the first display.
148
+
149
+The ≡ button for the hamburger menu is added to the menu bar by the following
150
+TH1 command in the default skin header.txt, right before the menu bar links:
151
+
152
+ html "<a id='hbbtn' href='#'>&#9776;</a>"
153
+
154
+The hamburger button can be repositioned between the other menu links (but the
155
+drop-down menu is always left-aligned with the menu bar), or it can be removed
156
+by deleting the above statement (the Javascript logic detects this case and
157
+remains idle, so it's not necessary to modify the default skin js.txt).
158
+
159
+The following empty element at the bottom of the default skin header.txt serves
160
+as the panel to hold the drop-down menu elements:
161
+
162
+ <div id='hbdrop'></div>
163
+
164
+Out of the box, the contents of the panel is populated with the [Site
165
+Map](../../../sitemap), but only if the panel does not already contain any HTML
166
+elements (that is, not just comments, plain text or non-presentational white
167
+space). So the hamburger menu can be customized by replacing the empty `<div
168
+id='hbdrop'></div>` element with a menu structure knitted according to the
169
+following template:
170
+
171
+ <div id="hbdrop" data-anim-ms="400">
172
+ <ul class="columns" style="column-width: 20em; column-count: auto">
173
+ <!-- NEW GROUP WITH HEADING LINK -->
174
+ <li>
175
+ <a href="$home$index_page">Link: Home</a>
176
+ <ul>
177
+ <li><a href="$home/timeline">Link: Timeline</a></li>
178
+ <li><a href="$home/dir?ci=tip">Link: File List</a></li>
179
+ </ul>
180
+ </li>
181
+ <!-- NEW GROUP WITH HEADING TEXT -->
182
+ <li>
183
+ Heading Text
184
+ <ul>
185
+ <li><a href="$home/doc/trunk/www/customskin.md">Link: Theming</a></li>
186
+ <li><a href="$home/doc/trunk/www/th1.md">Link: TH1 Scripts</a></li>
187
+ </ul>
188
+ </li>
189
+ <!-- NEXT GROUP GOES HERE -->
190
+ </ul>
191
+ </div>
192
+
193
+The custom `data-anim-ms` attribute can be added to the panel element to direct
194
+the Javascript logic to override the default menu animation duration of 400 ms.
195
+A faster animation duration of 80-200 ms may be preferred for smaller menus. The
196
+animation is disabled by setting the attribute to `"0"`.
197
+
139198
TH1 Variables
140199
-------------
141200
142201
Before expanding the TH1 within the header and footer, Fossil first
143202
initializes a number of TH1 variables to values that depend on
144203
--- www/customskin.md
+++ www/customskin.md
@@ -1,16 +1,16 @@
1 Theming
2 =======
3
4 Every HTML page generated by Fossil has the following basic structure:
5
6
7 <blockquote><table border=1 cellpadding=10><tbody>
8 <tr><td style='background-color:lightblue;text-align:center;'>Header</td></tr>
9 <tr><td style='background-color:lightgreen;text-align:center;'>
10 Fossil-Generated Content</td></tr>
11 <tr><td style='background-color:lightblue;text-align:center;'>Footer</td></tr>
 
12 </tbody></table></blockquote>
13
14 The header and footer control the "look" of Fossil pages. Those
15 two sections can be customized separately for each repository to
16 develop a new theme.
@@ -134,10 +134,69 @@
134
135 The same TH1 interpreter is used for both the header and the footer
136 and for all scripts contained within them both. Hence, any global
137 TH1 variables that are set by the header are available to the footer.
138
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139 TH1 Variables
140 -------------
141
142 Before expanding the TH1 within the header and footer, Fossil first
143 initializes a number of TH1 variables to values that depend on
144
--- www/customskin.md
+++ www/customskin.md
@@ -1,16 +1,16 @@
1 Theming
2 =======
3
4 Every HTML page generated by Fossil has the following basic structure:
5
 
6 <blockquote><table border=1 cellpadding=10><tbody>
7 <tr><td style='background-color:lightblue;text-align:center;'>Header</td></tr>
8 <tr><td style='background-color:lightgreen;text-align:center;'>
9 Fossil-Generated Content</td></tr>
10 <tr><td style='background-color:lightblue;text-align:center;'>Footer</td></tr>
11 <tr><td style='background-color:lightyellow;text-align:center;'>Javascript (optional)</td></tr>
12 </tbody></table></blockquote>
13
14 The header and footer control the "look" of Fossil pages. Those
15 two sections can be customized separately for each repository to
16 develop a new theme.
@@ -134,10 +134,69 @@
134
135 The same TH1 interpreter is used for both the header and the footer
136 and for all scripts contained within them both. Hence, any global
137 TH1 variables that are set by the header are available to the footer.
138
139 Customizing the ≡ Hamburger Menu
140 --------------------------------
141
142 The menu bar of the default skin has an entry to open a drop-down menu with
143 additional navigation links, represented by the ≡ button (hence the name
144 "hamburger menu"). The Javascript logic to open and close the hamburger menu
145 when the button is clicked is contained in the optional Javascript part (js.txt)
146 of the default skin. Out of the box, the drop-down menu shows the [Site
147 Map](../../../sitemap), loaded by an AJAX request prior to the first display.
148
149 The ≡ button for the hamburger menu is added to the menu bar by the following
150 TH1 command in the default skin header.txt, right before the menu bar links:
151
152 html "<a id='hbbtn' href='#'>&#9776;</a>"
153
154 The hamburger button can be repositioned between the other menu links (but the
155 drop-down menu is always left-aligned with the menu bar), or it can be removed
156 by deleting the above statement (the Javascript logic detects this case and
157 remains idle, so it's not necessary to modify the default skin js.txt).
158
159 The following empty element at the bottom of the default skin header.txt serves
160 as the panel to hold the drop-down menu elements:
161
162 <div id='hbdrop'></div>
163
164 Out of the box, the contents of the panel is populated with the [Site
165 Map](../../../sitemap), but only if the panel does not already contain any HTML
166 elements (that is, not just comments, plain text or non-presentational white
167 space). So the hamburger menu can be customized by replacing the empty `<div
168 id='hbdrop'></div>` element with a menu structure knitted according to the
169 following template:
170
171 <div id="hbdrop" data-anim-ms="400">
172 <ul class="columns" style="column-width: 20em; column-count: auto">
173 <!-- NEW GROUP WITH HEADING LINK -->
174 <li>
175 <a href="$home$index_page">Link: Home</a>
176 <ul>
177 <li><a href="$home/timeline">Link: Timeline</a></li>
178 <li><a href="$home/dir?ci=tip">Link: File List</a></li>
179 </ul>
180 </li>
181 <!-- NEW GROUP WITH HEADING TEXT -->
182 <li>
183 Heading Text
184 <ul>
185 <li><a href="$home/doc/trunk/www/customskin.md">Link: Theming</a></li>
186 <li><a href="$home/doc/trunk/www/th1.md">Link: TH1 Scripts</a></li>
187 </ul>
188 </li>
189 <!-- NEXT GROUP GOES HERE -->
190 </ul>
191 </div>
192
193 The custom `data-anim-ms` attribute can be added to the panel element to direct
194 the Javascript logic to override the default menu animation duration of 400 ms.
195 A faster animation duration of 80-200 ms may be preferred for smaller menus. The
196 animation is disabled by setting the attribute to `"0"`.
197
198 TH1 Variables
199 -------------
200
201 Before expanding the TH1 within the header and footer, Fossil first
202 initializes a number of TH1 variables to values that depend on
203
--- www/fileformat.wiki
+++ www/fileformat.wiki
@@ -305,11 +305,11 @@
305305
as in a manifest.
306306
307307
The <b>T</b> card represents a [./branching.wiki#tags | tag or property]
308308
that is applied to
309309
some other artifact. The <b>T</b> card has two or three values. The
310
-second argument is the 40 character lowercase artifact ID of the artifact
310
+second argument is the lowercase artifact ID of the artifact
311311
to which the tag is to be applied. The
312312
first value is the tag name. The first character of the tag
313313
is either "+", "-", or "*". The "+" means the tag should be added
314314
to the artifact. The "-" means the tag should be removed.
315315
The "*" character means the tag should be added to the artifact
@@ -437,11 +437,11 @@
437437
</blockquote>
438438
439439
The <b>A</b> card specifies a filename for the attachment in its first argument.
440440
The second argument to the <b>A</b> card is the name of the wiki page or
441441
ticket or technical note to which the attachment is connected. The
442
-third argument is either missing or else it is the 40-character artifact
442
+third argument is either missing or else it is the lower-case artifact
443443
ID of the attachment itself. A missing third argument means that the
444444
attachment should be deleted.
445445
446446
The <b>C</b> card is an optional comment describing what the attachment is about.
447447
The <b>C</b> card is optional, but there can only be one.
448448
--- www/fileformat.wiki
+++ www/fileformat.wiki
@@ -305,11 +305,11 @@
305 as in a manifest.
306
307 The <b>T</b> card represents a [./branching.wiki#tags | tag or property]
308 that is applied to
309 some other artifact. The <b>T</b> card has two or three values. The
310 second argument is the 40 character lowercase artifact ID of the artifact
311 to which the tag is to be applied. The
312 first value is the tag name. The first character of the tag
313 is either "+", "-", or "*". The "+" means the tag should be added
314 to the artifact. The "-" means the tag should be removed.
315 The "*" character means the tag should be added to the artifact
@@ -437,11 +437,11 @@
437 </blockquote>
438
439 The <b>A</b> card specifies a filename for the attachment in its first argument.
440 The second argument to the <b>A</b> card is the name of the wiki page or
441 ticket or technical note to which the attachment is connected. The
442 third argument is either missing or else it is the 40-character artifact
443 ID of the attachment itself. A missing third argument means that the
444 attachment should be deleted.
445
446 The <b>C</b> card is an optional comment describing what the attachment is about.
447 The <b>C</b> card is optional, but there can only be one.
448
--- www/fileformat.wiki
+++ www/fileformat.wiki
@@ -305,11 +305,11 @@
305 as in a manifest.
306
307 The <b>T</b> card represents a [./branching.wiki#tags | tag or property]
308 that is applied to
309 some other artifact. The <b>T</b> card has two or three values. The
310 second argument is the lowercase artifact ID of the artifact
311 to which the tag is to be applied. The
312 first value is the tag name. The first character of the tag
313 is either "+", "-", or "*". The "+" means the tag should be added
314 to the artifact. The "-" means the tag should be removed.
315 The "*" character means the tag should be added to the artifact
@@ -437,11 +437,11 @@
437 </blockquote>
438
439 The <b>A</b> card specifies a filename for the attachment in its first argument.
440 The second argument to the <b>A</b> card is the name of the wiki page or
441 ticket or technical note to which the attachment is connected. The
442 third argument is either missing or else it is the lower-case artifact
443 ID of the attachment itself. A missing third argument means that the
444 attachment should be deleted.
445
446 The <b>C</b> card is an optional comment describing what the attachment is about.
447 The <b>C</b> card is optional, but there can only be one.
448

Keyboard Shortcuts

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