tmf: Move TmfTraceType and custom parsers to tmf.core
[deliverable/tracecompass.git] / org.eclipse.linuxtools.tmf.ui / src / org / eclipse / linuxtools / tmf / ui / project / wizards / importtrace / BatchImportTraceWizard.java
1 /*******************************************************************************
2 * Copyright (c) 2013 Ericsson
3 *
4 * All rights reserved. This program and the accompanying materials are
5 * made available under the terms of the Eclipse Public License v1.0 which
6 * accompanies this distribution, and is available at
7 * http://www.eclipse.org/legal/epl-v10.html
8 *
9 * Contributors:
10 * Matthew Khouzam - Initial API and implementation
11 * Marc-Andre Laperle - Log some exceptions
12 *******************************************************************************/
13
14 package org.eclipse.linuxtools.tmf.ui.project.wizards.importtrace;
15
16 import java.io.File;
17 import java.io.FileInputStream;
18 import java.lang.reflect.InvocationTargetException;
19 import java.util.ArrayList;
20 import java.util.Collections;
21 import java.util.Comparator;
22 import java.util.HashMap;
23 import java.util.HashSet;
24 import java.util.Iterator;
25 import java.util.List;
26 import java.util.Map;
27 import java.util.Set;
28 import java.util.TreeSet;
29 import java.util.concurrent.BlockingQueue;
30
31 import org.eclipse.core.resources.IFile;
32 import org.eclipse.core.resources.IFolder;
33 import org.eclipse.core.resources.IResource;
34 import org.eclipse.core.resources.ResourcesPlugin;
35 import org.eclipse.core.runtime.CoreException;
36 import org.eclipse.core.runtime.IPath;
37 import org.eclipse.core.runtime.IProgressMonitor;
38 import org.eclipse.core.runtime.IStatus;
39 import org.eclipse.core.runtime.NullProgressMonitor;
40 import org.eclipse.core.runtime.Path;
41 import org.eclipse.core.runtime.Status;
42 import org.eclipse.core.runtime.SubMonitor;
43 import org.eclipse.jface.dialogs.ErrorDialog;
44 import org.eclipse.jface.dialogs.IDialogSettings;
45 import org.eclipse.jface.operation.IRunnableWithProgress;
46 import org.eclipse.jface.viewers.IStructuredSelection;
47 import org.eclipse.jface.wizard.IWizardPage;
48 import org.eclipse.jface.wizard.Wizard;
49 import org.eclipse.jface.wizard.WizardDialog;
50 import org.eclipse.linuxtools.internal.tmf.ui.Activator;
51 import org.eclipse.linuxtools.internal.tmf.ui.project.model.TmfImportHelper;
52 import org.eclipse.linuxtools.tmf.core.project.model.TmfTraceType;
53 import org.eclipse.linuxtools.tmf.core.project.model.TraceTypeHelper;
54 import org.eclipse.linuxtools.tmf.core.project.model.TraceValidationHelper;
55 import org.eclipse.linuxtools.tmf.core.trace.ITmfTrace;
56 import org.eclipse.linuxtools.tmf.ui.project.model.TmfProjectElement;
57 import org.eclipse.linuxtools.tmf.ui.project.model.TmfProjectRegistry;
58 import org.eclipse.linuxtools.tmf.ui.project.model.TmfTraceElement;
59 import org.eclipse.linuxtools.tmf.ui.project.model.TmfTraceFolder;
60 import org.eclipse.linuxtools.tmf.ui.project.model.TmfTraceTypeUIUtils;
61 import org.eclipse.ui.IImportWizard;
62 import org.eclipse.ui.IWorkbench;
63 import org.eclipse.ui.dialogs.IOverwriteQuery;
64 import org.eclipse.ui.wizards.datatransfer.FileSystemStructureProvider;
65 import org.eclipse.ui.wizards.datatransfer.ImportOperation;
66
67 /**
68 * Batch Import trace wizard.
69 *
70 * @author Matthew Khouzam
71 * @since 2.0
72 */
73 public class BatchImportTraceWizard extends Wizard implements IImportWizard {
74
75 private static final int WIN_HEIGHT = 400;
76 private static final int WIN_WIDTH = 800;
77 private static final Status CANCEL_STATUS = new Status(IStatus.CANCEL, Activator.PLUGIN_ID, ""); //$NON-NLS-1$
78 private static final int TOTALWORK = 65536;
79 // -----------------
80 // Constants
81 // -----------------
82
83 private static final int MAX_FILES = TOTALWORK - 1;
84 private static final String BATCH_IMPORT_WIZARD = "BatchImportTraceWizard"; //$NON-NLS-1$
85
86 // ------------------
87 // Fields
88 // ------------------
89
90 private IWizardPage fSelectDirectoriesPage;
91 private ImportTraceWizardScanPage fScanPage;
92 private IWizardPage fSelectTypePage;
93 private IWizardPage fOptions;
94
95 private final List<String> fTraceTypesToScan = new ArrayList<>();
96 private final Set<String> fParentFilesToScan = new HashSet<>();
97
98 private ImportTraceContentProvider fScannedTraces = new ImportTraceContentProvider(fTraceTypesToScan, fParentFilesToScan);
99
100 private final Map<TraceValidationHelper, Boolean> fResults = new HashMap<>();
101 private boolean fOverwrite = true;
102 private boolean fLinked = true;
103
104 private BlockingQueue<TraceValidationHelper> fTracesToScan;
105 private final Set<FileAndName> fTraces = new TreeSet<>();
106
107 private Map<String, Set<String>> fParentFiles = new HashMap<>();
108
109 // Target import directory ('Traces' folder)
110 private IFolder fTargetFolder;
111
112 /**
113 * Returns the ScannedTraces model
114 *
115 * @return the ScannedTraces model
116 */
117 public ImportTraceContentProvider getScannedTraces() {
118 return fScannedTraces;
119 }
120
121 /**
122 * Constructor
123 */
124 public BatchImportTraceWizard() {
125 IDialogSettings workbenchSettings = Activator.getDefault().getDialogSettings();
126 IDialogSettings section = workbenchSettings.getSection(BATCH_IMPORT_WIZARD);
127 if (section == null) {
128 section = workbenchSettings.addNewSection(BATCH_IMPORT_WIZARD);
129 }
130 setDialogSettings(section);
131 setNeedsProgressMonitor(true);
132 }
133
134 @Override
135 public void init(IWorkbench workbench, IStructuredSelection selection) {
136
137 fSelectDirectoriesPage = new ImportTraceWizardSelectDirectoriesPage(workbench, selection);
138 fScanPage = new ImportTraceWizardScanPage(workbench, selection);
139 fSelectTypePage = new ImportTraceWizardSelectTraceTypePage(workbench, selection);
140 fOptions = new ImportTraceWizardPageOptions(workbench, selection);
141 // keep in case it's called later
142 Iterator<?> iter = selection.iterator();
143 while (iter.hasNext()) {
144 Object selected = iter.next();
145 if (selected instanceof TmfTraceFolder) {
146 fTargetFolder = ((TmfTraceFolder) selected).getResource();
147 break;
148 }
149 }
150 fResults.clear();
151 }
152
153 @Override
154 public void addPages() {
155 addPage(fSelectTypePage);
156 addPage(fSelectDirectoriesPage);
157 addPage(fScanPage);
158 addPage(fOptions);
159 final WizardDialog container = (WizardDialog) getContainer();
160 if (container != null) {
161 container.setPageSize(WIN_WIDTH, WIN_HEIGHT);
162 }
163 }
164
165 /**
166 * Add a file to scan
167 *
168 * @param fileName
169 * the file to scan
170 */
171 public void addFileToScan(final String fileName) {
172 String absolutePath = new File(fileName).getAbsolutePath();
173 if (!fParentFiles.containsKey(absolutePath)) {
174 fParentFiles.put(absolutePath, new HashSet<String>());
175 startUpdateTask(Messages.BatchImportTraceWizardAdd + ' ' + absolutePath, absolutePath);
176
177 }
178
179 }
180
181 /**
182 * Remove files from selection
183 *
184 * @param fileName
185 * the name of the file to remove
186 */
187 public void removeFile(final String fileName) {
188 fParentFiles.remove(fileName);
189 fParentFilesToScan.remove(fileName);
190 startUpdateTask(Messages.BatchImportTraceWizardRemove + ' ' + fileName, null);
191 }
192
193 private void startUpdateTask(final String taskName, final String fileAbsolutePath) {
194 try {
195 this.getContainer().run(true, true, new IRunnableWithProgress() {
196
197 @Override
198 public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException {
199 synchronized (BatchImportTraceWizard.this) { // this should
200 // only run one
201 // at a time
202 SubMonitor sm;
203 sm = SubMonitor.convert(monitor);
204 sm.setTaskName(taskName);
205 sm.setWorkRemaining(TOTALWORK);
206 updateFiles(sm, fileAbsolutePath);
207 sm.done();
208 }
209 }
210 });
211 } catch (InvocationTargetException e) {
212 Activator.getDefault().logError(Messages.ImportTraceWizardImportProblem, e);
213 } catch (InterruptedException e) {
214 }
215 }
216
217 /**
218 * The set of names of the selected files
219 *
220 * @return the set of names of the selected files
221 */
222 public Set<String> getFileNames() {
223 return fParentFilesToScan;
224 }
225
226 /**
227 * Reset the trace list to import
228 */
229 public void clearTraces() {
230 fTraces.clear();
231 }
232
233 @Override
234 public boolean performFinish() {
235 if (fTraces.isEmpty()) {
236 return false;
237 }
238 // if this turns out to be too slow, put in a progress monitor. Does not
239 // appear to be slow for the moment.
240 boolean success = importTraces();
241 return success;
242 }
243
244 private boolean importTraces() {
245 boolean success = false;
246 IOverwriteQuery overwriteQuery = new IOverwriteQuery() {
247 @Override
248 public String queryOverwrite(String file) {
249 return fOverwrite ? IOverwriteQuery.ALL : IOverwriteQuery.NO_ALL;
250 }
251 };
252 FileSystemStructureProvider fileSystemStructureProvider = FileSystemStructureProvider.INSTANCE;
253
254 for (FileAndName traceToImport : fTraces) {
255 try {
256 if (fLinked) {
257 if (TmfImportHelper.createLink(fTargetFolder, Path.fromOSString(traceToImport.getFile().getAbsolutePath()), traceToImport.getName()) == null) {
258 success = false;
259 }
260 else {
261 success = setTraceType(traceToImport).isOK();
262 }
263 }
264 else {
265 List<File> subList = new ArrayList<>();
266 IPath path = fTargetFolder.getFullPath();
267 File parentFile = traceToImport.getFile();
268 final boolean isFile = parentFile.isFile();
269 if (isFile) {
270 IFile resource = ResourcesPlugin.getWorkspace().getRoot().getFile(path.append(traceToImport.getName()));
271 if (fOverwrite || !resource.exists()) {
272 subList.add(parentFile);
273 parentFile = parentFile.getParentFile();
274 try (final FileInputStream source = new FileInputStream(traceToImport.getFile());) {
275 if (resource.exists()) {
276 resource.delete(IResource.FORCE, new NullProgressMonitor());
277 }
278 resource.create(source, true, new NullProgressMonitor());
279 }
280 setTraceType(traceToImport);
281 success = true;
282 }
283 } else {
284 // Add trace directory
285 subList.add(traceToImport.getFile());
286 // Add all files in trace directory
287 File[] fileList = traceToImport.getFile().listFiles();
288 for (File child : fileList) {
289 subList.add(child);
290 }
291
292 Collections.sort(subList, new Comparator<File>() {
293 @Override
294 public int compare(File o1, File o2) {
295 return o1.getAbsolutePath().compareTo(o2.getAbsolutePath());
296 }
297 });
298 ImportOperation operation = new ImportOperation(
299 path,
300 parentFile.getParentFile(),
301 fileSystemStructureProvider,
302 overwriteQuery,
303 subList);
304 operation.setContext(getShell());
305 operation.setCreateContainerStructure(false);
306 if (executeImportOperation(operation)) {
307 setTraceType(traceToImport);
308 success = true;
309 }
310 }
311
312 }
313 } catch (Exception e) {
314 }
315 }
316 return success;
317 }
318
319 private IStatus setTraceType(FileAndName traceToImport) {
320 IStatus validate = Status.OK_STATUS;
321 IPath path = fTargetFolder.getFullPath().append(traceToImport.getName());
322 IResource resource = ResourcesPlugin.getWorkspace().getRoot().findMember(path);
323 if (resource != null) {
324 try {
325 // Set the trace type for this resource
326 String traceTypeId = traceToImport.getTraceTypeId();
327 TraceTypeHelper traceType = TmfTraceType.getInstance().getTraceType(traceTypeId);
328 if (traceType != null) {
329 TmfTraceTypeUIUtils.setTraceType(path, traceType);
330 }
331
332 TmfProjectElement tmfProject =
333 TmfProjectRegistry.getProject(resource.getProject());
334 if (tmfProject != null) {
335 final TmfTraceFolder tracesFolder = tmfProject.getTracesFolder();
336
337 List<TmfTraceElement> traces = tracesFolder.getTraces();
338 boolean found = false;
339 for (TmfTraceElement traceElement : traces) {
340 if (traceElement.getName().equals(resource.getName())) {
341 traceElement.refreshTraceType();
342 found = true;
343 break;
344 }
345 }
346 if (!found) {
347 TmfTraceElement te = new TmfTraceElement(traceToImport.getName(), resource, tracesFolder);
348 te.refreshTraceType();
349 traces = tracesFolder.getTraces();
350 for (TmfTraceElement traceElement : traces) {
351 if (traceElement.getName().equals(resource.getName())) {
352 traceElement.refreshTraceType();
353 ITmfTrace tmfTrace = null;
354 try {
355 tmfTrace = traceElement.instantiateTrace();
356 if (tmfTrace != null) {
357 validate = tmfTrace.validate(tmfProject.getResource(), traceElement.getLocation().getPath());
358 } else {
359 return new Status(IStatus.ERROR, traceElement.getName(), "File does not exist : " + traceElement.getLocation().getPath()); //$NON-NLS-1$
360 }
361 } finally {
362 if (tmfTrace != null) {
363 tmfTrace.dispose();
364 }
365 }
366 break;
367 }
368 }
369
370 }
371
372 }
373 } catch (CoreException e) {
374 Activator.getDefault().logError(Messages.BatchImportTraceWizardErrorImportingTraceResource
375 + ' ' + resource.getName(), e);
376 }
377 }
378 return validate;
379 }
380
381 @Override
382 public boolean canFinish() {
383 return super.canFinish() && hasTracesToImport() && !hasConflicts() && (fTargetFolder != null);
384 }
385
386 /**
387 * Returns if a trace to import is selected
388 *
389 * @return if there are traces to import
390 */
391 public boolean hasTracesToImport() {
392 return fTraces.size() > 0;
393 }
394
395 /**
396 * Reset the files to scan
397 */
398 public void clearFilesToScan() {
399 fTracesToScan.clear();
400 }
401
402 /**
403 * Set the trace types to scan
404 *
405 * @param tracesToScan
406 * a list of trace types to scan for
407 */
408 public void setTraceTypesToScan(List<String> tracesToScan) {
409 // intersection to know if there's a diff.
410 // if there's a diff, we need to re-enque everything
411 List<String> added = new ArrayList<>();
412 for (String traceLoc : tracesToScan) {
413 if (!fTraceTypesToScan.contains(traceLoc)) {
414 added.add(traceLoc);
415 }
416 }
417 fTraceTypesToScan.clear();
418 fTraceTypesToScan.addAll(tracesToScan);
419 updateTracesToScan(added);
420 }
421
422 /**
423 * Get the trace types to scan
424 *
425 * @return a list of traces to Scan for
426 */
427 public List<String> getTraceTypesToScan() {
428 return fTraceTypesToScan;
429 }
430
431 /**
432 * Add files to Import
433 *
434 * @param element
435 * add the file and tracetype to import
436 */
437 public void addFileToImport(FileAndName element) {
438 fTraces.add(element);
439 updateConflicts();
440 }
441
442 /**
443 * Remove the file to scan
444 *
445 * @param element
446 * the element to remove
447 */
448 public void removeFileToImport(FileAndName element) {
449 fTraces.remove(element);
450 element.setConflictingName(false);
451 updateConflicts();
452 }
453
454 /**
455 * Updates the trace to see if there are conflicts.
456 */
457 public void updateConflicts() {
458 final FileAndName[] fChildren = fTraces.toArray(new FileAndName[0]);
459 for (int i = 0; i < fChildren.length; i++) {
460 fChildren[i].setConflictingName(false);
461 }
462 for (int i = 1; i < fChildren.length; i++) {
463 for (int j = 0; j < i; j++) {
464 if (fChildren[i].getName().equals(fChildren[j].getName())) {
465 fChildren[i].setConflictingName(true);
466 fChildren[j].setConflictingName(true);
467 }
468 }
469 }
470 getContainer().updateButtons();
471 }
472
473 /**
474 * Is there a name conflict
475 */
476 boolean hasConflicts() {
477 boolean conflict = false;
478 for (FileAndName child : fTraces) {
479 conflict |= child.isConflictingName();
480 }
481 return conflict;
482 }
483
484 private boolean executeImportOperation(ImportOperation op) {
485 initializeOperation(op);
486
487 try {
488 getContainer().run(true, true, op);
489 } catch (InterruptedException e) {
490 return false;
491 } catch (InvocationTargetException e) {
492 Activator.getDefault().logError(Messages.ImportTraceWizardImportProblem, e);
493 return false;
494 }
495
496 IStatus status = op.getStatus();
497 if (!status.isOK()) {
498 ErrorDialog.openError(getContainer().getShell(), Messages.ImportTraceWizardImportProblem, null, status);
499 return false;
500 }
501
502 return true;
503 }
504
505 private static void initializeOperation(ImportOperation op) {
506 op.setCreateContainerStructure(false);
507 op.setOverwriteResources(false);
508 op.setVirtualFolders(false);
509 }
510
511 /**
512 * Override existing resources
513 *
514 * @param selection
515 * true or false
516 */
517 public void setOverwrite(boolean selection) {
518 fOverwrite = selection;
519 }
520
521 /**
522 * Is the trace linked?
523 *
524 * @param isLink
525 * true or false
526 */
527 public void setLinked(boolean isLink) {
528 fLinked = isLink;
529 }
530
531 /**
532 * @param tracesToScan
533 * sets the common traces to scan
534 */
535 public void setTracesToScan(BlockingQueue<TraceValidationHelper> tracesToScan) {
536 fTracesToScan = tracesToScan;
537 }
538
539 /**
540 * @param traceToScan
541 * The trace to scan
542 * @return if the trace has been scanned yet or not
543 * @since 3.0
544 */
545 public boolean hasScanned(TraceValidationHelper traceToScan) {
546 return fResults.containsKey(traceToScan);
547 }
548
549 /**
550 * Add a result to a cache
551 *
552 * @param traceToScan
553 * The trace that has been scanned
554 * @param validate
555 * if the trace is valid
556 * @since 3.0
557 */
558 public void addResult(TraceValidationHelper traceToScan, boolean validate) {
559 fResults.put(traceToScan, validate);
560 }
561
562 /**
563 * Gets if the trace has been scanned or not
564 *
565 * @param traceToScan
566 * the scanned trace
567 * @return whether it passes or not
568 * @since 3.0
569 */
570 public Boolean getResult(TraceValidationHelper traceToScan) {
571 return fResults.get(traceToScan);
572 }
573
574 /**
575 * Returns the amount of files scanned
576 *
577 * @return the amount of files scanned
578 */
579 public int getNumberOfResults() {
580 return fResults.size();
581 }
582
583 private void updateTracesToScan(final List<String> added) {
584 // Treeset is used instead of a hashset since the traces should be read
585 // in the order they were added.
586 final Set<String> filesToScan = new TreeSet<>();
587 for (String name : fParentFiles.keySet()) {
588 filesToScan.addAll(fParentFiles.get(name));
589 }
590 IProgressMonitor pm = new NullProgressMonitor();
591 try {
592 updateScanQueue(pm, filesToScan, added);
593 } catch (InterruptedException e) {
594 }
595
596 }
597
598 /*
599 * I am a job. Make me work
600 */
601 private synchronized IStatus updateFiles(IProgressMonitor monitor, String traceToScanAbsPath) {
602 final Set<String> filesToScan = new TreeSet<>();
603
604 int workToDo = 1;
605 for (String name : fParentFiles.keySet()) {
606
607 final File file = new File(name);
608 final File[] listFiles = file.listFiles();
609 if (listFiles != null) {
610 workToDo += listFiles.length;
611 }
612 }
613 int step = TOTALWORK / workToDo;
614 try {
615 for (String name : fParentFiles.keySet()) {
616 final File fileToAdd = new File(name);
617 final Set<String> parentFilesToScan = fParentFiles.get(fileToAdd.getAbsolutePath());
618 recurse(parentFilesToScan, fileToAdd, monitor, step);
619 if (monitor.isCanceled()) {
620 fParentFilesToScan.remove(traceToScanAbsPath);
621 fParentFiles.remove(traceToScanAbsPath);
622 return CANCEL_STATUS;
623 }
624 }
625 filesToScan.clear();
626 for (String name : fParentFiles.keySet()) {
627 filesToScan.addAll(fParentFiles.get(name));
628 fParentFilesToScan.add(name);
629 }
630 IStatus cancelled = updateScanQueue(monitor, filesToScan, fTraceTypesToScan);
631 if (cancelled.matches(IStatus.CANCEL)) {
632 fParentFilesToScan.remove(traceToScanAbsPath);
633 fParentFiles.remove(traceToScanAbsPath);
634 }
635 } catch (InterruptedException e) {
636 monitor.done();
637 return new Status(IStatus.ERROR, Activator.PLUGIN_ID, e.getMessage(), e);
638 }
639
640 monitor.done();
641 return Status.OK_STATUS;
642 }
643
644 private IStatus updateScanQueue(IProgressMonitor monitor, final Set<String> filesToScan, final List<String> traceTypes) throws InterruptedException {
645 for (String fileToScan : filesToScan) {
646 for (String traceCat : traceTypes) {
647 TraceValidationHelper tv = new TraceValidationHelper(fileToScan, traceCat);
648 // for thread safety, keep checks in this order.
649 if (!fResults.containsKey(tv)) {
650 if (!fTracesToScan.contains(tv)) {
651 fTracesToScan.put(tv);
652 monitor.subTask(tv.getTraceToScan());
653 if (monitor.isCanceled()) {
654 fScanPage.refresh();
655 return CANCEL_STATUS;
656 }
657 }
658 }
659 }
660 }
661 fScanPage.refresh();
662 return Status.OK_STATUS;
663 }
664
665 private IStatus recurse(Set<String> filesToScan, File fileToAdd, IProgressMonitor monitor, int step) {
666 final String absolutePath = fileToAdd.getAbsolutePath();
667 if (!filesToScan.contains(absolutePath) && (filesToScan.size() < MAX_FILES)) {
668 filesToScan.add(absolutePath);
669 final File[] listFiles = fileToAdd.listFiles();
670 if (null != listFiles) {
671 for (File child : listFiles) {
672 monitor.subTask(child.getName());
673 if (monitor.isCanceled()) {
674 return CANCEL_STATUS;
675 }
676 IStatus retVal = recurse(filesToScan, child, monitor);
677 if (retVal.matches(IStatus.CANCEL)) {
678 return retVal;
679 }
680 monitor.worked(step);
681 }
682 }
683 }
684 return Status.OK_STATUS;
685 }
686
687 private IStatus recurse(Set<String> filesToScan, File fileToAdd, IProgressMonitor monitor) {
688 final String absolutePath = fileToAdd.getAbsolutePath();
689 if (!filesToScan.contains(absolutePath) && (filesToScan.size() < MAX_FILES)) {
690 filesToScan.add(absolutePath);
691 final File[] listFiles = fileToAdd.listFiles();
692 if (null != listFiles) {
693 for (File child : listFiles) {
694 if (monitor.isCanceled()) {
695 return CANCEL_STATUS;
696 }
697 IStatus retVal = recurse(filesToScan, child, monitor);
698 if (retVal.matches(IStatus.CANCEL)) {
699 return retVal;
700 }
701 }
702 }
703 }
704 return Status.OK_STATUS;
705 }
706
707 /**
708 * Gets the folder in the resource (project)
709 *
710 * @param targetFolder
711 * the folder to import to
712 */
713 public void setTraceFolder(IFolder targetFolder) {
714 fTargetFolder = targetFolder;
715 if (this.getContainer() != null && this.getContainer().getCurrentPage() != null) {
716 this.getContainer().updateButtons();
717 }
718 }
719
720 /**
721 * Gets the target folder
722 *
723 * @return the target folder
724 */
725 public IFolder getTargetFolder() {
726 return fTargetFolder;
727 }
728
729 }
This page took 0.055692 seconds and 5 git commands to generate.