mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-03-27 12:03:06 -04:00
Some checks are pending
Integration tests for the release process / release-simulation (push) Waiting to run
/ release (push) Waiting to run
testing-integration / test-unit (push) Waiting to run
testing-integration / test-sqlite (push) Waiting to run
testing-integration / test-mariadb (v10.6) (push) Waiting to run
testing-integration / test-mariadb (v11.8) (push) Waiting to run
testing / backend-checks (push) Waiting to run
testing / frontend-checks (push) Waiting to run
testing / test-unit (push) Blocked by required conditions
testing / test-e2e (push) Blocked by required conditions
testing / test-remote-cacher (redis) (push) Blocked by required conditions
testing / test-remote-cacher (valkey) (push) Blocked by required conditions
testing / test-remote-cacher (garnet) (push) Blocked by required conditions
testing / test-remote-cacher (redict) (push) Blocked by required conditions
testing / test-mysql (push) Blocked by required conditions
testing / test-pgsql (push) Blocked by required conditions
testing / test-sqlite (push) Blocked by required conditions
testing / security-check (push) Blocked by required conditions
Fixes #2705. Fixes #7635. This PR fixes the commit graph showing false connections for orphan/root commits. Connection lines are now shown only when a parent/child relationship exists. Visible relationships are determined using `git log`'s `%P` output by the new `ComputeGlyphConnectivity` function. The SVG template is adapted to render vertical lines conditionally. Unit tests for `ComputeGlyphConnectivity` cover regular linear commit history, orphan commits, merge commits, and non-commit glyphs (`|`, `/`, `\`). Unit tests also cover the changes to the `git log` parsing. The SVG template was verified manually. Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/10484 Reviewed-by: Gusted <gusted@noreply.codeberg.org> Co-authored-by: Bram Hagens <bram@bramh.me> Co-committed-by: Bram Hagens <bram@bramh.me>
849 lines
16 KiB
Go
849 lines
16 KiB
Go
// Copyright 2016 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package gitgraph
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
"forgejo.org/modules/git"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func BenchmarkGetCommitGraph(b *testing.B) {
|
|
currentRepo, err := git.OpenRepository(git.DefaultContext, ".")
|
|
if err != nil || currentRepo == nil {
|
|
b.Error("Could not open repository")
|
|
}
|
|
defer currentRepo.Close()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
graph, err := GetCommitGraph(currentRepo, 1, 0, false, nil, nil)
|
|
if err != nil {
|
|
b.Error("Could get commit graph")
|
|
}
|
|
|
|
if len(graph.Commits) < 100 {
|
|
b.Error("Should get 100 log lines.")
|
|
}
|
|
}
|
|
}
|
|
|
|
func BenchmarkParseCommitString(b *testing.B) {
|
|
testString := "* DATA:abc123||4e61bacab44e9b4730e44a6615d04098dd3a8eaf|Tue, 20 Dec 2016 21:10:41 +0100|4e61bac|Add route for graph"
|
|
|
|
parser := &Parser{}
|
|
parser.Reset()
|
|
for i := 0; i < b.N; i++ {
|
|
parser.Reset()
|
|
graph := NewGraph()
|
|
if err := parser.AddLineToGraph(graph, 0, []byte(testString)); err != nil {
|
|
b.Error("could not parse teststring")
|
|
}
|
|
if graph.Flows[1].Commits[0].Rev != "4e61bacab44e9b4730e44a6615d04098dd3a8eaf" {
|
|
b.Error("Did not get expected data")
|
|
}
|
|
}
|
|
}
|
|
|
|
func BenchmarkParseGlyphs(b *testing.B) {
|
|
parser := &Parser{}
|
|
parser.Reset()
|
|
tgBytes := []byte(testglyphs)
|
|
var tg []byte
|
|
for i := 0; i < b.N; i++ {
|
|
parser.Reset()
|
|
tg = tgBytes
|
|
idx := bytes.Index(tg, []byte("\n"))
|
|
for idx > 0 {
|
|
parser.ParseGlyphs(tg[:idx])
|
|
tg = tg[idx+1:]
|
|
idx = bytes.Index(tg, []byte("\n"))
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestReleaseUnusedColors(t *testing.T) {
|
|
testcases := []struct {
|
|
availableColors []int
|
|
oldColors []int
|
|
firstInUse int // these values have to be either be correct or suggest less is
|
|
firstAvailable int // available than possibly is - i.e. you cannot say 10 is available when it
|
|
}{
|
|
{
|
|
availableColors: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
|
|
oldColors: []int{1, 1, 1, 1, 1},
|
|
firstAvailable: -1,
|
|
firstInUse: 1,
|
|
},
|
|
{
|
|
availableColors: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
|
|
oldColors: []int{1, 2, 3, 4},
|
|
firstAvailable: 6,
|
|
firstInUse: 0,
|
|
},
|
|
{
|
|
availableColors: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
|
|
oldColors: []int{6, 0, 3, 5, 3, 4, 0, 0},
|
|
firstAvailable: 6,
|
|
firstInUse: 0,
|
|
},
|
|
{
|
|
availableColors: []int{1, 2, 3, 4, 5, 6, 7},
|
|
oldColors: []int{6, 1, 3, 5, 3, 4, 2, 7},
|
|
firstAvailable: -1,
|
|
firstInUse: 0,
|
|
},
|
|
{
|
|
availableColors: []int{1, 2, 3, 4, 5, 6, 7},
|
|
oldColors: []int{6, 0, 3, 5, 3, 4, 2, 7},
|
|
firstAvailable: -1,
|
|
firstInUse: 0,
|
|
},
|
|
}
|
|
for _, testcase := range testcases {
|
|
parser := &Parser{}
|
|
parser.Reset()
|
|
parser.availableColors = append([]int{}, testcase.availableColors...)
|
|
parser.oldColors = append(parser.oldColors, testcase.oldColors...)
|
|
parser.firstAvailable = testcase.firstAvailable
|
|
parser.firstInUse = testcase.firstInUse
|
|
parser.releaseUnusedColors()
|
|
|
|
if parser.firstAvailable == -1 {
|
|
// All in use
|
|
for _, color := range parser.availableColors {
|
|
found := false
|
|
for _, oldColor := range parser.oldColors {
|
|
if oldColor == color {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("In testcase:\n%d\t%d\t%d %d =>\n%d\t%d\t%d %d: %d should be available but is not",
|
|
testcase.availableColors,
|
|
testcase.oldColors,
|
|
testcase.firstAvailable,
|
|
testcase.firstInUse,
|
|
parser.availableColors,
|
|
parser.oldColors,
|
|
parser.firstAvailable,
|
|
parser.firstInUse,
|
|
color)
|
|
}
|
|
}
|
|
} else if parser.firstInUse != -1 {
|
|
// Some in use
|
|
for i := parser.firstInUse; i != parser.firstAvailable; i = (i + 1) % len(parser.availableColors) {
|
|
color := parser.availableColors[i]
|
|
found := false
|
|
for _, oldColor := range parser.oldColors {
|
|
if oldColor == color {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("In testcase:\n%d\t%d\t%d %d =>\n%d\t%d\t%d %d: %d should be available but is not",
|
|
testcase.availableColors,
|
|
testcase.oldColors,
|
|
testcase.firstAvailable,
|
|
testcase.firstInUse,
|
|
parser.availableColors,
|
|
parser.oldColors,
|
|
parser.firstAvailable,
|
|
parser.firstInUse,
|
|
color)
|
|
}
|
|
}
|
|
for i := parser.firstAvailable; i != parser.firstInUse; i = (i + 1) % len(parser.availableColors) {
|
|
color := parser.availableColors[i]
|
|
found := false
|
|
for _, oldColor := range parser.oldColors {
|
|
if oldColor == color {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if found {
|
|
t.Errorf("In testcase:\n%d\t%d\t%d %d =>\n%d\t%d\t%d %d: %d should not be available but is",
|
|
testcase.availableColors,
|
|
testcase.oldColors,
|
|
testcase.firstAvailable,
|
|
testcase.firstInUse,
|
|
parser.availableColors,
|
|
parser.oldColors,
|
|
parser.firstAvailable,
|
|
parser.firstInUse,
|
|
color)
|
|
}
|
|
}
|
|
} else {
|
|
// None in use
|
|
for _, color := range parser.oldColors {
|
|
if color != 0 {
|
|
t.Errorf("In testcase:\n%d\t%d\t%d %d =>\n%d\t%d\t%d %d: %d should not be available but is",
|
|
testcase.availableColors,
|
|
testcase.oldColors,
|
|
testcase.firstAvailable,
|
|
testcase.firstInUse,
|
|
parser.availableColors,
|
|
parser.oldColors,
|
|
parser.firstAvailable,
|
|
parser.firstInUse,
|
|
color)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseGlyphs(t *testing.T) {
|
|
parser := &Parser{}
|
|
parser.Reset()
|
|
tgBytes := []byte(testglyphs)
|
|
tg := tgBytes
|
|
idx := bytes.Index(tg, []byte("\n"))
|
|
row := 0
|
|
for idx > 0 {
|
|
parser.ParseGlyphs(tg[:idx])
|
|
tg = tg[idx+1:]
|
|
idx = bytes.Index(tg, []byte("\n"))
|
|
if parser.flows[0] != 1 {
|
|
t.Errorf("First column flow should be 1 but was %d", parser.flows[0])
|
|
}
|
|
colorToFlow := map[int]int64{}
|
|
flowToColor := map[int64]int{}
|
|
|
|
for i, flow := range parser.flows {
|
|
if flow == 0 {
|
|
continue
|
|
}
|
|
color := parser.colors[i]
|
|
|
|
if fColor, in := flowToColor[flow]; in && fColor != color {
|
|
t.Errorf("Row %d column %d flow %d has color %d but should be %d", row, i, flow, color, fColor)
|
|
}
|
|
flowToColor[flow] = color
|
|
if cFlow, in := colorToFlow[color]; in && cFlow != flow {
|
|
t.Errorf("Row %d column %d flow %d has color %d but conflicts with flow %d", row, i, flow, color, cFlow)
|
|
}
|
|
colorToFlow[color] = flow
|
|
}
|
|
row++
|
|
}
|
|
if len(parser.availableColors) != 9 {
|
|
t.Errorf("Expected 9 colors but have %d", len(parser.availableColors))
|
|
}
|
|
}
|
|
|
|
func TestCommitStringParsing(t *testing.T) {
|
|
dataFirstPart := "* DATA:abc123||4e61bacab44e9b4730e44a6615d04098dd3a8eaf|Tue, 20 Dec 2016 21:10:41 +0100|4e61bac|"
|
|
tests := []struct {
|
|
shouldPass bool
|
|
testName string
|
|
commitMessage string
|
|
}{
|
|
{true, "normal", "not a fancy message"},
|
|
{true, "extra pipe", "An extra pipe: |"},
|
|
{true, "extra 'Data:'", "DATA: might be trouble"},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.testName, func(t *testing.T) {
|
|
testString := fmt.Sprintf("%s%s", dataFirstPart, test.commitMessage)
|
|
idx := strings.Index(testString, "DATA:")
|
|
commit, err := NewCommit(0, 0, []byte(testString[idx+5:]))
|
|
if err != nil && test.shouldPass {
|
|
t.Errorf("Could not parse %s", testString)
|
|
return
|
|
}
|
|
|
|
if test.commitMessage != commit.Subject {
|
|
t.Errorf("%s does not match %s", test.commitMessage, commit.Subject)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewCommitParentHashes(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
data string
|
|
expectedParents []string
|
|
}{
|
|
{
|
|
name: "no parents (orphan)",
|
|
data: "||4e61bacab44e9b4730e44a6615d04098dd3a8eaf|Tue, 20 Dec 2016 21:10:41 +0100|4e61bac|subject",
|
|
expectedParents: nil,
|
|
},
|
|
{
|
|
name: "single parent",
|
|
data: "abc123||4e61bacab44e9b4730e44a6615d04098dd3a8eaf|Tue, 20 Dec 2016 21:10:41 +0100|4e61bac|subject",
|
|
expectedParents: []string{"abc123"},
|
|
},
|
|
{
|
|
name: "multiple parents (merge)",
|
|
data: "abc123 def456||4e61bacab44e9b4730e44a6615d04098dd3a8eaf|Tue, 20 Dec 2016 21:10:41 +0100|4e61bac|subject",
|
|
expectedParents: []string{"abc123", "def456"},
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
commit, err := NewCommit(0, 0, []byte(test.data))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, test.expectedParents, commit.ParentHashes)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestComputeGlyphConnectivity(t *testing.T) {
|
|
addCommit := func(graph *Graph, row, col int, hash string, parents []string) {
|
|
flowID := int64(col + 1)
|
|
commit := &Commit{Row: row, Column: col, Rev: hash, ParentHashes: parents, Flow: flowID}
|
|
graph.AddGlyph(row, col, flowID, 1, '*')
|
|
graph.Commits = append(graph.Commits, commit)
|
|
graph.Flows[flowID].Commits = append(graph.Flows[flowID].Commits, commit)
|
|
}
|
|
|
|
getCommitConnectivity := func(graph *Graph, row, col int) (up, down bool) {
|
|
for _, flow := range graph.Flows {
|
|
for _, g := range flow.Glyphs {
|
|
if g.Row == row && g.Column == col && g.Glyph == '*' {
|
|
return g.ConnectsUp, g.ConnectsDown
|
|
}
|
|
}
|
|
}
|
|
return false, false
|
|
}
|
|
|
|
t.Run("ConnectsDown/no parents", func(t *testing.T) {
|
|
graph := NewGraph()
|
|
addCommit(graph, 0, 0, "orphan", nil)
|
|
graph.ComputeGlyphConnectivity()
|
|
|
|
_, down := getCommitConnectivity(graph, 0, 0)
|
|
assert.False(t, down)
|
|
})
|
|
|
|
t.Run("ConnectsDown/has parent in graph", func(t *testing.T) {
|
|
graph := NewGraph()
|
|
addCommit(graph, 0, 0, "child", []string{"parent"})
|
|
addCommit(graph, 1, 0, "parent", nil)
|
|
graph.ComputeGlyphConnectivity()
|
|
|
|
_, down := getCommitConnectivity(graph, 0, 0)
|
|
assert.True(t, down)
|
|
})
|
|
|
|
t.Run("ConnectsDown/has parent outside graph", func(t *testing.T) {
|
|
graph := NewGraph()
|
|
addCommit(graph, 0, 0, "child", []string{"parent-not-in-graph"})
|
|
graph.ComputeGlyphConnectivity()
|
|
|
|
_, down := getCommitConnectivity(graph, 0, 0)
|
|
assert.True(t, down)
|
|
})
|
|
|
|
t.Run("ConnectsUp/no child, no glyph above, no continuation", func(t *testing.T) {
|
|
graph := NewGraph()
|
|
addCommit(graph, 0, 0, "orphan", nil)
|
|
graph.ComputeGlyphConnectivity()
|
|
|
|
up, _ := getCommitConnectivity(graph, 0, 0)
|
|
assert.False(t, up)
|
|
})
|
|
|
|
t.Run("ConnectsUp/has visible child", func(t *testing.T) {
|
|
graph := NewGraph()
|
|
addCommit(graph, 0, 0, "child", []string{"parent"})
|
|
addCommit(graph, 1, 0, "parent", nil)
|
|
graph.ComputeGlyphConnectivity()
|
|
|
|
up, _ := getCommitConnectivity(graph, 1, 0)
|
|
assert.True(t, up)
|
|
})
|
|
|
|
t.Run("ConnectsUp/has non-commit glyph above", func(t *testing.T) {
|
|
graph := NewGraph()
|
|
graph.AddGlyph(0, 0, 1, 1, '|')
|
|
addCommit(graph, 1, 0, "commit", nil)
|
|
graph.ComputeGlyphConnectivity()
|
|
|
|
up, _ := getCommitConnectivity(graph, 1, 0)
|
|
assert.True(t, up)
|
|
})
|
|
|
|
t.Run("ConnectsUp/has continuationAbove", func(t *testing.T) {
|
|
graph := NewGraph()
|
|
addCommit(graph, 0, 0, "commit", nil)
|
|
graph.continuationAbove[[2]int{0, 0}] = true
|
|
graph.ComputeGlyphConnectivity()
|
|
|
|
up, _ := getCommitConnectivity(graph, 0, 0)
|
|
assert.True(t, up)
|
|
})
|
|
|
|
t.Run("non-commit glyphs always connect both ways", func(t *testing.T) {
|
|
for _, glyph := range []byte{'|', '/', '\\'} {
|
|
graph := NewGraph()
|
|
graph.AddGlyph(0, 0, 1, 1, glyph)
|
|
graph.ComputeGlyphConnectivity()
|
|
|
|
g := graph.Flows[1].Glyphs[0]
|
|
assert.True(t, g.ConnectsUp, "glyph %q should connect up", glyph)
|
|
assert.True(t, g.ConnectsDown, "glyph %q should connect down", glyph)
|
|
}
|
|
})
|
|
}
|
|
|
|
var testglyphs = `*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
|\
|
|
* |
|
|
* |
|
|
* |
|
|
* |
|
|
* |
|
|
| *
|
|
* |
|
|
| *
|
|
| |\
|
|
* | |
|
|
| | *
|
|
| | |\
|
|
* | | \
|
|
|\ \ \ \
|
|
| * | | |
|
|
| |\| | |
|
|
* | | | |
|
|
|/ / / /
|
|
| | | *
|
|
| * | |
|
|
| * | |
|
|
| * | |
|
|
* | | |
|
|
* | | |
|
|
* | | |
|
|
* | | |
|
|
* | | |
|
|
|\ \ \ \
|
|
| | * | |
|
|
| | |\| |
|
|
| | | * |
|
|
| | | | *
|
|
* | | | |
|
|
* | | | |
|
|
* | | | |
|
|
* | | | |
|
|
* | | | |
|
|
|\ \ \ \ \
|
|
| * | | | |
|
|
|/| | | | |
|
|
| | |/ / /
|
|
| |/| | |
|
|
| | | | *
|
|
| * | | |
|
|
|/| | | |
|
|
| * | | |
|
|
|/| | | |
|
|
| | |/ /
|
|
| |/| |
|
|
| * | |
|
|
| * | |
|
|
| |\ \ \
|
|
| | * | |
|
|
| |/| | |
|
|
| | | |/
|
|
| | |/|
|
|
| * | |
|
|
| * | |
|
|
| * | |
|
|
| | * |
|
|
| | |\ \
|
|
| | | * |
|
|
| | |/| |
|
|
| | | * |
|
|
| | | |\ \
|
|
| | | | * |
|
|
| | | |/| |
|
|
| | * | | |
|
|
| | * | | |
|
|
| | |\ \ \ \
|
|
| | | * | | |
|
|
| | |/| | | |
|
|
| | | | | * |
|
|
| | | | |/ /
|
|
* | | | / /
|
|
|/ / / / /
|
|
* | | | |
|
|
|\ \ \ \ \
|
|
| * | | | |
|
|
|/| | | | |
|
|
| * | | | |
|
|
| * | | | |
|
|
| |\ \ \ \ \
|
|
| | | * \ \ \
|
|
| | | |\ \ \ \
|
|
| | | | * | | |
|
|
| | | |/| | | |
|
|
| | | | | |/ /
|
|
| | | | |/| |
|
|
* | | | | | |
|
|
* | | | | | |
|
|
* | | | | | |
|
|
| | | | * | |
|
|
* | | | | | |
|
|
| | * | | | |
|
|
| |/| | | | |
|
|
* | | | | | |
|
|
| |/ / / / /
|
|
|/| | | | |
|
|
| | | | * |
|
|
| | | |/ /
|
|
| | |/| |
|
|
| * | | |
|
|
| | | | *
|
|
| | * | |
|
|
| | |\ \ \
|
|
| | | * | |
|
|
| | |/| | |
|
|
| | | |/ /
|
|
| | | * |
|
|
| | * | |
|
|
| | |\ \ \
|
|
| | | * | |
|
|
| | |/| | |
|
|
| | | |/ /
|
|
| | | * |
|
|
* | | | |
|
|
|\ \ \ \ \
|
|
| * \ \ \ \
|
|
| |\ \ \ \ \
|
|
| | | |/ / /
|
|
| | |/| | |
|
|
| | | | * |
|
|
| | | | * |
|
|
* | | | | |
|
|
* | | | | |
|
|
|/ / / / /
|
|
| | | * |
|
|
* | | | |
|
|
* | | | |
|
|
* | | | |
|
|
* | | | |
|
|
|\ \ \ \ \
|
|
| * | | | |
|
|
|/| | | | |
|
|
| | * | | |
|
|
| | |\ \ \ \
|
|
| | | * | | |
|
|
| | |/| | | |
|
|
| |/| | |/ /
|
|
| | | |/| |
|
|
| | | | | *
|
|
| |_|_|_|/
|
|
|/| | | |
|
|
| | * | |
|
|
| |/ / /
|
|
* | | |
|
|
* | | |
|
|
| | * |
|
|
* | | |
|
|
* | | |
|
|
| * | |
|
|
| | * |
|
|
| * | |
|
|
* | | |
|
|
|\ \ \ \
|
|
| * | | |
|
|
|/| | | |
|
|
| |/ / /
|
|
| * | |
|
|
| |\ \ \
|
|
| | * | |
|
|
| |/| | |
|
|
| | |/ /
|
|
| | * |
|
|
| | |\ \
|
|
| | | * |
|
|
| | |/| |
|
|
* | | | |
|
|
* | | | |
|
|
|\ \ \ \ \
|
|
| * | | | |
|
|
|/| | | | |
|
|
| | * | | |
|
|
| | * | | |
|
|
| | * | | |
|
|
| |/ / / /
|
|
| * | | |
|
|
| |\ \ \ \
|
|
| | * | | |
|
|
| |/| | | |
|
|
* | | | | |
|
|
* | | | | |
|
|
* | | | | |
|
|
* | | | | |
|
|
* | | | | |
|
|
| | | | * |
|
|
* | | | | |
|
|
|\ \ \ \ \ \
|
|
| * | | | | |
|
|
|/| | | | | |
|
|
| | | | | * |
|
|
| | | | |/ /
|
|
* | | | | |
|
|
|\ \ \ \ \ \
|
|
* | | | | | |
|
|
* | | | | | |
|
|
| | | | * | |
|
|
* | | | | | |
|
|
* | | | | | |
|
|
|\ \ \ \ \ \ \
|
|
| | |_|_|/ / /
|
|
| |/| | | | |
|
|
| | | | * | |
|
|
| | | | * | |
|
|
| | | | * | |
|
|
| | | | * | |
|
|
| | | | * | |
|
|
| | | | * | |
|
|
| | | |/ / /
|
|
| | | * | |
|
|
| | | * | |
|
|
| | | * | |
|
|
| | |/| | |
|
|
| | | * | |
|
|
| | |/| | |
|
|
| | | |/ /
|
|
| | * | |
|
|
| |/| | |
|
|
| | | * |
|
|
| | |/ /
|
|
| | * |
|
|
| * | |
|
|
| |\ \ \
|
|
| * | | |
|
|
| | * | |
|
|
| |/| | |
|
|
| | |/ /
|
|
| | * |
|
|
| | |\ \
|
|
| | * | |
|
|
* | | | |
|
|
|\| | | |
|
|
| * | | |
|
|
| * | | |
|
|
| * | | |
|
|
| | * | |
|
|
| * | | |
|
|
| |\| | |
|
|
| * | | |
|
|
| | * | |
|
|
| | * | |
|
|
| * | | |
|
|
| * | | |
|
|
| * | | |
|
|
| * | | |
|
|
| * | | |
|
|
| * | | |
|
|
| * | | |
|
|
| * | | |
|
|
| | * | |
|
|
| * | | |
|
|
| * | | |
|
|
| * | | |
|
|
| * | | |
|
|
| | * | |
|
|
* | | | |
|
|
|\| | | |
|
|
| | * | |
|
|
| * | | |
|
|
| |\| | |
|
|
| | * | |
|
|
| | * | |
|
|
| | * | |
|
|
| | | * |
|
|
* | | | |
|
|
|\| | | |
|
|
| | * | |
|
|
| | |/ /
|
|
| * | |
|
|
| * | |
|
|
| |\| |
|
|
* | | |
|
|
|\| | |
|
|
| | * |
|
|
| | * |
|
|
| | * |
|
|
| * | |
|
|
| | * |
|
|
| * | |
|
|
| | * |
|
|
| | * |
|
|
| | * |
|
|
| * | |
|
|
| * | |
|
|
| * | |
|
|
| * | |
|
|
| * | |
|
|
| * | |
|
|
| * | |
|
|
* | | |
|
|
|\| | |
|
|
| * | |
|
|
| |\| |
|
|
| | * |
|
|
| | |\ \
|
|
* | | | |
|
|
|\| | | |
|
|
| * | | |
|
|
| |\| | |
|
|
| | * | |
|
|
| | | * |
|
|
| | |/ /
|
|
* | | |
|
|
* | | |
|
|
|\| | |
|
|
| * | |
|
|
| |\| |
|
|
| | * |
|
|
| | * |
|
|
| | * |
|
|
| | | *
|
|
* | | |
|
|
|\| | |
|
|
| * | |
|
|
| * | |
|
|
| | | *
|
|
| | | |\
|
|
* | | | |
|
|
| |_|_|/
|
|
|/| | |
|
|
| * | |
|
|
| |\| |
|
|
| | * |
|
|
| | * |
|
|
| | * |
|
|
| | * |
|
|
| | * |
|
|
| * | |
|
|
* | | |
|
|
|\| | |
|
|
| * | |
|
|
|/| | |
|
|
| |/ /
|
|
| * |
|
|
| |\ \
|
|
| * | |
|
|
| * | |
|
|
* | | |
|
|
|\| | |
|
|
| | * |
|
|
| * | |
|
|
| * | |
|
|
| * | |
|
|
* | | |
|
|
|\| | |
|
|
| * | |
|
|
| * | |
|
|
| | * |
|
|
| | |\ \
|
|
| | |/ /
|
|
| |/| |
|
|
| * | |
|
|
* | | |
|
|
|\| | |
|
|
| * | |
|
|
* | | |
|
|
|\| | |
|
|
| * | |
|
|
| |\ \ \
|
|
| * | | |
|
|
| * | | |
|
|
| | | * |
|
|
| * | | |
|
|
| * | | |
|
|
| | |/ /
|
|
| |/| |
|
|
| | * |
|
|
* | | |
|
|
|\| | |
|
|
| * | |
|
|
| * | |
|
|
| * | |
|
|
| * | |
|
|
| * | |
|
|
| |\ \ \
|
|
* | | | |
|
|
|\| | | |
|
|
| * | | |
|
|
| * | | |
|
|
* | | | |
|
|
* | | | |
|
|
|\| | | |
|
|
| | | | *
|
|
| | | | |\
|
|
| |_|_|_|/
|
|
|/| | | |
|
|
| * | | |
|
|
* | | | |
|
|
* | | | |
|
|
|\| | | |
|
|
| * | | |
|
|
| |\ \ \ \
|
|
| | | |/ /
|
|
| | |/| |
|
|
| * | | |
|
|
| * | | |
|
|
| * | | |
|
|
| * | | |
|
|
| | * | |
|
|
| | | * |
|
|
| | |/ /
|
|
| |/| |
|
|
* | | |
|
|
|\| | |
|
|
| * | |
|
|
| * | |
|
|
| * | |
|
|
| * | |
|
|
| * | |
|
|
* | | |
|
|
|\| | |
|
|
| * | |
|
|
| * | |
|
|
* | | |
|
|
| * | |
|
|
| * | |
|
|
| * | |
|
|
* | | |
|
|
* | | |
|
|
* | | |
|
|
|\| | |
|
|
| * | |
|
|
* | | |
|
|
* | | |
|
|
* | | |
|
|
* | | |
|
|
| | | *
|
|
* | | |
|
|
|\| | |
|
|
| * | |
|
|
| * | |
|
|
| * | |
|
|
`
|