Chris@57
|
1 /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */
|
Chris@57
|
2
|
Chris@57
|
3 /*
|
Chris@57
|
4 EasyMercurial
|
Chris@57
|
5
|
Chris@57
|
6 Based on HgExplorer by Jari Korhonen
|
Chris@57
|
7 Copyright (c) 2010 Jari Korhonen
|
Chris@644
|
8 Copyright (c) 2013 Chris Cannam
|
Chris@644
|
9 Copyright (c) 2013 Queen Mary, University of London
|
Chris@57
|
10
|
Chris@57
|
11 This program is free software; you can redistribute it and/or
|
Chris@57
|
12 modify it under the terms of the GNU General Public License as
|
Chris@57
|
13 published by the Free Software Foundation; either version 2 of the
|
Chris@57
|
14 License, or (at your option) any later version. See the file
|
Chris@57
|
15 COPYING included with this distribution for more information.
|
Chris@57
|
16 */
|
Chris@44
|
17
|
Chris@44
|
18 #include "grapher.h"
|
Chris@46
|
19 #include "connectionitem.h"
|
Chris@114
|
20 #include "debug.h"
|
Chris@119
|
21 #include "changesetscene.h"
|
Chris@44
|
22
|
Chris@273
|
23 #include <QSettings>
|
Chris@273
|
24
|
Chris@44
|
25 #include <iostream>
|
Chris@44
|
26
|
Chris@691
|
27 //#define GRAPHER_VERBOSE_DEBUG 1
|
Chris@691
|
28
|
Chris@273
|
29 Grapher::Grapher(ChangesetScene *scene) :
|
Chris@273
|
30 m_scene(scene)
|
Chris@273
|
31 {
|
Chris@273
|
32 QSettings settings;
|
Chris@273
|
33 settings.beginGroup("Presentation");
|
Chris@273
|
34 m_showDates = (settings.value("dateformat", 0) == 1);
|
Chris@512
|
35 m_showClosedBranches = (settings.value("showclosedbranches", false).toBool());
|
Chris@273
|
36 }
|
Chris@273
|
37
|
Chris@268
|
38 int Grapher::findAvailableColumn(int row, int parent, bool preferParentCol)
|
cannam@45
|
39 {
|
cannam@45
|
40 int col = parent;
|
cannam@45
|
41 if (preferParentCol) {
|
Chris@145
|
42 if (isAvailable(row, col)) return col;
|
cannam@45
|
43 }
|
cannam@45
|
44 while (col > 0) {
|
Chris@145
|
45 if (isAvailable(row, --col)) return col;
|
cannam@45
|
46 }
|
cannam@45
|
47 while (col < 0) {
|
Chris@145
|
48 if (isAvailable(row, ++col)) return col;
|
cannam@45
|
49 }
|
cannam@45
|
50 col = parent;
|
Chris@268
|
51 int sign = (col < 0 ? -1 : 1);
|
cannam@45
|
52 while (1) {
|
Chris@106
|
53 col += sign;
|
Chris@145
|
54 if (isAvailable(row, col)) return col;
|
cannam@45
|
55 }
|
cannam@45
|
56 }
|
Chris@44
|
57
|
Chris@145
|
58 bool Grapher::isAvailable(int row, int col)
|
Chris@145
|
59 {
|
Chris@145
|
60 if (m_alloc.contains(row) && m_alloc[row].contains(col)) return false;
|
Chris@145
|
61 if (!m_haveAllocatedUncommittedColumn) return true;
|
Chris@145
|
62 if (!m_uncommitted) return true;
|
Chris@145
|
63 return !(row <= m_uncommittedParentRow && col == m_uncommitted->column());
|
Chris@145
|
64 }
|
Chris@145
|
65
|
Chris@106
|
66 void Grapher::layoutRow(QString id)
|
Chris@44
|
67 {
|
cannam@45
|
68 if (m_handled.contains(id)) {
|
Chris@106
|
69 return;
|
Chris@44
|
70 }
|
Chris@46
|
71 if (!m_changesets.contains(id)) {
|
Chris@106
|
72 throw LayoutException(QString("Changeset %1 not in ID map").arg(id));
|
Chris@44
|
73 }
|
cannam@45
|
74 if (!m_items.contains(id)) {
|
Chris@512
|
75 return;
|
Chris@44
|
76 }
|
Chris@46
|
77 Changeset *cs = m_changesets[id];
|
cannam@45
|
78 ChangesetItem *item = m_items[id];
|
Chris@691
|
79 #ifdef GRAPHER_VERBOSE_DEBUG
|
Chris@114
|
80 DEBUG << "layoutRow: Looking at " << id.toStdString() << endl;
|
Chris@691
|
81 #endif
|
cannam@45
|
82
|
Chris@44
|
83 int row = 0;
|
cannam@45
|
84 int nparents = cs->parents().size();
|
cannam@45
|
85
|
cannam@45
|
86 if (nparents > 0) {
|
Chris@106
|
87 bool haveRow = false;
|
Chris@106
|
88 foreach (QString parentId, cs->parents()) {
|
cannam@45
|
89
|
Chris@106
|
90 if (!m_changesets.contains(parentId)) continue;
|
Chris@106
|
91 if (!m_items.contains(parentId)) continue;
|
cannam@45
|
92
|
Chris@106
|
93 if (!m_handled.contains(parentId)) {
|
Chris@106
|
94 layoutRow(parentId);
|
Chris@106
|
95 }
|
cannam@45
|
96
|
Chris@106
|
97 ChangesetItem *parentItem = m_items[parentId];
|
Chris@106
|
98 if (!haveRow || parentItem->row() < row) {
|
Chris@106
|
99 row = parentItem->row();
|
Chris@106
|
100 haveRow = true;
|
Chris@106
|
101 }
|
Chris@106
|
102 }
|
Chris@106
|
103 row = row - 1;
|
cannam@45
|
104 }
|
cannam@45
|
105
|
Chris@52
|
106 // row is now an upper bound on our eventual row (because we want
|
Chris@52
|
107 // to be above all parents). But we also want to ensure we are
|
Chris@52
|
108 // above all nodes that have earlier dates (to the nearest day).
|
Chris@52
|
109 // m_rowDates maps each row to a date: use that.
|
Chris@52
|
110
|
Chris@273
|
111 QString date;
|
Chris@273
|
112 if (m_showDates) {
|
Chris@273
|
113 date = cs->date();
|
Chris@273
|
114 } else {
|
Chris@273
|
115 date = cs->age();
|
Chris@273
|
116 }
|
Chris@53
|
117 while (m_rowDates.contains(row) && m_rowDates[row] != date) {
|
Chris@106
|
118 --row;
|
Chris@52
|
119 }
|
Chris@52
|
120
|
Chris@52
|
121 // We have already laid out all nodes that have earlier timestamps
|
Chris@52
|
122 // than this one, so we know (among other things) that we can
|
Chris@52
|
123 // safely fill in this row has having this date, if it isn't in
|
Chris@52
|
124 // the map yet (it cannot have an earlier date)
|
Chris@52
|
125
|
Chris@52
|
126 if (!m_rowDates.contains(row)) {
|
Chris@106
|
127 m_rowDates[row] = date;
|
Chris@52
|
128 }
|
Chris@52
|
129
|
Chris@145
|
130 // If we're the parent of the uncommitted item, make a note of our
|
Chris@145
|
131 // row (we need it later, to avoid overwriting the connecting line)
|
Chris@153
|
132 if (!m_uncommittedParents.empty() && m_uncommittedParents[0] == id) {
|
Chris@145
|
133 m_uncommittedParentRow = row;
|
Chris@145
|
134 }
|
Chris@145
|
135
|
Chris@691
|
136 #ifdef GRAPHER_VERBOSE_DEBUG
|
Chris@114
|
137 DEBUG << "putting " << cs->id().toStdString() << " at row " << row
|
Chris@114
|
138 << endl;
|
Chris@691
|
139 #endif
|
Chris@691
|
140
|
Chris@44
|
141 item->setRow(row);
|
cannam@45
|
142 m_handled.insert(id);
|
Chris@44
|
143 }
|
Chris@44
|
144
|
Chris@106
|
145 void Grapher::layoutCol(QString id)
|
Chris@44
|
146 {
|
cannam@45
|
147 if (m_handled.contains(id)) {
|
Chris@691
|
148 #ifdef GRAPHER_VERBOSE_DEBUG
|
Chris@114
|
149 DEBUG << "Already looked at " << id.toStdString() << endl;
|
Chris@691
|
150 #endif
|
Chris@106
|
151 return;
|
cannam@45
|
152 }
|
Chris@46
|
153 if (!m_changesets.contains(id)) {
|
Chris@106
|
154 throw LayoutException(QString("Changeset %1 not in ID map").arg(id));
|
cannam@45
|
155 }
|
cannam@45
|
156 if (!m_items.contains(id)) {
|
Chris@512
|
157 return;
|
cannam@45
|
158 }
|
Chris@53
|
159
|
Chris@46
|
160 Changeset *cs = m_changesets[id];
|
Chris@691
|
161 #ifdef GRAPHER_VERBOSE_DEBUG
|
Chris@122
|
162 DEBUG << "layoutCol: Looking at " << id.toStdString() << endl;
|
Chris@691
|
163 #endif
|
Chris@53
|
164
|
cannam@45
|
165 ChangesetItem *item = m_items[id];
|
Chris@47
|
166
|
cannam@45
|
167 int col = 0;
|
Chris@46
|
168 int row = item->row();
|
Chris@46
|
169 QString branch = cs->branch();
|
Chris@47
|
170
|
cannam@45
|
171 int nparents = cs->parents().size();
|
Chris@46
|
172 QString parentId;
|
Chris@46
|
173 int parentsOnSameBranch = 0;
|
cannam@45
|
174
|
Chris@46
|
175 switch (nparents) {
|
cannam@45
|
176
|
Chris@46
|
177 case 0:
|
Chris@268
|
178 col = m_branchHomes[cs->branch()];
|
Chris@268
|
179 col = findAvailableColumn(row, col, true);
|
Chris@106
|
180 break;
|
cannam@45
|
181
|
Chris@46
|
182 case 1:
|
Chris@106
|
183 parentId = cs->parents()[0];
|
cannam@45
|
184
|
Chris@106
|
185 if (!m_changesets.contains(parentId) ||
|
Chris@153
|
186 !m_changesets[parentId]->isOnBranch(branch)) {
|
Chris@106
|
187 // new branch
|
Chris@106
|
188 col = m_branchHomes[branch];
|
Chris@512
|
189 } else if (m_items.contains(parentId)) {
|
Chris@106
|
190 col = m_items[parentId]->column();
|
Chris@106
|
191 }
|
cannam@45
|
192
|
Chris@268
|
193 col = findAvailableColumn(row, col, true);
|
Chris@106
|
194 break;
|
Chris@46
|
195
|
Chris@46
|
196 case 2:
|
Chris@106
|
197 // a merge: behave differently if parents are both on the same
|
Chris@106
|
198 // branch (we also want to behave differently for nodes that
|
Chris@106
|
199 // have multiple children on the same branch -- spreading them
|
Chris@106
|
200 // out rather than having affinity to a specific branch)
|
Chris@46
|
201
|
Chris@106
|
202 foreach (QString parentId, cs->parents()) {
|
Chris@106
|
203 if (!m_changesets.contains(parentId)) continue;
|
Chris@153
|
204 if (m_changesets[parentId]->isOnBranch(branch)) {
|
Chris@512
|
205 if (m_items.contains(parentId)) {
|
Chris@512
|
206 ChangesetItem *parentItem = m_items[parentId];
|
Chris@512
|
207 col += parentItem->column();
|
Chris@512
|
208 parentsOnSameBranch++;
|
Chris@512
|
209 }
|
Chris@106
|
210 }
|
Chris@106
|
211 }
|
Chris@46
|
212
|
Chris@106
|
213 if (parentsOnSameBranch > 0) {
|
Chris@106
|
214 col /= parentsOnSameBranch;
|
Chris@268
|
215 col = findAvailableColumn(item->row(), col, true);
|
Chris@106
|
216 } else {
|
Chris@268
|
217 col = findAvailableColumn(item->row(), col, false);
|
Chris@106
|
218 }
|
Chris@106
|
219 break;
|
Chris@44
|
220 }
|
Chris@44
|
221
|
Chris@691
|
222 #ifdef GRAPHER_VERBOSE_DEBUG
|
Chris@122
|
223 DEBUG << "putting " << cs->id().toStdString() << " at col " << col << endl;
|
Chris@691
|
224 #endif
|
Chris@691
|
225
|
Chris@46
|
226 m_alloc[row].insert(col);
|
cannam@45
|
227 item->setColumn(col);
|
cannam@45
|
228 m_handled.insert(id);
|
Chris@47
|
229
|
Chris@153
|
230 // If we're the first parent of the uncommitted item, it should be
|
Chris@153
|
231 // given the same column as us (we already noted that its
|
Chris@153
|
232 // connecting line would end at our row)
|
Chris@145
|
233
|
Chris@153
|
234 if (m_uncommittedParents.contains(id)) {
|
Chris@153
|
235 if (m_uncommittedParents[0] == id) {
|
Chris@268
|
236 int ucol = findAvailableColumn(row-1, col, true);
|
Chris@153
|
237 m_uncommitted->setColumn(ucol);
|
Chris@153
|
238 m_haveAllocatedUncommittedColumn = true;
|
Chris@153
|
239 }
|
Chris@153
|
240 // also, if the uncommitted item has a different branch from
|
Chris@153
|
241 // any of its parents, tell it to show the branch
|
Chris@153
|
242 if (!cs->isOnBranch(m_uncommitted->branch())) {
|
Chris@153
|
243 DEBUG << "Uncommitted branch " << m_uncommitted->branch()
|
Chris@153
|
244 << " differs from my branch " << cs->branch()
|
Chris@153
|
245 << ", asking it to show branch" << endl;
|
Chris@153
|
246 m_uncommitted->setShowBranch(true);
|
Chris@153
|
247 }
|
Chris@145
|
248 }
|
Chris@145
|
249
|
Chris@311
|
250
|
Chris@51
|
251 // Normally the children will lay out themselves, but we can do
|
Chris@51
|
252 // a better job in some special cases:
|
Chris@51
|
253
|
Chris@47
|
254 int nchildren = cs->children().size();
|
Chris@49
|
255
|
Chris@53
|
256 // look for merging children and children distant from us but in a
|
Chris@53
|
257 // straight line, and make sure nobody is going to overwrite their
|
Chris@53
|
258 // connection lines
|
Chris@51
|
259
|
Chris@49
|
260 foreach (QString childId, cs->children()) {
|
Chris@691
|
261 #ifdef GRAPHER_VERBOSE_DEBUG
|
Chris@139
|
262 DEBUG << "reserving connection line space" << endl;
|
Chris@691
|
263 #endif
|
Chris@512
|
264 if (!m_items.contains(childId)) continue;
|
Chris@49
|
265 Changeset *child = m_changesets[childId];
|
Chris@106
|
266 int childRow = m_items[childId]->row();
|
Chris@55
|
267 if (child->parents().size() > 1 ||
|
Chris@153
|
268 child->isOnBranch(cs->branch())) {
|
Chris@55
|
269 for (int r = row-1; r > childRow; --r) {
|
Chris@49
|
270 m_alloc[r].insert(col);
|
Chris@49
|
271 }
|
Chris@106
|
272 }
|
Chris@49
|
273 }
|
Chris@49
|
274
|
Chris@51
|
275 // look for the case where exactly two children have the same
|
Chris@51
|
276 // branch as us: split them to a little either side of our position
|
Chris@51
|
277
|
Chris@47
|
278 if (nchildren > 1) {
|
Chris@106
|
279 QList<QString> special;
|
Chris@106
|
280 foreach (QString childId, cs->children()) {
|
Chris@512
|
281 if (!m_items.contains(childId)) continue;
|
Chris@106
|
282 Changeset *child = m_changesets[childId];
|
Chris@153
|
283 if (child->isOnBranch(branch) &&
|
Chris@106
|
284 child->parents().size() == 1) {
|
Chris@106
|
285 special.push_back(childId);
|
Chris@106
|
286 }
|
Chris@106
|
287 }
|
Chris@106
|
288 if (special.size() == 2) {
|
Chris@691
|
289 #ifdef GRAPHER_VERBOSE_DEBUG
|
Chris@139
|
290 DEBUG << "handling split-in-two for children " << special[0] << " and " << special[1] << endl;
|
Chris@691
|
291 #endif
|
Chris@106
|
292 for (int i = 0; i < 2; ++i) {
|
Chris@106
|
293 int off = i * 2 - 1; // 0 -> -1, 1 -> 1
|
Chris@106
|
294 ChangesetItem *it = m_items[special[i]];
|
Chris@268
|
295 it->setColumn(findAvailableColumn(it->row(), col + off, true));
|
Chris@106
|
296 for (int r = row-1; r >= it->row(); --r) {
|
Chris@106
|
297 m_alloc[r].insert(it->column());
|
Chris@106
|
298 }
|
Chris@106
|
299 m_handled.insert(special[i]);
|
Chris@106
|
300 }
|
Chris@106
|
301 }
|
Chris@47
|
302 }
|
cannam@45
|
303 }
|
cannam@45
|
304
|
Chris@106
|
305 bool Grapher::rangesConflict(const Range &r1, const Range &r2)
|
Chris@46
|
306 {
|
Chris@46
|
307 // allow some additional space at edges. we really ought also to
|
Chris@46
|
308 // permit space at the end of a branch up to the point where the
|
Chris@46
|
309 // merge happens
|
Chris@46
|
310 int a1 = r1.first - 2, b1 = r1.second + 2;
|
Chris@46
|
311 int a2 = r2.first - 2, b2 = r2.second + 2;
|
Chris@46
|
312 if (a1 > b2 || b1 < a2) return false;
|
Chris@46
|
313 if (a2 > b1 || b2 < a1) return false;
|
Chris@46
|
314 return true;
|
Chris@46
|
315 }
|
Chris@46
|
316
|
Chris@106
|
317 void Grapher::allocateBranchHomes(Changesets csets)
|
Chris@46
|
318 {
|
Chris@46
|
319 foreach (Changeset *cs, csets) {
|
Chris@512
|
320 QString id = cs->id();
|
Chris@512
|
321 if (!m_items.contains(id)) continue;
|
Chris@512
|
322 ChangesetItem *item = m_items[id];
|
Chris@106
|
323 QString branch = cs->branch();
|
Chris@106
|
324 int row = item->row();
|
Chris@106
|
325 if (!m_branchRanges.contains(branch)) {
|
Chris@106
|
326 m_branchRanges[branch] = Range(row, row);
|
Chris@106
|
327 } else {
|
Chris@106
|
328 Range p = m_branchRanges[branch];
|
Chris@106
|
329 if (row < p.first) p.first = row;
|
Chris@106
|
330 if (row > p.second) p.second = row;
|
Chris@106
|
331 m_branchRanges[branch] = p;
|
Chris@106
|
332 }
|
Chris@46
|
333 }
|
Chris@46
|
334
|
Chris@46
|
335 m_branchHomes[""] = 0;
|
Chris@153
|
336 m_branchHomes["default"] = 0;
|
Chris@46
|
337
|
Chris@268
|
338 foreach (QString branch, m_branchRanges.keys()) {
|
Chris@106
|
339 if (branch == "") continue;
|
Chris@106
|
340 QSet<int> taken;
|
Chris@106
|
341 taken.insert(0);
|
Chris@106
|
342 Range myRange = m_branchRanges[branch];
|
Chris@106
|
343 foreach (QString other, m_branchRanges.keys()) {
|
Chris@106
|
344 if (other == branch || other == "") continue;
|
Chris@106
|
345 Range otherRange = m_branchRanges[other];
|
Chris@106
|
346 if (rangesConflict(myRange, otherRange)) {
|
Chris@106
|
347 if (m_branchHomes.contains(other)) {
|
Chris@106
|
348 taken.insert(m_branchHomes[other]);
|
Chris@106
|
349 }
|
Chris@106
|
350 }
|
Chris@106
|
351 }
|
Chris@106
|
352 int home = 2;
|
Chris@106
|
353 while (taken.contains(home)) {
|
Chris@268
|
354 if (home > 0) {
|
Chris@268
|
355 if (home % 2 == 1) {
|
Chris@268
|
356 home = -home;
|
Chris@268
|
357 } else {
|
Chris@268
|
358 home = home + 1;
|
Chris@268
|
359 }
|
Chris@268
|
360 } else {
|
Chris@268
|
361 if ((-home) % 2 == 1) {
|
Chris@268
|
362 home = home + 1;
|
Chris@268
|
363 } else {
|
Chris@268
|
364 home = -(home-2);
|
Chris@268
|
365 }
|
Chris@268
|
366 }
|
Chris@106
|
367 }
|
Chris@106
|
368 m_branchHomes[branch] = home;
|
Chris@46
|
369 }
|
Chris@46
|
370
|
Chris@691
|
371 #ifdef GRAPHER_VERBOSE_DEBUG
|
Chris@268
|
372 foreach (QString branch, m_branchRanges.keys()) {
|
Chris@268
|
373 DEBUG << branch.toStdString() << ": " << m_branchRanges[branch].first << " - " << m_branchRanges[branch].second << ", home " << m_branchHomes[branch] << endl;
|
Chris@46
|
374 }
|
Chris@691
|
375 #endif
|
Chris@46
|
376 }
|
Chris@46
|
377
|
Chris@52
|
378 static bool
|
Chris@145
|
379 compareChangesetsByDate(Changeset *const &a, Changeset *const &b)
|
Chris@52
|
380 {
|
Chris@52
|
381 return a->timestamp() < b->timestamp();
|
Chris@52
|
382 }
|
Chris@52
|
383
|
Chris@53
|
384 ChangesetItem *
|
Chris@145
|
385 Grapher::getItemFor(Changeset *cs)
|
Chris@53
|
386 {
|
Chris@371
|
387 if (!cs) return 0;
|
Chris@371
|
388 return getItemFor(cs->id());
|
Chris@371
|
389 }
|
Chris@371
|
390
|
Chris@371
|
391 ChangesetItem *
|
Chris@371
|
392 Grapher::getItemFor(QString id)
|
Chris@371
|
393 {
|
Chris@371
|
394 if (!m_items.contains(id)) return 0;
|
Chris@371
|
395 return m_items[id];
|
Chris@53
|
396 }
|
Chris@53
|
397
|
Chris@517
|
398 void
|
Chris@517
|
399 Grapher::markClosedChangesets()
|
Chris@517
|
400 {
|
Chris@517
|
401 // Ensure the closed branch changesets are all marked as closed.
|
Chris@517
|
402
|
Chris@517
|
403 QSet<QString> deferred;
|
Chris@517
|
404
|
Chris@517
|
405 foreach (QString id, m_closedIds) {
|
Chris@517
|
406 markClosedChangesetsFrom(id, deferred);
|
Chris@517
|
407 // std::cerr << "after closed id " << id << ": candidates now contains " << deferred.size() << " element(s)" << std::endl;
|
Chris@517
|
408 }
|
Chris@517
|
409
|
Chris@517
|
410 while (!deferred.empty()) {
|
Chris@517
|
411 foreach (QString id, deferred) {
|
Chris@517
|
412 markClosedChangesetsFrom(id, deferred);
|
Chris@517
|
413 deferred.remove(id);
|
Chris@517
|
414 // std::cerr << "after id " << id << ": deferred now contains " << deferred.size() << " element(s)" << std::endl;
|
Chris@517
|
415 }
|
Chris@517
|
416 }
|
Chris@517
|
417 }
|
Chris@517
|
418
|
Chris@517
|
419 void
|
Chris@517
|
420 Grapher::markClosedChangesetsFrom(QString id, QSet<QString> &deferred)
|
Chris@517
|
421 {
|
Chris@517
|
422 // A changeset should be marked as closed (i) if it is in the list
|
Chris@517
|
423 // of closed heads [and has no children]; or (ii) all of its
|
Chris@517
|
424 // children that have the same branch name as it are marked as
|
Chris@517
|
425 // closed [and there is at least one of those]
|
Chris@517
|
426
|
Chris@517
|
427 if (!m_changesets.contains(id)) {
|
Chris@517
|
428 // std::cerr << "no good" << std::endl;
|
Chris@517
|
429 return;
|
Chris@517
|
430 }
|
Chris@517
|
431
|
Chris@517
|
432 // std::cerr << "looking at id " << id << std::endl;
|
Chris@517
|
433
|
Chris@517
|
434 Changeset *cs = m_changesets[id];
|
Chris@517
|
435 QString branch = cs->branch();
|
Chris@517
|
436
|
Chris@517
|
437 bool closed = false;
|
Chris@517
|
438
|
Chris@517
|
439 if (m_closedIds.contains(id)) {
|
Chris@517
|
440
|
Chris@517
|
441 closed = true;
|
Chris@517
|
442
|
Chris@517
|
443 } else {
|
Chris@517
|
444
|
Chris@517
|
445 closed = false;
|
Chris@517
|
446 foreach (QString childId, cs->children()) {
|
Chris@517
|
447 if (!m_changesets.contains(childId)) {
|
Chris@517
|
448 continue;
|
Chris@517
|
449 }
|
Chris@517
|
450 Changeset *ccs = m_changesets[childId];
|
Chris@517
|
451 if (ccs->isOnBranch(branch)) {
|
Chris@517
|
452 if (ccs->closed()) {
|
Chris@517
|
453 // closed becomes true only when we see a closed
|
Chris@517
|
454 // child on the same branch
|
Chris@517
|
455 closed = true;
|
Chris@517
|
456 } else {
|
Chris@517
|
457 // and it becomes false as soon as we see any
|
Chris@517
|
458 // un-closed child on the same branch
|
Chris@517
|
459 closed = false;
|
Chris@517
|
460 break;
|
Chris@517
|
461 }
|
Chris@517
|
462 }
|
Chris@517
|
463 }
|
Chris@517
|
464 }
|
Chris@517
|
465
|
Chris@517
|
466 if (closed) {
|
Chris@517
|
467 // set closed on this cset and its direct simple parents
|
Chris@517
|
468 QString csid = id;
|
Chris@517
|
469 while (cs) {
|
Chris@517
|
470 cs->setClosed(true);
|
Chris@517
|
471 if (cs->parents().size() == 1) {
|
Chris@517
|
472 QString pid = cs->parents()[0];
|
Chris@517
|
473 if (!m_changesets.contains(pid)) break;
|
Chris@517
|
474 cs = m_changesets[pid];
|
Chris@517
|
475 if (cs->children().size() > 1) {
|
Chris@517
|
476 // std::cerr << "adding pid " << pid << " (it has more than one child)" << std::endl;
|
Chris@517
|
477 deferred.insert(pid); // examine later
|
Chris@517
|
478 cs = 0;
|
Chris@517
|
479 }
|
Chris@517
|
480 } else if (cs->parents().size() > 1) {
|
Chris@517
|
481 foreach (QString pid, cs->parents()) {
|
Chris@517
|
482 // std::cerr << "recursing to pid " << pid << " (it is one of multiple parents)" << std::endl;
|
Chris@517
|
483 markClosedChangesetsFrom(pid, deferred);
|
Chris@517
|
484 }
|
Chris@517
|
485 cs = 0;
|
Chris@517
|
486 } else {
|
Chris@517
|
487 cs = 0;
|
Chris@517
|
488 }
|
Chris@517
|
489 }
|
Chris@517
|
490 } else {
|
Chris@517
|
491 cs->setClosed(false);
|
Chris@517
|
492 }
|
Chris@517
|
493
|
Chris@517
|
494 // std::cerr << "finished with id " << id << std::endl;
|
Chris@517
|
495 }
|
Chris@517
|
496
|
Chris@153
|
497 void Grapher::layout(Changesets csets,
|
Chris@153
|
498 QStringList uncommittedParents,
|
Chris@153
|
499 QString uncommittedBranch)
|
cannam@45
|
500 {
|
Chris@46
|
501 m_changesets.clear();
|
cannam@45
|
502 m_items.clear();
|
cannam@45
|
503 m_alloc.clear();
|
Chris@46
|
504 m_branchHomes.clear();
|
cannam@45
|
505
|
Chris@153
|
506 m_uncommittedParents = uncommittedParents;
|
Chris@145
|
507 m_haveAllocatedUncommittedColumn = false;
|
Chris@145
|
508 m_uncommittedParentRow = 0;
|
Chris@145
|
509 m_uncommitted = 0;
|
Chris@145
|
510
|
Chris@139
|
511 DEBUG << "Grapher::layout: Have " << csets.size() << " changesets" << endl;
|
Chris@139
|
512
|
Chris@53
|
513 if (csets.empty()) return;
|
Chris@53
|
514
|
Chris@509
|
515 // Initialise changesets hash
|
Chris@145
|
516
|
Chris@44
|
517 foreach (Changeset *cs, csets) {
|
cannam@45
|
518
|
Chris@106
|
519 QString id = cs->id();
|
cannam@45
|
520
|
Chris@106
|
521 if (id == "") {
|
Chris@106
|
522 throw LayoutException("Changeset has no ID");
|
Chris@106
|
523 }
|
Chris@106
|
524 if (m_changesets.contains(id)) {
|
Chris@145
|
525 DEBUG << "Duplicate changeset ID " << id
|
Chris@145
|
526 << " in Grapher::layout()" << endl;
|
Chris@106
|
527 throw LayoutException(QString("Duplicate changeset ID %1").arg(id));
|
Chris@106
|
528 }
|
cannam@45
|
529
|
Chris@106
|
530 m_changesets[id] = cs;
|
Chris@509
|
531 }
|
Chris@509
|
532
|
Chris@509
|
533 // Set the children for each changeset
|
cannam@45
|
534
|
Chris@509
|
535 foreach (Changeset *cs, csets) {
|
Chris@509
|
536 QString id = cs->id();
|
Chris@509
|
537 foreach (QString parentId, cs->parents()) {
|
Chris@509
|
538 if (!m_changesets.contains(parentId)) continue;
|
Chris@509
|
539 Changeset *parent = m_changesets[parentId];
|
Chris@509
|
540 parent->addChild(id);
|
Chris@509
|
541 }
|
Chris@509
|
542 }
|
Chris@511
|
543
|
Chris@515
|
544 // Ensure the closed branch changesets are all marked as closed.
|
Chris@511
|
545
|
Chris@517
|
546 markClosedChangesets();
|
Chris@509
|
547
|
Chris@509
|
548 // Create (but don't yet position) the changeset items
|
Chris@509
|
549
|
Chris@509
|
550 foreach (Changeset *cs, csets) {
|
Chris@512
|
551 if (cs->closed() && !m_showClosedBranches) continue;
|
Chris@509
|
552 QString id = cs->id();
|
cannam@45
|
553 ChangesetItem *item = new ChangesetItem(cs);
|
cannam@45
|
554 item->setX(0);
|
cannam@45
|
555 item->setY(0);
|
Chris@250
|
556 item->setZValue(0);
|
Chris@106
|
557 m_items[id] = item;
|
Chris@141
|
558 m_scene->addChangesetItem(item);
|
cannam@45
|
559 }
|
Chris@145
|
560
|
Chris@511
|
561 // Ensure the closing changeset items are appropriately marked
|
Chris@506
|
562
|
Chris@506
|
563 foreach (QString closedId, m_closedIds) {
|
Chris@506
|
564 if (!m_items.contains(closedId)) continue;
|
Chris@511
|
565 m_items[closedId]->setClosingCommit(true);
|
Chris@506
|
566 }
|
Chris@506
|
567
|
Chris@509
|
568 // Add the connecting lines
|
Chris@509
|
569
|
Chris@509
|
570 foreach (Changeset *cs, csets) {
|
Chris@509
|
571 QString id = cs->id();
|
Chris@512
|
572 if (!m_items.contains(id)) continue;
|
Chris@509
|
573 ChangesetItem *item = m_items[id];
|
Chris@509
|
574 bool merge = (cs->parents().size() > 1);
|
Chris@509
|
575 foreach (QString parentId, cs->parents()) {
|
Chris@516
|
576 if (!m_changesets.contains(parentId)) continue;
|
Chris@509
|
577 ConnectionItem *conn = new ConnectionItem();
|
Chris@509
|
578 if (merge) conn->setConnectionType(ConnectionItem::Merge);
|
Chris@509
|
579 conn->setChild(item);
|
Chris@509
|
580 conn->setZValue(-1);
|
Chris@516
|
581 if (m_items.contains(parentId)) {
|
Chris@516
|
582 conn->setParent(m_items[parentId]);
|
Chris@516
|
583 } else {
|
Chris@516
|
584 conn->setMergedBranch(m_changesets[parentId]->branch());
|
Chris@516
|
585 }
|
Chris@509
|
586 m_scene->addItem(conn);
|
Chris@509
|
587 }
|
Chris@509
|
588 }
|
Chris@509
|
589
|
Chris@145
|
590 // Add uncommitted item and connecting line as necessary
|
Chris@145
|
591
|
Chris@153
|
592 if (!m_uncommittedParents.empty()) {
|
Chris@311
|
593
|
Chris@145
|
594 m_uncommitted = new UncommittedItem();
|
Chris@153
|
595 m_uncommitted->setBranch(uncommittedBranch);
|
Chris@250
|
596 m_uncommitted->setZValue(10);
|
Chris@149
|
597 m_scene->addUncommittedItem(m_uncommitted);
|
Chris@311
|
598
|
Chris@311
|
599 bool haveParentOnBranch = false;
|
Chris@153
|
600 foreach (QString p, m_uncommittedParents) {
|
Chris@512
|
601 if (!m_items.contains(p)) continue;
|
Chris@153
|
602 ConnectionItem *conn = new ConnectionItem();
|
Chris@153
|
603 conn->setConnectionType(ConnectionItem::Merge);
|
Chris@311
|
604 ChangesetItem *pitem = m_items[p];
|
Chris@311
|
605 conn->setParent(pitem);
|
Chris@153
|
606 conn->setChild(m_uncommitted);
|
Chris@388
|
607 conn->setZValue(-1);
|
Chris@153
|
608 m_scene->addItem(conn);
|
Chris@311
|
609 if (pitem) {
|
Chris@409
|
610 if (pitem->getChangeset()->isOnBranch(uncommittedBranch)) {
|
Chris@311
|
611 haveParentOnBranch = true;
|
Chris@311
|
612 }
|
Chris@311
|
613 }
|
Chris@153
|
614 }
|
Chris@311
|
615
|
Chris@311
|
616 // If the uncommitted item has no parents on the same branch,
|
Chris@311
|
617 // tell it it has a new branch (the "show branch" flag is set
|
Chris@311
|
618 // elsewhere for this item)
|
Chris@311
|
619 m_uncommitted->setIsNewBranch(!haveParentOnBranch);
|
Chris@399
|
620
|
Chris@399
|
621 // Uncommitted is a merge if it has more than one parent
|
Chris@399
|
622 m_uncommitted->setIsMerge(m_uncommittedParents.size() > 1);
|
Chris@145
|
623 }
|
Chris@46
|
624
|
Chris@74
|
625 // Add the branch labels
|
Chris@145
|
626
|
Chris@74
|
627 foreach (Changeset *cs, csets) {
|
Chris@74
|
628 QString id = cs->id();
|
Chris@512
|
629 if (!m_items.contains(id)) continue;
|
Chris@74
|
630 ChangesetItem *item = m_items[id];
|
Chris@74
|
631 bool haveChildOnSameBranch = false;
|
Chris@74
|
632 foreach (QString childId, cs->children()) {
|
Chris@74
|
633 Changeset *child = m_changesets[childId];
|
Chris@74
|
634 if (child->branch() == cs->branch()) {
|
Chris@74
|
635 haveChildOnSameBranch = true;
|
Chris@74
|
636 break;
|
Chris@74
|
637 }
|
Chris@74
|
638 }
|
Chris@74
|
639 item->setShowBranch(!haveChildOnSameBranch);
|
Chris@74
|
640 }
|
Chris@74
|
641
|
Chris@52
|
642 // We need to lay out the changesets in forward chronological
|
Chris@52
|
643 // order. We have no guarantees about the order in which
|
Chris@52
|
644 // changesets appear in the list -- in a simple repository they
|
Chris@52
|
645 // will generally be reverse chronological, but that's far from
|
Chris@52
|
646 // guaranteed. So, sort explicitly using the date comparator
|
Chris@52
|
647 // above
|
Chris@51
|
648
|
Chris@52
|
649 qStableSort(csets.begin(), csets.end(), compareChangesetsByDate);
|
Chris@51
|
650
|
Chris@691
|
651 #ifdef GRAPHER_VERBOSE_DEBUG
|
Chris@53
|
652 foreach (Changeset *cs, csets) {
|
Chris@145
|
653 DEBUG << "id " << cs->id().toStdString() << ", ts " << cs->timestamp()
|
Chris@145
|
654 << ", date " << cs->datetime().toStdString() << endl;
|
Chris@53
|
655 }
|
Chris@691
|
656 #endif
|
Chris@53
|
657
|
cannam@45
|
658 m_handled.clear();
|
Chris@53
|
659 foreach (Changeset *cs, csets) {
|
Chris@106
|
660 layoutRow(cs->id());
|
cannam@45
|
661 }
|
Chris@46
|
662
|
Chris@46
|
663 allocateBranchHomes(csets);
|
Chris@46
|
664
|
cannam@45
|
665 m_handled.clear();
|
Chris@53
|
666 foreach (Changeset *cs, csets) {
|
Chris@106
|
667 foreach (QString parentId, cs->parents()) {
|
Chris@106
|
668 if (!m_handled.contains(parentId) &&
|
Chris@106
|
669 m_changesets.contains(parentId)) {
|
Chris@106
|
670 layoutCol(parentId);
|
Chris@106
|
671 }
|
Chris@106
|
672 }
|
Chris@106
|
673 layoutCol(cs->id());
|
Chris@53
|
674 }
|
Chris@53
|
675
|
Chris@145
|
676 // Find row and column extents. We know that 0 is an upper bound
|
Chris@145
|
677 // on row, and that mincol must be <= 0 and maxcol >= 0, so these
|
Chris@145
|
678 // initial values are good
|
Chris@55
|
679
|
Chris@53
|
680 int minrow = 0, maxrow = 0;
|
Chris@53
|
681 int mincol = 0, maxcol = 0;
|
Chris@53
|
682
|
Chris@53
|
683 foreach (int r, m_alloc.keys()) {
|
Chris@106
|
684 if (r < minrow) minrow = r;
|
Chris@106
|
685 if (r > maxrow) maxrow = r;
|
Chris@106
|
686 ColumnSet &c = m_alloc[r];
|
Chris@106
|
687 foreach (int i, c) {
|
Chris@106
|
688 if (i < mincol) mincol = i;
|
Chris@106
|
689 if (i > maxcol) maxcol = i;
|
Chris@106
|
690 }
|
Chris@53
|
691 }
|
Chris@53
|
692
|
Chris@153
|
693 int datemincol = mincol, datemaxcol = maxcol;
|
Chris@153
|
694
|
Chris@153
|
695 if (mincol == maxcol) {
|
Chris@153
|
696 --datemincol;
|
Chris@153
|
697 ++datemaxcol;
|
Chris@153
|
698 } else if (m_alloc[minrow].contains(mincol)) {
|
Chris@153
|
699 --datemincol;
|
Chris@153
|
700 }
|
Chris@153
|
701
|
Chris@145
|
702 // We've given the uncommitted item a column, but not a row yet --
|
Chris@145
|
703 // it always goes at the top
|
Chris@145
|
704
|
Chris@145
|
705 if (m_uncommitted) {
|
Chris@145
|
706 --minrow;
|
Chris@691
|
707 #ifdef GRAPHER_VERBOSE_DEBUG
|
Chris@145
|
708 DEBUG << "putting uncommitted item at row " << minrow << endl;
|
Chris@691
|
709 #endif
|
Chris@145
|
710 m_uncommitted->setRow(minrow);
|
Chris@145
|
711 }
|
Chris@145
|
712
|
Chris@145
|
713 // Changeset items that have nothing to either side of them can be
|
Chris@145
|
714 // made double-width
|
Chris@145
|
715
|
Chris@145
|
716 foreach (Changeset *cs, csets) {
|
Chris@512
|
717 QString id = cs->id();
|
Chris@512
|
718 if (!m_items.contains(id)) continue;
|
Chris@512
|
719 ChangesetItem *item = m_items[id];
|
Chris@145
|
720 if (isAvailable(item->row(), item->column()-1) &&
|
Chris@145
|
721 isAvailable(item->row(), item->column()+1)) {
|
Chris@145
|
722 item->setWide(true);
|
Chris@145
|
723 }
|
Chris@145
|
724 }
|
Chris@145
|
725
|
Chris@145
|
726 if (m_uncommitted) {
|
Chris@145
|
727 if (isAvailable(m_uncommitted->row(), m_uncommitted->column()-1) &&
|
Chris@145
|
728 isAvailable(m_uncommitted->row(), m_uncommitted->column()+1)) {
|
Chris@145
|
729 m_uncommitted->setWide(true);
|
Chris@145
|
730 }
|
Chris@145
|
731 }
|
Chris@145
|
732
|
Chris@53
|
733 QString prevDate;
|
Chris@53
|
734 int changeRow = 0;
|
Chris@53
|
735
|
Chris@53
|
736 bool even = false;
|
Chris@53
|
737 int n = 0;
|
Chris@53
|
738
|
Chris@53
|
739 for (int row = minrow; row <= maxrow; ++row) {
|
Chris@53
|
740
|
Chris@106
|
741 QString date = m_rowDates[row];
|
Chris@106
|
742 n++;
|
Chris@106
|
743
|
Chris@106
|
744 if (date != prevDate) {
|
Chris@106
|
745 if (prevDate != "") {
|
Chris@397
|
746 m_scene->addDateRange(prevDate, changeRow, n, even);
|
Chris@106
|
747 even = !even;
|
Chris@106
|
748 }
|
Chris@106
|
749 prevDate = date;
|
Chris@106
|
750 changeRow = row;
|
Chris@106
|
751 n = 0;
|
Chris@106
|
752 }
|
Chris@53
|
753 }
|
Chris@53
|
754
|
Chris@53
|
755 if (n > 0) {
|
Chris@397
|
756 m_scene->addDateRange(prevDate, changeRow, n+1, even);
|
Chris@106
|
757 even = !even;
|
cannam@45
|
758 }
|
Chris@397
|
759
|
Chris@397
|
760 m_scene->itemAddCompleted();
|
Chris@44
|
761 }
|
Chris@44
|
762
|