Better log parsing

This commit is contained in:
Thomas Miceli 2023-03-18 23:18:20 +01:00
parent 24f790fb9c
commit 527be16183
No known key found for this signature in database
GPG key ID: D86C6F6390AF050F
6 changed files with 158 additions and 103 deletions

View file

@ -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

View file

@ -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
}

View file

@ -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 {

View file

@ -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,
}) })

View file

@ -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 }}

View file

@ -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) }}