- Updated the status component template to include a new trigger for task status checks. - Modified the status component's Go implementation to reflect the new trigger in the HTML output. - Improved the UpdateIssue handler to conditionally set the assignee based on form input. - Enhanced the dashboard template to include a new dependencies section for issues, allowing users to add dependencies dynamically. - Refactored the dashboard's issue rows to utilize JavaScript functions for rendering status, type, and priority badges, improving maintainability and readability.
238 lines
6.1 KiB
Go
238 lines
6.1 KiB
Go
package tasks
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"charm.land/huh/v2"
|
|
"github.com/LazyBachelor/LazyPM/internal/models"
|
|
"github.com/LazyBachelor/LazyPM/internal/utils/check"
|
|
"github.com/go-git/go-git/v6"
|
|
)
|
|
|
|
const gitTaskDescription = `You are tasked with performing a Git operation.
|
|
|
|
Modify a file in a Git repository and commit the change.
|
|
|
|
Your task:
|
|
1. Assign the given issue to yourself as 'Me'.
|
|
2. A folder called "task" is created in this directory.
|
|
3. Inside the folder you will find README.md.
|
|
- Edit this file and add something to it.
|
|
- The file must be different from its original content.
|
|
4. Commit your change:
|
|
- Open a terminal and change into the task folder.
|
|
- Run "git add ." to stage the changes.
|
|
- Run "git commit -m "updated codebase"" to commit.
|
|
5. Set the Issue status to "Closed" when done.
|
|
|
|
The repository is in the task folder (./task).`
|
|
|
|
const gitTaskReadmeContent = "This is a Git task. Add your name or a short note below this line to complete it, then commit your change."
|
|
|
|
type GitTask struct {
|
|
app *App
|
|
done bool
|
|
repo *git.Repository
|
|
setupIssue *Issue
|
|
lastModified time.Time
|
|
}
|
|
|
|
var gitTaskInProgress = false
|
|
|
|
func NewGitTask(app *App) *GitTask {
|
|
return &GitTask{app: app, done: false}
|
|
}
|
|
|
|
func (t *GitTask) Config() Config {
|
|
return BaseConfig().WithStatisticsStoragePath("./.pm/git-task-stats.json")
|
|
}
|
|
|
|
func (t *GitTask) Details(interfaceType InterfaceType) TaskDetails {
|
|
return BaseDetails(interfaceType).
|
|
WithTitle("Git Task").
|
|
WithDescription(gitTaskDescription).
|
|
WithDifficulty("Hard").WithTimeToComplete("5m")
|
|
}
|
|
|
|
func (t *GitTask) Questions(interfaceType InterfaceType) Questions {
|
|
return BaseQuestions(interfaceType).
|
|
With(
|
|
huh.NewGroup(
|
|
huh.NewSelect[string]().Key("git_interface_normally").
|
|
Title("What Git Interface do you normally use?").
|
|
Description("If you regularly use multiple interfaces, select the one you prefer most.").
|
|
Options(
|
|
huh.Option[string]{Value: "cli", Key: "Command Line Interface"},
|
|
huh.Option[string]{Value: "tui", Key: "Terminal User Interface"},
|
|
huh.Option[string]{Value: "gui", Key: "Graphical User Interface"},
|
|
),
|
|
))
|
|
}
|
|
|
|
func (t *GitTask) QuestionnaireKeys(_ InterfaceType) []string {
|
|
return BaseKeys().With("git_interface_normally")
|
|
}
|
|
|
|
func (t *GitTask) Setup(ctx context.Context) error {
|
|
gitTaskInProgress = false
|
|
if err := ClearIssues(t.app); err != nil {
|
|
return err
|
|
}
|
|
|
|
_ = os.RemoveAll("./task")
|
|
|
|
var err error
|
|
t.repo, err = t.initRepo()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_ = os.WriteFile("./task/README.md", []byte(gitTaskReadmeContent), 0o644)
|
|
_ = os.WriteFile("./task/.gitattributes", []byte("* text=auto\n"), 0o644)
|
|
|
|
t.setupIssue = NewIssueBuilder().
|
|
WithTitle("Upgrade the codebase").
|
|
WithDescription(gitTaskDescription).
|
|
WithIssueType(models.TypeTask).
|
|
Build()
|
|
|
|
if err := t.app.Issues.CreateIssue(ctx, t.setupIssue, ""); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Initialize lastModified and start file watcher
|
|
if stat, err := os.Stat("./task/README.md"); err == nil {
|
|
t.lastModified = stat.ModTime()
|
|
}
|
|
t.startFileWatcher(ctx)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (t *GitTask) startFileWatcher(ctx context.Context) {
|
|
go func() {
|
|
ticker := time.NewTicker(2 * time.Second)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-ticker.C:
|
|
if t.app.SubmitChan == nil {
|
|
continue
|
|
}
|
|
|
|
stat, err := os.Stat("./task/README.md")
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
if stat.ModTime().After(t.lastModified) {
|
|
t.lastModified = stat.ModTime()
|
|
select {
|
|
case t.app.SubmitChan <- models.ValidationTrigger{Source: models.ValidationTriggerAutoPoll}:
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
func (t *GitTask) Validate(ctx context.Context) ValidationFeedback {
|
|
expect := check.NewExpector()
|
|
|
|
issue, err := t.app.Issues.GetIssue(ctx, t.setupIssue.ID)
|
|
if err != nil {
|
|
return expect.Fatal("Could not fetch issue")
|
|
}
|
|
if issue == nil {
|
|
expect.Fail("Issue could not be found. It may have been deleted; please recreate it and try again.")
|
|
return expect.ValidationFeedback
|
|
}
|
|
|
|
expect.NotEmptyAndEqual(issue.Assignee, "Me", "Issue Assignee")
|
|
|
|
if !expect.Valid() {
|
|
return expect.ValidationFeedback
|
|
}
|
|
|
|
// Ensure the repository is available.
|
|
if t.repo == nil {
|
|
t.repo, err = t.initRepo()
|
|
if err != nil {
|
|
expect.Fail("Failed to open the Git repository: " + err.Error())
|
|
return expect.ValidationFeedback
|
|
}
|
|
}
|
|
|
|
headRef, err := t.repo.Head()
|
|
if err != nil {
|
|
expect.Fail("No commits found in the Git repository. Please commit your changes.")
|
|
return expect.ValidationFeedback
|
|
}
|
|
|
|
commit, err := t.repo.CommitObject(headRef.Hash())
|
|
if err != nil {
|
|
expect.Fail("Failed to read the latest commit: " + err.Error())
|
|
return expect.ValidationFeedback
|
|
}
|
|
|
|
expect.NotEmptyAndEqual(strings.TrimSpace(commit.Message), "updated codebase", "Git commit message")
|
|
|
|
tree, err := commit.Tree()
|
|
if err != nil {
|
|
expect.Fail("Failed to read commit tree: " + err.Error())
|
|
return expect.ValidationFeedback
|
|
}
|
|
|
|
readmeFile, err := tree.File("README.md")
|
|
if err != nil {
|
|
expect.Fail("README.md should be part of the committed changes.")
|
|
return expect.ValidationFeedback
|
|
}
|
|
|
|
readmeContent, err := readmeFile.Contents()
|
|
if err != nil {
|
|
expect.Fail("Failed to read README.md from the commit: " + err.Error())
|
|
return expect.ValidationFeedback
|
|
}
|
|
|
|
expect.Assert(readmeContent != gitTaskReadmeContent,
|
|
"You should modify README.md content before committing")
|
|
|
|
if wt, err := t.repo.Worktree(); err == nil {
|
|
if status, err := wt.Status(); err == nil {
|
|
expect.Assert(status.IsClean(), "The working tree should be clean after committing")
|
|
}
|
|
}
|
|
|
|
if !expect.Valid() {
|
|
return expect.ValidationFeedback
|
|
}
|
|
|
|
expect.Equal(issue.Status, models.StatusClosed, "Issue Status")
|
|
|
|
return expect.Complete()
|
|
}
|
|
|
|
func (t *GitTask) initRepo() (*git.Repository, error) {
|
|
repo, err := git.PlainInit("./task", false)
|
|
if err != nil {
|
|
if errors.Is(err, git.ErrTargetDirNotEmpty) {
|
|
repo, err = git.PlainOpen("./task")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return repo, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
return repo, nil
|
|
}
|