Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
OpenDAS
ollama
Commits
e40145a3
Commit
e40145a3
authored
May 21, 2024
by
Michael Yang
Browse files
lint
parent
c895a7d1
Changes
31
Show whitespace changes
Inline
Side-by-side
Showing
20 changed files
with
52 additions
and
54 deletions
+52
-54
.golangci.yaml
.golangci.yaml
+6
-0
api/types_test.go
api/types_test.go
+4
-4
app/lifecycle/paths.go
app/lifecycle/paths.go
+0
-1
app/lifecycle/server.go
app/lifecycle/server.go
+1
-2
app/lifecycle/updater.go
app/lifecycle/updater.go
+3
-3
app/store/store.go
app/store/store.go
+0
-1
cmd/cmd.go
cmd/cmd.go
+0
-2
cmd/interactive_test.go
cmd/interactive_test.go
+5
-4
convert/convert.go
convert/convert.go
+1
-1
convert/torch.go
convert/torch.go
+0
-1
format/format_test.go
format/format_test.go
+0
-1
gpu/assets.go
gpu/assets.go
+1
-1
gpu/gpu_test.go
gpu/gpu_test.go
+3
-2
llm/gguf.go
llm/gguf.go
+4
-4
llm/memory.go
llm/memory.go
+1
-1
llm/server.go
llm/server.go
+2
-3
openai/openai.go
openai/openai.go
+0
-1
parser/parser_test.go
parser/parser_test.go
+15
-16
progress/progress.go
progress/progress.go
+2
-2
readline/buffer.go
readline/buffer.go
+4
-4
No files found.
.golangci.yaml
View file @
e40145a3
...
...
@@ -12,8 +12,14 @@ linters:
# FIXME: for some reason this errors on windows
# - gofmt
# - goimports
-
intrange
-
misspell
-
nilerr
-
nolintlint
-
nosprintfhostport
-
testifylint
-
unconvert
-
unused
-
usestdlibvars
-
wastedassign
-
whitespace
api/types_test.go
View file @
e40145a3
...
...
@@ -72,13 +72,13 @@ func TestDurationMarshalUnmarshal(t *testing.T) {
},
{
"positive duration"
,
time
.
Duration
(
42
*
time
.
Second
)
,
time
.
Duration
(
42
*
time
.
Second
)
,
42
*
time
.
Second
,
42
*
time
.
Second
,
},
{
"another positive duration"
,
time
.
Duration
(
42
*
time
.
Minute
)
,
time
.
Duration
(
42
*
time
.
Minute
)
,
42
*
time
.
Minute
,
42
*
time
.
Minute
,
},
{
"zero duration"
,
...
...
app/lifecycle/paths.go
View file @
e40145a3
...
...
@@ -69,7 +69,6 @@ func init() {
slog
.
Error
(
fmt
.
Sprintf
(
"create ollama dir %s: %v"
,
AppDataDir
,
err
))
}
}
}
else
if
runtime
.
GOOS
==
"darwin"
{
// TODO
AppName
+=
".app"
...
...
app/lifecycle/server.go
View file @
e40145a3
...
...
@@ -15,7 +15,7 @@ import (
)
func
getCLIFullPath
(
command
string
)
string
{
cmdPath
:=
""
var
cmdPath
string
appExe
,
err
:=
os
.
Executable
()
if
err
==
nil
{
cmdPath
=
filepath
.
Join
(
filepath
.
Dir
(
appExe
),
command
)
...
...
@@ -65,7 +65,6 @@ func start(ctx context.Context, command string) (*exec.Cmd, error) {
if
err
!=
nil
{
if
!
errors
.
Is
(
err
,
os
.
ErrNotExist
)
{
return
nil
,
fmt
.
Errorf
(
"stat ollama server log dir %s: %v"
,
logDir
,
err
)
}
if
err
:=
os
.
MkdirAll
(
logDir
,
0
o755
);
err
!=
nil
{
...
...
app/lifecycle/updater.go
View file @
e40145a3
...
...
@@ -78,7 +78,7 @@ func IsNewReleaseAvailable(ctx context.Context) (bool, UpdateResponse) {
}
defer
resp
.
Body
.
Close
()
if
resp
.
StatusCode
==
204
{
if
resp
.
StatusCode
==
http
.
StatusNoContent
{
slog
.
Debug
(
"check update response 204 (current version is up to date)"
)
return
false
,
updateResp
}
...
...
@@ -87,7 +87,7 @@ func IsNewReleaseAvailable(ctx context.Context) (bool, UpdateResponse) {
slog
.
Warn
(
fmt
.
Sprintf
(
"failed to read body response: %s"
,
err
))
}
if
resp
.
StatusCode
!=
200
{
if
resp
.
StatusCode
!=
http
.
StatusOK
{
slog
.
Info
(
fmt
.
Sprintf
(
"check update error %d - %.96s"
,
resp
.
StatusCode
,
string
(
body
)))
return
false
,
updateResp
}
...
...
@@ -114,7 +114,7 @@ func DownloadNewRelease(ctx context.Context, updateResp UpdateResponse) error {
if
err
!=
nil
{
return
fmt
.
Errorf
(
"error checking update: %w"
,
err
)
}
if
resp
.
StatusCode
!=
200
{
if
resp
.
StatusCode
!=
http
.
StatusOK
{
return
fmt
.
Errorf
(
"unexpected status attempting to download update %d"
,
resp
.
StatusCode
)
}
resp
.
Body
.
Close
()
...
...
app/store/store.go
View file @
e40145a3
...
...
@@ -29,7 +29,6 @@ func GetID() string {
initStore
()
}
return
store
.
ID
}
func
GetFirstTimeRun
()
bool
{
...
...
cmd/cmd.go
View file @
e40145a3
...
...
@@ -746,7 +746,6 @@ func displayResponse(content string, wordWrap bool, state *displayResponseState)
if
wordWrap
&&
termWidth
>=
10
{
for
_
,
ch
:=
range
content
{
if
state
.
lineLength
+
1
>
termWidth
-
5
{
if
runewidth
.
StringWidth
(
state
.
wordBuffer
)
>
termWidth
-
10
{
fmt
.
Printf
(
"%s%c"
,
state
.
wordBuffer
,
ch
)
state
.
wordBuffer
=
""
...
...
@@ -1044,7 +1043,6 @@ func waitForServer(ctx context.Context, client *api.Client) error {
}
}
}
}
func
checkServerHeartbeat
(
cmd
*
cobra
.
Command
,
_
[]
string
)
error
{
...
...
cmd/interactive_test.go
View file @
e40145a3
...
...
@@ -6,6 +6,7 @@ import (
"text/template"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ollama/ollama/api"
)
...
...
@@ -85,11 +86,11 @@ MESSAGE assistant """Yes it is true, I am half horse, half shark."""
`
tmpl
,
err
:=
template
.
New
(
""
)
.
Parse
(
expectedModelfile
)
assert
.
Nil
(
t
,
err
)
require
.
NoError
(
t
,
err
)
var
buf
bytes
.
Buffer
err
=
tmpl
.
Execute
(
&
buf
,
opts
)
assert
.
Nil
(
t
,
err
)
require
.
NoError
(
t
,
err
)
assert
.
Equal
(
t
,
buf
.
String
(),
mf
)
opts
.
ParentModel
=
"horseshark"
...
...
@@ -107,10 +108,10 @@ MESSAGE assistant """Yes it is true, I am half horse, half shark."""
`
tmpl
,
err
=
template
.
New
(
""
)
.
Parse
(
expectedModelfile
)
assert
.
Nil
(
t
,
err
)
require
.
NoError
(
t
,
err
)
var
parentBuf
bytes
.
Buffer
err
=
tmpl
.
Execute
(
&
parentBuf
,
opts
)
assert
.
Nil
(
t
,
err
)
require
.
NoError
(
t
,
err
)
assert
.
Equal
(
t
,
parentBuf
.
String
(),
mf
)
}
convert/convert.go
View file @
e40145a3
...
...
@@ -189,7 +189,7 @@ func LoadSentencePieceTokens(dirpath string, params *Params) (*Vocab, error) {
if
params
.
VocabSize
>
len
(
v
.
Tokens
)
{
missingTokens
:=
params
.
VocabSize
-
len
(
v
.
Tokens
)
slog
.
Warn
(
fmt
.
Sprintf
(
"vocab is missing %d tokens"
,
missingTokens
))
for
cnt
:=
0
;
cnt
<
missingTokens
;
cnt
++
{
for
cnt
:=
range
missingTokens
{
v
.
Tokens
=
append
(
v
.
Tokens
,
fmt
.
Sprintf
(
"<dummy%05d>"
,
cnt
+
1
))
v
.
Scores
=
append
(
v
.
Scores
,
-
1
)
v
.
Types
=
append
(
v
.
Types
,
tokenTypeUserDefined
)
...
...
convert/torch.go
View file @
e40145a3
...
...
@@ -104,7 +104,6 @@ func (tf *TorchFormat) GetTensors(dirpath string, params *Params) ([]llm.Tensor,
}
return
tensors
,
nil
}
func
getAltParams
(
dirpath
string
)
(
*
Params
,
error
)
{
...
...
format/format_test.go
View file @
e40145a3
...
...
@@ -5,7 +5,6 @@ import (
)
func
TestHumanNumber
(
t
*
testing
.
T
)
{
type
testCase
struct
{
input
uint64
expected
string
...
...
gpu/assets.go
View file @
e40145a3
...
...
@@ -80,7 +80,7 @@ func cleanupTmpDirs() {
if
err
==
nil
{
pid
,
err
:=
strconv
.
Atoi
(
string
(
raw
))
if
err
==
nil
{
if
proc
,
err
:=
os
.
FindProcess
(
int
(
pid
)
)
;
err
==
nil
&&
!
errors
.
Is
(
proc
.
Signal
(
syscall
.
Signal
(
0
)),
os
.
ErrProcessDone
)
{
if
proc
,
err
:=
os
.
FindProcess
(
pid
);
err
==
nil
&&
!
errors
.
Is
(
proc
.
Signal
(
syscall
.
Signal
(
0
)),
os
.
ErrProcessDone
)
{
// Another running ollama, ignore this tmpdir
continue
}
...
...
gpu/gpu_test.go
View file @
e40145a3
...
...
@@ -5,11 +5,12 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func
TestBasicGetGPUInfo
(
t
*
testing
.
T
)
{
info
:=
GetGPUInfo
()
assert
.
Greater
(
t
,
len
(
info
)
,
0
)
assert
.
NotEmpty
(
t
,
len
(
info
))
assert
.
Contains
(
t
,
"cuda rocm cpu metal"
,
info
[
0
]
.
Library
)
if
info
[
0
]
.
Library
!=
"cpu"
{
assert
.
Greater
(
t
,
info
[
0
]
.
TotalMemory
,
uint64
(
0
))
...
...
@@ -19,7 +20,7 @@ func TestBasicGetGPUInfo(t *testing.T) {
func
TestCPUMemInfo
(
t
*
testing
.
T
)
{
info
,
err
:=
GetCPUMem
()
assert
.
NoError
(
t
,
err
)
require
.
NoError
(
t
,
err
)
switch
runtime
.
GOOS
{
case
"darwin"
:
t
.
Skip
(
"CPU memory not populated on darwin"
)
...
...
llm/gguf.go
View file @
e40145a3
...
...
@@ -592,8 +592,8 @@ func (llm *gguf) Encode(ws io.WriteSeeker, kv KV, tensors []Tensor) error {
return
err
}
dims
:=
0
for
cnt
:=
0
;
cnt
<
len
(
tensor
.
Shape
)
;
cnt
++
{
var
dims
int
for
cnt
:=
range
len
(
tensor
.
Shape
)
{
if
tensor
.
Shape
[
cnt
]
>
0
{
dims
++
}
...
...
@@ -603,8 +603,8 @@ func (llm *gguf) Encode(ws io.WriteSeeker, kv KV, tensors []Tensor) error {
return
err
}
for
i
:=
0
;
i
<
dims
;
i
++
{
if
err
:=
binary
.
Write
(
ws
,
llm
.
ByteOrder
,
uint64
(
tensor
.
Shape
[
dims
-
1
-
i
])
)
;
err
!=
nil
{
for
i
:=
range
dims
{
if
err
:=
binary
.
Write
(
ws
,
llm
.
ByteOrder
,
tensor
.
Shape
[
dims
-
1
-
i
]);
err
!=
nil
{
return
err
}
}
...
...
llm/memory.go
View file @
e40145a3
...
...
@@ -103,7 +103,7 @@ func EstimateGPULayers(gpus []gpu.GpuInfo, ggml *GGML, projectors []string, opts
}
var
layerCount
int
for
i
:=
0
;
i
<
int
(
ggml
.
KV
()
.
BlockCount
())
;
i
++
{
for
i
:=
range
int
(
ggml
.
KV
()
.
BlockCount
())
{
if
blk
,
ok
:=
layers
[
fmt
.
Sprintf
(
"blk.%d"
,
i
)];
ok
{
memoryLayer
:=
blk
.
size
()
...
...
llm/server.go
View file @
e40145a3
...
...
@@ -85,7 +85,6 @@ func NewLlamaServer(gpus gpu.GpuInfoList, model string, ggml *GGML, adapters, pr
var
systemMemory
uint64
gpuCount
:=
len
(
gpus
)
if
(
len
(
gpus
)
==
1
&&
gpus
[
0
]
.
Library
==
"cpu"
)
||
opts
.
NumGPU
==
0
{
// TODO evaluate system memory to see if we should block the load, or force an unload of another CPU runner
cpuRunner
=
serverForCpu
()
...
...
@@ -233,7 +232,7 @@ func NewLlamaServer(gpus gpu.GpuInfoList, model string, ggml *GGML, adapters, pr
params
=
append
(
params
,
"--parallel"
,
fmt
.
Sprintf
(
"%d"
,
numParallel
))
for
i
:=
0
;
i
<
len
(
servers
)
;
i
++
{
for
i
:=
range
len
(
servers
)
{
dir
:=
availableServers
[
servers
[
i
]]
if
dir
==
""
{
// Shouldn't happen
...
...
@@ -316,7 +315,7 @@ func NewLlamaServer(gpus gpu.GpuInfoList, model string, ggml *GGML, adapters, pr
s
.
cmd
.
Stdout
=
os
.
Stdout
s
.
cmd
.
Stderr
=
s
.
status
visibleDevicesEnv
,
visibleDevicesEnvVal
:=
gpu
.
GpuInfoList
(
gpus
)
.
GetVisibleDevicesEnv
()
visibleDevicesEnv
,
visibleDevicesEnvVal
:=
gpu
s
.
GetVisibleDevicesEnv
()
pathEnvVal
:=
strings
.
Join
(
libraryPaths
,
string
(
filepath
.
ListSeparator
))
// Update or add the path and visible devices variable with our adjusted version
...
...
openai/openai.go
View file @
e40145a3
...
...
@@ -245,7 +245,6 @@ func (w *writer) writeResponse(data []byte) (int, error) {
d
,
err
:=
json
.
Marshal
(
toChunk
(
w
.
id
,
chatResponse
))
if
err
!=
nil
{
return
0
,
err
}
w
.
ResponseWriter
.
Header
()
.
Set
(
"Content-Type"
,
"text/event-stream"
)
...
...
parser/parser_test.go
View file @
e40145a3
...
...
@@ -10,6 +10,7 @@ import (
"unicode/utf16"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func
TestParseFileFile
(
t
*
testing
.
T
)
{
...
...
@@ -25,7 +26,7 @@ TEMPLATE template1
reader
:=
strings
.
NewReader
(
input
)
modelfile
,
err
:=
ParseFile
(
reader
)
assert
.
NoError
(
t
,
err
)
require
.
NoError
(
t
,
err
)
expectedCommands
:=
[]
Command
{
{
Name
:
"model"
,
Args
:
"model1"
},
...
...
@@ -88,7 +89,7 @@ func TestParseFileFrom(t *testing.T) {
for
_
,
c
:=
range
cases
{
t
.
Run
(
""
,
func
(
t
*
testing
.
T
)
{
modelfile
,
err
:=
ParseFile
(
strings
.
NewReader
(
c
.
input
))
assert
.
ErrorIs
(
t
,
err
,
c
.
err
)
require
.
ErrorIs
(
t
,
err
,
c
.
err
)
if
modelfile
!=
nil
{
assert
.
Equal
(
t
,
c
.
expected
,
modelfile
.
Commands
)
}
...
...
@@ -105,7 +106,7 @@ PARAMETER param1
reader
:=
strings
.
NewReader
(
input
)
_
,
err
:=
ParseFile
(
reader
)
assert
.
ErrorIs
(
t
,
err
,
io
.
ErrUnexpectedEOF
)
require
.
ErrorIs
(
t
,
err
,
io
.
ErrUnexpectedEOF
)
}
func
TestParseFileBadCommand
(
t
*
testing
.
T
)
{
...
...
@@ -114,8 +115,7 @@ FROM foo
BADCOMMAND param1 value1
`
_
,
err
:=
ParseFile
(
strings
.
NewReader
(
input
))
assert
.
ErrorIs
(
t
,
err
,
errInvalidCommand
)
require
.
ErrorIs
(
t
,
err
,
errInvalidCommand
)
}
func
TestParseFileMessages
(
t
*
testing
.
T
)
{
...
...
@@ -201,7 +201,7 @@ MESSAGE system`,
for
_
,
c
:=
range
cases
{
t
.
Run
(
""
,
func
(
t
*
testing
.
T
)
{
modelfile
,
err
:=
ParseFile
(
strings
.
NewReader
(
c
.
input
))
assert
.
ErrorIs
(
t
,
err
,
c
.
err
)
require
.
ErrorIs
(
t
,
err
,
c
.
err
)
if
modelfile
!=
nil
{
assert
.
Equal
(
t
,
c
.
expected
,
modelfile
.
Commands
)
}
...
...
@@ -355,7 +355,7 @@ TEMPLATE """
for
_
,
c
:=
range
cases
{
t
.
Run
(
""
,
func
(
t
*
testing
.
T
)
{
modelfile
,
err
:=
ParseFile
(
strings
.
NewReader
(
c
.
multiline
))
assert
.
ErrorIs
(
t
,
err
,
c
.
err
)
require
.
ErrorIs
(
t
,
err
,
c
.
err
)
if
modelfile
!=
nil
{
assert
.
Equal
(
t
,
c
.
expected
,
modelfile
.
Commands
)
}
...
...
@@ -413,7 +413,7 @@ func TestParseFileParameters(t *testing.T) {
fmt
.
Fprintln
(
&
b
,
"FROM foo"
)
fmt
.
Fprintln
(
&
b
,
"PARAMETER"
,
k
)
modelfile
,
err
:=
ParseFile
(
&
b
)
assert
.
NoError
(
t
,
err
)
require
.
NoError
(
t
,
err
)
assert
.
Equal
(
t
,
[]
Command
{
{
Name
:
"model"
,
Args
:
"foo"
},
...
...
@@ -442,7 +442,7 @@ FROM foo
for
_
,
c
:=
range
cases
{
t
.
Run
(
""
,
func
(
t
*
testing
.
T
)
{
modelfile
,
err
:=
ParseFile
(
strings
.
NewReader
(
c
.
input
))
assert
.
NoError
(
t
,
err
)
require
.
NoError
(
t
,
err
)
assert
.
Equal
(
t
,
c
.
expected
,
modelfile
.
Commands
)
})
}
...
...
@@ -501,15 +501,14 @@ SYSTEM ""
for
_
,
c
:=
range
cases
{
t
.
Run
(
""
,
func
(
t
*
testing
.
T
)
{
modelfile
,
err
:=
ParseFile
(
strings
.
NewReader
(
c
))
assert
.
NoError
(
t
,
err
)
require
.
NoError
(
t
,
err
)
modelfile2
,
err
:=
ParseFile
(
strings
.
NewReader
(
modelfile
.
String
()))
assert
.
NoError
(
t
,
err
)
require
.
NoError
(
t
,
err
)
assert
.
Equal
(
t
,
modelfile
,
modelfile2
)
})
}
}
func
TestParseFileUTF16ParseFile
(
t
*
testing
.
T
)
{
...
...
@@ -522,10 +521,10 @@ SYSTEM You are a utf16 file.
utf16File
:=
utf16
.
Encode
(
append
([]
rune
{
'\ufffe'
},
[]
rune
(
data
)
...
))
buf
:=
new
(
bytes
.
Buffer
)
err
:=
binary
.
Write
(
buf
,
binary
.
LittleEndian
,
utf16File
)
assert
.
NoError
(
t
,
err
)
require
.
NoError
(
t
,
err
)
actual
,
err
:=
ParseFile
(
buf
)
assert
.
NoError
(
t
,
err
)
require
.
NoError
(
t
,
err
)
expected
:=
[]
Command
{
{
Name
:
"model"
,
Args
:
"bob"
},
...
...
@@ -539,9 +538,9 @@ SYSTEM You are a utf16 file.
// simulate a utf16 be file
buf
=
new
(
bytes
.
Buffer
)
err
=
binary
.
Write
(
buf
,
binary
.
BigEndian
,
utf16File
)
assert
.
NoError
(
t
,
err
)
require
.
NoError
(
t
,
err
)
actual
,
err
=
ParseFile
(
buf
)
assert
.
NoError
(
t
,
err
)
require
.
NoError
(
t
,
err
)
assert
.
Equal
(
t
,
expected
,
actual
.
Commands
)
}
progress/progress.go
View file @
e40145a3
...
...
@@ -59,7 +59,7 @@ func (p *Progress) StopAndClear() bool {
stopped
:=
p
.
stop
()
if
stopped
{
// clear all progress lines
for
i
:=
0
;
i
<
p
.
pos
;
i
++
{
for
i
:=
range
p
.
pos
{
if
i
>
0
{
fmt
.
Fprint
(
p
.
w
,
"
\0
33[A"
)
}
...
...
@@ -85,7 +85,7 @@ func (p *Progress) render() {
defer
fmt
.
Fprint
(
p
.
w
,
"
\0
33[?25h"
)
// clear already rendered progress lines
for
i
:=
0
;
i
<
p
.
pos
;
i
++
{
for
i
:=
range
p
.
pos
{
if
i
>
0
{
fmt
.
Fprint
(
p
.
w
,
"
\0
33[A"
)
}
...
...
readline/buffer.go
View file @
e40145a3
...
...
@@ -154,7 +154,7 @@ func (b *Buffer) MoveToStart() {
if
b
.
Pos
>
0
{
currLine
:=
b
.
DisplayPos
/
b
.
LineWidth
if
currLine
>
0
{
for
cnt
:=
0
;
cnt
<
currLine
;
cnt
++
{
for
range
currLine
{
fmt
.
Print
(
CursorUp
)
}
}
...
...
@@ -169,7 +169,7 @@ func (b *Buffer) MoveToEnd() {
currLine
:=
b
.
DisplayPos
/
b
.
LineWidth
totalLines
:=
b
.
DisplaySize
()
/
b
.
LineWidth
if
currLine
<
totalLines
{
for
cnt
:=
0
;
cnt
<
totalLines
-
currLine
;
cnt
++
{
for
range
totalLines
-
currLine
{
fmt
.
Print
(
CursorDown
)
}
remainder
:=
b
.
DisplaySize
()
%
b
.
LineWidth
...
...
@@ -451,7 +451,7 @@ func (b *Buffer) DeleteBefore() {
func
(
b
*
Buffer
)
DeleteRemaining
()
{
if
b
.
DisplaySize
()
>
0
&&
b
.
Pos
<
b
.
DisplaySize
()
{
charsToDel
:=
b
.
Buf
.
Size
()
-
b
.
Pos
for
cnt
:=
0
;
cnt
<
charsToDel
;
cnt
++
{
for
range
charsToDel
{
b
.
Delete
()
}
}
...
...
@@ -495,7 +495,7 @@ func (b *Buffer) ClearScreen() {
if
currPos
>
0
{
targetLine
:=
currPos
/
b
.
LineWidth
if
targetLine
>
0
{
for
cnt
:=
0
;
cnt
<
targetLine
;
cnt
++
{
for
range
targetLine
{
fmt
.
Print
(
CursorDown
)
}
}
...
...
Prev
1
2
Next
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment