Update go-ini dependency and remove semicolon hack in translations (#2913)

pull/2915/head
Lauris BH 7 years ago committed by Lunny Xiao
parent bd23e36bec
commit a6f337046f
  1. 4
      modules/templates/helper.go
  2. 4
      options/locale/locale_en-US.ini
  3. 2
      templates/repo/issue/view_content/comments.tmpl
  4. 12
      vendor/github.com/Unknwon/i18n/Makefile
  5. 6
      vendor/github.com/Unknwon/i18n/README.md
  6. 32
      vendor/github.com/Unknwon/i18n/i18n.go
  7. 2
      vendor/gopkg.in/ini.v1/LICENSE
  8. 5
      vendor/gopkg.in/ini.v1/Makefile
  9. 29
      vendor/gopkg.in/ini.v1/README.md
  10. 29
      vendor/gopkg.in/ini.v1/README_ZH.md
  11. 392
      vendor/gopkg.in/ini.v1/file.go
  12. 381
      vendor/gopkg.in/ini.v1/ini.go
  13. 42
      vendor/gopkg.in/ini.v1/key.go
  14. 36
      vendor/gopkg.in/ini.v1/parser.go
  15. 9
      vendor/gopkg.in/ini.v1/section.go
  16. 16
      vendor/gopkg.in/ini.v1/struct.go
  17. 12
      vendor/vendor.json

@ -160,10 +160,6 @@ func NewFuncMap() []template.FuncMap {
return setting.DisableGitHooks return setting.DisableGitHooks
}, },
"TrN": TrN, "TrN": TrN,
// TODO: Remove this once go-ini parser supports unescaping comment characters
"UnescapeLocale": func(str string) string {
return strings.NewReplacer("\\;", ";", "\\#", "#").Replace(str)
},
}} }}
} }

@ -627,8 +627,8 @@ issues.label_templates.info = There are not any labels yet. You can click on the
issues.label_templates.helper = Select a label set issues.label_templates.helper = Select a label set
issues.label_templates.use = Use this label set issues.label_templates.use = Use this label set
issues.label_templates.fail_to_load_file = Failed to load label template file '%s': %v issues.label_templates.fail_to_load_file = Failed to load label template file '%s': %v
issues.add_label_at = `added the <div class="ui label" style="color: %s\; background-color: %s">%s</div> label %s` issues.add_label_at = added the <div class="ui label" style="color: %s\; background-color: %s">%s</div> label %s
issues.remove_label_at = `removed the <div class="ui label" style="color: %s\; background-color: %s">%s</div> label %s` issues.remove_label_at = removed the <div class="ui label" style="color: %s\; background-color: %s">%s</div> label %s
issues.add_milestone_at = `added this to the <b>%s</b> milestone %s` issues.add_milestone_at = `added this to the <b>%s</b> milestone %s`
issues.change_milestone_at = `modified the milestone from <b>%s</b> to <b>%s</b> %s` issues.change_milestone_at = `modified the milestone from <b>%s</b> to <b>%s</b> %s`
issues.remove_milestone_at = `removed this from the <b>%s</b> milestone %s` issues.remove_milestone_at = `removed this from the <b>%s</b> milestone %s`

@ -96,7 +96,7 @@
<img src="{{.Poster.RelAvatarLink}}"> <img src="{{.Poster.RelAvatarLink}}">
</a> </a>
<span class="text grey"><a href="{{.Poster.HomeLink}}">{{.Poster.Name}}</a> <span class="text grey"><a href="{{.Poster.HomeLink}}">{{.Poster.Name}}</a>
{{if .Content}}{{$.i18n.Tr "repo.issues.add_label_at" .Label.ForegroundColor .Label.Color .Label.Name $createdStr | UnescapeLocale | Safe}}{{else}}{{$.i18n.Tr "repo.issues.remove_label_at" .Label.ForegroundColor .Label.Color .Label.Name $createdStr | UnescapeLocale | Safe}}{{end}}</span> {{if .Content}}{{$.i18n.Tr "repo.issues.add_label_at" .Label.ForegroundColor .Label.Color .Label.Name $createdStr | Safe}}{{else}}{{$.i18n.Tr "repo.issues.remove_label_at" .Label.ForegroundColor .Label.Color .Label.Name $createdStr | Safe}}{{end}}</span>
</div> </div>
{{end}} {{end}}
{{else if eq .Type 8}} {{else if eq .Type 8}}

@ -0,0 +1,12 @@
.PHONY: build test bench vet
build: vet bench
test:
go test -v -cover
bench:
go test -v -cover -test.bench=. -test.benchmem
vet:
go vet

@ -1,4 +1,4 @@
i18n i18n [![GoDoc](https://godoc.org/github.com/Unknwon/i18n?status.svg)](https://godoc.org/github.com/Unknwon/i18n) [![Sourcegraph](https://sourcegraph.com/github.com/Unknwon/i18n/-/badge.svg)](https://sourcegraph.com/github.com/Unknwon/i18n?badge)
==== ====
Package i18n is for app Internationalization and Localization. Package i18n is for app Internationalization and Localization.
@ -131,4 +131,6 @@ This command can operate 1 or more files in one command.
## More information ## More information
If the key does not exist, then i18n will return the key string to caller. For instance, when key name is `hi` and it does not exist in locale file, simply return `hi` as output. - The first locale you load to the module is considered as **default locale**.
- When matching non-default locale and didn't find the string, i18n will have a second try on default locale.
- If i18n still cannot find string in the default locale, raw string will be returned. For instance, when the string is `hi` and it does not exist in locale file, simply return `hi` as output.

@ -156,7 +156,10 @@ func GetDescriptionByLang(lang string) string {
} }
func SetMessageWithDesc(lang, langDesc string, localeFile interface{}, otherLocaleFiles ...interface{}) error { func SetMessageWithDesc(lang, langDesc string, localeFile interface{}, otherLocaleFiles ...interface{}) error {
message, err := ini.Load(localeFile, otherLocaleFiles...) message, err := ini.LoadSources(ini.LoadOptions{
IgnoreInlineComment: true,
UnescapeValueCommentSymbols: true,
}, localeFile, otherLocaleFiles...)
if err == nil { if err == nil {
message.BlockMode = false message.BlockMode = false
lc := new(locale) lc := new(locale)
@ -194,10 +197,11 @@ func (l Locale) Index() int {
// Tr translates content to target language. // Tr translates content to target language.
func Tr(lang, format string, args ...interface{}) string { func Tr(lang, format string, args ...interface{}) string {
var section string var section string
parts := strings.SplitN(format, ".", 2)
if len(parts) == 2 { idx := strings.IndexByte(format, '.')
section = parts[0] if idx > 0 {
format = parts[1] section = format[:idx]
format = format[idx+1:]
} }
value, ok := locales.Get(lang, section, format) value, ok := locales.Get(lang, section, format)
@ -208,15 +212,17 @@ func Tr(lang, format string, args ...interface{}) string {
if len(args) > 0 { if len(args) > 0 {
params := make([]interface{}, 0, len(args)) params := make([]interface{}, 0, len(args))
for _, arg := range args { for _, arg := range args {
if arg != nil { if arg == nil {
val := reflect.ValueOf(arg) continue
if val.Kind() == reflect.Slice { }
for i := 0; i < val.Len(); i++ {
params = append(params, val.Index(i).Interface()) val := reflect.ValueOf(arg)
} if val.Kind() == reflect.Slice {
} else { for i := 0; i < val.Len(); i++ {
params = append(params, arg) params = append(params, val.Index(i).Interface())
} }
} else {
params = append(params, arg)
} }
} }
return fmt.Sprintf(format, params...) return fmt.Sprintf(format, params...)

2
vendor/gopkg.in/ini.v1/LICENSE generated vendored

@ -176,7 +176,7 @@ recommend that a file or class name and description of purpose be included on
the same "printed page" as the copyright notice for easier identification within the same "printed page" as the copyright notice for easier identification within
third-party archives. third-party archives.
Copyright [yyyy] [name of copyright owner] Copyright 2014 Unknwon
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

5
vendor/gopkg.in/ini.v1/Makefile generated vendored

@ -1,4 +1,4 @@
.PHONY: build test bench vet .PHONY: build test bench vet coverage
build: vet bench build: vet bench
@ -10,3 +10,6 @@ bench:
vet: vet:
go vet go vet
coverage:
go test -coverprofile=c.out && go tool cover -html=c.out && rm c.out

29
vendor/gopkg.in/ini.v1/README.md generated vendored

@ -101,7 +101,7 @@ skip-name-resolve
By default, this is considered as missing value. But if you know you're going to deal with those cases, you can assign advanced load options: By default, this is considered as missing value. But if you know you're going to deal with those cases, you can assign advanced load options:
```go ```go
cfg, err := LoadSources(LoadOptions{AllowBooleanKeys: true}, "my.cnf")) cfg, err := ini.LoadSources(ini.LoadOptions{AllowBooleanKeys: true}, "my.cnf"))
``` ```
The value of those keys are always `true`, and when you save to a file, it will keep in the same foramt as you read. The value of those keys are always `true`, and when you save to a file, it will keep in the same foramt as you read.
@ -125,7 +125,7 @@ If you want to save a value with `#` or `;`, please quote them with ``` ` ``` or
Alternatively, you can use following `LoadOptions` to completely ignore inline comments: Alternatively, you can use following `LoadOptions` to completely ignore inline comments:
```go ```go
cfg, err := LoadSources(LoadOptions{IgnoreInlineComment: true}, "app.ini")) cfg, err := ini.LoadSources(ini.LoadOptions{IgnoreInlineComment: true}, "app.ini"))
``` ```
### Working with sections ### Working with sections
@ -329,6 +329,20 @@ foo = "some value" // foo: some value
bar = 'some value' // bar: some value bar = 'some value' // bar: some value
``` ```
Sometimes you downloaded file from [Crowdin](https://crowdin.com/) has values like the following (value is surrounded by double quotes and quotes in the value are escaped):
```ini
create_repo="created repository <a href=\"%s\">%s</a>"
```
How do you transform this to regular format automatically?
```go
cfg, err := ini.LoadSources(ini.LoadOptions{UnescapeValueDoubleQuotes: true}, "en-US.ini"))
cfg.Section("<name of your section>").Key("create_repo").String()
// You got: created repository <a href="%s">%s</a>
```
That's all? Hmm, no. That's all? Hmm, no.
#### Helper methods of working with values #### Helper methods of working with values
@ -480,7 +494,7 @@ cfg.Section("package.sub").ParentKeys() // ["CLONE_URL"]
Sometimes, you have sections that do not contain key-value pairs but raw content, to handle such case, you can use `LoadOptions.UnparsableSections`: Sometimes, you have sections that do not contain key-value pairs but raw content, to handle such case, you can use `LoadOptions.UnparsableSections`:
```go ```go
cfg, err := LoadSources(LoadOptions{UnparseableSections: []string{"COMMENTS"}}, `[COMMENTS] cfg, err := ini.LoadSources(ini.LoadOptions{UnparseableSections: []string{"COMMENTS"}}, `[COMMENTS]
<1><L.Slide#2> This slide has the fuel listed in the wrong units <e.1>`)) <1><L.Slide#2> This slide has the fuel listed in the wrong units <e.1>`))
body := cfg.Section("COMMENTS").Body() body := cfg.Section("COMMENTS").Body()
@ -573,7 +587,7 @@ Why not?
```go ```go
type Embeded struct { type Embeded struct {
Dates []time.Time `delim:"|"` Dates []time.Time `delim:"|" comment:"Time data"`
Places []string `ini:"places,omitempty"` Places []string `ini:"places,omitempty"`
None []int `ini:",omitempty"` None []int `ini:",omitempty"`
} }
@ -581,10 +595,10 @@ type Embeded struct {
type Author struct { type Author struct {
Name string `ini:"NAME"` Name string `ini:"NAME"`
Male bool Male bool
Age int Age int `comment:"Author's age"`
GPA float64 GPA float64
NeverMind string `ini:"-"` NeverMind string `ini:"-"`
*Embeded *Embeded `comment:"Embeded section"`
} }
func main() { func main() {
@ -605,10 +619,13 @@ So, what do I get?
```ini ```ini
NAME = Unknwon NAME = Unknwon
Male = true Male = true
; Author's age
Age = 21 Age = 21
GPA = 2.8 GPA = 2.8
; Embeded section
[Embeded] [Embeded]
; Time data
Dates = 2015-08-07T22:14:22+08:00|2015-08-07T22:14:22+08:00 Dates = 2015-08-07T22:14:22+08:00|2015-08-07T22:14:22+08:00
places = HangZhou,Boston places = HangZhou,Boston
``` ```

@ -94,7 +94,7 @@ skip-name-resolve
默认情况下这被认为是缺失值而无法完成解析,但可以通过高级的加载选项对它们进行处理: 默认情况下这被认为是缺失值而无法完成解析,但可以通过高级的加载选项对它们进行处理:
```go ```go
cfg, err := LoadSources(LoadOptions{AllowBooleanKeys: true}, "my.cnf")) cfg, err := ini.LoadSources(ini.LoadOptions{AllowBooleanKeys: true}, "my.cnf"))
``` ```
这些键的值永远为 `true`,且在保存到文件时也只会输出键名。 这些键的值永远为 `true`,且在保存到文件时也只会输出键名。
@ -118,7 +118,7 @@ key, err := sec.NewBooleanKey("skip-host-cache")
除此之外,您还可以通过 `LoadOptions` 完全忽略行内注释: 除此之外,您还可以通过 `LoadOptions` 完全忽略行内注释:
```go ```go
cfg, err := LoadSources(LoadOptions{IgnoreInlineComment: true}, "app.ini")) cfg, err := ini.LoadSources(ini.LoadOptions{IgnoreInlineComment: true}, "app.ini"))
``` ```
### 操作分区(Section) ### 操作分区(Section)
@ -322,6 +322,20 @@ foo = "some value" // foo: some value
bar = 'some value' // bar: some value bar = 'some value' // bar: some value
``` ```
有时您会获得像从 [Crowdin](https://crowdin.com/) 网站下载的文件那样具有特殊格式的值(值使用双引号括起来,内部的双引号被转义):
```ini
create_repo="创建了仓库 <a href=\"%s\">%s</a>"
```
那么,怎么自动地将这类值进行处理呢?
```go
cfg, err := ini.LoadSources(ini.LoadOptions{UnescapeValueDoubleQuotes: true}, "en-US.ini"))
cfg.Section("<name of your section>").Key("create_repo").String()
// You got: 创建了仓库 <a href="%s">%s</a>
```
这就是全部了?哈哈,当然不是。 这就是全部了?哈哈,当然不是。
#### 操作键值的辅助方法 #### 操作键值的辅助方法
@ -473,7 +487,7 @@ cfg.Section("package.sub").ParentKeys() // ["CLONE_URL"]
如果遇到一些比较特殊的分区,它们不包含常见的键值对,而是没有固定格式的纯文本,则可以使用 `LoadOptions.UnparsableSections` 进行处理: 如果遇到一些比较特殊的分区,它们不包含常见的键值对,而是没有固定格式的纯文本,则可以使用 `LoadOptions.UnparsableSections` 进行处理:
```go ```go
cfg, err := LoadSources(LoadOptions{UnparseableSections: []string{"COMMENTS"}}, `[COMMENTS] cfg, err := LoadSources(ini.LoadOptions{UnparseableSections: []string{"COMMENTS"}}, `[COMMENTS]
<1><L.Slide#2> This slide has the fuel listed in the wrong units <e.1>`)) <1><L.Slide#2> This slide has the fuel listed in the wrong units <e.1>`))
body := cfg.Section("COMMENTS").Body() body := cfg.Section("COMMENTS").Body()
@ -564,7 +578,7 @@ p := &Person{
```go ```go
type Embeded struct { type Embeded struct {
Dates []time.Time `delim:"|"` Dates []time.Time `delim:"|" comment:"Time data"`
Places []string `ini:"places,omitempty"` Places []string `ini:"places,omitempty"`
None []int `ini:",omitempty"` None []int `ini:",omitempty"`
} }
@ -572,10 +586,10 @@ type Embeded struct {
type Author struct { type Author struct {
Name string `ini:"NAME"` Name string `ini:"NAME"`
Male bool Male bool
Age int Age int `comment:"Author's age"`
GPA float64 GPA float64
NeverMind string `ini:"-"` NeverMind string `ini:"-"`
*Embeded *Embeded `comment:"Embeded section"`
} }
func main() { func main() {
@ -596,10 +610,13 @@ func main() {
```ini ```ini
NAME = Unknwon NAME = Unknwon
Male = true Male = true
; Author's age
Age = 21 Age = 21
GPA = 2.8 GPA = 2.8
; Embeded section
[Embeded] [Embeded]
; Time data
Dates = 2015-08-07T22:14:22+08:00|2015-08-07T22:14:22+08:00 Dates = 2015-08-07T22:14:22+08:00|2015-08-07T22:14:22+08:00
places = HangZhou,Boston places = HangZhou,Boston
``` ```

392
vendor/gopkg.in/ini.v1/file.go generated vendored

@ -0,0 +1,392 @@
// Copyright 2017 Unknwon
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
package ini
import (
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"strings"
"sync"
)
// File represents a combination of a or more INI file(s) in memory.
type File struct {
options LoadOptions
dataSources []dataSource
// Should make things safe, but sometimes doesn't matter.
BlockMode bool
lock sync.RWMutex
// To keep data in order.
sectionList []string
// Actual data is stored here.
sections map[string]*Section
NameMapper
ValueMapper
}
// newFile initializes File object with given data sources.
func newFile(dataSources []dataSource, opts LoadOptions) *File {
return &File{
BlockMode: true,
dataSources: dataSources,
sections: make(map[string]*Section),
sectionList: make([]string, 0, 10),
options: opts,
}
}
// Empty returns an empty file object.
func Empty() *File {
// Ignore error here, we sure our data is good.
f, _ := Load([]byte(""))
return f
}
// NewSection creates a new section.
func (f *File) NewSection(name string) (*Section, error) {
if len(name) == 0 {
return nil, errors.New("error creating new section: empty section name")
} else if f.options.Insensitive && name != DEFAULT_SECTION {
name = strings.ToLower(name)
}
if f.BlockMode {
f.lock.Lock()
defer f.lock.Unlock()
}
if inSlice(name, f.sectionList) {
return f.sections[name], nil
}
f.sectionList = append(f.sectionList, name)
f.sections[name] = newSection(f, name)
return f.sections[name], nil
}
// NewRawSection creates a new section with an unparseable body.
func (f *File) NewRawSection(name, body string) (*Section, error) {
section, err := f.NewSection(name)
if err != nil {
return nil, err
}
section.isRawSection = true
section.rawBody = body
return section, nil
}
// NewSections creates a list of sections.
func (f *File) NewSections(names ...string) (err error) {
for _, name := range names {
if _, err = f.NewSection(name); err != nil {
return err
}
}
return nil
}
// GetSection returns section by given name.
func (f *File) GetSection(name string) (*Section, error) {
if len(name) == 0 {
name = DEFAULT_SECTION
}
if f.options.Insensitive {
name = strings.ToLower(name)
}
if f.BlockMode {
f.lock.RLock()
defer f.lock.RUnlock()
}
sec := f.sections[name]
if sec == nil {
return nil, fmt.Errorf("section '%s' does not exist", name)
}
return sec, nil
}
// Section assumes named section exists and returns a zero-value when not.
func (f *File) Section(name string) *Section {
sec, err := f.GetSection(name)
if err != nil {
// Note: It's OK here because the only possible error is empty section name,
// but if it's empty, this piece of code won't be executed.
sec, _ = f.NewSection(name)
return sec
}
return sec
}
// Section returns list of Section.
func (f *File) Sections() []*Section {
sections := make([]*Section, len(f.sectionList))
for i := range f.sectionList {
sections[i] = f.Section(f.sectionList[i])
}
return sections
}
// ChildSections returns a list of child sections of given section name.
func (f *File) ChildSections(name string) []*Section {
return f.Section(name).ChildSections()
}
// SectionStrings returns list of section names.
func (f *File) SectionStrings() []string {
list := make([]string, len(f.sectionList))
copy(list, f.sectionList)
return list
}
// DeleteSection deletes a section.
func (f *File) DeleteSection(name string) {
if f.BlockMode {
f.lock.Lock()
defer f.lock.Unlock()
}
if len(name) == 0 {
name = DEFAULT_SECTION
}
for i, s := range f.sectionList {
if s == name {
f.sectionList = append(f.sectionList[:i], f.sectionList[i+1:]...)
delete(f.sections, name)
return
}
}
}
func (f *File) reload(s dataSource) error {
r, err := s.ReadCloser()
if err != nil {
return err
}
defer r.Close()
return f.parse(r)
}
// Reload reloads and parses all data sources.
func (f *File) Reload() (err error) {
for _, s := range f.dataSources {
if err = f.reload(s); err != nil {
// In loose mode, we create an empty default section for nonexistent files.
if os.IsNotExist(err) && f.options.Loose {
f.parse(bytes.NewBuffer(nil))
continue
}
return err
}
}
return nil
}
// Append appends one or more data sources and reloads automatically.
func (f *File) Append(source interface{}, others ...interface{}) error {
ds, err := parseDataSource(source)
if err != nil {
return err
}
f.dataSources = append(f.dataSources, ds)
for _, s := range others {
ds, err = parseDataSource(s)
if err != nil {
return err
}
f.dataSources = append(f.dataSources, ds)
}
return f.Reload()
}
func (f *File) writeToBuffer(indent string) (*bytes.Buffer, error) {
equalSign := "="
if PrettyFormat {
equalSign = " = "
}
// Use buffer to make sure target is safe until finish encoding.
buf := bytes.NewBuffer(nil)
for i, sname := range f.sectionList {
sec := f.Section(sname)
if len(sec.Comment) > 0 {
if sec.Comment[0] != '#' && sec.Comment[0] != ';' {
sec.Comment = "; " + sec.Comment
} else {
sec.Comment = sec.Comment[:1] + " " + strings.TrimSpace(sec.Comment[1:])
}
if _, err := buf.WriteString(sec.Comment + LineBreak); err != nil {
return nil, err
}
}
if i > 0 || DefaultHeader {
if _, err := buf.WriteString("[" + sname + "]" + LineBreak); err != nil {
return nil, err
}
} else {
// Write nothing if default section is empty
if len(sec.keyList) == 0 {
continue
}
}
if sec.isRawSection {
if _, err := buf.WriteString(sec.rawBody); err != nil {
return nil, err
}
if PrettySection {
// Put a line between sections
if _, err := buf.WriteString(LineBreak); err != nil {
return nil, err
}
}
continue
}
// Count and generate alignment length and buffer spaces using the
// longest key. Keys may be modifed if they contain certain characters so
// we need to take that into account in our calculation.
alignLength := 0
if PrettyFormat {
for _, kname := range sec.keyList {
keyLength := len(kname)
// First case will surround key by ` and second by """
if strings.ContainsAny(kname, "\"=:") {
keyLength += 2
} else if strings.Contains(kname, "`") {
keyLength += 6
}
if keyLength > alignLength {
alignLength = keyLength
}
}
}
alignSpaces := bytes.Repeat([]byte(" "), alignLength)
KEY_LIST:
for _, kname := range sec.keyList {
key := sec.Key(kname)
if len(key.Comment) > 0 {
if len(indent) > 0 && sname != DEFAULT_SECTION {
buf.WriteString(indent)
}
if key.Comment[0] != '#' && key.Comment[0] != ';' {
key.Comment = "; " + key.Comment
} else {
key.Comment = key.Comment[:1] + " " + strings.TrimSpace(key.Comment[1:])
}
if _, err := buf.WriteString(key.Comment + LineBreak); err != nil {
return nil, err
}
}
if len(indent) > 0 && sname != DEFAULT_SECTION {
buf.WriteString(indent)
}
switch {
case key.isAutoIncrement:
kname = "-"
case strings.ContainsAny(kname, "\"=:"):
kname = "`" + kname + "`"
case strings.Contains(kname, "`"):
kname = `"""` + kname + `"""`
}
for _, val := range key.ValueWithShadows() {
if _, err := buf.WriteString(kname); err != nil {
return nil, err
}
if key.isBooleanType {
if kname != sec.keyList[len(sec.keyList)-1] {
buf.WriteString(LineBreak)
}
continue KEY_LIST
}
// Write out alignment spaces before "=" sign
if PrettyFormat {
buf.Write(alignSpaces[:alignLength-len(kname)])
}
// In case key value contains "\n", "`", "\"", "#" or ";"
if strings.ContainsAny(val, "\n`") {
val = `"""` + val + `"""`
} else if !f.options.IgnoreInlineComment && strings.ContainsAny(val, "#;") {
val = "`" + val + "`"
}
if _, err := buf.WriteString(equalSign + val + LineBreak); err != nil {
return nil, err
}
}
}
if PrettySection {
// Put a line between sections
if _, err := buf.WriteString(LineBreak); err != nil {
return nil, err
}
}
}
return buf, nil
}
// WriteToIndent writes content into io.Writer with given indention.
// If PrettyFormat has been set to be true,
// it will align "=" sign with spaces under each section.
func (f *File) WriteToIndent(w io.Writer, indent string) (int64, error) {
buf, err := f.writeToBuffer(indent)
if err != nil {
return 0, err
}
return buf.WriteTo(w)
}
// WriteTo writes file content into io.Writer.
func (f *File) WriteTo(w io.Writer) (int64, error) {
return f.WriteToIndent(w, "")
}
// SaveToIndent writes content to file system with given value indention.
func (f *File) SaveToIndent(filename, indent string) error {
// Note: Because we are truncating with os.Create,
// so it's safer to save to a temporary file location and rename afte done.
buf, err := f.writeToBuffer(indent)
if err != nil {
return err
}
return ioutil.WriteFile(filename, buf.Bytes(), 0666)
}
// SaveTo writes content to file system.
func (f *File) SaveTo(filename string) error {
return f.SaveToIndent(filename, "")
}

381
vendor/gopkg.in/ini.v1/ini.go generated vendored

@ -17,15 +17,12 @@ package ini
import ( import (
"bytes" "bytes"
"errors"
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"os" "os"
"regexp" "regexp"
"runtime" "runtime"
"strings"
"sync"
) )
const ( const (
@ -35,7 +32,7 @@ const (
// Maximum allowed depth when recursively substituing variable names. // Maximum allowed depth when recursively substituing variable names.
_DEPTH_VALUES = 99 _DEPTH_VALUES = 99
_VERSION = "1.28.2" _VERSION = "1.31.1"
) )
// Version returns current package version literal. // Version returns current package version literal.
@ -92,18 +89,6 @@ func (s sourceFile) ReadCloser() (_ io.ReadCloser, err error) {
return os.Open(s.name) return os.Open(s.name)
} }
type bytesReadCloser struct {
reader io.Reader
}
func (rc *bytesReadCloser) Read(p []byte) (n int, err error) {
return rc.reader.Read(p)
}
func (rc *bytesReadCloser) Close() error {
return nil
}
// sourceData represents an object that contains content in memory. // sourceData represents an object that contains content in memory.
type sourceData struct { type sourceData struct {
data []byte data []byte
@ -122,38 +107,6 @@ func (s *sourceReadCloser) ReadCloser() (io.ReadCloser, error) {
return s.reader, nil return s.reader, nil
} }
// File represents a combination of a or more INI file(s) in memory.
type File struct {
// Should make things safe, but sometimes doesn't matter.
BlockMode bool
// Make sure data is safe in multiple goroutines.
lock sync.RWMutex
// Allow combination of multiple data sources.
dataSources []dataSource
// Actual data is stored here.
sections map[string]*Section
// To keep data in order.
sectionList []string
options LoadOptions
NameMapper
ValueMapper
}
// newFile initializes File object with given data sources.
func newFile(dataSources []dataSource, opts LoadOptions) *File {
return &File{
BlockMode: true,
dataSources: dataSources,
sections: make(map[string]*Section),
sectionList: make([]string, 0, 10),
options: opts,
}
}
func parseDataSource(source interface{}) (dataSource, error) { func parseDataSource(source interface{}) (dataSource, error) {
switch s := source.(type) { switch s := source.(type) {
case string: case string:
@ -181,6 +134,13 @@ type LoadOptions struct {
AllowBooleanKeys bool AllowBooleanKeys bool
// AllowShadows indicates whether to keep track of keys with same name under same section. // AllowShadows indicates whether to keep track of keys with same name under same section.
AllowShadows bool AllowShadows bool
// UnescapeValueDoubleQuotes indicates whether to unescape double quotes inside value to regular format
// when value is surrounded by double quotes, e.g. key="a \"value\"" => key=a "value"
UnescapeValueDoubleQuotes bool
// UnescapeValueCommentSymbols indicates to unescape comment symbols (\# and \;) inside value to regular format
// when value is NOT surrounded by any quotes.
// Note: UNSTABLE, behavior might change to only unescape inside double quotes but may noy necessary at all.
UnescapeValueCommentSymbols bool
// Some INI formats allow group blocks that store a block of raw content that doesn't otherwise // Some INI formats allow group blocks that store a block of raw content that doesn't otherwise
// conform to key/value pairs. Specify the names of those blocks here. // conform to key/value pairs. Specify the names of those blocks here.
UnparseableSections []string UnparseableSections []string
@ -229,328 +189,3 @@ func InsensitiveLoad(source interface{}, others ...interface{}) (*File, error) {
func ShadowLoad(source interface{}, others ...interface{}) (*File, error) { func ShadowLoad(source interface{}, others ...interface{}) (*File, error) {
return LoadSources(LoadOptions{AllowShadows: true}, source, others...) return LoadSources(LoadOptions{AllowShadows: true}, source, others...)
} }
// Empty returns an empty file object.
func Empty() *File {
// Ignore error here, we sure our data is good.
f, _ := Load([]byte(""))
return f
}
// NewSection creates a new section.
func (f *File) NewSection(name string) (*Section, error) {
if len(name) == 0 {
return nil, errors.New("error creating new section: empty section name")
} else if f.options.Insensitive && name != DEFAULT_SECTION {
name = strings.ToLower(name)
}
if f.BlockMode {
f.lock.Lock()
defer f.lock.Unlock()
}
if inSlice(name, f.sectionList) {
return f.sections[name], nil
}
f.sectionList = append(f.sectionList, name)
f.sections[name] = newSection(f, name)
return f.sections[name], nil
}
// NewRawSection creates a new section with an unparseable body.
func (f *File) NewRawSection(name, body string) (*Section, error) {
section, err := f.NewSection(name)
if err != nil {
return nil, err
}
section.isRawSection = true
section.rawBody = body
return section, nil
}
// NewSections creates a list of sections.
func (f *File) NewSections(names ...string) (err error) {
for _, name := range names {
if _, err = f.NewSection(name); err != nil {
return err
}
}
return nil
}
// GetSection returns section by given name.
func (f *File) GetSection(name string) (*Section, error) {
if len(name) == 0 {
name = DEFAULT_SECTION
} else if f.options.Insensitive {
name = strings.ToLower(name)
}
if f.BlockMode {
f.lock.RLock()
defer f.lock.RUnlock()
}
sec := f.sections[name]
if sec == nil {
return nil, fmt.Errorf("section '%s' does not exist", name)
}
return sec, nil
}
// Section assumes named section exists and returns a zero-value when not.
func (f *File) Section(name string) *Section {
sec, err := f.GetSection(name)
if err != nil {
// Note: It's OK here because the only possible error is empty section name,
// but if it's empty, this piece of code won't be executed.
sec, _ = f.NewSection(name)
return sec
}
return sec
}
// Section returns list of Section.
func (f *File) Sections() []*Section {
sections := make([]*Section, len(f.sectionList))
for i := range f.sectionList {
sections[i] = f.Section(f.sectionList[i])
}
return sections
}
// ChildSections returns a list of child sections of given section name.
func (f *File) ChildSections(name string) []*Section {
return f.Section(name).ChildSections()
}
// SectionStrings returns list of section names.
func (f *File) SectionStrings() []string {
list := make([]string, len(f.sectionList))
copy(list, f.sectionList)
return list
}
// DeleteSection deletes a section.
func (f *File) DeleteSection(name string) {
if f.BlockMode {
f.lock.Lock()
defer f.lock.Unlock()
}
if len(name) == 0 {
name = DEFAULT_SECTION
}
for i, s := range f.sectionList {
if s == name {
f.sectionList = append(f.sectionList[:i], f.sectionList[i+1:]...)
delete(f.sections, name)
return
}
}
}
func (f *File) reload(s dataSource) error {
r, err := s.ReadCloser()
if err != nil {
return err
}
defer r.Close()
return f.parse(r)
}
// Reload reloads and parses all data sources.
func (f *File) Reload() (err error) {
for _, s := range f.dataSources {
if err = f.reload(s); err != nil {
// In loose mode, we create an empty default section for nonexistent files.
if os.IsNotExist(err) && f.options.Loose {
f.parse(bytes.NewBuffer(nil))
continue
}
return err
}
}
return nil
}
// Append appends one or more data sources and reloads automatically.
func (f *File) Append(source interface{}, others ...interface{}) error {
ds, err := parseDataSource(source)
if err != nil {
return err
}
f.dataSources = append(f.dataSources, ds)
for _, s := range others {
ds, err = parseDataSource(s)
if err != nil {
return err
}
f.dataSources = append(f.dataSources, ds)
}
return f.Reload()
}
func (f *File) writeToBuffer(indent string) (*bytes.Buffer, error) {
equalSign := "="
if PrettyFormat {
equalSign = " = "
}
// Use buffer to make sure target is safe until finish encoding.
buf := bytes.NewBuffer(nil)
for i, sname := range f.sectionList {
sec := f.Section(sname)
if len(sec.Comment) > 0 {
if sec.Comment[0] != '#' && sec.Comment[0] != ';' {
sec.Comment = "; " + sec.Comment
}
if _, err := buf.WriteString(sec.Comment + LineBreak); err != nil {
return nil, err
}
}
if i > 0 || DefaultHeader {
if _, err := buf.WriteString("[" + sname + "]" + LineBreak); err != nil {
return nil, err
}
} else {
// Write nothing if default section is empty
if len(sec.keyList) == 0 {
continue
}
}
if sec.isRawSection {
if _, err := buf.WriteString(sec.rawBody); err != nil {
return nil, err
}
continue
}
// Count and generate alignment length and buffer spaces using the
// longest key. Keys may be modifed if they contain certain characters so
// we need to take that into account in our calculation.
alignLength := 0
if PrettyFormat {
for _, kname := range sec.keyList {
keyLength := len(kname)
// First case will surround key by ` and second by """
if strings.ContainsAny(kname, "\"=:") {
keyLength += 2
} else if strings.Contains(kname, "`") {
keyLength += 6
}
if keyLength > alignLength {
alignLength = keyLength
}
}
}
alignSpaces := bytes.Repeat([]byte(" "), alignLength)
KEY_LIST:
for _, kname := range sec.keyList {
key := sec.Key(kname)
if len(key.Comment) > 0 {
if len(indent) > 0 && sname != DEFAULT_SECTION {
buf.WriteString(indent)
}
if key.Comment[0] != '#' && key.Comment[0] != ';' {
key.Comment = "; " + key.Comment
}
if _, err := buf.WriteString(key.Comment + LineBreak); err != nil {
return nil, err
}
}
if len(indent) > 0 && sname != DEFAULT_SECTION {
buf.WriteString(indent)
}
switch {
case key.isAutoIncrement:
kname = "-"
case strings.ContainsAny(kname, "\"=:"):
kname = "`" + kname + "`"
case strings.Contains(kname, "`"):
kname = `"""` + kname + `"""`
}
for _, val := range key.ValueWithShadows() {
if _, err := buf.WriteString(kname); err != nil {
return nil, err
}
if key.isBooleanType {
if kname != sec.keyList[len(sec.keyList)-1] {
buf.WriteString(LineBreak)
}
continue KEY_LIST
}
// Write out alignment spaces before "=" sign
if PrettyFormat {
buf.Write(alignSpaces[:alignLength-len(kname)])
}
// In case key value contains "\n", "`", "\"", "#" or ";"
if strings.ContainsAny(val, "\n`") {
val = `"""` + val + `"""`
} else if !f.options.IgnoreInlineComment && strings.ContainsAny(val, "#;") {
val = "`" + val + "`"
}
if _, err := buf.WriteString(equalSign + val + LineBreak); err != nil {
return nil, err
}
}
}
if PrettySection {
// Put a line between sections
if _, err := buf.WriteString(LineBreak); err != nil {
return nil, err
}
}
}
return buf, nil
}
// WriteToIndent writes content into io.Writer with given indention.
// If PrettyFormat has been set to be true,
// it will align "=" sign with spaces under each section.
func (f *File) WriteToIndent(w io.Writer, indent string) (int64, error) {
buf, err := f.writeToBuffer(indent)
if err != nil {
return 0, err
}
return buf.WriteTo(w)
}
// WriteTo writes file content into io.Writer.
func (f *File) WriteTo(w io.Writer) (int64, error) {
return f.WriteToIndent(w, "")
}
// SaveToIndent writes content to file system with given value indention.
func (f *File) SaveToIndent(filename, indent string) error {
// Note: Because we are truncating with os.Create,
// so it's safer to save to a temporary file location and rename afte done.
buf, err := f.writeToBuffer(indent)
if err != nil {
return err
}
return ioutil.WriteFile(filename, buf.Bytes(), 0666)
}
// SaveTo writes content to file system.
func (f *File) SaveTo(filename string) error {
return f.SaveToIndent(filename, "")
}

42
vendor/gopkg.in/ini.v1/key.go generated vendored

@ -15,6 +15,7 @@
package ini package ini
import ( import (
"bytes"
"errors" "errors"
"fmt" "fmt"
"strconv" "strconv"
@ -25,6 +26,7 @@ import (
// Key represents a key under a section. // Key represents a key under a section.
type Key struct { type Key struct {
s *Section s *Section
Comment string
name string name string
value string value string
isAutoIncrement bool isAutoIncrement bool
@ -32,8 +34,6 @@ type Key struct {
isShadow bool isShadow bool
shadows []*Key shadows []*Key
Comment string
} }
// newKey simply return a key object with given values. // newKey simply return a key object with given values.
@ -114,7 +114,7 @@ func (k *Key) transformValue(val string) string {
// Search in the same section. // Search in the same section.
nk, err := k.s.GetKey(noption) nk, err := k.s.GetKey(noption)
if err != nil { if err != nil || k == nk {
// Search again in default section. // Search again in default section.
nk, _ = k.s.f.Section("").GetKey(noption) nk, _ = k.s.f.Section("").GetKey(noption)
} }
@ -444,11 +444,39 @@ func (k *Key) Strings(delim string) []string {
return []string{} return []string{}
} }
vals := strings.Split(str, delim) runes := []rune(str)
for i := range vals { vals := make([]string, 0, 2)
// vals[i] = k.transformValue(strings.TrimSpace(vals[i])) var buf bytes.Buffer
vals[i] = strings.TrimSpace(vals[i]) escape := false
idx := 0
for {
if escape {
escape = false
if runes[idx] != '\\' && !strings.HasPrefix(string(runes[idx:]), delim) {
buf.WriteRune('\\')
}
buf.WriteRune(runes[idx])
} else {
if runes[idx] == '\\' {
escape = true
} else if strings.HasPrefix(string(runes[idx:]), delim) {
idx += len(delim) - 1
vals = append(vals, strings.TrimSpace(buf.String()))
buf.Reset()
} else {
buf.WriteRune(runes[idx])
}
}
idx += 1
if idx == len(runes) {
break
}
}
if buf.Len() > 0 {
vals = append(vals, strings.TrimSpace(buf.String()))
} }
return vals return vals
} }

36
vendor/gopkg.in/ini.v1/parser.go generated vendored

@ -193,7 +193,9 @@ func hasSurroundedQuote(in string, quote byte) bool {
strings.IndexByte(in[1:], quote) == len(in)-2 strings.IndexByte(in[1:], quote) == len(in)-2
} }
func (p *parser) readValue(in []byte, ignoreContinuation, ignoreInlineComment bool) (string, error) { func (p *parser) readValue(in []byte,
ignoreContinuation, ignoreInlineComment, unescapeValueDoubleQuotes, unescapeValueCommentSymbols bool) (string, error) {
line := strings.TrimLeftFunc(string(in), unicode.IsSpace) line := strings.TrimLeftFunc(string(in), unicode.IsSpace)
if len(line) == 0 { if len(line) == 0 {
return "", nil return "", nil
@ -204,6 +206,8 @@ func (p *parser) readValue(in []byte, ignoreContinuation, ignoreInlineComment bo
valQuote = `"""` valQuote = `"""`
} else if line[0] == '`' { } else if line[0] == '`' {
valQuote = "`" valQuote = "`"
} else if unescapeValueDoubleQuotes && line[0] == '"' {
valQuote = `"`
} }
if len(valQuote) > 0 { if len(valQuote) > 0 {
@ -214,6 +218,9 @@ func (p *parser) readValue(in []byte, ignoreContinuation, ignoreInlineComment bo
return p.readMultilines(line, line[startIdx:], valQuote) return p.readMultilines(line, line[startIdx:], valQuote)
} }
if unescapeValueDoubleQuotes && valQuote == `"` {
return strings.Replace(line[startIdx:pos+startIdx], `\"`, `"`, -1), nil
}
return line[startIdx : pos+startIdx], nil return line[startIdx : pos+startIdx], nil
} }
@ -234,10 +241,17 @@ func (p *parser) readValue(in []byte, ignoreContinuation, ignoreInlineComment bo
} }
} }
// Trim single quotes // Trim single and double quotes
if hasSurroundedQuote(line, '\'') || if hasSurroundedQuote(line, '\'') ||
hasSurroundedQuote(line, '"') { hasSurroundedQuote(line, '"') {
line = line[1 : len(line)-1] line = line[1 : len(line)-1]
} else if len(valQuote) == 0 && unescapeValueCommentSymbols {
if strings.Contains(line, `\;`) {
line = strings.Replace(line, `\;`, ";", -1)
}
if strings.Contains(line, `\#`) {
line = strings.Replace(line, `\#`, "#", -1)
}
} }
return line, nil return line, nil
} }
@ -250,7 +264,11 @@ func (f *File) parse(reader io.Reader) (err error) {
} }
// Ignore error because default section name is never empty string. // Ignore error because default section name is never empty string.
section, _ := f.NewSection(DEFAULT_SECTION) name := DEFAULT_SECTION
if f.options.Insensitive {
name = strings.ToLower(DEFAULT_SECTION)
}
section, _ := f.NewSection(name)
var line []byte var line []byte
var inUnparseableSection bool var inUnparseableSection bool
@ -321,7 +339,11 @@ func (f *File) parse(reader io.Reader) (err error) {
if err != nil { if err != nil {
// Treat as boolean key when desired, and whole line is key name. // Treat as boolean key when desired, and whole line is key name.
if IsErrDelimiterNotFound(err) && f.options.AllowBooleanKeys { if IsErrDelimiterNotFound(err) && f.options.AllowBooleanKeys {
kname, err := p.readValue(line, f.options.IgnoreContinuation, f.options.IgnoreInlineComment) kname, err := p.readValue(line,
f.options.IgnoreContinuation,
f.options.IgnoreInlineComment,
f.options.UnescapeValueDoubleQuotes,
f.options.UnescapeValueCommentSymbols)
if err != nil { if err != nil {
return err return err
} }
@ -344,7 +366,11 @@ func (f *File) parse(reader io.Reader) (err error) {
p.count++ p.count++
} }
value, err := p.readValue(line[offset:], f.options.IgnoreContinuation, f.options.IgnoreInlineComment) value, err := p.readValue(line[offset:],
f.options.IgnoreContinuation,
f.options.IgnoreInlineComment,
f.options.UnescapeValueDoubleQuotes,
f.options.UnescapeValueCommentSymbols)
if err != nil { if err != nil {
return err return err
} }

@ -54,6 +54,14 @@ func (s *Section) Body() string {
return strings.TrimSpace(s.rawBody) return strings.TrimSpace(s.rawBody)
} }
// SetBody updates body content only if section is raw.
func (s *Section) SetBody(body string) {
if !s.isRawSection {
return
}
s.rawBody = body
}
// NewKey creates a new key to given section. // NewKey creates a new key to given section.
func (s *Section) NewKey(name, val string) (*Key, error) { func (s *Section) NewKey(name, val string) (*Key, error) {
if len(name) == 0 { if len(name) == 0 {
@ -136,6 +144,7 @@ func (s *Section) HasKey(name string) bool {
} }
// Haskey is a backwards-compatible name for HasKey. // Haskey is a backwards-compatible name for HasKey.
// TODO: delete me in v2
func (s *Section) Haskey(name string) bool { func (s *Section) Haskey(name string) bool {
return s.HasKey(name) return s.HasKey(name)
} }

16
vendor/gopkg.in/ini.v1/struct.go generated vendored

@ -113,7 +113,7 @@ func setSliceWithProperType(key *Key, field reflect.Value, delim string, allowSh
default: default:
return fmt.Errorf("unsupported type '[]%s'", sliceOf) return fmt.Errorf("unsupported type '[]%s'", sliceOf)
} }
if isStrict { if err != nil && isStrict {
return err return err
} }
@ -166,7 +166,7 @@ func setWithProperType(t reflect.Type, key *Key, field reflect.Value, delim stri
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
durationVal, err := key.Duration() durationVal, err := key.Duration()
// Skip zero value // Skip zero value
if err == nil && int(durationVal) > 0 { if err == nil && int64(durationVal) > 0 {
field.Set(reflect.ValueOf(durationVal)) field.Set(reflect.ValueOf(durationVal))
return nil return nil
} }
@ -450,6 +450,12 @@ func (s *Section) reflectFrom(val reflect.Value) error {
// Note: fieldName can never be empty here, ignore error. // Note: fieldName can never be empty here, ignore error.
sec, _ = s.f.NewSection(fieldName) sec, _ = s.f.NewSection(fieldName)
} }
// Add comment from comment tag
if len(sec.Comment) == 0 {
sec.Comment = tpField.Tag.Get("comment")
}
if err = sec.reflectFrom(field); err != nil { if err = sec.reflectFrom(field); err != nil {
return fmt.Errorf("error reflecting field (%s): %v", fieldName, err) return fmt.Errorf("error reflecting field (%s): %v", fieldName, err)
} }
@ -461,6 +467,12 @@ func (s *Section) reflectFrom(val reflect.Value) error {
if err != nil { if err != nil {
key, _ = s.NewKey(fieldName, "") key, _ = s.NewKey(fieldName, "")
} }
// Add comment from comment tag
if len(key.Comment) == 0 {
key.Comment = tpField.Tag.Get("comment")
}
if err = reflectWithProperType(tpField.Type, key, field, parseDelim(tpField.Tag.Get("delim"))); err != nil { if err = reflectWithProperType(tpField.Type, key, field, parseDelim(tpField.Tag.Get("delim"))); err != nil {
return fmt.Errorf("error reflecting field (%s): %v", fieldName, err) return fmt.Errorf("error reflecting field (%s): %v", fieldName, err)
} }

12
vendor/vendor.json vendored

@ -39,10 +39,10 @@
"revisionTime": "2015-10-08T13:54:07Z" "revisionTime": "2015-10-08T13:54:07Z"
}, },
{ {
"checksumSHA1": "gSAaJ38R4iqG2CEsZe/ftOs3V9w=", "checksumSHA1": "GwPkXd1UL3D7F3IuHHM+V0r4MB4=",
"path": "github.com/Unknwon/i18n", "path": "github.com/Unknwon/i18n",
"revision": "39d6f2727e0698b1021ceb6a77c1801aa92e7d5d", "revision": "b64d336589669d317928070e70ba0ae558f16633",
"revisionTime": "2016-06-03T08:28:25Z" "revisionTime": "2017-11-14T19:46:41Z"
}, },
{ {
"checksumSHA1": "BkrPsaiF83hWNDvM2o/rMBdUz5o=", "checksumSHA1": "BkrPsaiF83hWNDvM2o/rMBdUz5o=",
@ -1490,10 +1490,10 @@
"revisionTime": "2016-04-11T21:29:32Z" "revisionTime": "2016-04-11T21:29:32Z"
}, },
{ {
"checksumSHA1": "RVc+qy5SP9wOZye+2/5LPJg/SHE=", "checksumSHA1": "PDsaJzdBVB5Ocolpgh89M+9+ysU=",
"path": "gopkg.in/ini.v1", "path": "gopkg.in/ini.v1",
"revision": "20b96f641a5ea98f2f8619ff4f3e061cff4833bd", "revision": "7e7da451323b6766da368f8a1e8ec9a88a16b4a0",
"revisionTime": "2017-08-13T05:15:16Z" "revisionTime": "2017-11-14T01:13:26Z"
}, },
{ {
"checksumSHA1": "7jPSjzw3mckHVQ2SjY4NvtIJR4g=", "checksumSHA1": "7jPSjzw3mckHVQ2SjY4NvtIJR4g=",

Loading…
Cancel
Save