Better log parsing

This commit is contained in:
Thomas Miceli
2023-03-18 23:18:20 +01:00
parent 24f790fb9c
commit 527be16183
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)
}
func GetLog(user string, gist string, skip string) (string, error) {
func GetLog(user string, gist string, skip string) ([]*Commit, error) {
repositoryPath := RepositoryPath(user, gist)
cmd := exec.Command(
@ -130,20 +130,22 @@ func GetLog(user string, gist string, skip string) (string, error) {
"log",
"-n",
"11",
"--no-prefix",
"--no-color",
"-p",
"--skip",
skip,
"--format=format:%n=commit %H:%aN:%at",
"--format=format:c %H%na %aN%nt %at",
"--shortstat",
"--ignore-missing", // avoid errors if a wrong hash is given
"HEAD",
)
cmd.Dir = repositoryPath
stdout, _ := cmd.StdoutPipe()
err := cmd.Start()
if err != nil {
return nil, err
}
stdout, err := cmd.Output()
return string(stdout), err
return parseLog(stdout), nil
}
func CloneTmp(user string, gist string, gistTmpId string) error {
@ -213,7 +215,7 @@ func AddAll(gistTmpId string) error {
return cmd.Run()
}
func Commit(gistTmpId string) error {
func CommitRepository(gistTmpId string) error {
cmd := exec.Command("git", "commit", "--allow-empty", "-m", `"Opengist commit"`)
tmpPath := TmpRepositoryPath(gistTmpId)
cmd.Dir = tmpPath

View File

@ -1,10 +1,29 @@
package git
import (
"bufio"
"bytes"
"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) {
var buf []byte
var err error
@ -31,3 +50,96 @@ func truncateCommandOutput(out io.Reader, maxBytes int64) (string, bool, error)
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
}
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 {
// Decrement fork counter if the gist was forked
err := tx.Model(&Gist{}).
@ -195,8 +180,8 @@ func (gist *Gist) DeleteRepository() error {
return git.DeleteRepository(gist.User.Username, gist.Uuid)
}
func (gist *Gist) Files(revision string) ([]*File, error) {
var files []*File
func (gist *Gist) Files(revision string) ([]*git.File, error) {
var files []*git.File
filesStr, err := git.GetFilesOfRepository(gist.User.Username, gist.Uuid, revision)
if err != nil {
// if the revision or the file do not exist
@ -218,7 +203,7 @@ func (gist *Gist) Files(revision string) ([]*File, error) {
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)
// 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 &File{
return &git.File{
Filename: filename,
Content: content,
Truncated: truncated,
}, err
}
func (gist *Gist) Log(skip string) error {
_, err := git.GetLog(gist.User.Username, gist.Uuid, skip)
return err
func (gist *Gist) Log(skip string) ([]*git.Commit, error) {
return git.GetLog(gist.User.Username, gist.Uuid, skip)
}
func (gist *Gist) NbCommits() (string, error) {
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 {
return err
}
@ -258,7 +241,7 @@ func (gist *Gist) AddAndCommitFiles(files *[]File) error {
return err
}
if err := git.Commit(gist.Uuid); err != nil {
if err := git.CommitRepository(gist.Uuid); err != nil {
return err
}
@ -280,10 +263,10 @@ func (gist *Gist) RPC(service string) ([]byte, error) {
// -- DTO -- //
type GistDTO struct {
Title string `validate:"max=50" form:"title"`
Description string `validate:"max=150" form:"description"`
Private bool `form:"private"`
Files []File `validate:"min=1,dive"`
Title string `validate:"max=50" form:"title"`
Description string `validate:"max=150" form:"description"`
Private bool `form:"private"`
Files []git.File `validate:"min=1,dive"`
}
func (dto *GistDTO) ToGist() *Gist {

View File

@ -162,50 +162,9 @@ func revisions(ctx echo.Context) error {
pageInt := getPage(ctx)
nbCommits := getData(ctx, "nbCommits")
commits := make([]*models.Commit, 0)
if nbCommits != "0" {
gitlogStr, err := git.GetLog(userName, gistName, strconv.Itoa((pageInt-1)*10))
if err != nil {
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)
}
commits, err := gist.Log(strconv.Itoa((pageInt - 1) * 10))
if err != nil {
return errorRes(500, "Error fetching commits log", err)
}
if err := paginate(ctx, commits, pageInt, 10, "commits", userName+"/"+gistName+"/revisions", 2); err != nil {
@ -249,7 +208,7 @@ func processCreate(ctx echo.Context) error {
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++ {
name := ctx.Request().PostForm["name"][i]
content := ctx.Request().PostForm["content"][i]
@ -263,7 +222,7 @@ func processCreate(ctx echo.Context) error {
return errorRes(400, "Invalid character unescaped", err)
}
dto.Files = append(dto.Files, models.File{
dto.Files = append(dto.Files, git.File{
Filename: name,
Content: escapedValue,
})