diff --git a/internal/git/commands.go b/internal/git/commands.go index 77610d2..a5128d2 100644 --- a/internal/git/commands.go +++ b/internal/git/commands.go @@ -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 diff --git a/internal/git/output_parser.go b/internal/git/output_parser.go index 3ba0504..8e95779 100644 --- a/internal/git/output_parser.go +++ b/internal/git/output_parser.go @@ -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 +} diff --git a/internal/models/gist.go b/internal/models/gist.go index 3c1c0d6..6f54eb0 100644 --- a/internal/models/gist.go +++ b/internal/models/gist.go @@ -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 { diff --git a/internal/web/gist.go b/internal/web/gist.go index 2384756..e2e33e0 100644 --- a/internal/web/gist.go +++ b/internal/web/gist.go @@ -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, }) diff --git a/templates/pages/gist.html b/templates/pages/gist.html index 4a5c22f..3169896 100644 --- a/templates/pages/gist.html +++ b/templates/pages/gist.html @@ -26,13 +26,15 @@
{{$i}} | {{ $line }} |
{{$i}} | {{ $line }} |
- File renamed without changes. -
+ {{ if and (eq $file.Content "") (ne $file.OldFilename "") }} +File renamed without changes
+ {{ else if eq $file.Content "" }} +Empty file
{{ else }}