mirror of
https://github.com/thomiceli/opengist.git
synced 2024-12-23 04:52:40 +00:00
Better log parsing
This commit is contained in:
parent
24f790fb9c
commit
527be16183
6 changed files with 158 additions and 103 deletions
|
@ -121,7 +121,7 @@ func GetFileContent(user string, gist string, revision string, filename string,
|
||||||
return truncateCommandOutput(stdout, maxBytes)
|
return truncateCommandOutput(stdout, maxBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetLog(user string, gist string, skip string) (string, error) {
|
func GetLog(user string, gist string, skip string) ([]*Commit, error) {
|
||||||
repositoryPath := RepositoryPath(user, gist)
|
repositoryPath := RepositoryPath(user, gist)
|
||||||
|
|
||||||
cmd := exec.Command(
|
cmd := exec.Command(
|
||||||
|
@ -130,20 +130,22 @@ func GetLog(user string, gist string, skip string) (string, error) {
|
||||||
"log",
|
"log",
|
||||||
"-n",
|
"-n",
|
||||||
"11",
|
"11",
|
||||||
"--no-prefix",
|
|
||||||
"--no-color",
|
"--no-color",
|
||||||
"-p",
|
"-p",
|
||||||
"--skip",
|
"--skip",
|
||||||
skip,
|
skip,
|
||||||
"--format=format:%n=commit %H:%aN:%at",
|
"--format=format:c %H%na %aN%nt %at",
|
||||||
"--shortstat",
|
"--shortstat",
|
||||||
"--ignore-missing", // avoid errors if a wrong hash is given
|
|
||||||
"HEAD",
|
"HEAD",
|
||||||
)
|
)
|
||||||
cmd.Dir = repositoryPath
|
cmd.Dir = repositoryPath
|
||||||
|
stdout, _ := cmd.StdoutPipe()
|
||||||
|
err := cmd.Start()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
stdout, err := cmd.Output()
|
return parseLog(stdout), nil
|
||||||
return string(stdout), err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func CloneTmp(user string, gist string, gistTmpId string) error {
|
func CloneTmp(user string, gist string, gistTmpId string) error {
|
||||||
|
@ -213,7 +215,7 @@ func AddAll(gistTmpId string) error {
|
||||||
return cmd.Run()
|
return cmd.Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
func Commit(gistTmpId string) error {
|
func CommitRepository(gistTmpId string) error {
|
||||||
cmd := exec.Command("git", "commit", "--allow-empty", "-m", `"Opengist commit"`)
|
cmd := exec.Command("git", "commit", "--allow-empty", "-m", `"Opengist commit"`)
|
||||||
tmpPath := TmpRepositoryPath(gistTmpId)
|
tmpPath := TmpRepositoryPath(gistTmpId)
|
||||||
cmd.Dir = tmpPath
|
cmd.Dir = tmpPath
|
||||||
|
|
|
@ -1,10 +1,29 @@
|
||||||
package git
|
package git
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"io"
|
"io"
|
||||||
|
"regexp"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type File struct {
|
||||||
|
Filename string `validate:"excludes=\x2f,excludes=\x5c,max=50"`
|
||||||
|
OldFilename string `validate:"excludes=\x2f,excludes=\x5c,max=50"`
|
||||||
|
Content string `validate:"required"`
|
||||||
|
Truncated bool
|
||||||
|
IsCreated bool
|
||||||
|
IsDeleted bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type Commit struct {
|
||||||
|
Hash string
|
||||||
|
Author string
|
||||||
|
Timestamp string
|
||||||
|
Changed string
|
||||||
|
Files []File
|
||||||
|
}
|
||||||
|
|
||||||
func truncateCommandOutput(out io.Reader, maxBytes int64) (string, bool, error) {
|
func truncateCommandOutput(out io.Reader, maxBytes int64) (string, bool, error) {
|
||||||
var buf []byte
|
var buf []byte
|
||||||
var err error
|
var err error
|
||||||
|
@ -31,3 +50,96 @@ func truncateCommandOutput(out io.Reader, maxBytes int64) (string, bool, error)
|
||||||
|
|
||||||
return string(buf), truncated, nil
|
return string(buf), truncated, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseLog(out io.Reader) []*Commit {
|
||||||
|
scanner := bufio.NewScanner(out)
|
||||||
|
|
||||||
|
var commits []*Commit
|
||||||
|
var currentCommit *Commit
|
||||||
|
var currentFile *File
|
||||||
|
var isContent bool
|
||||||
|
|
||||||
|
for scanner.Scan() {
|
||||||
|
// new commit found
|
||||||
|
currentFile = nil
|
||||||
|
currentCommit = &Commit{Hash: string(scanner.Bytes()[2:]), Files: []File{}}
|
||||||
|
|
||||||
|
scanner.Scan()
|
||||||
|
currentCommit.Author = string(scanner.Bytes()[2:])
|
||||||
|
|
||||||
|
scanner.Scan()
|
||||||
|
currentCommit.Timestamp = string(scanner.Bytes()[2:])
|
||||||
|
|
||||||
|
scanner.Scan()
|
||||||
|
changed := scanner.Bytes()[1:]
|
||||||
|
changed = bytes.ReplaceAll(changed, []byte("(+)"), []byte(""))
|
||||||
|
changed = bytes.ReplaceAll(changed, []byte("(-)"), []byte(""))
|
||||||
|
currentCommit.Changed = string(changed)
|
||||||
|
|
||||||
|
// twice because --shortstat adds a new line
|
||||||
|
scanner.Scan()
|
||||||
|
scanner.Scan()
|
||||||
|
// commit header parsed
|
||||||
|
|
||||||
|
// files changes inside the commit
|
||||||
|
for {
|
||||||
|
line := scanner.Bytes()
|
||||||
|
|
||||||
|
// end of content of file
|
||||||
|
if len(line) == 0 {
|
||||||
|
isContent = false
|
||||||
|
if currentFile != nil {
|
||||||
|
currentCommit.Files = append(currentCommit.Files, *currentFile)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// new file found
|
||||||
|
if bytes.HasPrefix(line, []byte("diff --git")) {
|
||||||
|
// current file is finished, we can add it to the commit
|
||||||
|
if currentFile != nil {
|
||||||
|
currentCommit.Files = append(currentCommit.Files, *currentFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a new file
|
||||||
|
isContent = false
|
||||||
|
currentFile = &File{}
|
||||||
|
filenameRegex := regexp.MustCompile(`^diff --git a/(.+) b/(.+)$`)
|
||||||
|
matches := filenameRegex.FindStringSubmatch(string(line))
|
||||||
|
if len(matches) == 3 {
|
||||||
|
currentFile.Filename = matches[2]
|
||||||
|
if matches[1] != matches[2] {
|
||||||
|
currentFile.OldFilename = matches[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scanner.Scan()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if bytes.HasPrefix(line, []byte("new")) {
|
||||||
|
currentFile.IsCreated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if bytes.HasPrefix(line, []byte("deleted")) {
|
||||||
|
currentFile.IsDeleted = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// file content found
|
||||||
|
if line[0] == '@' {
|
||||||
|
isContent = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if isContent {
|
||||||
|
currentFile.Content += string(line) + "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
scanner.Scan()
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentCommit != nil {
|
||||||
|
commits = append(commits, currentCommit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return commits
|
||||||
|
}
|
||||||
|
|
|
@ -28,21 +28,6 @@ type Gist struct {
|
||||||
ForkedID uint
|
ForkedID uint
|
||||||
}
|
}
|
||||||
|
|
||||||
type File struct {
|
|
||||||
Filename string `validate:"excludes=\x2f,excludes=\x5c,max=50"`
|
|
||||||
OldFilename string `validate:"excludes=\x2f,excludes=\x5c,max=50"`
|
|
||||||
Content string `validate:"required"`
|
|
||||||
Truncated bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type Commit struct {
|
|
||||||
Hash string
|
|
||||||
Author string
|
|
||||||
Timestamp string
|
|
||||||
Changed string
|
|
||||||
Files []File
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gist *Gist) BeforeDelete(tx *gorm.DB) error {
|
func (gist *Gist) BeforeDelete(tx *gorm.DB) error {
|
||||||
// Decrement fork counter if the gist was forked
|
// Decrement fork counter if the gist was forked
|
||||||
err := tx.Model(&Gist{}).
|
err := tx.Model(&Gist{}).
|
||||||
|
@ -195,8 +180,8 @@ func (gist *Gist) DeleteRepository() error {
|
||||||
return git.DeleteRepository(gist.User.Username, gist.Uuid)
|
return git.DeleteRepository(gist.User.Username, gist.Uuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gist *Gist) Files(revision string) ([]*File, error) {
|
func (gist *Gist) Files(revision string) ([]*git.File, error) {
|
||||||
var files []*File
|
var files []*git.File
|
||||||
filesStr, err := git.GetFilesOfRepository(gist.User.Username, gist.Uuid, revision)
|
filesStr, err := git.GetFilesOfRepository(gist.User.Username, gist.Uuid, revision)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// if the revision or the file do not exist
|
// if the revision or the file do not exist
|
||||||
|
@ -218,7 +203,7 @@ func (gist *Gist) Files(revision string) ([]*File, error) {
|
||||||
return files, err
|
return files, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gist *Gist) File(revision string, filename string, truncate bool) (*File, error) {
|
func (gist *Gist) File(revision string, filename string, truncate bool) (*git.File, error) {
|
||||||
content, truncated, err := git.GetFileContent(gist.User.Username, gist.Uuid, revision, filename, truncate)
|
content, truncated, err := git.GetFileContent(gist.User.Username, gist.Uuid, revision, filename, truncate)
|
||||||
|
|
||||||
// if the revision or the file do not exist
|
// if the revision or the file do not exist
|
||||||
|
@ -226,24 +211,22 @@ func (gist *Gist) File(revision string, filename string, truncate bool) (*File,
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return &File{
|
return &git.File{
|
||||||
Filename: filename,
|
Filename: filename,
|
||||||
Content: content,
|
Content: content,
|
||||||
Truncated: truncated,
|
Truncated: truncated,
|
||||||
}, err
|
}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gist *Gist) Log(skip string) error {
|
func (gist *Gist) Log(skip string) ([]*git.Commit, error) {
|
||||||
_, err := git.GetLog(gist.User.Username, gist.Uuid, skip)
|
return git.GetLog(gist.User.Username, gist.Uuid, skip)
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gist *Gist) NbCommits() (string, error) {
|
func (gist *Gist) NbCommits() (string, error) {
|
||||||
return git.GetNumberOfCommitsOfRepository(gist.User.Username, gist.Uuid)
|
return git.GetNumberOfCommitsOfRepository(gist.User.Username, gist.Uuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gist *Gist) AddAndCommitFiles(files *[]File) error {
|
func (gist *Gist) AddAndCommitFiles(files *[]git.File) error {
|
||||||
if err := git.CloneTmp(gist.User.Username, gist.Uuid, gist.Uuid); err != nil {
|
if err := git.CloneTmp(gist.User.Username, gist.Uuid, gist.Uuid); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -258,7 +241,7 @@ func (gist *Gist) AddAndCommitFiles(files *[]File) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := git.Commit(gist.Uuid); err != nil {
|
if err := git.CommitRepository(gist.Uuid); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -283,7 +266,7 @@ type GistDTO struct {
|
||||||
Title string `validate:"max=50" form:"title"`
|
Title string `validate:"max=50" form:"title"`
|
||||||
Description string `validate:"max=150" form:"description"`
|
Description string `validate:"max=150" form:"description"`
|
||||||
Private bool `form:"private"`
|
Private bool `form:"private"`
|
||||||
Files []File `validate:"min=1,dive"`
|
Files []git.File `validate:"min=1,dive"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dto *GistDTO) ToGist() *Gist {
|
func (dto *GistDTO) ToGist() *Gist {
|
||||||
|
|
|
@ -162,52 +162,11 @@ func revisions(ctx echo.Context) error {
|
||||||
|
|
||||||
pageInt := getPage(ctx)
|
pageInt := getPage(ctx)
|
||||||
|
|
||||||
nbCommits := getData(ctx, "nbCommits")
|
commits, err := gist.Log(strconv.Itoa((pageInt - 1) * 10))
|
||||||
commits := make([]*models.Commit, 0)
|
|
||||||
if nbCommits != "0" {
|
|
||||||
gitlogStr, err := git.GetLog(userName, gistName, strconv.Itoa((pageInt-1)*10))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorRes(500, "Error fetching commits log", err)
|
return errorRes(500, "Error fetching commits log", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
gitlog := strings.Split(gitlogStr, "\n=commit ")
|
|
||||||
for _, commitStr := range gitlog[1:] {
|
|
||||||
logContent := strings.SplitN(commitStr, "\n", 3)
|
|
||||||
|
|
||||||
header := strings.Split(logContent[0], ":")
|
|
||||||
commitStruct := models.Commit{
|
|
||||||
Hash: header[0],
|
|
||||||
Author: header[1],
|
|
||||||
Timestamp: header[2],
|
|
||||||
Files: make([]models.File, 0),
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(logContent) > 2 {
|
|
||||||
changed := strings.ReplaceAll(logContent[1], "(+)", "")
|
|
||||||
changed = strings.ReplaceAll(changed, "(-)", "")
|
|
||||||
commitStruct.Changed = changed
|
|
||||||
}
|
|
||||||
|
|
||||||
files := strings.Split(logContent[len(logContent)-1], "diff --git ")
|
|
||||||
if len(files) > 1 {
|
|
||||||
for _, fileStr := range files {
|
|
||||||
content := strings.SplitN(fileStr, "\n@@", 2)
|
|
||||||
if len(content) > 1 {
|
|
||||||
header := strings.Split(content[0], "\n")
|
|
||||||
commitStruct.Files = append(commitStruct.Files, models.File{Content: "@@" + content[1], Filename: header[len(header)-1][4:], OldFilename: header[len(header)-2][4:]})
|
|
||||||
} else {
|
|
||||||
// in case there is no content but a file renamed
|
|
||||||
header := strings.Split(content[0], "\n")
|
|
||||||
if len(header) > 3 {
|
|
||||||
commitStruct.Files = append(commitStruct.Files, models.File{Content: "", Filename: header[3][10:], OldFilename: header[2][12:]})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
commits = append(commits, &commitStruct)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := paginate(ctx, commits, pageInt, 10, "commits", userName+"/"+gistName+"/revisions", 2); err != nil {
|
if err := paginate(ctx, commits, pageInt, 10, "commits", userName+"/"+gistName+"/revisions", 2); err != nil {
|
||||||
return errorRes(404, "Page not found", nil)
|
return errorRes(404, "Page not found", nil)
|
||||||
}
|
}
|
||||||
|
@ -249,7 +208,7 @@ func processCreate(ctx echo.Context) error {
|
||||||
return errorRes(400, "Cannot bind data", err)
|
return errorRes(400, "Cannot bind data", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
dto.Files = make([]models.File, 0)
|
dto.Files = make([]git.File, 0)
|
||||||
for i := 0; i < len(ctx.Request().PostForm["content"]); i++ {
|
for i := 0; i < len(ctx.Request().PostForm["content"]); i++ {
|
||||||
name := ctx.Request().PostForm["name"][i]
|
name := ctx.Request().PostForm["name"][i]
|
||||||
content := ctx.Request().PostForm["content"][i]
|
content := ctx.Request().PostForm["content"][i]
|
||||||
|
@ -263,7 +222,7 @@ func processCreate(ctx echo.Context) error {
|
||||||
return errorRes(400, "Invalid character unescaped", err)
|
return errorRes(400, "Invalid character unescaped", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
dto.Files = append(dto.Files, models.File{
|
dto.Files = append(dto.Files, git.File{
|
||||||
Filename: name,
|
Filename: name,
|
||||||
Content: escapedValue,
|
Content: escapedValue,
|
||||||
})
|
})
|
||||||
|
|
2
templates/pages/gist.html
vendored
2
templates/pages/gist.html
vendored
|
@ -26,6 +26,7 @@
|
||||||
<div class="markdown markdown-body p-8">{{ $file.Content }}</div>
|
<div class="markdown markdown-body p-8">{{ $file.Content }}</div>
|
||||||
{{ else }}
|
{{ else }}
|
||||||
{{ $fileslug := slug $file.Filename }}
|
{{ $fileslug := slug $file.Filename }}
|
||||||
|
{{ if ne $file.Content "" }}
|
||||||
<table class="table-code w-full whitespace-pre" data-filename-slug="{{ $fileslug }}" data-filename="{{ $file.Filename }}" style="font-size: 0.8em; border-spacing: 0; border-collapse: collapse;">
|
<table class="table-code w-full whitespace-pre" data-filename-slug="{{ $fileslug }}" data-filename="{{ $file.Filename }}" style="font-size: 0.8em; border-spacing: 0; border-collapse: collapse;">
|
||||||
<tbody>
|
<tbody>
|
||||||
{{ $ii := "1" }}
|
{{ $ii := "1" }}
|
||||||
|
@ -34,6 +35,7 @@
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
23
templates/pages/revisions.html
vendored
23
templates/pages/revisions.html
vendored
|
@ -27,24 +27,22 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 flex text-slate-300" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 flex text-slate-300" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
|
||||||
</svg>
|
</svg>
|
||||||
{{ if eq $file.Filename $file.OldFilename }}
|
{{ if $file.IsCreated }}
|
||||||
<span class="flex text-sm ml-2 text-slate-300">{{ $file.Filename }}</span>
|
|
||||||
{{ else }}
|
|
||||||
{{ if eq $file.OldFilename "/dev/null" }}
|
|
||||||
<span class="flex text-sm ml-2 text-slate-300">{{ $file.Filename }}<span class="italic text-gray-400 ml-1">(file created)</span></span>
|
<span class="flex text-sm ml-2 text-slate-300">{{ $file.Filename }}<span class="italic text-gray-400 ml-1">(file created)</span></span>
|
||||||
{{ else if eq $file.Filename "/dev/null" }}
|
{{ else if $file.IsDeleted }}
|
||||||
<span class="flex text-sm ml-2 text-slate-300">{{ $file.OldFilename }} <span class="italic text-gray-400 ml-1">(file deleted)</span></span>
|
<span class="flex text-sm ml-2 text-slate-300">{{ $file.Filename }} <span class="italic text-gray-400 ml-1">(file deleted)</span></span>
|
||||||
{{ else }}
|
{{ else if ne $file.OldFilename "" }}
|
||||||
<span class="flex text-sm ml-2 text-slate-300">{{ $file.OldFilename }} <span class="italic text-gray-400 mx-1">renamed to</span> {{ $file.Filename }}</span>
|
<span class="flex text-sm ml-2 text-slate-300">{{ $file.OldFilename }} <span class="italic text-gray-400 mx-1">renamed to</span> {{ $file.Filename }}</span>
|
||||||
{{ end }}
|
{{ else }}
|
||||||
|
<span class="flex text-sm ml-2 text-slate-300">{{ $file.Filename }}</span>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="code overflow-auto">
|
<div class="code overflow-auto">
|
||||||
{{ if eq $file.Content "" }}
|
{{ if and (eq $file.Content "") (ne $file.OldFilename "") }}
|
||||||
<p class="m-2 ml-4 text-sm">
|
<p class="m-2 ml-4 text-xs">File renamed without changes</p>
|
||||||
File renamed without changes.
|
{{ else if eq $file.Content "" }}
|
||||||
</p>
|
<p class="m-2 ml-4 text-xs">Empty file</p>
|
||||||
{{ else }}
|
{{ else }}
|
||||||
<table class="table-code w-full whitespace-pre" data-filename="{{ $file.Filename }}" style="font-size: 0.8em; border-spacing: 0">
|
<table class="table-code w-full whitespace-pre" data-filename="{{ $file.Filename }}" style="font-size: 0.8em; border-spacing: 0">
|
||||||
<tbody>
|
<tbody>
|
||||||
|
@ -52,7 +50,6 @@
|
||||||
{{ $right := 0 }}
|
{{ $right := 0 }}
|
||||||
{{ range $line := split $file.Content "\n" }}
|
{{ range $line := split $file.Content "\n" }}
|
||||||
{{ if ne $line "" }}{{ if ne (index $line 0) 92 }}
|
{{ if ne $line "" }}{{ if ne (index $line 0) 92 }}
|
||||||
|
|
||||||
{{ if eq (index $line 0) 64 }}
|
{{ if eq (index $line 0) 64 }}
|
||||||
{{ $left = toInt (index (splitGit (index (split $line "-") 1)) 0) }}
|
{{ $left = toInt (index (splitGit (index (split $line "-") 1)) 0) }}
|
||||||
{{ $right = toInt (index (splitGit (index (split $line "+") 1)) 0) }}
|
{{ $right = toInt (index (splitGit (index (split $line "+") 1)) 0) }}
|
||||||
|
|
Loading…
Reference in a new issue