From f99db33c66d3fb36d8427ad52198b514f3213d4b Mon Sep 17 00:00:00 2001 From: vhespanha Date: Thu, 15 Aug 2024 15:40:13 -0300 Subject: [PATCH 01/81] Switch linters --- Makefile | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index 1045913..7ad25e1 100644 --- a/Makefile +++ b/Makefile @@ -22,7 +22,7 @@ LDFLAGS=-ldflags "-X $(PACKAGE)/internal/version.GitCommit=$(GIT_COMMIT) -X $(PA PLATFORMS=windows/amd64 darwin/amd64 darwin/arm64 linux/amd64 linux/arm64 # Tools -STATICCHECK := $(shell command -v staticcheck 2> /dev/null) +GOLANGCI_LINT := $(shell command -v golangci-lint 2> /dev/null) GOFUMPT := $(shell command -v gofumpt 2> /dev/null) GOIMPORTS := $(shell command -v goimports 2> /dev/null) GOLINES := $(shell command -v golines 2> /dev/null) @@ -46,9 +46,9 @@ deps: else \ echo "Dependencies updated. Please review changes in go.mod and go.sum."; \ fi -ifndef STATICCHECK - @echo "Installing staticcheck..." - @go install honnef.co/go/tools/cmd/staticcheck@latest +ifndef GOLANGCI_LINT + @echo "Installing golangci-lint..." + @curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.60.1 endif ifndef GOFUMPT @echo "Installing gofumpt..." @@ -65,9 +65,8 @@ endif lint: @echo "Running linter..." - @if $(STATICCHECK) ./...; then \ - echo "No linting issues found."; \ - fi + @golangci-lint run --fix -c .golangci.yml ./... + format: @echo "Formatting code..." @@ -107,7 +106,7 @@ help: @echo " make build - Build for the current platform" @echo " make clean - Remove build artifacts" @echo " make deps - Download dependencies and install tools" - @echo " make lint - Run staticcheck for linting" + @echo " make lint - Run golangci-lint for linting" @echo " make format - Format code using gofumpt, goimports, and golines" @echo " make build-all - Build for all specified platforms" @echo " make version - Display the current git commit and build date" From 60cb7ec748761d2f98a0aa1019ce4541be4c63e8 Mon Sep 17 00:00:00 2001 From: vhespanha Date: Thu, 15 Aug 2024 15:41:00 -0300 Subject: [PATCH 02/81] Add golangci-lint config --- .golangci.yml | 348 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 348 insertions(+) create mode 100644 .golangci.yml diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..f5acfcf --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,348 @@ +# This code is licensed under the terms of the MIT license https://opensource.org/license/mit +# Copyright (c) 2021 Marat Reymers + +## Golden config for golangci-lint v1.59.1 +# +# This is the best config for golangci-lint based on my experience and opinion. +# It is very strict, but not extremely strict. +# Feel free to adapt and change it for your needs. + +run: + # Timeout for analysis, e.g. 30s, 5m. + # Default: 1m + timeout: 3m + +# This file contains only configs which differ from defaults. +# All possible options can be found here https://github.com/golangci/golangci-lint/blob/master/.golangci.reference.yml +linters-settings: + cyclop: + # The maximal code complexity to report. + # Default: 10 + max-complexity: 30 + # The maximal average package complexity. + # If it's higher than 0.0 (float) the check is enabled + # Default: 0.0 + package-average: 10.0 + + errcheck: + # Report about not checking of errors in type assertions: `a := b.(MyStruct)`. + # Such cases aren't reported by default. + # Default: false + check-type-assertions: true + + exhaustive: + # Program elements to check for exhaustiveness. + # Default: [ switch ] + check: + - switch + - map + + exhaustruct: + # List of regular expressions to exclude struct packages and their names from checks. + # Regular expressions must match complete canonical struct package/name/structname. + # Default: [] + exclude: + # std libs + - '^net/http.Client$' + - '^net/http.Cookie$' + - '^net/http.Request$' + - '^net/http.Response$' + - '^net/http.Server$' + - '^net/http.Transport$' + - '^net/url.URL$' + - '^os/exec.Cmd$' + - '^reflect.StructField$' + # public libs + - '^github.com/Shopify/sarama.Config$' + - '^github.com/Shopify/sarama.ProducerMessage$' + - '^github.com/mitchellh/mapstructure.DecoderConfig$' + - '^github.com/prometheus/client_golang/.+Opts$' + - '^github.com/spf13/cobra.Command$' + - '^github.com/spf13/cobra.CompletionOptions$' + - '^github.com/stretchr/testify/mock.Mock$' + - '^github.com/testcontainers/testcontainers-go.+Request$' + - '^github.com/testcontainers/testcontainers-go.FromDockerfile$' + - '^golang.org/x/tools/go/analysis.Analyzer$' + - '^google.golang.org/protobuf/.+Options$' + - '^gopkg.in/yaml.v3.Node$' + + funlen: + # Checks the number of lines in a function. + # If lower than 0, disable the check. + # Default: 60 + lines: 100 + # Checks the number of statements in a function. + # If lower than 0, disable the check. + # Default: 40 + statements: 50 + # Ignore comments when counting lines. + # Default false + ignore-comments: true + + gocognit: + # Minimal code complexity to report. + # Default: 30 (but we recommend 10-20) + min-complexity: 20 + + gocritic: + # Settings passed to gocritic. + # The settings key is the name of a supported gocritic checker. + # The list of supported checkers can be find in https://go-critic.github.io/overview. + settings: + captLocal: + # Whether to restrict checker to params only. + # Default: true + paramsOnly: false + underef: + # Whether to skip (*x).method() calls where x is a pointer receiver. + # Default: true + skipRecvDeref: false + + gomodguard: + blocked: + # List of blocked modules. + # Default: [] + modules: + - github.com/golang/protobuf: + recommendations: + - google.golang.org/protobuf + reason: 'see https://developers.google.com/protocol-buffers/docs/reference/go/faq#modules' + - github.com/satori/go.uuid: + recommendations: + - github.com/google/uuid + reason: "satori's package is not maintained" + - github.com/gofrs/uuid: + recommendations: + - github.com/gofrs/uuid/v5 + reason: "gofrs' package was not go module before v5" + + govet: + # Enable all analyzers. + # Default: false + enable-all: true + # Disable analyzers by name. + # Run `go tool vet help` to see all analyzers. + # Default: [] + disable: + - fieldalignment # too strict + # Settings per analyzer. + settings: + shadow: + # Whether to be strict about shadowing; can be noisy. + # Default: false + strict: true + + inamedparam: + # Skips check for interface methods with only a single parameter. + # Default: false + skip-single-param: true + + mnd: + # List of function patterns to exclude from analysis. + # Values always ignored: `time.Date`, + # `strconv.FormatInt`, `strconv.FormatUint`, `strconv.FormatFloat`, + # `strconv.ParseInt`, `strconv.ParseUint`, `strconv.ParseFloat`. + # Default: [] + ignored-functions: + - args.Error + - flag.Arg + - flag.Duration.* + - flag.Float.* + - flag.Int.* + - flag.Uint.* + - os.Chmod + - os.Mkdir.* + - os.OpenFile + - os.WriteFile + - prometheus.ExponentialBuckets.* + - prometheus.LinearBuckets + + nakedret: + # Make an issue if func has more lines of code than this setting, and it has naked returns. + # Default: 30 + max-func-lines: 0 + + nolintlint: + # Exclude following linters from requiring an explanation. + # Default: [] + allow-no-explanation: [funlen, gocognit, lll] + # Enable to require an explanation of nonzero length after each nolint directive. + # Default: false + require-explanation: true + # Enable to require nolint directives to mention the specific linter being suppressed. + # Default: false + require-specific: true + + perfsprint: + # Optimizes into strings concatenation. + # Default: true + strconcat: false + + rowserrcheck: + # database/sql is always checked + # Default: [] + packages: + - github.com/jmoiron/sqlx + + sloglint: + # Enforce not using global loggers. + # Values: + # - "": disabled + # - "all": report all global loggers + # - "default": report only the default slog logger + # https://github.com/go-simpler/sloglint?tab=readme-ov-file#no-global + # Default: "" + no-global: 'all' + # Enforce using methods that accept a context. + # Values: + # - "": disabled + # - "all": report all contextless calls + # - "scope": report only if a context exists in the scope of the outermost function + # https://github.com/go-simpler/sloglint?tab=readme-ov-file#context-only + # Default: "" + context: 'scope' + + tenv: + # The option `all` will run against whole test files (`_test.go`) regardless of method/function signatures. + # Otherwise, only methods that take `*testing.T`, `*testing.B`, and `testing.TB` as arguments are checked. + # Default: false + all: true + +linters: + disable-all: true + enable: + ## enabled by default + - errcheck # checking for unchecked errors, these unchecked errors can be critical bugs in some cases + - gosimple # specializes in simplifying a code + - govet # reports suspicious constructs, such as Printf calls whose arguments do not align with the format string + - ineffassign # detects when assignments to existing variables are not used + - staticcheck # is a go vet on steroids, applying a ton of static analysis checks + - typecheck # like the front-end of a Go compiler, parses and type-checks Go code + - unused # checks for unused constants, variables, functions and types + ## disabled by default + - asasalint # checks for pass []any as any in variadic func(...any) + - asciicheck # checks that your code does not contain non-ASCII identifiers + - bidichk # checks for dangerous unicode character sequences + - bodyclose # checks whether HTTP response body is closed successfully + - canonicalheader # checks whether net/http.Header uses canonical header + - cyclop # checks function and package cyclomatic complexity + - dupl # tool for code clone detection + - durationcheck # checks for two durations multiplied together + - errname # checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error + - errorlint # finds code that will cause problems with the error wrapping scheme introduced in Go 1.13 + - exhaustive # checks exhaustiveness of enum switch statements + - fatcontext # detects nested contexts in loops + - forbidigo # forbids identifiers + - funlen # tool for detection of long functions + - gocheckcompilerdirectives # validates go compiler directive comments (//go:) + - gochecknoglobals # checks that no global variables exist + - gochecknoinits # checks that no init functions are present in Go code + - gochecksumtype # checks exhaustiveness on Go "sum types" + - gocognit # computes and checks the cognitive complexity of functions + - goconst # finds repeated strings that could be replaced by a constant + - gocritic # provides diagnostics that check for bugs, performance and style issues + - gocyclo # computes and checks the cyclomatic complexity of functions + - godot # checks if comments end in a period + - goimports # in addition to fixing imports, goimports also formats your code in the same style as gofmt + - gomoddirectives # manages the use of 'replace', 'retract', and 'excludes' directives in go.mod + - gomodguard # allow and block lists linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations + - goprintffuncname # checks that printf-like functions are named with f at the end + - gosec # inspects source code for security problems + - intrange # finds places where for loops could make use of an integer range + - lll # reports long lines + - loggercheck # checks key value pairs for common logger libraries (kitlog,klog,logr,zap) + - makezero # finds slice declarations with non-zero initial length + - mirror # reports wrong mirror patterns of bytes/strings usage + - mnd # detects magic numbers + - musttag # enforces field tags in (un)marshaled structs + - nakedret # finds naked returns in functions greater than a specified function length + - nestif # reports deeply nested if statements + - nilerr # finds the code that returns nil even if it checks that the error is not nil + - nilnil # checks that there is no simultaneous return of nil error and an invalid value + - noctx # finds sending http request without context.Context + - nolintlint # reports ill-formed or insufficient nolint directives + - nonamedreturns # reports all named returns + - nosprintfhostport # checks for misuse of Sprintf to construct a host with port in a URL + - perfsprint # checks that fmt.Sprintf can be replaced with a faster alternative + - predeclared # finds code that shadows one of Go's predeclared identifiers + - promlinter # checks Prometheus metrics naming via promlint + - protogetter # reports direct reads from proto message fields when getters should be used + - reassign # checks that package variables are not reassigned + - revive # fast, configurable, extensible, flexible, and beautiful linter for Go, drop-in replacement of golint + - rowserrcheck # checks whether Err of rows is checked successfully + - sloglint # ensure consistent code style when using log/slog + - spancheck # checks for mistakes with OpenTelemetry/Census spans + - sqlclosecheck # checks that sql.Rows and sql.Stmt are closed + - stylecheck # is a replacement for golint + - tenv # detects using os.Setenv instead of t.Setenv since Go1.17 + - testableexamples # checks if examples are testable (have an expected output) + - testifylint # checks usage of github.com/stretchr/testify + - testpackage # makes you use a separate _test package + - tparallel # detects inappropriate usage of t.Parallel() method in your Go test codes + - unconvert # removes unnecessary type conversions + - unparam # reports unused function parameters + - usestdlibvars # detects the possibility to use variables/constants from the Go standard library + - wastedassign # finds wasted assignment statements + - whitespace # detects leading and trailing whitespace + + ## you may want to enable + #- decorder # checks declaration order and count of types, constants, variables and functions + #- exhaustruct # [highly recommend to enable] checks if all structure fields are initialized + #- gci # controls golang package import order and makes it always deterministic + #- ginkgolinter # [if you use ginkgo/gomega] enforces standards of using ginkgo and gomega + #- godox # detects FIXME, TODO and other comment keywords + #- goheader # checks is file header matches to pattern + #- inamedparam # [great idea, but too strict, need to ignore a lot of cases by default] reports interfaces with unnamed method parameters + #- interfacebloat # checks the number of methods inside an interface + #- ireturn # accept interfaces, return concrete types + #- prealloc # [premature optimization, but can be used in some cases] finds slice declarations that could potentially be preallocated + #- tagalign # checks that struct tags are well aligned + #- varnamelen # [great idea, but too many false positives] checks that the length of a variable's name matches its scope + #- wrapcheck # checks that errors returned from external packages are wrapped + #- zerologlint # detects the wrong usage of zerolog that a user forgets to dispatch zerolog.Event + + ## disabled + #- containedctx # detects struct contained context.Context field + #- contextcheck # [too many false positives] checks the function whether use a non-inherited context + #- copyloopvar # [not necessary from Go 1.22] detects places where loop variables are copied + #- depguard # [replaced by gomodguard] checks if package imports are in a list of acceptable packages + #- dogsled # checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) + #- dupword # [useless without config] checks for duplicate words in the source code + #- err113 # [too strict] checks the errors handling expressions + #- errchkjson # [don't see profit + I'm against of omitting errors like in the first example https://github.com/breml/errchkjson] checks types passed to the json encoding functions. Reports unsupported types and optionally reports occasions, where the check for the returned error can be omitted + #- execinquery # [deprecated] checks query string in Query function which reads your Go src files and warning it finds + #- exportloopref # [not necessary from Go 1.22] checks for pointers to enclosing loop variables + #- forcetypeassert # [replaced by errcheck] finds forced type assertions + #- gofmt # [replaced by goimports] checks whether code was gofmt-ed + #- gofumpt # [replaced by goimports, gofumports is not available yet] checks whether code was gofumpt-ed + #- gosmopolitan # reports certain i18n/l10n anti-patterns in your Go codebase + #- grouper # analyzes expression groups + #- importas # enforces consistent import aliases + #- maintidx # measures the maintainability index of each function + #- misspell # [useless] finds commonly misspelled English words in comments + #- nlreturn # [too strict and mostly code is not more readable] checks for a new line before return and branch statements to increase code clarity + #- paralleltest # [too many false positives] detects missing usage of t.Parallel() method in your Go test + #- tagliatelle # checks the struct tags + #- thelper # detects golang test helpers without t.Helper() call and checks the consistency of test helpers + #- wsl # [too strict and mostly code is not more readable] whitespace linter forces you to use empty lines + +issues: + # Maximum count of issues with the same text. + # Set to 0 to disable. + # Default: 3 + max-same-issues: 50 + + exclude-rules: + - source: '(noinspection|TODO)' + linters: [godot] + - source: '//noinspection' + linters: [gocritic] + - path: "_test\\.go" + linters: + - bodyclose + - dupl + - funlen + - goconst + - gosec + - noctx + - wrapcheck From 24f601c72b6d2094b0d79236ada41ebbc4cdc55f Mon Sep 17 00:00:00 2001 From: vhespanha Date: Thu, 15 Aug 2024 15:41:52 -0300 Subject: [PATCH 03/81] Better golines command on makefile --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 7ad25e1..7ed05e7 100644 --- a/Makefile +++ b/Makefile @@ -71,7 +71,7 @@ lint: format: @echo "Formatting code..." @gofumpt -l -w . - @goimports -w . + @golines -l -m 120 -t 4 -w . @golines -w . echo "Code formatted."; \ From 2bff0adbda66177278797143c09708b06accd467 Mon Sep 17 00:00:00 2001 From: vhespanha Date: Thu, 15 Aug 2024 15:43:26 -0300 Subject: [PATCH 04/81] Apply new formatting rules --- cmd/list.go | 22 +++++++++++++++++----- pkg/models/task.go | 2 +- pkg/utils/helpers.go | 1 - 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/cmd/list.go b/cmd/list.go index 979d199..5b1153d 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -207,10 +207,16 @@ func printTaskTable(repo *repository.Repository, tasks []*models.Task) { table.Render() } -func printProjectTree(repo *repository.Repository, projects []*models.Project, parentID *int, level int) { +func printProjectTree( + repo *repository.Repository, + projects []*models.Project, + parentID *int, + level int, +) { indent := strings.Repeat("│ ", level) for i, project := range projects { - if (parentID == nil && project.ParentProjectId == nil) || (parentID != nil && project.ParentProjectId != nil && *project.ParentProjectId == *parentID) { + if (parentID == nil && project.ParentProjectId == nil) || + (parentID != nil && project.ParentProjectId != nil && *project.ParentProjectId == *parentID) { prefix := "├──" if i == len(projects)-1 { prefix = "└──" @@ -224,15 +230,21 @@ func printProjectTree(repo *repository.Repository, projects []*models.Project, p func printTaskTree(repo *repository.Repository, tasks []*models.Task, parentID *int, level int) { indent := strings.Repeat("│ ", level) for i, task := range tasks { - if (parentID == nil && task.ParentTaskId == nil) || (parentID != nil && task.ParentTaskId != nil && *task.ParentTaskId == *parentID) { + if (parentID == nil && task.ParentTaskId == nil) || + (parentID != nil && task.ParentTaskId != nil && *task.ParentTaskId == *parentID) { prefix := "├──" if i == len(tasks)-1 { prefix = "└──" } fmt.Printf("%s%s %s (ID: %d)\n", indent, prefix, task.Name, task.ID) fmt.Printf("%s Description: %s\n", indent, task.Description) - fmt.Printf("%s Due Date: %s, Completed: %v, Priority: %s\n", - indent, utils.FormatDate(task.DueDate), task.TaskCompleted, utils.GetPriorityString(task.Priority)) + fmt.Printf( + "%s Due Date: %s, Completed: %v, Priority: %s\n", + indent, + utils.FormatDate(task.DueDate), + task.TaskCompleted, + utils.GetPriorityString(task.Priority), + ) printTaskTree(repo, tasks, &task.ID, level+1) } } diff --git a/pkg/models/task.go b/pkg/models/task.go index a634515..7b5e640 100644 --- a/pkg/models/task.go +++ b/pkg/models/task.go @@ -13,5 +13,5 @@ type Task struct { CreationDate time.Time LastUpdatedDate time.Time Priority int - ParentTaskId *int + ParentTaskId *int } diff --git a/pkg/utils/helpers.go b/pkg/utils/helpers.go index c74cd1b..7be6bc4 100644 --- a/pkg/utils/helpers.go +++ b/pkg/utils/helpers.go @@ -67,7 +67,6 @@ func FormatDate(t *time.Time) string { return t.Format("2006-01-02 15:04") } - func ColoredPastDue(dueDate *time.Time, completed bool) string { if dueDate == nil { return color.GreenString("no") From 6a08aba1e3167416cc2f1ad36362245469f328f6 Mon Sep 17 00:00:00 2001 From: vhespanha Date: Thu, 15 Aug 2024 15:53:54 -0300 Subject: [PATCH 05/81] Add error checking for shell completion functions --- cmd/completion.go | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/cmd/completion.go b/cmd/completion.go index aab9e71..6bea6af 100644 --- a/cmd/completion.go +++ b/cmd/completion.go @@ -1,6 +1,7 @@ package cmd import ( + "fmt" "os" "github.com/spf13/cobra" @@ -48,19 +49,33 @@ PowerShell: PS> clido completion powershell > clido.ps1 # and source this file from your PowerShell profile. `, + DisableFlagsInUseLine: true, ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), + Run: func(cmd *cobra.Command, args []string) { switch args[0] { + case "bash": - cmd.Root().GenBashCompletion(os.Stdout) + if err := cmd.Root().GenBashCompletion(os.Stdout); err != nil { + panic(fmt.Errorf("Error generating bash completion: %w", err)) + } + case "zsh": - cmd.Root().GenZshCompletion(os.Stdout) + if err := cmd.Root().GenZshCompletion(os.Stdout); err != nil { + panic(fmt.Errorf("Error generating ZSH completion: %w", err)) + } + case "fish": - cmd.Root().GenFishCompletion(os.Stdout, true) + if err := cmd.Root().GenFishCompletion(os.Stdout, true); err != nil { + panic(fmt.Errorf("Error generating Fish completion: %w", err)) + } + case "powershell": - cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout) + if err := cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout); err != nil { + panic(fmt.Errorf("Error generating PowerShell completion: %w", err)) + } } }, } From ae1062b694aa9fd507f0f726694bc20210484f72 Mon Sep 17 00:00:00 2001 From: vhespanha Date: Thu, 15 Aug 2024 15:58:40 -0300 Subject: [PATCH 06/81] Remove global variables from verioning system --- Makefile | 2 +- internal/version/version.go | 18 ++++++++++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/Makefile b/Makefile index 7ed05e7..0f1943b 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ GIT_COMMIT=$(shell git rev-parse HEAD) BUILD_DATE=$(shell date -u +"%Y-%m-%dT%H:%M:%SZ") # Linker flags -LDFLAGS=-ldflags "-X $(PACKAGE)/internal/version.GitCommit=$(GIT_COMMIT) -X $(PACKAGE)/internal/version.BuildDate=$(BUILD_DATE)" +LDFLAGS=-ldflags "-X $(PACKAGE)/internal/version.version=$(VERSION) -X $(PACKAGE)/internal/version.buildDate=$(BUILD_DATE) -X $(PACKAGE)/internal/version.gitCommit=$(GIT_COMMIT)" # Platforms to build for PLATFORMS=windows/amd64 darwin/amd64 darwin/arm64 linux/amd64 linux/arm64 diff --git a/internal/version/version.go b/internal/version/version.go index 3e63615..46622c7 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -6,19 +6,21 @@ import ( ) var ( - Version = "1.1.0" - - BuildDate = "unknown" - - GitCommit = "unknown" + version = "1.1.2" + buildDate = "unknown" + gitCommit = "unknown" ) +func Version() string { return version } +func BuildDate() string { return buildDate } +func GitCommit() string { return gitCommit } + func FullVersion() string { return fmt.Sprintf( "Clido version %s\nBuild date: %s\nGit commit: %s\nGo version: %s\nOS/Arch: %s/%s", - Version, - BuildDate, - GitCommit, + Version(), + BuildDate(), + GitCommit(), runtime.Version(), runtime.GOOS, runtime.GOARCH, From 6b1a64cea7bf59e77ebe6f4e080437aa575cba03 Mon Sep 17 00:00:00 2001 From: vhespanha Date: Thu, 15 Aug 2024 16:09:42 -0300 Subject: [PATCH 07/81] Rename ID vars for consistent naming --- cmd/edit.go | 6 +++--- cmd/list.go | 16 ++++++++-------- cmd/new.go | 4 ++-- pkg/models/project.go | 2 +- pkg/models/task.go | 2 +- pkg/repository/project_repo.go | 24 ++++++++++++------------ pkg/repository/repository.go | 6 +++--- pkg/repository/task_repo.go | 24 ++++++++++++------------ 8 files changed, 42 insertions(+), 42 deletions(-) diff --git a/cmd/edit.go b/cmd/edit.go index e3a95a3..bddbea7 100644 --- a/cmd/edit.go +++ b/cmd/edit.go @@ -76,14 +76,14 @@ func editProject(cmd *cobra.Command, repo *repository.Repository, id int) { if parentProjectIdentifier != "" { if utils.IsNumeric(parentProjectIdentifier) { parentID, _ := strconv.Atoi(parentProjectIdentifier) - project.ParentProjectId = &parentID + project.ParentProjectID = &parentID } else { parentProject, err := repo.GetProjectByName(parentProjectIdentifier) if err != nil || parentProject == nil { fmt.Printf("Parent project '%s' not found.\n", parentProjectIdentifier) return } - project.ParentProjectId = &parentProject.ID + project.ParentProjectID = &parentProject.ID } } @@ -133,7 +133,7 @@ func editTask(cmd *cobra.Command, repo *repository.Repository, id int) { if parentTaskIdentifier != "" { if utils.IsNumeric(parentTaskIdentifier) { parentID, _ := strconv.Atoi(parentTaskIdentifier) - task.ParentTaskId = &parentID + task.ParentTaskID = &parentID } else { fmt.Println("Parent task must be identified by a numeric ID.") return diff --git a/cmd/list.go b/cmd/list.go index 5b1153d..17297a3 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -135,9 +135,9 @@ func printProjectTable(repo *repository.Repository, projects []*models.Project) for _, project := range projects { typeField := "Parent" parentChildField := "None" - if project.ParentProjectId != nil { + if project.ParentProjectID != nil { typeField = "Child" - parentProject, _ := repo.GetProjectByID(*project.ParentProjectId) + parentProject, _ := repo.GetProjectByID(*project.ParentProjectID) if parentProject != nil { parentChildField = parentProject.Name } @@ -171,9 +171,9 @@ func printTaskTable(repo *repository.Repository, tasks []*models.Task) { for _, task := range tasks { typeField := "Parent" parentChildField := "None" - if task.ParentTaskId != nil { + if task.ParentTaskID != nil { typeField = "Child" - parentTask, _ := repo.GetTaskByID(*task.ParentTaskId) + parentTask, _ := repo.GetTaskByID(*task.ParentTaskID) if parentTask != nil { parentChildField = parentTask.Name } @@ -215,8 +215,8 @@ func printProjectTree( ) { indent := strings.Repeat("│ ", level) for i, project := range projects { - if (parentID == nil && project.ParentProjectId == nil) || - (parentID != nil && project.ParentProjectId != nil && *project.ParentProjectId == *parentID) { + if (parentID == nil && project.ParentProjectID == nil) || + (parentID != nil && project.ParentProjectID != nil && *project.ParentProjectID == *parentID) { prefix := "├──" if i == len(projects)-1 { prefix = "└──" @@ -230,8 +230,8 @@ func printProjectTree( func printTaskTree(repo *repository.Repository, tasks []*models.Task, parentID *int, level int) { indent := strings.Repeat("│ ", level) for i, task := range tasks { - if (parentID == nil && task.ParentTaskId == nil) || - (parentID != nil && task.ParentTaskId != nil && *task.ParentTaskId == *parentID) { + if (parentID == nil && task.ParentTaskID == nil) || + (parentID != nil && task.ParentTaskID != nil && *task.ParentTaskID == *parentID) { prefix := "├──" if i == len(tasks)-1 { prefix = "└──" diff --git a/cmd/new.go b/cmd/new.go index 9a39cf9..1281fcd 100644 --- a/cmd/new.go +++ b/cmd/new.go @@ -79,7 +79,7 @@ func createProject(cmd *cobra.Command, repo *repository.Repository) { project := &models.Project{ Name: name, Description: description, - ParentProjectId: parentProjectID, + ParentProjectID: parentProjectID, } err := repo.CreateProject(project) @@ -150,7 +150,7 @@ func createTask(cmd *cobra.Command, repo *repository.Repository) { ProjectID: projectID, DueDate: dueDate, Priority: priority, - ParentTaskId: parentTaskID, + ParentTaskID: parentTaskID, } err := repo.CreateTask(task) diff --git a/pkg/models/project.go b/pkg/models/project.go index 8d00b34..b7e91f9 100644 --- a/pkg/models/project.go +++ b/pkg/models/project.go @@ -8,5 +8,5 @@ type Project struct { Description string CreationDate time.Time LastModifiedDate time.Time - ParentProjectId *int + ParentProjectID *int } diff --git a/pkg/models/task.go b/pkg/models/task.go index 7b5e640..bbdb97a 100644 --- a/pkg/models/task.go +++ b/pkg/models/task.go @@ -13,5 +13,5 @@ type Task struct { CreationDate time.Time LastUpdatedDate time.Time Priority int - ParentTaskId *int + ParentTaskID *int } diff --git a/pkg/repository/project_repo.go b/pkg/repository/project_repo.go index 11d5e10..5be70bd 100644 --- a/pkg/repository/project_repo.go +++ b/pkg/repository/project_repo.go @@ -23,13 +23,13 @@ func (r *Repository) CreateProject(project *models.Project) error { // Insert the project with the found ID _, err = r.db.Exec( - `INSERT INTO Projects (ID, Name, Description, CreationDate, LastModifiedDate, ParentProjectId) VALUES (?, ?, ?, ?, ?, ?)`, + `INSERT INTO Projects (ID, Name, Description, CreationDate, LastModifiedDate, ParentProjectID) VALUES (?, ?, ?, ?, ?, ?)`, id, project.Name, project.Description, project.CreationDate, project.LastModifiedDate, - project.ParentProjectId, + project.ParentProjectID, ) if err != nil { return err @@ -41,8 +41,8 @@ func (r *Repository) CreateProject(project *models.Project) error { func (r *Repository) GetProjectByID(id int) (*models.Project, error) { project := &models.Project{} - err := r.db.QueryRow(`SELECT ID, Name, Description, CreationDate, LastModifiedDate, ParentProjectId FROM Projects WHERE ID = ?`, id). - Scan(&project.ID, &project.Name, &project.Description, &project.CreationDate, &project.LastModifiedDate, &project.ParentProjectId) + err := r.db.QueryRow(`SELECT ID, Name, Description, CreationDate, LastModifiedDate, ParentProjectID FROM Projects WHERE ID = ?`, id). + Scan(&project.ID, &project.Name, &project.Description, &project.CreationDate, &project.LastModifiedDate, &project.ParentProjectID) if err != nil { return nil, err } @@ -51,8 +51,8 @@ func (r *Repository) GetProjectByID(id int) (*models.Project, error) { func (r *Repository) GetProjectByName(name string) (*models.Project, error) { project := &models.Project{} - err := r.db.QueryRow(`SELECT ID, Name, Description, CreationDate, LastModifiedDate, ParentProjectId FROM Projects WHERE Name = ?`, name). - Scan(&project.ID, &project.Name, &project.Description, &project.CreationDate, &project.LastModifiedDate, &project.ParentProjectId) + err := r.db.QueryRow(`SELECT ID, Name, Description, CreationDate, LastModifiedDate, ParentProjectID FROM Projects WHERE Name = ?`, name). + Scan(&project.ID, &project.Name, &project.Description, &project.CreationDate, &project.LastModifiedDate, &project.ParentProjectID) if err != nil { return nil, err } @@ -61,7 +61,7 @@ func (r *Repository) GetProjectByName(name string) (*models.Project, error) { func (r *Repository) GetAllProjects() ([]*models.Project, error) { rows, err := r.db.Query( - `SELECT ID, Name, Description, CreationDate, LastModifiedDate, ParentProjectId FROM Projects`, + `SELECT ID, Name, Description, CreationDate, LastModifiedDate, ParentProjectID FROM Projects`, ) if err != nil { return nil, err @@ -77,7 +77,7 @@ func (r *Repository) GetAllProjects() ([]*models.Project, error) { &project.Description, &project.CreationDate, &project.LastModifiedDate, - &project.ParentProjectId, + &project.ParentProjectID, ) if err != nil { return nil, err @@ -89,7 +89,7 @@ func (r *Repository) GetAllProjects() ([]*models.Project, error) { func (r *Repository) GetSubprojects(parentProjectID int) ([]*models.Project, error) { rows, err := r.db.Query( - `SELECT ID, Name, Description, CreationDate, LastModifiedDate, ParentProjectId FROM Projects WHERE ParentProjectId = ?`, + `SELECT ID, Name, Description, CreationDate, LastModifiedDate, ParentProjectID FROM Projects WHERE ParentProjectID = ?`, parentProjectID, ) if err != nil { @@ -106,7 +106,7 @@ func (r *Repository) GetSubprojects(parentProjectID int) ([]*models.Project, err &project.Description, &project.CreationDate, &project.LastModifiedDate, - &project.ParentProjectId, + &project.ParentProjectID, ) if err != nil { return nil, err @@ -119,11 +119,11 @@ func (r *Repository) GetSubprojects(parentProjectID int) ([]*models.Project, err func (r *Repository) UpdateProject(project *models.Project) error { project.LastModifiedDate = time.Now() _, err := r.db.Exec( - `UPDATE Projects SET Name = ?, Description = ?, LastModifiedDate = ?, ParentProjectId = ? WHERE ID = ?`, + `UPDATE Projects SET Name = ?, Description = ?, LastModifiedDate = ?, ParentProjectID = ? WHERE ID = ?`, project.Name, project.Description, project.LastModifiedDate, - project.ParentProjectId, + project.ParentProjectID, project.ID, ) return err diff --git a/pkg/repository/repository.go b/pkg/repository/repository.go index a0228cf..e5c3745 100644 --- a/pkg/repository/repository.go +++ b/pkg/repository/repository.go @@ -58,7 +58,7 @@ func (r *Repository) init() error { Description TEXT, CreationDate DATETIME NOT NULL, LastModifiedDate DATETIME NOT NULL, - ParentProjectId INTEGER, + ParentProjectID INTEGER, FOREIGN KEY (ParentProjectID) REFERENCES Projects(ID) );` @@ -74,8 +74,8 @@ func (r *Repository) init() error { CreationDate DATETIME NOT NULL, LastUpdatedDate DATETIME NOT NULL, Priority INTEGER NOT NULL DEFAULT 4, - ParentTaskId INTEGER, - FOREIGN KEY (ParentTaskId) REFERENCES Tasks(ID), + ParentTaskID INTEGER, + FOREIGN KEY (ParentTaskID) REFERENCES Tasks(ID), FOREIGN KEY (ProjectID) REFERENCES Projects(ID) );` diff --git a/pkg/repository/task_repo.go b/pkg/repository/task_repo.go index f50e57b..c277d07 100644 --- a/pkg/repository/task_repo.go +++ b/pkg/repository/task_repo.go @@ -23,7 +23,7 @@ func (r *Repository) CreateTask(task *models.Task) error { // Insert the task with the found ID _, err = r.db.Exec( - `INSERT INTO Tasks (ID, Name, Description, ProjectID, TaskCompleted, DueDate, CompletionDate, CreationDate, LastUpdatedDate, Priority, ParentTaskId) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + `INSERT INTO Tasks (ID, Name, Description, ProjectID, TaskCompleted, DueDate, CompletionDate, CreationDate, LastUpdatedDate, Priority, ParentTaskID) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, id, task.Name, task.Description, @@ -34,7 +34,7 @@ func (r *Repository) CreateTask(task *models.Task) error { task.CreationDate, task.LastUpdatedDate, task.Priority, - task.ParentTaskId, + task.ParentTaskID, ) if err != nil { return err @@ -46,8 +46,8 @@ func (r *Repository) CreateTask(task *models.Task) error { func (r *Repository) GetTaskByID(id int) (*models.Task, error) { task := &models.Task{} - err := r.db.QueryRow(`SELECT ID, Name, Description, ProjectID, TaskCompleted, DueDate, CompletionDate, CreationDate, LastUpdatedDate, Priority, ParentTaskId FROM Tasks WHERE ID = ?`, id). - Scan(&task.ID, &task.Name, &task.Description, &task.ProjectID, &task.TaskCompleted, &task.DueDate, &task.CompletionDate, &task.CreationDate, &task.LastUpdatedDate, &task.Priority, &task.ParentTaskId) + err := r.db.QueryRow(`SELECT ID, Name, Description, ProjectID, TaskCompleted, DueDate, CompletionDate, CreationDate, LastUpdatedDate, Priority, ParentTaskID FROM Tasks WHERE ID = ?`, id). + Scan(&task.ID, &task.Name, &task.Description, &task.ProjectID, &task.TaskCompleted, &task.DueDate, &task.CompletionDate, &task.CreationDate, &task.LastUpdatedDate, &task.Priority, &task.ParentTaskID) if err != nil { return nil, err } @@ -56,7 +56,7 @@ func (r *Repository) GetTaskByID(id int) (*models.Task, error) { func (r *Repository) GetAllTasks() ([]*models.Task, error) { rows, err := r.db.Query( - `SELECT ID, Name, Description, ProjectID, TaskCompleted, DueDate, CompletionDate, CreationDate, LastUpdatedDate, Priority, ParentTaskId FROM Tasks`, + `SELECT ID, Name, Description, ProjectID, TaskCompleted, DueDate, CompletionDate, CreationDate, LastUpdatedDate, Priority, ParentTaskID FROM Tasks`, ) if err != nil { return nil, err @@ -77,7 +77,7 @@ func (r *Repository) GetAllTasks() ([]*models.Task, error) { &task.CreationDate, &task.LastUpdatedDate, &task.Priority, - &task.ParentTaskId, + &task.ParentTaskID, ) if err != nil { return nil, err @@ -89,7 +89,7 @@ func (r *Repository) GetAllTasks() ([]*models.Task, error) { func (r *Repository) GetTasksByProjectID(projectID int) ([]*models.Task, error) { rows, err := r.db.Query( - `SELECT ID, Name, Description, ProjectID, TaskCompleted, DueDate, CompletionDate, CreationDate, LastUpdatedDate, Priority, ParentTaskId FROM Tasks WHERE ProjectID = ?`, + `SELECT ID, Name, Description, ProjectID, TaskCompleted, DueDate, CompletionDate, CreationDate, LastUpdatedDate, Priority, ParentTaskID FROM Tasks WHERE ProjectID = ?`, projectID, ) if err != nil { @@ -111,7 +111,7 @@ func (r *Repository) GetTasksByProjectID(projectID int) ([]*models.Task, error) &task.CreationDate, &task.LastUpdatedDate, &task.Priority, - &task.ParentTaskId, + &task.ParentTaskID, ) if err != nil { return nil, err @@ -123,7 +123,7 @@ func (r *Repository) GetTasksByProjectID(projectID int) ([]*models.Task, error) func (r *Repository) GetSubtasks(parentTaskID int) ([]*models.Task, error) { rows, err := r.db.Query( - `SELECT ID, Name, Description, ProjectID, TaskCompleted, DueDate, CompletionDate, CreationDate, LastUpdatedDate, Priority, ParentTaskId FROM Tasks WHERE ParentTaskId = ?`, + `SELECT ID, Name, Description, ProjectID, TaskCompleted, DueDate, CompletionDate, CreationDate, LastUpdatedDate, Priority, ParentTaskID FROM Tasks WHERE ParentTaskID = ?`, parentTaskID, ) if err != nil { @@ -145,7 +145,7 @@ func (r *Repository) GetSubtasks(parentTaskID int) ([]*models.Task, error) { &task.CreationDate, &task.LastUpdatedDate, &task.Priority, - &task.ParentTaskId, + &task.ParentTaskID, ) if err != nil { return nil, err @@ -158,7 +158,7 @@ func (r *Repository) GetSubtasks(parentTaskID int) ([]*models.Task, error) { func (r *Repository) UpdateTask(task *models.Task) error { task.LastUpdatedDate = time.Now() _, err := r.db.Exec( - `UPDATE Tasks SET Name = ?, Description = ?, ProjectID = ?, TaskCompleted = ?, DueDate = ?, CompletionDate = ?, LastUpdatedDate = ?, Priority = ?, ParentTaskId = ? WHERE ID = ?`, + `UPDATE Tasks SET Name = ?, Description = ?, ProjectID = ?, TaskCompleted = ?, DueDate = ?, CompletionDate = ?, LastUpdatedDate = ?, Priority = ?, ParentTaskID = ? WHERE ID = ?`, task.Name, task.Description, task.ProjectID, @@ -167,7 +167,7 @@ func (r *Repository) UpdateTask(task *models.Task) error { task.CompletionDate, task.LastUpdatedDate, task.Priority, - task.ParentTaskId, + task.ParentTaskID, task.ID, ) return err From b8bfda3bd27b16d0643712b7b77372f04b62d5dc Mon Sep 17 00:00:00 2001 From: vhespanha Date: Thu, 15 Aug 2024 16:25:38 -0300 Subject: [PATCH 08/81] Change priority type to enable a clearer switch statement --- cmd/edit.go | 2 +- cmd/new.go | 4 ++-- pkg/models/task.go | 8 ++++++-- pkg/utils/helpers.go | 17 +++++++++++++---- 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/cmd/edit.go b/cmd/edit.go index bddbea7..596e9f9 100644 --- a/cmd/edit.go +++ b/cmd/edit.go @@ -125,7 +125,7 @@ func editTask(cmd *cobra.Command, repo *repository.Repository, id int) { } if priority != 0 { if priority >= 1 && priority <= 4 { - task.Priority = priority + task.Priority = utils.Priority(priority) } else { fmt.Println("Invalid priority. Keeping the existing priority.") } diff --git a/cmd/new.go b/cmd/new.go index 1281fcd..e055d8f 100644 --- a/cmd/new.go +++ b/cmd/new.go @@ -149,7 +149,7 @@ func createTask(cmd *cobra.Command, repo *repository.Repository) { Description: description, ProjectID: projectID, DueDate: dueDate, - Priority: priority, + Priority: utils.Priority(priority), ParentTaskID: parentTaskID, } @@ -162,6 +162,6 @@ func createTask(cmd *cobra.Command, repo *repository.Repository) { fmt.Printf( "Task '%s' created successfully with priority %s.\n", name, - utils.GetPriorityString(priority), + utils.GetPriorityString(utils.Priority(priority)), ) } diff --git a/pkg/models/task.go b/pkg/models/task.go index bbdb97a..5c673f1 100644 --- a/pkg/models/task.go +++ b/pkg/models/task.go @@ -1,6 +1,10 @@ package models -import "time" +import ( + "time" + + "github.com/d4r1us-drk/clido/pkg/utils" +) type Task struct { ID int @@ -12,6 +16,6 @@ type Task struct { CompletionDate *time.Time CreationDate time.Time LastUpdatedDate time.Time - Priority int + Priority utils.Priority ParentTaskID *int } diff --git a/pkg/utils/helpers.go b/pkg/utils/helpers.go index 7be6bc4..e8220eb 100644 --- a/pkg/utils/helpers.go +++ b/pkg/utils/helpers.go @@ -47,13 +47,22 @@ func WrapText(text string, maxLength int) string { return result } -func GetPriorityString(priority int) string { +type Priority int + +const ( + PriorityHigh Priority = 1 + PriorityMedium Priority = 2 + PriorityLow Priority = 3 + PriorityNone Priority = 0 +) + +func GetPriorityString(priority Priority) string { switch priority { - case 1: + case PriorityHigh: return "High" - case 2: + case PriorityMedium: return "Medium" - case 3: + case PriorityLow: return "Low" default: return "None" From 47e8ce479979177b3abe6ffc2c302e1fabe0fd6f Mon Sep 17 00:00:00 2001 From: vhespanha Date: Thu, 15 Aug 2024 16:28:22 -0300 Subject: [PATCH 09/81] Remove global variables from version package (now for real) --- Makefile | 2 +- internal/version/version.go | 27 ++++++++++++++++----------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/Makefile b/Makefile index 0f1943b..8b8bb6e 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ GIT_COMMIT=$(shell git rev-parse HEAD) BUILD_DATE=$(shell date -u +"%Y-%m-%dT%H:%M:%SZ") # Linker flags -LDFLAGS=-ldflags "-X $(PACKAGE)/internal/version.version=$(VERSION) -X $(PACKAGE)/internal/version.buildDate=$(BUILD_DATE) -X $(PACKAGE)/internal/version.gitCommit=$(GIT_COMMIT)" +LDFLAGS=-ldflags "-X '$(PACKAGE)/internal/version.Get().Version=$(VERSION)' -X '$(PACKAGE)/internal/version.Get().BuildDate=$(BUILD_DATE)' -X '$(PACKAGE)/internal/version.Get().GitCommit=$(GIT_COMMIT)'" # Platforms to build for PLATFORMS=windows/amd64 darwin/amd64 darwin/arm64 linux/amd64 linux/arm64 diff --git a/internal/version/version.go b/internal/version/version.go index 46622c7..9686fd3 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -5,22 +5,27 @@ import ( "runtime" ) -var ( - version = "1.1.2" - buildDate = "unknown" - gitCommit = "unknown" -) +type Info struct { + Version string + BuildDate string + GitCommit string +} -func Version() string { return version } -func BuildDate() string { return buildDate } -func GitCommit() string { return gitCommit } +func Get() Info { + return Info{ + Version: "dev", + BuildDate: "unknown", + GitCommit: "unknown", + } +} func FullVersion() string { + info := Get() return fmt.Sprintf( "Clido version %s\nBuild date: %s\nGit commit: %s\nGo version: %s\nOS/Arch: %s/%s", - Version(), - BuildDate(), - GitCommit(), + info.Version, + info.BuildDate, + info.GitCommit, runtime.Version(), runtime.GOOS, runtime.GOARCH, From dea06b0924153351aebcd9aa114a6c07a5418cdf Mon Sep 17 00:00:00 2001 From: vhespanha Date: Thu, 15 Aug 2024 16:36:33 -0300 Subject: [PATCH 10/81] Split scan parameters for better readability --- pkg/repository/project_repo.go | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/pkg/repository/project_repo.go b/pkg/repository/project_repo.go index 5be70bd..f57ad29 100644 --- a/pkg/repository/project_repo.go +++ b/pkg/repository/project_repo.go @@ -42,7 +42,14 @@ func (r *Repository) CreateProject(project *models.Project) error { func (r *Repository) GetProjectByID(id int) (*models.Project, error) { project := &models.Project{} err := r.db.QueryRow(`SELECT ID, Name, Description, CreationDate, LastModifiedDate, ParentProjectID FROM Projects WHERE ID = ?`, id). - Scan(&project.ID, &project.Name, &project.Description, &project.CreationDate, &project.LastModifiedDate, &project.ParentProjectID) + Scan( + &project.ID, + &project.Name, + &project.Description, + &project.CreationDate, + &project.LastModifiedDate, + &project.ParentProjectID, + ) if err != nil { return nil, err } @@ -52,7 +59,14 @@ func (r *Repository) GetProjectByID(id int) (*models.Project, error) { func (r *Repository) GetProjectByName(name string) (*models.Project, error) { project := &models.Project{} err := r.db.QueryRow(`SELECT ID, Name, Description, CreationDate, LastModifiedDate, ParentProjectID FROM Projects WHERE Name = ?`, name). - Scan(&project.ID, &project.Name, &project.Description, &project.CreationDate, &project.LastModifiedDate, &project.ParentProjectID) + Scan( + &project.ID, + &project.Name, + &project.Description, + &project.CreationDate, + &project.LastModifiedDate, + &project.ParentProjectID, + ) if err != nil { return nil, err } From 1fc7dad656f9e7db6a9e62376af27b8c03c93716 Mon Sep 17 00:00:00 2001 From: vhespanha Date: Thu, 15 Aug 2024 16:37:54 -0300 Subject: [PATCH 11/81] Split (some) sql queries for readability --- pkg/repository/project_repo.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pkg/repository/project_repo.go b/pkg/repository/project_repo.go index f57ad29..5e1bf6c 100644 --- a/pkg/repository/project_repo.go +++ b/pkg/repository/project_repo.go @@ -41,7 +41,10 @@ func (r *Repository) CreateProject(project *models.Project) error { func (r *Repository) GetProjectByID(id int) (*models.Project, error) { project := &models.Project{} - err := r.db.QueryRow(`SELECT ID, Name, Description, CreationDate, LastModifiedDate, ParentProjectID FROM Projects WHERE ID = ?`, id). + err := r.db.QueryRow( + `SELECT ID, Name, Description, CreationDate, LastModifiedDate, ParentProjectID FROM Projects WHERE ID = ?`, + id, + ). Scan( &project.ID, &project.Name, @@ -58,7 +61,10 @@ func (r *Repository) GetProjectByID(id int) (*models.Project, error) { func (r *Repository) GetProjectByName(name string) (*models.Project, error) { project := &models.Project{} - err := r.db.QueryRow(`SELECT ID, Name, Description, CreationDate, LastModifiedDate, ParentProjectID FROM Projects WHERE Name = ?`, name). + err := r.db.QueryRow( + `SELECT ID, Name, Description, CreationDate, LastModifiedDate, ParentProjectID FROM Projects WHERE Name = ?`, + name, + ). Scan( &project.ID, &project.Name, From 9d63610219f182529558e9d65297b08cc4acad59 Mon Sep 17 00:00:00 2001 From: vhespanha Date: Thu, 15 Aug 2024 16:39:37 -0300 Subject: [PATCH 12/81] Split more queries --- pkg/repository/task_repo.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/repository/task_repo.go b/pkg/repository/task_repo.go index c277d07..62b1f6f 100644 --- a/pkg/repository/task_repo.go +++ b/pkg/repository/task_repo.go @@ -46,7 +46,10 @@ func (r *Repository) CreateTask(task *models.Task) error { func (r *Repository) GetTaskByID(id int) (*models.Task, error) { task := &models.Task{} - err := r.db.QueryRow(`SELECT ID, Name, Description, ProjectID, TaskCompleted, DueDate, CompletionDate, CreationDate, LastUpdatedDate, Priority, ParentTaskID FROM Tasks WHERE ID = ?`, id). + err := r.db.QueryRow( + `SELECT ID, Name, Description, ProjectID, TaskCompleted, DueDate, CompletionDate, CreationDate, LastUpdatedDate, Priority, ParentTaskID FROM Tasks WHERE ID = ?`, + id, + ). Scan(&task.ID, &task.Name, &task.Description, &task.ProjectID, &task.TaskCompleted, &task.DueDate, &task.CompletionDate, &task.CreationDate, &task.LastUpdatedDate, &task.Priority, &task.ParentTaskID) if err != nil { return nil, err From d3fea913af49dfbdb991b2ab5401a3de513eba9e Mon Sep 17 00:00:00 2001 From: vhespanha Date: Thu, 15 Aug 2024 16:41:32 -0300 Subject: [PATCH 13/81] Fewer long lines --- pkg/repository/project_repo.go | 6 ++++-- pkg/repository/task_repo.go | 23 +++++++++++++++++++---- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/pkg/repository/project_repo.go b/pkg/repository/project_repo.go index 5e1bf6c..ea1fec8 100644 --- a/pkg/repository/project_repo.go +++ b/pkg/repository/project_repo.go @@ -23,7 +23,8 @@ func (r *Repository) CreateProject(project *models.Project) error { // Insert the project with the found ID _, err = r.db.Exec( - `INSERT INTO Projects (ID, Name, Description, CreationDate, LastModifiedDate, ParentProjectID) VALUES (?, ?, ?, ?, ?, ?)`, + `INSERT INTO Projects (ID, Name, Description, CreationDate, LastModifiedDate, ParentProjectID) + VALUES (?, ?, ?, ?, ?, ?)`, id, project.Name, project.Description, @@ -109,7 +110,8 @@ func (r *Repository) GetAllProjects() ([]*models.Project, error) { func (r *Repository) GetSubprojects(parentProjectID int) ([]*models.Project, error) { rows, err := r.db.Query( - `SELECT ID, Name, Description, CreationDate, LastModifiedDate, ParentProjectID FROM Projects WHERE ParentProjectID = ?`, + `SELECT ID, Name, Description, CreationDate, LastModifiedDate, ParentProjectID FROM Projects WHERE + ParentProjectID = ?`, parentProjectID, ) if err != nil { diff --git a/pkg/repository/task_repo.go b/pkg/repository/task_repo.go index 62b1f6f..b7f3d74 100644 --- a/pkg/repository/task_repo.go +++ b/pkg/repository/task_repo.go @@ -23,7 +23,8 @@ func (r *Repository) CreateTask(task *models.Task) error { // Insert the task with the found ID _, err = r.db.Exec( - `INSERT INTO Tasks (ID, Name, Description, ProjectID, TaskCompleted, DueDate, CompletionDate, CreationDate, LastUpdatedDate, Priority, ParentTaskID) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + `INSERT INTO Tasks (ID, Name, Description, ProjectID, TaskCompleted, DueDate, CompletionDate, CreationDate, + LastUpdatedDate, Priority, ParentTaskID) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, id, task.Name, task.Description, @@ -47,10 +48,23 @@ func (r *Repository) CreateTask(task *models.Task) error { func (r *Repository) GetTaskByID(id int) (*models.Task, error) { task := &models.Task{} err := r.db.QueryRow( - `SELECT ID, Name, Description, ProjectID, TaskCompleted, DueDate, CompletionDate, CreationDate, LastUpdatedDate, Priority, ParentTaskID FROM Tasks WHERE ID = ?`, + `SELECT ID, Name, Description, ProjectID, TaskCompleted, DueDate, CompletionDate, CreationDate, LastUpdatedDate, + Priority, ParentTaskID FROM Tasks WHERE ID = ?`, id, ). - Scan(&task.ID, &task.Name, &task.Description, &task.ProjectID, &task.TaskCompleted, &task.DueDate, &task.CompletionDate, &task.CreationDate, &task.LastUpdatedDate, &task.Priority, &task.ParentTaskID) + Scan( + &task.ID, + &task.Name, + &task.Description, + &task.ProjectID, + &task.TaskCompleted, + &task.DueDate, + &task.CompletionDate, + &task.CreationDate, + &task.LastUpdatedDate, + &task.Priority, + &task.ParentTaskID, + ) if err != nil { return nil, err } @@ -59,7 +73,8 @@ func (r *Repository) GetTaskByID(id int) (*models.Task, error) { func (r *Repository) GetAllTasks() ([]*models.Task, error) { rows, err := r.db.Query( - `SELECT ID, Name, Description, ProjectID, TaskCompleted, DueDate, CompletionDate, CreationDate, LastUpdatedDate, Priority, ParentTaskID FROM Tasks`, + `SELECT ID, Name, Description, ProjectID, TaskCompleted, DueDate, CompletionDate, CreationDate, LastUpdatedDate, + Priority, ParentTaskID FROM Tasks`, ) if err != nil { return nil, err From 7044c6dcc58ee456563fb0eb552ae54580a8aa16 Mon Sep 17 00:00:00 2001 From: vhespanha Date: Thu, 15 Aug 2024 17:15:07 -0300 Subject: [PATCH 14/81] Provide explanation for blank imports --- pkg/repository/repository.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/repository/repository.go b/pkg/repository/repository.go index e5c3745..5b355ca 100644 --- a/pkg/repository/repository.go +++ b/pkg/repository/repository.go @@ -7,7 +7,7 @@ import ( "path/filepath" "runtime" - _ "github.com/mattn/go-sqlite3" + _ "github.com/mattn/go-sqlite3" // SQLite3 driver registration ) type Repository struct { From 0b2324a462977d8464e2a850702ba829e8315f17 Mon Sep 17 00:00:00 2001 From: vhespanha Date: Thu, 15 Aug 2024 17:16:26 -0300 Subject: [PATCH 15/81] Error strings should not be capitalized --- cmd/completion.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/completion.go b/cmd/completion.go index 6bea6af..b4340a6 100644 --- a/cmd/completion.go +++ b/cmd/completion.go @@ -59,22 +59,22 @@ PowerShell: case "bash": if err := cmd.Root().GenBashCompletion(os.Stdout); err != nil { - panic(fmt.Errorf("Error generating bash completion: %w", err)) + panic(fmt.Errorf("error generating bash completion: %w", err)) } case "zsh": if err := cmd.Root().GenZshCompletion(os.Stdout); err != nil { - panic(fmt.Errorf("Error generating ZSH completion: %w", err)) + panic(fmt.Errorf("error generating ZSH completion: %w", err)) } case "fish": if err := cmd.Root().GenFishCompletion(os.Stdout, true); err != nil { - panic(fmt.Errorf("Error generating Fish completion: %w", err)) + panic(fmt.Errorf("error generating Fish completion: %w", err)) } case "powershell": if err := cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout); err != nil { - panic(fmt.Errorf("Error generating PowerShell completion: %w", err)) + panic(fmt.Errorf("error generating PowerShell completion: %w", err)) } } }, From f5fd50a6366dff9d0aaf7da2a50657278dbc679a Mon Sep 17 00:00:00 2001 From: vhespanha Date: Thu, 15 Aug 2024 17:18:37 -0300 Subject: [PATCH 16/81] Remove unused const 'PriorityNone' --- pkg/utils/helpers.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/utils/helpers.go b/pkg/utils/helpers.go index e8220eb..e0c7a74 100644 --- a/pkg/utils/helpers.go +++ b/pkg/utils/helpers.go @@ -53,7 +53,6 @@ const ( PriorityHigh Priority = 1 PriorityMedium Priority = 2 PriorityLow Priority = 3 - PriorityNone Priority = 0 ) func GetPriorityString(priority Priority) string { From f213472351404349c2ea74087ca9b32494cce7b7 Mon Sep 17 00:00:00 2001 From: vhespanha Date: Thu, 15 Aug 2024 17:19:25 -0300 Subject: [PATCH 17/81] Split more queries --- pkg/repository/task_repo.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/repository/task_repo.go b/pkg/repository/task_repo.go index b7f3d74..94e0fea 100644 --- a/pkg/repository/task_repo.go +++ b/pkg/repository/task_repo.go @@ -107,7 +107,8 @@ func (r *Repository) GetAllTasks() ([]*models.Task, error) { func (r *Repository) GetTasksByProjectID(projectID int) ([]*models.Task, error) { rows, err := r.db.Query( - `SELECT ID, Name, Description, ProjectID, TaskCompleted, DueDate, CompletionDate, CreationDate, LastUpdatedDate, Priority, ParentTaskID FROM Tasks WHERE ProjectID = ?`, + `SELECT ID, Name, Description, ProjectID, TaskCompleted, DueDate, CompletionDate, CreationDate, LastUpdatedDate, + Priority, ParentTaskID FROM Tasks WHERE ProjectID = ?`, projectID, ) if err != nil { @@ -141,7 +142,8 @@ func (r *Repository) GetTasksByProjectID(projectID int) ([]*models.Task, error) func (r *Repository) GetSubtasks(parentTaskID int) ([]*models.Task, error) { rows, err := r.db.Query( - `SELECT ID, Name, Description, ProjectID, TaskCompleted, DueDate, CompletionDate, CreationDate, LastUpdatedDate, Priority, ParentTaskID FROM Tasks WHERE ParentTaskID = ?`, + `SELECT ID, Name, Description, ProjectID, TaskCompleted, DueDate, CompletionDate, CreationDate, LastUpdatedDate, + Priority, ParentTaskID FROM Tasks WHERE ParentTaskID = ?`, parentTaskID, ) if err != nil { From 40e1c136253f2a19f2143bb55a42721b3704f0cb Mon Sep 17 00:00:00 2001 From: vhespanha Date: Thu, 15 Aug 2024 17:20:44 -0300 Subject: [PATCH 18/81] . --- pkg/repository/task_repo.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/repository/task_repo.go b/pkg/repository/task_repo.go index 94e0fea..5ebff9b 100644 --- a/pkg/repository/task_repo.go +++ b/pkg/repository/task_repo.go @@ -178,7 +178,8 @@ func (r *Repository) GetSubtasks(parentTaskID int) ([]*models.Task, error) { func (r *Repository) UpdateTask(task *models.Task) error { task.LastUpdatedDate = time.Now() _, err := r.db.Exec( - `UPDATE Tasks SET Name = ?, Description = ?, ProjectID = ?, TaskCompleted = ?, DueDate = ?, CompletionDate = ?, LastUpdatedDate = ?, Priority = ?, ParentTaskID = ? WHERE ID = ?`, + `UPDATE Tasks SET Name = ?, Description = ?, ProjectID = ?, TaskCompleted = ?, DueDate = ?, CompletionDate = ?, + LastUpdatedDate = ?, Priority = ?, ParentTaskID = ? WHERE ID = ?`, task.Name, task.Description, task.ProjectID, From cff4871ad14deadaf674f06c6fc2a3687f433f23 Mon Sep 17 00:00:00 2001 From: vhespanha Date: Thu, 15 Aug 2024 17:21:37 -0300 Subject: [PATCH 19/81] Use correct verb on Errorf --- pkg/repository/repository.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/repository/repository.go b/pkg/repository/repository.go index 5b355ca..0c4c74e 100644 --- a/pkg/repository/repository.go +++ b/pkg/repository/repository.go @@ -33,7 +33,7 @@ func NewRepository() (*Repository, error) { dbDir := filepath.Dir(dbPath) if err := os.MkdirAll(dbDir, 0o755); err != nil { - return nil, fmt.Errorf("error creating database directory: %v", err) + return nil, fmt.Errorf("error creating database directory: %w", err) } db, err := sql.Open("sqlite3", dbPath) From 87f6b0186bdf6432af90b9b9f67e9b4c2d20405b Mon Sep 17 00:00:00 2001 From: vhespanha Date: Thu, 15 Aug 2024 17:24:21 -0300 Subject: [PATCH 20/81] Removed shadowed err declarations --- pkg/repository/task_repo.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/repository/task_repo.go b/pkg/repository/task_repo.go index 5ebff9b..fda3999 100644 --- a/pkg/repository/task_repo.go +++ b/pkg/repository/task_repo.go @@ -84,7 +84,7 @@ func (r *Repository) GetAllTasks() ([]*models.Task, error) { var tasks []*models.Task for rows.Next() { task := &models.Task{} - err := rows.Scan( + err = rows.Scan( &task.ID, &task.Name, &task.Description, @@ -119,7 +119,7 @@ func (r *Repository) GetTasksByProjectID(projectID int) ([]*models.Task, error) var tasks []*models.Task for rows.Next() { task := &models.Task{} - err := rows.Scan( + err = rows.Scan( &task.ID, &task.Name, &task.Description, @@ -154,7 +154,7 @@ func (r *Repository) GetSubtasks(parentTaskID int) ([]*models.Task, error) { var tasks []*models.Task for rows.Next() { task := &models.Task{} - err := rows.Scan( + err = rows.Scan( &task.ID, &task.Name, &task.Description, From 9fd3a2da98795a0dc31d016ee2148f0d596a0add Mon Sep 17 00:00:00 2001 From: vhespanha Date: Thu, 15 Aug 2024 17:31:26 -0300 Subject: [PATCH 21/81] errors.New instead of Errorf since no verb is used --- pkg/repository/repository.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/repository/repository.go b/pkg/repository/repository.go index 0c4c74e..562f0b9 100644 --- a/pkg/repository/repository.go +++ b/pkg/repository/repository.go @@ -2,6 +2,7 @@ package repository import ( "database/sql" + "errors" "fmt" "os" "path/filepath" @@ -20,13 +21,13 @@ func NewRepository() (*Repository, error) { if runtime.GOOS == "windows" { appDataPath := os.Getenv("APPDATA") if appDataPath == "" { - return nil, fmt.Errorf("the APPDATA environment variable is not set") + return nil, errors.New("the APPDATA environment variable is not set") } dbPath = filepath.Join(appDataPath, "clido", "data.db") } else { homePath := os.Getenv("HOME") if homePath == "" { - return nil, fmt.Errorf("the HOME environment variable is not set") + return nil, errors.New("the HOME environment variable is not set") } dbPath = filepath.Join(homePath, ".local", "share", "clido", "data.db") } From abf14925516fd45c0681389fd925bdb9e08c3ee6 Mon Sep 17 00:00:00 2001 From: vhespanha Date: Thu, 15 Aug 2024 17:31:55 -0300 Subject: [PATCH 22/81] Remove more shadowed err definitions --- pkg/repository/project_repo.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/repository/project_repo.go b/pkg/repository/project_repo.go index ea1fec8..0ccf146 100644 --- a/pkg/repository/project_repo.go +++ b/pkg/repository/project_repo.go @@ -92,7 +92,7 @@ func (r *Repository) GetAllProjects() ([]*models.Project, error) { var projects []*models.Project for rows.Next() { project := &models.Project{} - err := rows.Scan( + err = rows.Scan( &project.ID, &project.Name, &project.Description, @@ -122,7 +122,7 @@ func (r *Repository) GetSubprojects(parentProjectID int) ([]*models.Project, err var projects []*models.Project for rows.Next() { project := &models.Project{} - err := rows.Scan( + err = rows.Scan( &project.ID, &project.Name, &project.Description, From b2cf39fa80bc5c054660a303c4081cca9db103fb Mon Sep 17 00:00:00 2001 From: vhespanha Date: Thu, 15 Aug 2024 17:58:45 -0300 Subject: [PATCH 23/81] Removed more shadowed err declarations --- cmd/edit.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cmd/edit.go b/cmd/edit.go index 596e9f9..5aa21fc 100644 --- a/cmd/edit.go +++ b/cmd/edit.go @@ -5,6 +5,7 @@ import ( "strconv" "time" + "github.com/d4r1us-drk/clido/pkg/models" "github.com/d4r1us-drk/clido/pkg/repository" "github.com/d4r1us-drk/clido/pkg/utils" "github.com/spf13/cobra" @@ -78,7 +79,8 @@ func editProject(cmd *cobra.Command, repo *repository.Repository, id int) { parentID, _ := strconv.Atoi(parentProjectIdentifier) project.ParentProjectID = &parentID } else { - parentProject, err := repo.GetProjectByName(parentProjectIdentifier) + var parentProject *models.Project + parentProject, err = repo.GetProjectByName(parentProjectIdentifier) if err != nil || parentProject == nil { fmt.Printf("Parent project '%s' not found.\n", parentProjectIdentifier) return @@ -116,7 +118,8 @@ func editTask(cmd *cobra.Command, repo *repository.Repository, id int) { task.Description = description } if dueDateStr != "" { - parsedDate, err := time.Parse("2006-01-02 15:04", dueDateStr) + var parsedDate time.Time + parsedDate, err = time.Parse("2006-01-02 15:04", dueDateStr) if err == nil { task.DueDate = &parsedDate } else { From 0d6b533151a19adddc4518dcd18c6a16b6bab6ab Mon Sep 17 00:00:00 2001 From: vhespanha Date: Thu, 15 Aug 2024 18:02:47 -0300 Subject: [PATCH 24/81] Use faster function to print task --- cmd/list.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/list.go b/cmd/list.go index 17297a3..3cde683 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -195,7 +195,7 @@ func printTaskTable(repo *repository.Repository, tasks []*models.Task) { utils.WrapText(task.Name, 20), utils.WrapText(task.Description, 30), utils.FormatDate(task.DueDate), - fmt.Sprintf("%v", task.TaskCompleted), + strconv.FormatBool(task.TaskCompleted), utils.ColoredPastDue(task.DueDate, task.TaskCompleted), utils.GetPriorityString(task.Priority), utils.WrapText(projectName, 20), From 65761efbabf2ddf841da4af5b98a9acf6b5b6cfa Mon Sep 17 00:00:00 2001 From: vhespanha Date: Thu, 15 Aug 2024 18:06:54 -0300 Subject: [PATCH 25/81] Check rows.Err errors too --- pkg/repository/project_repo.go | 10 ++++++++++ pkg/repository/task_repo.go | 15 +++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/pkg/repository/project_repo.go b/pkg/repository/project_repo.go index 0ccf146..fdf59ac 100644 --- a/pkg/repository/project_repo.go +++ b/pkg/repository/project_repo.go @@ -105,6 +105,11 @@ func (r *Repository) GetAllProjects() ([]*models.Project, error) { } projects = append(projects, project) } + + if err = rows.Err(); err != nil { + return nil, err + } + return projects, nil } @@ -135,6 +140,11 @@ func (r *Repository) GetSubprojects(parentProjectID int) ([]*models.Project, err } projects = append(projects, project) } + + if err = rows.Err(); err != nil { + return nil, err + } + return projects, nil } diff --git a/pkg/repository/task_repo.go b/pkg/repository/task_repo.go index fda3999..48dd2c7 100644 --- a/pkg/repository/task_repo.go +++ b/pkg/repository/task_repo.go @@ -102,6 +102,11 @@ func (r *Repository) GetAllTasks() ([]*models.Task, error) { } tasks = append(tasks, task) } + + if err = rows.Err(); err != nil { + return nil, err + } + return tasks, nil } @@ -137,6 +142,11 @@ func (r *Repository) GetTasksByProjectID(projectID int) ([]*models.Task, error) } tasks = append(tasks, task) } + + if err = rows.Err(); err != nil { + return nil, err + } + return tasks, nil } @@ -172,6 +182,11 @@ func (r *Repository) GetSubtasks(parentTaskID int) ([]*models.Task, error) { } tasks = append(tasks, task) } + + if err = rows.Err(); err != nil { + return nil, err + } + return tasks, nil } From 320b605962398070f640eda091188ae55a45d14c Mon Sep 17 00:00:00 2001 From: vhespanha Date: Thu, 15 Aug 2024 20:03:17 -0300 Subject: [PATCH 26/81] Create constants for clearer naming --- cmd/edit.go | 2 +- cmd/list.go | 10 +++++----- cmd/new.go | 3 +-- cmd/remove.go | 2 +- cmd/root.go | 9 +++++++++ 5 files changed, 17 insertions(+), 9 deletions(-) diff --git a/cmd/edit.go b/cmd/edit.go index 5aa21fc..f1279c7 100644 --- a/cmd/edit.go +++ b/cmd/edit.go @@ -16,7 +16,7 @@ var editCmd = &cobra.Command{ Short: "Edit an existing project or task", Long: `Edit the details of an existing project or task identified by its ID.`, Run: func(cmd *cobra.Command, args []string) { - if len(args) < 2 { + if len(args) < MinArgsLength { fmt.Println("Insufficient arguments. Use 'edit project ' or 'edit task '.") return } diff --git a/cmd/list.go b/cmd/list.go index 3cde683..bff8325 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -150,8 +150,8 @@ func printProjectTable(repo *repository.Repository, projects []*models.Project) table.Append([]string{ strconv.Itoa(project.ID), - utils.WrapText(project.Name, 30), - utils.WrapText(project.Description, 50), + utils.WrapText(project.Name, MaxProjectNameLength), + utils.WrapText(project.Description, MaxProjectDescLength), typeField, parentChildField, }) @@ -192,13 +192,13 @@ func printTaskTable(repo *repository.Repository, tasks []*models.Task) { table.Append([]string{ strconv.Itoa(task.ID), - utils.WrapText(task.Name, 20), - utils.WrapText(task.Description, 30), + utils.WrapText(task.Name, MaxTaskNameLength), + utils.WrapText(task.Description, MaxTaskDescLength), utils.FormatDate(task.DueDate), strconv.FormatBool(task.TaskCompleted), utils.ColoredPastDue(task.DueDate, task.TaskCompleted), utils.GetPriorityString(task.Priority), - utils.WrapText(projectName, 20), + utils.WrapText(projectName, MaxProjectNameWrapLength), typeField, parentChildField, }) diff --git a/cmd/new.go b/cmd/new.go index e055d8f..f3e1946 100644 --- a/cmd/new.go +++ b/cmd/new.go @@ -47,8 +47,7 @@ func init() { newCmd.Flags().StringP("project", "p", "", "Parent project name or ID for subprojects or tasks") newCmd.Flags().StringP("task", "t", "", "Parent task ID for subtasks") newCmd.Flags().StringP("due", "D", "", "Due date for the task (format: YYYY-MM-DD HH:MM)") - newCmd.Flags(). - IntP("priority", "r", 4, "Priority of the task (1: High, 2: Medium, 3: Low, 4: None)") + newCmd.Flags().IntP("priority", "pr", 0, "Priority of the task (1: High, 2: Medium, 3: Low, 4: None)") } func createProject(cmd *cobra.Command, repo *repository.Repository) { diff --git a/cmd/remove.go b/cmd/remove.go index 83ba7cb..26442a3 100644 --- a/cmd/remove.go +++ b/cmd/remove.go @@ -13,7 +13,7 @@ var removeCmd = &cobra.Command{ Short: "Remove a project or task along with all its subprojects or subtasks", Long: `Remove an existing project or task identified by its ID. This will also remove all associated subprojects or subtasks.`, Run: func(cmd *cobra.Command, args []string) { - if len(args) < 2 { + if len(args) < MinArgsLength { fmt.Println("Insufficient arguments. Use 'remove project ' or 'remove task '.") return } diff --git a/cmd/root.go b/cmd/root.go index 3bd7d75..4cf4b5e 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -8,6 +8,15 @@ import ( "github.com/spf13/cobra" ) +const ( + MaxProjectNameLength = 30 + MaxProjectDescLength = 50 + MaxTaskNameLength = 20 + MaxTaskDescLength = 30 + MaxProjectNameWrapLength = 20 + MinArgsLength = 2 +) + var rootCmd = &cobra.Command{ Use: "clido", Short: "Clido is an awesome CLI to-do list management application", From 90833cceb3cdc354c40382ce1cf3a6c7215ccccd Mon Sep 17 00:00:00 2001 From: vhespanha Date: Sat, 17 Aug 2024 17:28:24 -0300 Subject: [PATCH 27/81] Remove unnecessary linter options --- .golangci.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index f5acfcf..8ea2cc4 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -236,10 +236,8 @@ linters: - funlen # tool for detection of long functions - gocheckcompilerdirectives # validates go compiler directive comments (//go:) - gochecknoglobals # checks that no global variables exist - - gochecknoinits # checks that no init functions are present in Go code - gochecksumtype # checks exhaustiveness on Go "sum types" - gocognit # computes and checks the cognitive complexity of functions - - goconst # finds repeated strings that could be replaced by a constant - gocritic # provides diagnostics that check for bugs, performance and style issues - gocyclo # computes and checks the cyclomatic complexity of functions - godot # checks if comments end in a period From ec2c3938f2e01a103b44a2c62422dd377bc9fed2 Mon Sep 17 00:00:00 2001 From: vhespanha Date: Sat, 17 Aug 2024 17:46:23 -0300 Subject: [PATCH 28/81] Remove shadowed err var --- cmd/list.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/list.go b/cmd/list.go index bff8325..1fc537d 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -114,7 +114,8 @@ func listTasks(repo *repository.Repository, projectFilter string, outputJSON boo } if outputJSON { - jsonData, err := json.MarshalIndent(tasks, "", " ") + var jsonData []byte + jsonData, err = json.MarshalIndent(tasks, "", " ") if err != nil { fmt.Printf("Error marshalling tasks to JSON: %v\n", err) return From 8bac5427981b782647710b389f5ce747180b0747 Mon Sep 17 00:00:00 2001 From: vhespanha Date: Sat, 17 Aug 2024 17:49:46 -0300 Subject: [PATCH 29/81] Format --- cmd/remove.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/remove.go b/cmd/remove.go index 26442a3..79fb35a 100644 --- a/cmd/remove.go +++ b/cmd/remove.go @@ -11,7 +11,8 @@ import ( var removeCmd = &cobra.Command{ Use: "remove [project|task] ", Short: "Remove a project or task along with all its subprojects or subtasks", - Long: `Remove an existing project or task identified by its ID. This will also remove all associated subprojects or subtasks.`, + Long: `Remove an existing project or task identified by its ID. This will also remove all associated subprojects + or subtasks.`, Run: func(cmd *cobra.Command, args []string) { if len(args) < MinArgsLength { fmt.Println("Insufficient arguments. Use 'remove project ' or 'remove task '.") From 9d37fb893064598afc2bd00665133a72076f9ab1 Mon Sep 17 00:00:00 2001 From: vhespanha Date: Sat, 17 Aug 2024 17:54:26 -0300 Subject: [PATCH 30/81] Remove unused repo parameter from print functions --- cmd/list.go | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/cmd/list.go b/cmd/list.go index 1fc537d..6fdb20f 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -68,7 +68,7 @@ func listProjects(repo *repository.Repository, outputJSON bool, treeView bool) { } fmt.Println(string(jsonData)) } else if treeView { - printProjectTree(repo, projects, nil, 0) + printProjectTree(projects, nil, 0) } else { printProjectTable(repo, projects) } @@ -122,7 +122,7 @@ func listTasks(repo *repository.Repository, projectFilter string, outputJSON boo } fmt.Println(string(jsonData)) } else if treeView { - printTaskTree(repo, tasks, nil, 0) + printTaskTree(tasks, nil, 0) } else { printTaskTable(repo, tasks) } @@ -209,7 +209,6 @@ func printTaskTable(repo *repository.Repository, tasks []*models.Task) { } func printProjectTree( - repo *repository.Repository, projects []*models.Project, parentID *int, level int, @@ -223,12 +222,16 @@ func printProjectTree( prefix = "└──" } fmt.Printf("%s%s %s (ID: %d)\n", indent, prefix, project.Name, project.ID) - printProjectTree(repo, projects, &project.ID, level+1) + printProjectTree(projects, &project.ID, level+1) } } } -func printTaskTree(repo *repository.Repository, tasks []*models.Task, parentID *int, level int) { +func printTaskTree( + tasks []*models.Task, + parentID *int, + level int, +) { indent := strings.Repeat("│ ", level) for i, task := range tasks { if (parentID == nil && task.ParentTaskID == nil) || @@ -246,7 +249,7 @@ func printTaskTree(repo *repository.Repository, tasks []*models.Task, parentID * task.TaskCompleted, utils.GetPriorityString(task.Priority), ) - printTaskTree(repo, tasks, &task.ID, level+1) + printTaskTree(tasks, &task.ID, level+1) } } } From cbdebecbf94f5dc28406d65c3bc171eabbc79575 Mon Sep 17 00:00:00 2001 From: vhespanha Date: Sat, 17 Aug 2024 18:08:48 -0300 Subject: [PATCH 31/81] Refactor nested checks and handle errors better --- cmd/new.go | 95 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 60 insertions(+), 35 deletions(-) diff --git a/cmd/new.go b/cmd/new.go index f3e1946..0636563 100644 --- a/cmd/new.go +++ b/cmd/new.go @@ -1,6 +1,7 @@ package cmd import ( + "errors" "fmt" "strconv" "time" @@ -11,6 +12,11 @@ import ( "github.com/spf13/cobra" ) +var ( + ErrNoParentTask = errors.New("no parent task specified") + ErrNoDueDate = errors.New("no due date specified") +) + var newCmd = &cobra.Command{ Use: "new [project|task]", Short: "Create a new project or task", @@ -103,44 +109,21 @@ func createTask(cmd *cobra.Command, repo *repository.Repository) { return } - var projectID int - var parentTaskID *int - - if projectIdentifier != "" { - if utils.IsNumeric(projectIdentifier) { - id, _ := strconv.Atoi(projectIdentifier) - projectID = id - } else { - project, err := repo.GetProjectByName(projectIdentifier) - if err != nil || project == nil { - fmt.Printf("Project '%s' not found.\n", projectIdentifier) - return - } - projectID = project.ID - } - } else { - fmt.Println("Task must be associated with a project.") + projectID, err := getProjectID(projectIdentifier, repo) + if err != nil { + fmt.Println(err) return } - if parentTaskIdentifier != "" { - if utils.IsNumeric(parentTaskIdentifier) { - id, _ := strconv.Atoi(parentTaskIdentifier) - parentTaskID = &id - } else { - fmt.Println("Parent task must be identified by a numeric ID.") - return - } + parentTaskID, err := getParentTaskID(parentTaskIdentifier) + if err != nil && err != ErrNoParentTask { + fmt.Println(err) + return } - var dueDate *time.Time - if dueDateStr != "" { - parsedDate, err := time.Parse("2006-01-02 15:04", dueDateStr) - if err == nil { - dueDate = &parsedDate - } else { - fmt.Println("Invalid date format. Using no due date.") - } + dueDate, err := parseDueDate(dueDateStr) + if err != nil && err != ErrNoDueDate { + fmt.Println("Invalid date format. Using no due date.") } task := &models.Task{ @@ -152,8 +135,7 @@ func createTask(cmd *cobra.Command, repo *repository.Repository) { ParentTaskID: parentTaskID, } - err := repo.CreateTask(task) - if err != nil { + if err = repo.CreateTask(task); err != nil { fmt.Printf("Error creating task: %v\n", err) return } @@ -164,3 +146,46 @@ func createTask(cmd *cobra.Command, repo *repository.Repository) { utils.GetPriorityString(utils.Priority(priority)), ) } + +func getProjectID(identifier string, repo *repository.Repository) (int, error) { + if identifier == "" { + return 0, errors.New("task must be associated with a project") + } + + if utils.IsNumeric(identifier) { + return strconv.Atoi(identifier) + } + + project, err := repo.GetProjectByName(identifier) + if err != nil || project == nil { + return 0, fmt.Errorf("project '%s' not found", identifier) + } + + return project.ID, nil +} + +func getParentTaskID(identifier string) (*int, error) { + if identifier == "" { + return nil, ErrNoParentTask + } + + if !utils.IsNumeric(identifier) { + return nil, errors.New("parent task must be identified by a numeric ID") + } + + id, _ := strconv.Atoi(identifier) + return &id, nil +} + +func parseDueDate(dateStr string) (*time.Time, error) { + if dateStr == "" { + return nil, ErrNoDueDate + } + + parsedDate, err := time.Parse("2006-01-02 15:04", dateStr) + if err != nil { + return nil, err + } + + return &parsedDate, nil +} From 8759dceb3327ac9142735342534408f0d4aaab0c Mon Sep 17 00:00:00 2001 From: vhespanha Date: Sat, 17 Aug 2024 18:11:39 -0300 Subject: [PATCH 32/81] Unwrappable error checking --- cmd/new.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/new.go b/cmd/new.go index 0636563..64b01d2 100644 --- a/cmd/new.go +++ b/cmd/new.go @@ -116,13 +116,13 @@ func createTask(cmd *cobra.Command, repo *repository.Repository) { } parentTaskID, err := getParentTaskID(parentTaskIdentifier) - if err != nil && err != ErrNoParentTask { + if err != nil && !errors.Is(err, ErrNoParentTask) { fmt.Println(err) return } dueDate, err := parseDueDate(dueDateStr) - if err != nil && err != ErrNoDueDate { + if err != nil && !errors.Is(err, ErrNoDueDate) { fmt.Println("Invalid date format. Using no due date.") } From 82ab4fac52124c3fed968303cc3a3ec20fcebb94 Mon Sep 17 00:00:00 2001 From: vhespanha Date: Sat, 17 Aug 2024 18:13:51 -0300 Subject: [PATCH 33/81] Format string --- cmd/root.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/root.go b/cmd/root.go index 4cf4b5e..42a5f3f 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -21,7 +21,7 @@ var rootCmd = &cobra.Command{ Use: "clido", Short: "Clido is an awesome CLI to-do list management application", Long: `Clido is a simple yet powerful CLI tool designed to help you manage -your projects and tasks effectively from the terminal.`, + your projects and tasks effectively from the terminal.`, } func Execute() { From 64cf7b8a589959b2f074949fd3feb9a545924eaa Mon Sep 17 00:00:00 2001 From: vhespanha Date: Sat, 17 Aug 2024 18:15:23 -0300 Subject: [PATCH 34/81] Use uppercase P as priority shorthand --- cmd/edit.go | 3 +-- cmd/new.go | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/cmd/edit.go b/cmd/edit.go index f1279c7..401af6c 100644 --- a/cmd/edit.go +++ b/cmd/edit.go @@ -53,8 +53,7 @@ func init() { editCmd.Flags().StringP("project", "p", "", "New parent project name or ID") editCmd.Flags().StringP("task", "t", "", "New parent task ID for subtasks") editCmd.Flags().StringP("due", "D", "", "New due date for task (format: YYYY-MM-DD HH:MM)") - editCmd.Flags(). - IntP("priority", "r", 0, "New priority for task (1: High, 2: Medium, 3: Low, 4: None)") + editCmd.Flags().IntP("priority", "P", 0, "New priority for task (1: High, 2: Medium, 3: Low, 4: None)") } func editProject(cmd *cobra.Command, repo *repository.Repository, id int) { diff --git a/cmd/new.go b/cmd/new.go index 64b01d2..30f2f1c 100644 --- a/cmd/new.go +++ b/cmd/new.go @@ -53,7 +53,7 @@ func init() { newCmd.Flags().StringP("project", "p", "", "Parent project name or ID for subprojects or tasks") newCmd.Flags().StringP("task", "t", "", "Parent task ID for subtasks") newCmd.Flags().StringP("due", "D", "", "Due date for the task (format: YYYY-MM-DD HH:MM)") - newCmd.Flags().IntP("priority", "pr", 0, "Priority of the task (1: High, 2: Medium, 3: Low, 4: None)") + newCmd.Flags().IntP("priority", "P", 0, "Priority of the task (1: High, 2: Medium, 3: Low, 4: None)") } func createProject(cmd *cobra.Command, repo *repository.Repository) { From 71bedf41884ef6a79052f870543803ace001ffad Mon Sep 17 00:00:00 2001 From: vhespanha Date: Sat, 17 Aug 2024 18:17:07 -0300 Subject: [PATCH 35/81] Update readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ce5ac44..b772b6d 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ Clido allows you to manage projects and tasks with various commands. Below are s - Create a new task with priority: ```sh - clido new task -n "New Task" -d "Task Description" -D "2024-08-15 23:00" -p "Existing Project" -r 1 + clido new task -n "New Task" -d "Task Description" -D "2024-08-15 23:00" -p "Existing Project" -P 1 ``` Priority levels: 1 (High), 2 (Medium), 3 (Low), 4 (None) @@ -81,7 +81,7 @@ Clido allows you to manage projects and tasks with various commands. Below are s - Edit a task's priority: ```sh - clido edit task 1 -r 2 + clido edit task 1 -P 2 ``` - List all projects: From 1c1951eef21bbe5c4f18f83957bd06f6b6f064a6 Mon Sep 17 00:00:00 2001 From: vhespanha Date: Sat, 17 Aug 2024 20:47:26 -0300 Subject: [PATCH 36/81] Create develop branch --- internal/version/version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/version/version.go b/internal/version/version.go index 3e63615..789cb29 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -6,7 +6,7 @@ import ( ) var ( - Version = "1.1.0" + Version = "dev" BuildDate = "unknown" From 26ef42847662a09db2cfe204bd9b37dceed60057 Mon Sep 17 00:00:00 2001 From: vhespanha Date: Sat, 17 Aug 2024 20:59:28 -0300 Subject: [PATCH 37/81] Use switch statement instead of if --- cmd/list.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/cmd/list.go b/cmd/list.go index 6fdb20f..73e684d 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -60,16 +60,20 @@ func listProjects(repo *repository.Repository, outputJSON bool, treeView bool) { return } - if outputJSON { - jsonData, err := json.MarshalIndent(projects, "", " ") + switch { + case outputJSON: + var jsonData []byte + jsonData, err = json.MarshalIndent(projects, "", " ") if err != nil { fmt.Printf("Error marshalling projects to JSON: %v\n", err) return } fmt.Println(string(jsonData)) - } else if treeView { + + case treeView: printProjectTree(projects, nil, 0) - } else { + + default: printProjectTable(repo, projects) } } From 557240aa4f882a23ea8c2b3e4176346cdf4c2a3c Mon Sep 17 00:00:00 2001 From: vhespanha Date: Sat, 17 Aug 2024 21:05:24 -0300 Subject: [PATCH 38/81] New subfunctions to achieve more readable code --- cmd/list.go | 99 +++++++++++++++++++++++++++++++---------------------- 1 file changed, 58 insertions(+), 41 deletions(-) diff --git a/cmd/list.go b/cmd/list.go index 73e684d..94dede5 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -79,57 +79,74 @@ func listProjects(repo *repository.Repository, outputJSON bool, treeView bool) { } func listTasks(repo *repository.Repository, projectFilter string, outputJSON bool, treeView bool) { - var tasks []*models.Task - var err error + tasks, project, err := getTasks(repo, projectFilter) + if err != nil { + fmt.Println(err) + return + } - if projectFilter != "" { - var project *models.Project - if utils.IsNumeric(projectFilter) { - projectID, _ := strconv.Atoi(projectFilter) - project, err = repo.GetProjectByID(projectID) - } else { - project, err = repo.GetProjectByName(projectFilter) - } + if !outputJSON { + printTaskHeader(project) + } - if err != nil || project == nil { - fmt.Printf("Project '%s' not found.\n", projectFilter) - return - } + switch { + case outputJSON: + printTasksJSON(tasks) + case treeView: + printTaskTree(tasks, nil, 0) + default: + printTaskTable(repo, tasks) + } +} - tasks, err = repo.GetTasksByProjectID(project.ID) - if err != nil { - fmt.Printf("Error listing tasks: %v\n", err) - return - } +func getTasks(repo *repository.Repository, projectFilter string) ([]*models.Task, *models.Project, error) { + if projectFilter == "" { + tasks, err := repo.GetAllTasks() + return tasks, nil, err + } - if !outputJSON { - fmt.Printf("Tasks in project '%s':\n", project.Name) - } + project, err := getProject(repo, projectFilter) + if err != nil { + return nil, nil, err + } + + tasks, err := repo.GetTasksByProjectID(project.ID) + return tasks, project, err +} + +func getProject(repo *repository.Repository, projectFilter string) (*models.Project, error) { + var project *models.Project + var err error + + if utils.IsNumeric(projectFilter) { + projectID, _ := strconv.Atoi(projectFilter) + project, err = repo.GetProjectByID(projectID) } else { - tasks, err = repo.GetAllTasks() - if err != nil { - fmt.Printf("Error listing tasks: %v\n", err) - return - } + project, err = repo.GetProjectByName(projectFilter) + } - if !outputJSON { - fmt.Println("All Tasks:") - } + if err != nil || project == nil { + return nil, fmt.Errorf("project '%s' not found", projectFilter) } - if outputJSON { - var jsonData []byte - jsonData, err = json.MarshalIndent(tasks, "", " ") - if err != nil { - fmt.Printf("Error marshalling tasks to JSON: %v\n", err) - return - } - fmt.Println(string(jsonData)) - } else if treeView { - printTaskTree(tasks, nil, 0) + return project, nil +} + +func printTaskHeader(project *models.Project) { + if project != nil { + fmt.Printf("Tasks in project '%s':\n", project.Name) } else { - printTaskTable(repo, tasks) + fmt.Println("All Tasks:") + } +} + +func printTasksJSON(tasks []*models.Task) { + jsonData, err := json.MarshalIndent(tasks, "", " ") + if err != nil { + fmt.Printf("Error marshalling tasks to JSON: %v\n", err) + return } + fmt.Println(string(jsonData)) } func printProjectTable(repo *repository.Repository, projects []*models.Project) { From 26d05a2cdad5883c418e76c65331762e1441cfcd Mon Sep 17 00:00:00 2001 From: vhespanha Date: Sat, 17 Aug 2024 21:12:14 -0300 Subject: [PATCH 39/81] Atomize print tree logic and rendering --- cmd/list.go | 91 ++++++++++++++++++++++++++++++++--------------------- 1 file changed, 56 insertions(+), 35 deletions(-) diff --git a/cmd/list.go b/cmd/list.go index 94dede5..97894ef 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -229,48 +229,69 @@ func printTaskTable(repo *repository.Repository, tasks []*models.Task) { table.Render() } -func printProjectTree( - projects []*models.Project, - parentID *int, - level int, -) { +type TreeNode interface { + GetID() int + GetParentID() *int + GetName() string +} + +type ProjectNode struct { + *models.Project +} + +func (p ProjectNode) GetID() int { return p.ID } +func (p ProjectNode) GetParentID() *int { return p.ParentProjectID } +func (p ProjectNode) GetName() string { return p.Name } + +type TaskNode struct { + *models.Task +} + +func (t TaskNode) GetID() int { return t.ID } +func (t TaskNode) GetParentID() *int { return t.ParentTaskID } +func (t TaskNode) GetName() string { return t.Name } + +func printTree(nodes []TreeNode, parentID *int, level int, printDetails func(TreeNode, string)) { indent := strings.Repeat("│ ", level) - for i, project := range projects { - if (parentID == nil && project.ParentProjectID == nil) || - (parentID != nil && project.ParentProjectID != nil && *project.ParentProjectID == *parentID) { + for i, node := range nodes { + if (parentID == nil && node.GetParentID() == nil) || + (parentID != nil && node.GetParentID() != nil && *node.GetParentID() == *parentID) { prefix := "├──" - if i == len(projects)-1 { + if i == len(nodes)-1 { prefix = "└──" } - fmt.Printf("%s%s %s (ID: %d)\n", indent, prefix, project.Name, project.ID) - printProjectTree(projects, &project.ID, level+1) + fmt.Printf("%s%s %s (ID: %d)\n", indent, prefix, node.GetName(), node.GetID()) + if printDetails != nil { + printDetails(node, indent+" ") + } + nodeID := node.GetID() + printTree(nodes, &nodeID, level+1, printDetails) } } } -func printTaskTree( - tasks []*models.Task, - parentID *int, - level int, -) { - indent := strings.Repeat("│ ", level) - for i, task := range tasks { - if (parentID == nil && task.ParentTaskID == nil) || - (parentID != nil && task.ParentTaskID != nil && *task.ParentTaskID == *parentID) { - prefix := "├──" - if i == len(tasks)-1 { - prefix = "└──" - } - fmt.Printf("%s%s %s (ID: %d)\n", indent, prefix, task.Name, task.ID) - fmt.Printf("%s Description: %s\n", indent, task.Description) - fmt.Printf( - "%s Due Date: %s, Completed: %v, Priority: %s\n", - indent, - utils.FormatDate(task.DueDate), - task.TaskCompleted, - utils.GetPriorityString(task.Priority), - ) - printTaskTree(tasks, &task.ID, level+1) - } +func printProjectTree(projects []*models.Project, parentID *int, level int) { + nodes := make([]TreeNode, len(projects)) + for i, p := range projects { + nodes[i] = ProjectNode{p} + } + printTree(nodes, parentID, level, nil) +} + +func printTaskTree(tasks []*models.Task, parentID *int, level int) { + nodes := make([]TreeNode, len(tasks)) + for i, t := range tasks { + nodes[i] = TaskNode{t} } + printTree(nodes, parentID, level, func(node TreeNode, indent string) { + task := node.(TaskNode).Task + fmt.Printf("%sDescription: %s\n", indent, task.Description) + fmt.Printf( + "%sDue Date: %s, Completed: %v, Priority: %s\n", + indent, + utils.FormatDate(task.DueDate), + task.TaskCompleted, + utils.GetPriorityString(task.Priority), + ) + }) } From 0241a8a31c7fdaad79e8a71a4e669473ffd7c04f Mon Sep 17 00:00:00 2001 From: vhespanha Date: Sat, 17 Aug 2024 21:17:52 -0300 Subject: [PATCH 40/81] Add json tags to task and project models --- pkg/models/project.go | 12 ++++++------ pkg/models/task.go | 22 +++++++++++----------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/pkg/models/project.go b/pkg/models/project.go index b7e91f9..9a6178a 100644 --- a/pkg/models/project.go +++ b/pkg/models/project.go @@ -3,10 +3,10 @@ package models import "time" type Project struct { - ID int - Name string - Description string - CreationDate time.Time - LastModifiedDate time.Time - ParentProjectID *int + ID int `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + CreationDate time.Time `json:"creation_date"` + LastModifiedDate time.Time `json:"last_modified_date"` + ParentProjectID *int `json:"parent_project_id,omitempty"` } diff --git a/pkg/models/task.go b/pkg/models/task.go index 5c673f1..8f776c3 100644 --- a/pkg/models/task.go +++ b/pkg/models/task.go @@ -7,15 +7,15 @@ import ( ) type Task struct { - ID int - Name string - Description string - ProjectID int - TaskCompleted bool - DueDate *time.Time - CompletionDate *time.Time - CreationDate time.Time - LastUpdatedDate time.Time - Priority utils.Priority - ParentTaskID *int + ID int `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + ProjectID int `json:"project_id"` + TaskCompleted bool `json:"task_completed"` + DueDate *time.Time `json:"due_date,omitempty"` + CompletionDate *time.Time `json:"completion_date,omitempty"` + CreationDate time.Time `json:"creation_date"` + LastUpdatedDate time.Time `json:"last_updated_date"` + Priority utils.Priority `json:"priority"` + ParentTaskID *int `json:"parent_task_id,omitempty"` } From 7df320a2fbd59edf2eb2fc3c9734f905db5d5e08 Mon Sep 17 00:00:00 2001 From: vhespanha Date: Sat, 17 Aug 2024 21:37:32 -0300 Subject: [PATCH 41/81] Initialize DB w gorm on repository package --- pkg/repository/repository.go | 88 ++++++++++++++---------------------- 1 file changed, 34 insertions(+), 54 deletions(-) diff --git a/pkg/repository/repository.go b/pkg/repository/repository.go index 562f0b9..930556c 100644 --- a/pkg/repository/repository.go +++ b/pkg/repository/repository.go @@ -1,94 +1,74 @@ package repository import ( - "database/sql" "errors" "fmt" "os" "path/filepath" "runtime" - _ "github.com/mattn/go-sqlite3" // SQLite3 driver registration + "github.com/d4r1us-drk/clido/pkg/models" + "gorm.io/driver/sqlite" + "gorm.io/gorm" ) type Repository struct { - db *sql.DB + db *gorm.DB } func NewRepository() (*Repository, error) { + dbPath, err := getDBPath() + if err != nil { + return nil, err + } + + db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{}) + if err != nil { + return nil, fmt.Errorf("failed to connect database: %w", err) + } + + repo := &Repository{db: db} + err = repo.autoMigrate() + if err != nil { + return nil, fmt.Errorf("failed to run migrations: %w", err) + } + + return repo, nil +} + +func getDBPath() (string, error) { var dbPath string if runtime.GOOS == "windows" { appDataPath := os.Getenv("APPDATA") if appDataPath == "" { - return nil, errors.New("the APPDATA environment variable is not set") + return "", errors.New("the APPDATA environment variable is not set") } dbPath = filepath.Join(appDataPath, "clido", "data.db") } else { homePath := os.Getenv("HOME") if homePath == "" { - return nil, errors.New("the HOME environment variable is not set") + return "", errors.New("the HOME environment variable is not set") } dbPath = filepath.Join(homePath, ".local", "share", "clido", "data.db") } dbDir := filepath.Dir(dbPath) if err := os.MkdirAll(dbDir, 0o755); err != nil { - return nil, fmt.Errorf("error creating database directory: %w", err) + return "", fmt.Errorf("error creating database directory: %w", err) } - db, err := sql.Open("sqlite3", dbPath) - if err != nil { - return nil, err - } - - repo := &Repository{db: db} - err = repo.init() - if err != nil { - return nil, err - } - - return repo, nil + return dbPath, nil } -func (r *Repository) init() error { - createProjectTable := ` - CREATE TABLE IF NOT EXISTS Projects ( - ID INTEGER PRIMARY KEY AUTOINCREMENT, - Name TEXT NOT NULL UNIQUE, - Description TEXT, - CreationDate DATETIME NOT NULL, - LastModifiedDate DATETIME NOT NULL, - ParentProjectID INTEGER, - FOREIGN KEY (ParentProjectID) REFERENCES Projects(ID) - );` - - createTaskTable := ` - CREATE TABLE IF NOT EXISTS Tasks ( - ID INTEGER PRIMARY KEY AUTOINCREMENT, - Name TEXT NOT NULL, - Description TEXT, - ProjectID INTEGER NOT NULL, - TaskCompleted BOOLEAN NOT NULL, - DueDate DATETIME, - CompletionDate DATETIME, - CreationDate DATETIME NOT NULL, - LastUpdatedDate DATETIME NOT NULL, - Priority INTEGER NOT NULL DEFAULT 4, - ParentTaskID INTEGER, - FOREIGN KEY (ParentTaskID) REFERENCES Tasks(ID), - FOREIGN KEY (ProjectID) REFERENCES Projects(ID) - );` +func (r *Repository) autoMigrate() error { + return r.db.AutoMigrate(&models.Project{}, &models.Task{}) +} - _, err := r.db.Exec(createProjectTable) +func (r *Repository) Close() error { + sqlDB, err := r.db.DB() if err != nil { return err } - - _, err = r.db.Exec(createTaskTable) - return err -} - -func (r *Repository) Close() { - r.db.Close() + return sqlDB.Close() } From a9e9ae45aa3eb08f9348f3dfcd83a63473ed856e Mon Sep 17 00:00:00 2001 From: vhespanha Date: Sat, 17 Aug 2024 21:41:55 -0300 Subject: [PATCH 42/81] Add gorm tags on models package --- pkg/models/project.go | 11 +++++++---- pkg/models/task.go | 17 ++++++++++------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/pkg/models/project.go b/pkg/models/project.go index 9a6178a..69b6f32 100644 --- a/pkg/models/project.go +++ b/pkg/models/project.go @@ -3,10 +3,13 @@ package models import "time" type Project struct { - ID int `json:"id"` - Name string `json:"name"` + ID int `gorm:"primaryKey" json:"id"` + Name string `gorm:"unique;not null" json:"name"` Description string `json:"description"` - CreationDate time.Time `json:"creation_date"` - LastModifiedDate time.Time `json:"last_modified_date"` + CreationDate time.Time `gorm:"not null" json:"creation_date"` + LastModifiedDate time.Time `gorm:"not null" json:"last_modified_date"` ParentProjectID *int `json:"parent_project_id,omitempty"` + ParentProject *Project `gorm:"foreignKey:ParentProjectID" json:"-"` + SubProjects []Project `gorm:"foreignKey:ParentProjectID" json:"-"` + Tasks []Task `gorm:"foreignKey:ProjectID" json:"-"` } diff --git a/pkg/models/task.go b/pkg/models/task.go index 8f776c3..c69a51f 100644 --- a/pkg/models/task.go +++ b/pkg/models/task.go @@ -7,15 +7,18 @@ import ( ) type Task struct { - ID int `json:"id"` - Name string `json:"name"` + ID int `gorm:"primaryKey" json:"id"` + Name string `gorm:"not null" json:"name"` Description string `json:"description"` - ProjectID int `json:"project_id"` - TaskCompleted bool `json:"task_completed"` + ProjectID int `gorm:"not null" json:"project_id"` + Project Project `gorm:"foreignKey:ProjectID" json:"-"` + TaskCompleted bool `gorm:"not null" json:"task_completed"` DueDate *time.Time `json:"due_date,omitempty"` CompletionDate *time.Time `json:"completion_date,omitempty"` - CreationDate time.Time `json:"creation_date"` - LastUpdatedDate time.Time `json:"last_updated_date"` - Priority utils.Priority `json:"priority"` + CreationDate time.Time `gorm:"not null" json:"creation_date"` + LastUpdatedDate time.Time `gorm:"not null" json:"last_updated_date"` + Priority utils.Priority `gorm:"not null;default:4" json:"priority"` ParentTaskID *int `json:"parent_task_id,omitempty"` + ParentTask *Task `gorm:"foreignKey:ParentTaskID" json:"-"` + SubTasks []Task `gorm:"foreignKey:ParentTaskID" json:"-"` } From c7227cea1f72e165d92d68c4a75c7652a961975d Mon Sep 17 00:00:00 2001 From: vhespanha Date: Sat, 17 Aug 2024 21:44:01 -0300 Subject: [PATCH 43/81] Crude ORM on models --- pkg/models/project.go | 17 ++++++++++++++++- pkg/models/task.go | 12 ++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/pkg/models/project.go b/pkg/models/project.go index 69b6f32..3023277 100644 --- a/pkg/models/project.go +++ b/pkg/models/project.go @@ -1,6 +1,10 @@ package models -import "time" +import ( + "time" + + "gorm.io/gorm" +) type Project struct { ID int `gorm:"primaryKey" json:"id"` @@ -13,3 +17,14 @@ type Project struct { SubProjects []Project `gorm:"foreignKey:ParentProjectID" json:"-"` Tasks []Task `gorm:"foreignKey:ProjectID" json:"-"` } + +func (p *Project) BeforeCreate(_ *gorm.DB) error { + p.CreationDate = time.Now() + p.LastModifiedDate = time.Now() + return nil +} + +func (p *Project) BeforeUpdate(_ *gorm.DB) error { + p.LastModifiedDate = time.Now() + return nil +} diff --git a/pkg/models/task.go b/pkg/models/task.go index c69a51f..f5d07d4 100644 --- a/pkg/models/task.go +++ b/pkg/models/task.go @@ -4,6 +4,7 @@ import ( "time" "github.com/d4r1us-drk/clido/pkg/utils" + "gorm.io/gorm" ) type Task struct { @@ -22,3 +23,14 @@ type Task struct { ParentTask *Task `gorm:"foreignKey:ParentTaskID" json:"-"` SubTasks []Task `gorm:"foreignKey:ParentTaskID" json:"-"` } + +func (t *Task) BeforeCreate(_ *gorm.DB) error { + t.CreationDate = time.Now() + t.LastUpdatedDate = time.Now() + return nil +} + +func (t *Task) BeforeUpdate(_ *gorm.DB) error { + t.LastUpdatedDate = time.Now() + return nil +} From 67b123ce004379579c13a04fead5e6f1eabbbbd6 Mon Sep 17 00:00:00 2001 From: vhespanha Date: Sat, 17 Aug 2024 21:49:38 -0300 Subject: [PATCH 44/81] First try on using gorm, this seems to easy to work --- go.mod | 7 +- go.sum | 10 ++ pkg/repository/project_repo.go | 152 ++++--------------------- pkg/repository/task_repo.go | 201 +++------------------------------ 4 files changed, 53 insertions(+), 317 deletions(-) diff --git a/go.mod b/go.mod index f85692e..4b3e5f0 100644 --- a/go.mod +++ b/go.mod @@ -3,15 +3,20 @@ module github.com/d4r1us-drk/clido go 1.22.5 require ( - github.com/mattn/go-sqlite3 v1.14.22 github.com/olekukonko/tablewriter v0.0.5 github.com/spf13/cobra v1.8.1 + gorm.io/driver/sqlite v1.5.6 + gorm.io/gorm v1.25.11 ) require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect github.com/mattn/go-runewidth v0.0.9 // indirect + github.com/mattn/go-sqlite3 v1.14.22 // indirect github.com/spf13/pflag v1.0.5 // indirect + golang.org/x/text v0.14.0 // indirect ) require ( diff --git a/go.sum b/go.sum index fc88ff9..6c35e37 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,10 @@ github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -23,5 +27,11 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/sqlite v1.5.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE= +gorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4= +gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg= +gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= diff --git a/pkg/repository/project_repo.go b/pkg/repository/project_repo.go index fdf59ac..06cb5fd 100644 --- a/pkg/repository/project_repo.go +++ b/pkg/repository/project_repo.go @@ -1,167 +1,53 @@ package repository import ( - "time" - "github.com/d4r1us-drk/clido/pkg/models" ) func (r *Repository) CreateProject(project *models.Project) error { - project.CreationDate = time.Now() - project.LastModifiedDate = time.Now() - - // Find the lowest unused ID - var id int - err := r.db.QueryRow(` - SELECT COALESCE(MIN(p1.ID + 1), 1) - FROM Projects p1 - LEFT JOIN Projects p2 ON p1.ID + 1 = p2.ID - WHERE p2.ID IS NULL`).Scan(&id) - if err != nil { - return err - } - - // Insert the project with the found ID - _, err = r.db.Exec( - `INSERT INTO Projects (ID, Name, Description, CreationDate, LastModifiedDate, ParentProjectID) - VALUES (?, ?, ?, ?, ?, ?)`, - id, - project.Name, - project.Description, - project.CreationDate, - project.LastModifiedDate, - project.ParentProjectID, - ) - if err != nil { - return err - } - - project.ID = id - return nil + return r.db.Create(project).Error } func (r *Repository) GetProjectByID(id int) (*models.Project, error) { - project := &models.Project{} - err := r.db.QueryRow( - `SELECT ID, Name, Description, CreationDate, LastModifiedDate, ParentProjectID FROM Projects WHERE ID = ?`, - id, - ). - Scan( - &project.ID, - &project.Name, - &project.Description, - &project.CreationDate, - &project.LastModifiedDate, - &project.ParentProjectID, - ) + var project models.Project + err := r.db.First(&project, id).Error if err != nil { return nil, err } - return project, nil + return &project, nil } func (r *Repository) GetProjectByName(name string) (*models.Project, error) { - project := &models.Project{} - err := r.db.QueryRow( - `SELECT ID, Name, Description, CreationDate, LastModifiedDate, ParentProjectID FROM Projects WHERE Name = ?`, - name, - ). - Scan( - &project.ID, - &project.Name, - &project.Description, - &project.CreationDate, - &project.LastModifiedDate, - &project.ParentProjectID, - ) + var project models.Project + err := r.db.Where("name = ?", name).First(&project).Error if err != nil { return nil, err } - return project, nil + return &project, nil } func (r *Repository) GetAllProjects() ([]*models.Project, error) { - rows, err := r.db.Query( - `SELECT ID, Name, Description, CreationDate, LastModifiedDate, ParentProjectID FROM Projects`, - ) - if err != nil { - return nil, err - } - defer rows.Close() - var projects []*models.Project - for rows.Next() { - project := &models.Project{} - err = rows.Scan( - &project.ID, - &project.Name, - &project.Description, - &project.CreationDate, - &project.LastModifiedDate, - &project.ParentProjectID, - ) - if err != nil { - return nil, err - } - projects = append(projects, project) - } - - if err = rows.Err(); err != nil { - return nil, err - } - - return projects, nil + err := r.db.Find(&projects).Error + return projects, err } func (r *Repository) GetSubprojects(parentProjectID int) ([]*models.Project, error) { - rows, err := r.db.Query( - `SELECT ID, Name, Description, CreationDate, LastModifiedDate, ParentProjectID FROM Projects WHERE - ParentProjectID = ?`, - parentProjectID, - ) - if err != nil { - return nil, err - } - defer rows.Close() - var projects []*models.Project - for rows.Next() { - project := &models.Project{} - err = rows.Scan( - &project.ID, - &project.Name, - &project.Description, - &project.CreationDate, - &project.LastModifiedDate, - &project.ParentProjectID, - ) - if err != nil { - return nil, err - } - projects = append(projects, project) - } - - if err = rows.Err(); err != nil { - return nil, err - } - - return projects, nil + err := r.db.Where("parent_project_id = ?", parentProjectID).Find(&projects).Error + return projects, err } func (r *Repository) UpdateProject(project *models.Project) error { - project.LastModifiedDate = time.Now() - _, err := r.db.Exec( - `UPDATE Projects SET Name = ?, Description = ?, LastModifiedDate = ?, ParentProjectID = ? WHERE ID = ?`, - project.Name, - project.Description, - project.LastModifiedDate, - project.ParentProjectID, - project.ID, - ) - return err + return r.db.Save(project).Error } func (r *Repository) DeleteProject(id int) error { - _, err := r.db.Exec(`DELETE FROM Projects WHERE ID = ?`, id) - return err + return r.db.Delete(&models.Project{}, id).Error +} + +func (r *Repository) GetNextProjectID() (int, error) { + var maxID int + err := r.db.Model(&models.Project{}).Select("COALESCE(MAX(id), 0) + 1").Scan(&maxID).Error + return maxID, err } diff --git a/pkg/repository/task_repo.go b/pkg/repository/task_repo.go index 48dd2c7..3369fdb 100644 --- a/pkg/repository/task_repo.go +++ b/pkg/repository/task_repo.go @@ -1,215 +1,50 @@ package repository import ( - "time" - "github.com/d4r1us-drk/clido/pkg/models" ) func (r *Repository) CreateTask(task *models.Task) error { - task.CreationDate = time.Now() - task.LastUpdatedDate = time.Now() - - // Find the lowest unused ID - var id int - err := r.db.QueryRow(` - SELECT COALESCE(MIN(t1.ID + 1), 1) - FROM Tasks t1 - LEFT JOIN Tasks t2 ON t1.ID + 1 = t2.ID - WHERE t2.ID IS NULL`).Scan(&id) - if err != nil { - return err - } - - // Insert the task with the found ID - _, err = r.db.Exec( - `INSERT INTO Tasks (ID, Name, Description, ProjectID, TaskCompleted, DueDate, CompletionDate, CreationDate, - LastUpdatedDate, Priority, ParentTaskID) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, - id, - task.Name, - task.Description, - task.ProjectID, - task.TaskCompleted, - task.DueDate, - task.CompletionDate, - task.CreationDate, - task.LastUpdatedDate, - task.Priority, - task.ParentTaskID, - ) - if err != nil { - return err - } - - task.ID = id - return nil + return r.db.Create(task).Error } func (r *Repository) GetTaskByID(id int) (*models.Task, error) { - task := &models.Task{} - err := r.db.QueryRow( - `SELECT ID, Name, Description, ProjectID, TaskCompleted, DueDate, CompletionDate, CreationDate, LastUpdatedDate, - Priority, ParentTaskID FROM Tasks WHERE ID = ?`, - id, - ). - Scan( - &task.ID, - &task.Name, - &task.Description, - &task.ProjectID, - &task.TaskCompleted, - &task.DueDate, - &task.CompletionDate, - &task.CreationDate, - &task.LastUpdatedDate, - &task.Priority, - &task.ParentTaskID, - ) + var task models.Task + err := r.db.First(&task, id).Error if err != nil { return nil, err } - return task, nil + return &task, nil } func (r *Repository) GetAllTasks() ([]*models.Task, error) { - rows, err := r.db.Query( - `SELECT ID, Name, Description, ProjectID, TaskCompleted, DueDate, CompletionDate, CreationDate, LastUpdatedDate, - Priority, ParentTaskID FROM Tasks`, - ) - if err != nil { - return nil, err - } - defer rows.Close() - var tasks []*models.Task - for rows.Next() { - task := &models.Task{} - err = rows.Scan( - &task.ID, - &task.Name, - &task.Description, - &task.ProjectID, - &task.TaskCompleted, - &task.DueDate, - &task.CompletionDate, - &task.CreationDate, - &task.LastUpdatedDate, - &task.Priority, - &task.ParentTaskID, - ) - if err != nil { - return nil, err - } - tasks = append(tasks, task) - } - - if err = rows.Err(); err != nil { - return nil, err - } - - return tasks, nil + err := r.db.Find(&tasks).Error + return tasks, err } func (r *Repository) GetTasksByProjectID(projectID int) ([]*models.Task, error) { - rows, err := r.db.Query( - `SELECT ID, Name, Description, ProjectID, TaskCompleted, DueDate, CompletionDate, CreationDate, LastUpdatedDate, - Priority, ParentTaskID FROM Tasks WHERE ProjectID = ?`, - projectID, - ) - if err != nil { - return nil, err - } - defer rows.Close() - var tasks []*models.Task - for rows.Next() { - task := &models.Task{} - err = rows.Scan( - &task.ID, - &task.Name, - &task.Description, - &task.ProjectID, - &task.TaskCompleted, - &task.DueDate, - &task.CompletionDate, - &task.CreationDate, - &task.LastUpdatedDate, - &task.Priority, - &task.ParentTaskID, - ) - if err != nil { - return nil, err - } - tasks = append(tasks, task) - } - - if err = rows.Err(); err != nil { - return nil, err - } - - return tasks, nil + err := r.db.Where("project_id = ?", projectID).Find(&tasks).Error + return tasks, err } func (r *Repository) GetSubtasks(parentTaskID int) ([]*models.Task, error) { - rows, err := r.db.Query( - `SELECT ID, Name, Description, ProjectID, TaskCompleted, DueDate, CompletionDate, CreationDate, LastUpdatedDate, - Priority, ParentTaskID FROM Tasks WHERE ParentTaskID = ?`, - parentTaskID, - ) - if err != nil { - return nil, err - } - defer rows.Close() - var tasks []*models.Task - for rows.Next() { - task := &models.Task{} - err = rows.Scan( - &task.ID, - &task.Name, - &task.Description, - &task.ProjectID, - &task.TaskCompleted, - &task.DueDate, - &task.CompletionDate, - &task.CreationDate, - &task.LastUpdatedDate, - &task.Priority, - &task.ParentTaskID, - ) - if err != nil { - return nil, err - } - tasks = append(tasks, task) - } - - if err = rows.Err(); err != nil { - return nil, err - } - - return tasks, nil + err := r.db.Where("parent_task_id = ?", parentTaskID).Find(&tasks).Error + return tasks, err } func (r *Repository) UpdateTask(task *models.Task) error { - task.LastUpdatedDate = time.Now() - _, err := r.db.Exec( - `UPDATE Tasks SET Name = ?, Description = ?, ProjectID = ?, TaskCompleted = ?, DueDate = ?, CompletionDate = ?, - LastUpdatedDate = ?, Priority = ?, ParentTaskID = ? WHERE ID = ?`, - task.Name, - task.Description, - task.ProjectID, - task.TaskCompleted, - task.DueDate, - task.CompletionDate, - task.LastUpdatedDate, - task.Priority, - task.ParentTaskID, - task.ID, - ) - return err + return r.db.Save(task).Error } func (r *Repository) DeleteTask(id int) error { - _, err := r.db.Exec(`DELETE FROM Tasks WHERE ID = ?`, id) - return err + return r.db.Delete(&models.Task{}, id).Error +} + +func (r *Repository) GetNextTaskID() (int, error) { + var maxID int + err := r.db.Model(&models.Task{}).Select("COALESCE(MAX(id), 0) + 1").Scan(&maxID).Error + return maxID, err } From 5ff553fadfbac936d4565af74da420d0d87b13fb Mon Sep 17 00:00:00 2001 From: vhespanha Date: Sat, 17 Aug 2024 22:13:24 -0300 Subject: [PATCH 45/81] Implement automigrate and define migration system --- pkg/repository/migrations.go | 68 ++++++++++++++++++++++++++++++++++++ pkg/repository/repository.go | 18 +++++----- 2 files changed, 78 insertions(+), 8 deletions(-) create mode 100644 pkg/repository/migrations.go diff --git a/pkg/repository/migrations.go b/pkg/repository/migrations.go new file mode 100644 index 0000000..d83b0de --- /dev/null +++ b/pkg/repository/migrations.go @@ -0,0 +1,68 @@ +package repository + +import ( + "github.com/d4r1us-drk/clido/pkg/models" + "gorm.io/gorm" +) + +type Migration struct { + ID uint `gorm:"primaryKey"` + Version string `gorm:"uniqueIndex"` +} + +type Migrator struct { + migrations []struct { + version string + migrate func(*gorm.DB) error + } +} + +func NewMigrator() *Migrator { + return &Migrator{ + migrations: []struct { + version string + migrate func(*gorm.DB) error + }{ + { + version: "1.0", + migrate: func(db *gorm.DB) error { + return db.AutoMigrate(&models.Project{}, &models.Task{}) + }, + }, + + // Example migration for reference: + // { + // version: "1.1", + // migrate: func(db *gorm.DB) error { + // return db.Exec("ALTER TABLE projects ADD COLUMN status VARCHAR(50)").Error + // }, + // }, + }, + } +} + +func (m *Migrator) Migrate(db *gorm.DB) error { + err := db.AutoMigrate(&Migration{}) + if err != nil { + return err + } + + var lastMigration Migration + db.Order("version desc").First(&lastMigration) + + for _, migration := range m.migrations { + if migration.version > lastMigration.Version { + err = migration.migrate(db) + if err != nil { + return err + } + + err = db.Create(&Migration{Version: migration.version}).Error + if err != nil { + return err + } + } + } + + return nil +} diff --git a/pkg/repository/repository.go b/pkg/repository/repository.go index 930556c..77f0556 100644 --- a/pkg/repository/repository.go +++ b/pkg/repository/repository.go @@ -7,13 +7,13 @@ import ( "path/filepath" "runtime" - "github.com/d4r1us-drk/clido/pkg/models" "gorm.io/driver/sqlite" "gorm.io/gorm" ) type Repository struct { - db *gorm.DB + db *gorm.DB + migrator *Migrator } func NewRepository() (*Repository, error) { @@ -27,8 +27,14 @@ func NewRepository() (*Repository, error) { return nil, fmt.Errorf("failed to connect database: %w", err) } - repo := &Repository{db: db} - err = repo.autoMigrate() + migrator := NewMigrator() + + repo := &Repository{ + db: db, + migrator: migrator, + } + + err = repo.migrator.Migrate(repo.db) if err != nil { return nil, fmt.Errorf("failed to run migrations: %w", err) } @@ -61,10 +67,6 @@ func getDBPath() (string, error) { return dbPath, nil } -func (r *Repository) autoMigrate() error { - return r.db.AutoMigrate(&models.Project{}, &models.Task{}) -} - func (r *Repository) Close() error { sqlDB, err := r.db.DB() if err != nil { From a8cdae776c32a9b2bc67820c31a0fd0e9744c639 Mon Sep 17 00:00:00 2001 From: vhespanha Date: Sat, 17 Aug 2024 22:26:21 -0300 Subject: [PATCH 46/81] Add gorm tags on models --- pkg/models/project.go | 14 +++++++------- pkg/models/task.go | 24 ++++++++++++------------ pkg/repository/migrations.go | 1 - 3 files changed, 19 insertions(+), 20 deletions(-) diff --git a/pkg/models/project.go b/pkg/models/project.go index 3023277..4eec0cc 100644 --- a/pkg/models/project.go +++ b/pkg/models/project.go @@ -7,15 +7,15 @@ import ( ) type Project struct { - ID int `gorm:"primaryKey" json:"id"` - Name string `gorm:"unique;not null" json:"name"` - Description string `json:"description"` - CreationDate time.Time `gorm:"not null" json:"creation_date"` - LastModifiedDate time.Time `gorm:"not null" json:"last_modified_date"` - ParentProjectID *int `json:"parent_project_id,omitempty"` + ID int `gorm:"primaryKey" json:"id"` + Name string `gorm:"unique;not null" json:"name"` + Description string ` json:"description"` + CreationDate time.Time `gorm:"not null" json:"creation_date"` + LastModifiedDate time.Time `gorm:"not null" json:"last_modified_date"` + ParentProjectID *int ` json:"parent_project_id,omitempty"` ParentProject *Project `gorm:"foreignKey:ParentProjectID" json:"-"` SubProjects []Project `gorm:"foreignKey:ParentProjectID" json:"-"` - Tasks []Task `gorm:"foreignKey:ProjectID" json:"-"` + Tasks []Task `gorm:"foreignKey:ProjectID" json:"-"` } func (p *Project) BeforeCreate(_ *gorm.DB) error { diff --git a/pkg/models/task.go b/pkg/models/task.go index f5d07d4..ace33fd 100644 --- a/pkg/models/task.go +++ b/pkg/models/task.go @@ -8,18 +8,18 @@ import ( ) type Task struct { - ID int `gorm:"primaryKey" json:"id"` - Name string `gorm:"not null" json:"name"` - Description string `json:"description"` - ProjectID int `gorm:"not null" json:"project_id"` - Project Project `gorm:"foreignKey:ProjectID" json:"-"` - TaskCompleted bool `gorm:"not null" json:"task_completed"` - DueDate *time.Time `json:"due_date,omitempty"` - CompletionDate *time.Time `json:"completion_date,omitempty"` - CreationDate time.Time `gorm:"not null" json:"creation_date"` - LastUpdatedDate time.Time `gorm:"not null" json:"last_updated_date"` - Priority utils.Priority `gorm:"not null;default:4" json:"priority"` - ParentTaskID *int `json:"parent_task_id,omitempty"` + ID int `gorm:"primaryKey" json:"id"` + Name string `gorm:"not null" json:"name"` + Description string ` json:"description"` + ProjectID int `gorm:"not null" json:"project_id"` + Project Project `gorm:"foreignKey:ProjectID" json:"-"` + TaskCompleted bool `gorm:"not null" json:"task_completed"` + DueDate *time.Time ` json:"due_date,omitempty"` + CompletionDate *time.Time ` json:"completion_date,omitempty"` + CreationDate time.Time `gorm:"not null" json:"creation_date"` + LastUpdatedDate time.Time `gorm:"not null" json:"last_updated_date"` + Priority utils.Priority `gorm:"not null;default:4" json:"priority"` + ParentTaskID *int ` json:"parent_task_id,omitempty"` ParentTask *Task `gorm:"foreignKey:ParentTaskID" json:"-"` SubTasks []Task `gorm:"foreignKey:ParentTaskID" json:"-"` } diff --git a/pkg/repository/migrations.go b/pkg/repository/migrations.go index d83b0de..84c3eaf 100644 --- a/pkg/repository/migrations.go +++ b/pkg/repository/migrations.go @@ -29,7 +29,6 @@ func NewMigrator() *Migrator { return db.AutoMigrate(&models.Project{}, &models.Task{}) }, }, - // Example migration for reference: // { // version: "1.1", From 245f2ca97989f3db40796af42467bf415d0128fc Mon Sep 17 00:00:00 2001 From: vhespanha Date: Sat, 17 Aug 2024 22:26:29 -0300 Subject: [PATCH 47/81] Formatting --- cmd/list.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cmd/list.go b/cmd/list.go index 97894ef..a4c2415 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -99,7 +99,10 @@ func listTasks(repo *repository.Repository, projectFilter string, outputJSON boo } } -func getTasks(repo *repository.Repository, projectFilter string) ([]*models.Task, *models.Project, error) { +func getTasks( + repo *repository.Repository, + projectFilter string, +) ([]*models.Task, *models.Project, error) { if projectFilter == "" { tasks, err := repo.GetAllTasks() return tasks, nil, err From 4c42c84b7a7d8b303a44041010831ac1b289e520 Mon Sep 17 00:00:00 2001 From: Clay Gomera Date: Tue, 20 Aug 2024 13:29:58 -0400 Subject: [PATCH 48/81] Updated --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index cf97dbd..c301d2a 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,7 @@ go.work.sum # clido binaries clido -build/* \ No newline at end of file +build/* + +# goland +.idea/ \ No newline at end of file From 8ee78f75a9de7d5effc13d3143365611162d2f5e Mon Sep 17 00:00:00 2001 From: Lian Drake Date: Fri, 23 Aug 2024 15:04:53 -0400 Subject: [PATCH 49/81] Implemented custom GORM logger to supress verbose output --- pkg/repository/repository.go | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/pkg/repository/repository.go b/pkg/repository/repository.go index 77f0556..ffd9279 100644 --- a/pkg/repository/repository.go +++ b/pkg/repository/repository.go @@ -3,11 +3,14 @@ package repository import ( "errors" "fmt" + "log" "os" "path/filepath" "runtime" + "time" "gorm.io/driver/sqlite" + "gorm.io/gorm/logger" "gorm.io/gorm" ) @@ -22,7 +25,20 @@ func NewRepository() (*Repository, error) { return nil, err } - db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{}) + // Custom logger for GORM, we use this to disable GORM's verbose messages + newLogger := logger.New( + log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer + logger.Config{ + SlowThreshold: time.Second, // Slow SQL threshold + LogLevel: logger.Silent, // Log level + IgnoreRecordNotFoundError: true, // Ignore ErrRecordNotFound error for logger + Colorful: false, // Disable color + }, + ) + + db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{ + Logger: newLogger, + }) if err != nil { return nil, fmt.Errorf("failed to connect database: %w", err) } From 5773ffef53e18529098906c96788e9950b474d86 Mon Sep 17 00:00:00 2001 From: Lian Drake Date: Fri, 23 Aug 2024 15:05:39 -0400 Subject: [PATCH 50/81] Removed unnecesary config package, functionality is already present in pkg/repository/repository.go --- pkg/config/config.go | 42 ------------------------------------------ 1 file changed, 42 deletions(-) delete mode 100644 pkg/config/config.go diff --git a/pkg/config/config.go b/pkg/config/config.go deleted file mode 100644 index 75197f9..0000000 --- a/pkg/config/config.go +++ /dev/null @@ -1,42 +0,0 @@ -package config - -import ( - "os" - "path/filepath" - "runtime" -) - -type Config struct { - DBPath string -} - -func NewConfig() *Config { - return &Config{ - DBPath: getDBPath(), - } -} - -func getDBPath() string { - var dbPath string - - if runtime.GOOS == "windows" { - appDataPath := os.Getenv("APPDATA") - if appDataPath == "" { - panic("APPDATA environment variable is not set") - } - dbPath = filepath.Join(appDataPath, "clido", "data.db") - } else { - homePath := os.Getenv("HOME") - if homePath == "" { - panic("HOME environment variable is not set") - } - dbPath = filepath.Join(homePath, ".local", "share", "clido", "data.db") - } - - dbDir := filepath.Dir(dbPath) - if err := os.MkdirAll(dbDir, 0o755); err != nil { - panic(err) - } - - return dbPath -} From 128f3e87fb24b7778e6d3d796e3318ee265fccfd Mon Sep 17 00:00:00 2001 From: Lian Drake Date: Mon, 9 Sep 2024 19:43:46 -0400 Subject: [PATCH 51/81] Fixed linter install erros in Makefile --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 8b8bb6e..c0f16fb 100644 --- a/Makefile +++ b/Makefile @@ -48,7 +48,7 @@ deps: fi ifndef GOLANGCI_LINT @echo "Installing golangci-lint..." - @curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.60.1 + @curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(shell go env GOPATH)/bin v1.60.1 endif ifndef GOFUMPT @echo "Installing gofumpt..." From 896942abb2ad07745e790234f6cbe6f9f4469e27 Mon Sep 17 00:00:00 2001 From: Lian Drake Date: Mon, 9 Sep 2024 19:44:21 -0400 Subject: [PATCH 52/81] Refactored completion.go to use cobra's built in commands instead of fmt --- cmd/completion.go | 78 ++++++++++++++++++++++++++--------------------- 1 file changed, 43 insertions(+), 35 deletions(-) diff --git a/cmd/completion.go b/cmd/completion.go index b4340a6..f980ca3 100644 --- a/cmd/completion.go +++ b/cmd/completion.go @@ -1,16 +1,16 @@ package cmd import ( - "fmt" "os" "github.com/spf13/cobra" ) -var completionCmd = &cobra.Command{ - Use: "completion [bash|zsh|fish|powershell]", - Short: "Generate completion script", - Long: `To load completions: +func NewCompletionCmd() *cobra.Command { + return &cobra.Command{ + Use: "completion [bash|zsh|fish|powershell]", + Short: "Generate completion script", + Long: `To load completions: Bash: @@ -50,36 +50,44 @@ PowerShell: # and source this file from your PowerShell profile. `, - DisableFlagsInUseLine: true, - ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, - Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), - - Run: func(cmd *cobra.Command, args []string) { - switch args[0] { - - case "bash": - if err := cmd.Root().GenBashCompletion(os.Stdout); err != nil { - panic(fmt.Errorf("error generating bash completion: %w", err)) + DisableFlagsInUseLine: true, + ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, + Args: func(cmd *cobra.Command, args []string) error { + if len(args) != 1 { + cmd.PrintErrln( + "Error: requires exactly one argument: bash, zsh, fish, or powershell", + ) + return cobra.NoArgs(cmd, args) } - - case "zsh": - if err := cmd.Root().GenZshCompletion(os.Stdout); err != nil { - panic(fmt.Errorf("error generating ZSH completion: %w", err)) - } - - case "fish": - if err := cmd.Root().GenFishCompletion(os.Stdout, true); err != nil { - panic(fmt.Errorf("error generating Fish completion: %w", err)) + return cobra.OnlyValidArgs(cmd, args) + }, + + Run: func(cmd *cobra.Command, args []string) { + switch args[0] { + case "bash": + if err := cmd.Root().GenBashCompletion(os.Stdout); err != nil { + cmd.PrintErrf("Error generating bash completion: %v\n", err) + os.Exit(1) + } + + case "zsh": + if err := cmd.Root().GenZshCompletion(os.Stdout); err != nil { + cmd.PrintErrf("Error generating zsh completion: %v\n", err) + os.Exit(1) + } + + case "fish": + if err := cmd.Root().GenFishCompletion(os.Stdout, true); err != nil { + cmd.PrintErrf("Error generating fish completion: %v\n", err) + os.Exit(1) + } + + case "powershell": + if err := cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout); err != nil { + cmd.PrintErrf("Error generating PowerShell completion: %v\n", err) + os.Exit(1) + } } - - case "powershell": - if err := cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout); err != nil { - panic(fmt.Errorf("error generating PowerShell completion: %w", err)) - } - } - }, -} - -func init() { - rootCmd.AddCommand(completionCmd) + }, + } } From 921701dee2731b942c3d877a086e406b484102c2 Mon Sep 17 00:00:00 2001 From: Lian Drake Date: Mon, 9 Sep 2024 19:44:34 -0400 Subject: [PATCH 53/81] Refactored edit.go to use cobra's built in commands instead of fmt --- cmd/edit.go | 102 ++++++++++++++++++++++++++-------------------------- 1 file changed, 52 insertions(+), 50 deletions(-) diff --git a/cmd/edit.go b/cmd/edit.go index 401af6c..b1a1b4b 100644 --- a/cmd/edit.go +++ b/cmd/edit.go @@ -1,7 +1,6 @@ package cmd import ( - "fmt" "strconv" "time" @@ -11,55 +10,58 @@ import ( "github.com/spf13/cobra" ) -var editCmd = &cobra.Command{ - Use: "edit [project|task] ", - Short: "Edit an existing project or task", - Long: `Edit the details of an existing project or task identified by its ID.`, - Run: func(cmd *cobra.Command, args []string) { - if len(args) < MinArgsLength { - fmt.Println("Insufficient arguments. Use 'edit project ' or 'edit task '.") - return - } +// NewEditCmd creates and returns the edit command. +func NewEditCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "edit [project|task] ", + Short: "Edit an existing project or task", + Long: `Edit the details of an existing project or task identified by its ID.`, + Run: func(cmd *cobra.Command, args []string) { + if len(args) < MinArgsLength { + cmd.Println("Insufficient arguments. Use 'edit project ' or 'edit task '.") + return + } - repo, err := repository.NewRepository() - if err != nil { - fmt.Printf("Error initializing repository: %v\n", err) - return - } - defer repo.Close() + repo, err := repository.NewRepository() + if err != nil { + cmd.Printf("Error initializing repository: %v\n", err) + return + } + defer repo.Close() - id, err := strconv.Atoi(args[1]) - if err != nil { - fmt.Println("Invalid ID. Please provide a numeric ID.") - return - } + id, err := strconv.Atoi(args[1]) + if err != nil { + cmd.Println("Invalid ID. Please provide a numeric ID.") + return + } - switch args[0] { - case "project": - editProject(cmd, repo, id) - case "task": - editTask(cmd, repo, id) - default: - fmt.Println("Invalid option. Use 'edit project ' or 'edit task '.") - } - }, -} + switch args[0] { + case "project": + editProject(cmd, repo, id) + case "task": + editTask(cmd, repo, id) + default: + cmd.Println("Invalid option. Use 'edit project ' or 'edit task '.") + } + }, + } -func init() { - rootCmd.AddCommand(editCmd) + // Define flags for the edit command + cmd.Flags().StringP("name", "n", "", "New name") + cmd.Flags().StringP("description", "d", "", "New description") + cmd.Flags().StringP("project", "p", "", "New parent project name or ID") + cmd.Flags().StringP("task", "t", "", "New parent task ID for subtasks") + cmd.Flags().StringP("due", "D", "", "New due date for task (format: YYYY-MM-DD HH:MM)") + cmd.Flags(). + IntP("priority", "P", 0, "New priority for task (1: High, 2: Medium, 3: Low, 4: None)") - editCmd.Flags().StringP("name", "n", "", "New name") - editCmd.Flags().StringP("description", "d", "", "New description") - editCmd.Flags().StringP("project", "p", "", "New parent project name or ID") - editCmd.Flags().StringP("task", "t", "", "New parent task ID for subtasks") - editCmd.Flags().StringP("due", "D", "", "New due date for task (format: YYYY-MM-DD HH:MM)") - editCmd.Flags().IntP("priority", "P", 0, "New priority for task (1: High, 2: Medium, 3: Low, 4: None)") + return cmd } func editProject(cmd *cobra.Command, repo *repository.Repository, id int) { project, err := repo.GetProjectByID(id) if err != nil { - fmt.Printf("Error retrieving project: %v\n", err) + cmd.Printf("Error retrieving project: %v\n", err) return } @@ -81,7 +83,7 @@ func editProject(cmd *cobra.Command, repo *repository.Repository, id int) { var parentProject *models.Project parentProject, err = repo.GetProjectByName(parentProjectIdentifier) if err != nil || parentProject == nil { - fmt.Printf("Parent project '%s' not found.\n", parentProjectIdentifier) + cmd.Printf("Parent project '%s' not found.\n", parentProjectIdentifier) return } project.ParentProjectID = &parentProject.ID @@ -90,17 +92,17 @@ func editProject(cmd *cobra.Command, repo *repository.Repository, id int) { err = repo.UpdateProject(project) if err != nil { - fmt.Printf("Error updating project: %v\n", err) + cmd.Printf("Error updating project: %v\n", err) return } - fmt.Printf("Project '%s' updated successfully.\n", project.Name) + cmd.Printf("Project '%s' updated successfully.\n", project.Name) } func editTask(cmd *cobra.Command, repo *repository.Repository, id int) { task, err := repo.GetTaskByID(id) if err != nil { - fmt.Printf("Error retrieving task: %v\n", err) + cmd.Printf("Error retrieving task: %v\n", err) return } @@ -122,14 +124,14 @@ func editTask(cmd *cobra.Command, repo *repository.Repository, id int) { if err == nil { task.DueDate = &parsedDate } else { - fmt.Println("Invalid date format. Keeping the existing due date.") + cmd.Println("Invalid date format. Keeping the existing due date.") } } if priority != 0 { if priority >= 1 && priority <= 4 { task.Priority = utils.Priority(priority) } else { - fmt.Println("Invalid priority. Keeping the existing priority.") + cmd.Println("Invalid priority. Keeping the existing priority.") } } if parentTaskIdentifier != "" { @@ -137,19 +139,19 @@ func editTask(cmd *cobra.Command, repo *repository.Repository, id int) { parentID, _ := strconv.Atoi(parentTaskIdentifier) task.ParentTaskID = &parentID } else { - fmt.Println("Parent task must be identified by a numeric ID.") + cmd.Println("Parent task must be identified by a numeric ID.") return } } err = repo.UpdateTask(task) if err != nil { - fmt.Printf("Error updating task: %v\n", err) + cmd.Printf("Error updating task: %v\n", err) return } - fmt.Printf("Task '%s' updated successfully.\n", task.Name) - fmt.Printf("New details: Priority: %s, Due Date: %s\n", + cmd.Printf("Task '%s' updated successfully.\n", task.Name) + cmd.Printf("New details: Priority: %s, Due Date: %s\n", utils.GetPriorityString(task.Priority), utils.FormatDate(task.DueDate)) } From 186f51190cdf8a88cdf40eb7af7f2b58d78c40e8 Mon Sep 17 00:00:00 2001 From: Lian Drake Date: Mon, 9 Sep 2024 19:44:43 -0400 Subject: [PATCH 54/81] Refactored list.go to use cobra's built in commands instead of fmt --- cmd/list.go | 188 ++++++++++++++++++++++++++++------------------------ 1 file changed, 103 insertions(+), 85 deletions(-) diff --git a/cmd/list.go b/cmd/list.go index a4c2415..4fda5f0 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -2,7 +2,6 @@ package cmd import ( "encoding/json" - "fmt" "os" "strconv" "strings" @@ -14,49 +13,52 @@ import ( "github.com/spf13/cobra" ) -var listCmd = &cobra.Command{ - Use: "list [projects|tasks]", - Short: "List projects or tasks", - Long: `List all projects or tasks, optionally filtered by project for tasks.`, - Run: func(cmd *cobra.Command, args []string) { - if len(args) < 1 { - fmt.Println("Insufficient arguments. Use 'list projects' or 'list tasks'.") - return - } +// NewListCmd creates and returns the list command. +func NewListCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "list [projects|tasks]", + Short: "List projects or tasks", + Long: `List all projects or tasks, optionally filtered by project for tasks.`, + Run: func(cmd *cobra.Command, args []string) { + if len(args) < 1 { + cmd.Println("Insufficient arguments. Use 'list projects' or 'list tasks'.") + return + } - repo, err := repository.NewRepository() - if err != nil { - fmt.Printf("Error initializing repository: %v\n", err) - return - } - defer repo.Close() - - outputJSON, _ := cmd.Flags().GetBool("json") - treeView, _ := cmd.Flags().GetBool("tree") - - switch args[0] { - case "projects": - listProjects(repo, outputJSON, treeView) - case "tasks": - projectFilter, _ := cmd.Flags().GetString("project") - listTasks(repo, projectFilter, outputJSON, treeView) - default: - fmt.Println("Invalid option. Use 'list projects' or 'list tasks'.") - } - }, -} + repo, err := repository.NewRepository() + if err != nil { + cmd.Printf("Error initializing repository: %v\n", err) + return + } + defer repo.Close() + + outputJSON, _ := cmd.Flags().GetBool("json") + treeView, _ := cmd.Flags().GetBool("tree") + + switch args[0] { + case "projects": + listProjects(cmd, repo, outputJSON, treeView) + case "tasks": + projectFilter, _ := cmd.Flags().GetString("project") + listTasks(cmd, repo, projectFilter, outputJSON, treeView) + default: + cmd.Println("Invalid option. Use 'list projects' or 'list tasks'.") + } + }, + } -func init() { - rootCmd.AddCommand(listCmd) - listCmd.Flags().StringP("project", "p", "", "Filter tasks by project name or ID") - listCmd.Flags().BoolP("json", "j", false, "Output list in JSON format") - listCmd.Flags().BoolP("tree", "t", false, "Display projects or tasks in a tree-like structure") + // Define flags for the list command + cmd.Flags().StringP("project", "p", "", "Filter tasks by project name or ID") + cmd.Flags().BoolP("json", "j", false, "Output list in JSON format") + cmd.Flags().BoolP("tree", "t", false, "Display projects or tasks in a tree-like structure") + + return cmd } -func listProjects(repo *repository.Repository, outputJSON bool, treeView bool) { +func listProjects(cmd *cobra.Command, repo *repository.Repository, outputJSON bool, treeView bool) { projects, err := repo.GetAllProjects() if err != nil { - fmt.Printf("Error listing projects: %v\n", err) + cmd.Printf("Error listing projects: %v\n", err) return } @@ -65,35 +67,41 @@ func listProjects(repo *repository.Repository, outputJSON bool, treeView bool) { var jsonData []byte jsonData, err = json.MarshalIndent(projects, "", " ") if err != nil { - fmt.Printf("Error marshalling projects to JSON: %v\n", err) + cmd.Printf("Error marshalling projects to JSON: %v\n", err) return } - fmt.Println(string(jsonData)) + cmd.Println(string(jsonData)) case treeView: - printProjectTree(projects, nil, 0) + printProjectTree(cmd, projects, nil, 0) default: - printProjectTable(repo, projects) + printProjectTable(cmd, repo, projects) } } -func listTasks(repo *repository.Repository, projectFilter string, outputJSON bool, treeView bool) { +func listTasks( + cmd *cobra.Command, + repo *repository.Repository, + projectFilter string, + outputJSON bool, + treeView bool, +) { tasks, project, err := getTasks(repo, projectFilter) if err != nil { - fmt.Println(err) + cmd.Println(err) return } if !outputJSON { - printTaskHeader(project) + printTaskHeader(cmd, project) } switch { case outputJSON: - printTasksJSON(tasks) + printTasksJSON(cmd, tasks) case treeView: - printTaskTree(tasks, nil, 0) + printTaskTree(cmd, tasks, nil, 0) default: printTaskTable(repo, tasks) } @@ -129,30 +137,34 @@ func getProject(repo *repository.Repository, projectFilter string) (*models.Proj } if err != nil || project == nil { - return nil, fmt.Errorf("project '%s' not found", projectFilter) + return nil, err } return project, nil } -func printTaskHeader(project *models.Project) { +func printTaskHeader(cmd *cobra.Command, project *models.Project) { if project != nil { - fmt.Printf("Tasks in project '%s':\n", project.Name) + cmd.Printf("Tasks in project '%s':\n", project.Name) } else { - fmt.Println("All Tasks:") + cmd.Println("All Tasks:") } } -func printTasksJSON(tasks []*models.Task) { +func printTasksJSON(cmd *cobra.Command, tasks []*models.Task) { jsonData, err := json.MarshalIndent(tasks, "", " ") if err != nil { - fmt.Printf("Error marshalling tasks to JSON: %v\n", err) + cmd.Printf("Error marshalling tasks to JSON: %v\n", err) return } - fmt.Println(string(jsonData)) + cmd.Println(string(jsonData)) } -func printProjectTable(repo *repository.Repository, projects []*models.Project) { +func printProjectTable( + cmd *cobra.Command, + repo *repository.Repository, + projects []*models.Project, +) { table := tablewriter.NewWriter(os.Stdout) table.SetHeader([]string{"ID", "Name", "Description", "Type", "Child Of"}) table.SetRowLine(true) @@ -182,7 +194,7 @@ func printProjectTable(repo *repository.Repository, projects []*models.Project) }) } - fmt.Println("Projects:") + cmd.Println("Projects:") table.Render() } @@ -232,6 +244,32 @@ func printTaskTable(repo *repository.Repository, tasks []*models.Task) { table.Render() } +func printProjectTree(cmd *cobra.Command, projects []*models.Project, parentID *int, level int) { + nodes := make([]TreeNode, len(projects)) + for i, p := range projects { + nodes[i] = ProjectNode{p} + } + printTree(cmd, nodes, parentID, level, nil) +} + +func printTaskTree(cmd *cobra.Command, tasks []*models.Task, parentID *int, level int) { + nodes := make([]TreeNode, len(tasks)) + for i, t := range tasks { + nodes[i] = TaskNode{t} + } + printTree(cmd, nodes, parentID, level, func(node TreeNode, indent string) { + task := node.(TaskNode).Task + cmd.Printf("%sDescription: %s\n", indent, task.Description) + cmd.Printf( + "%sDue Date: %s, Completed: %v, Priority: %s\n", + indent, + utils.FormatDate(task.DueDate), + task.TaskCompleted, + utils.GetPriorityString(task.Priority), + ) + }) +} + type TreeNode interface { GetID() int GetParentID() *int @@ -254,7 +292,13 @@ func (t TaskNode) GetID() int { return t.ID } func (t TaskNode) GetParentID() *int { return t.ParentTaskID } func (t TaskNode) GetName() string { return t.Name } -func printTree(nodes []TreeNode, parentID *int, level int, printDetails func(TreeNode, string)) { +func printTree( + cmd *cobra.Command, + nodes []TreeNode, + parentID *int, + level int, + printDetails func(TreeNode, string), +) { indent := strings.Repeat("│ ", level) for i, node := range nodes { if (parentID == nil && node.GetParentID() == nil) || @@ -263,38 +307,12 @@ func printTree(nodes []TreeNode, parentID *int, level int, printDetails func(Tre if i == len(nodes)-1 { prefix = "└──" } - fmt.Printf("%s%s %s (ID: %d)\n", indent, prefix, node.GetName(), node.GetID()) + cmd.Printf("%s%s %s (ID: %d)\n", indent, prefix, node.GetName(), node.GetID()) if printDetails != nil { printDetails(node, indent+" ") } nodeID := node.GetID() - printTree(nodes, &nodeID, level+1, printDetails) + printTree(cmd, nodes, &nodeID, level+1, printDetails) } } } - -func printProjectTree(projects []*models.Project, parentID *int, level int) { - nodes := make([]TreeNode, len(projects)) - for i, p := range projects { - nodes[i] = ProjectNode{p} - } - printTree(nodes, parentID, level, nil) -} - -func printTaskTree(tasks []*models.Task, parentID *int, level int) { - nodes := make([]TreeNode, len(tasks)) - for i, t := range tasks { - nodes[i] = TaskNode{t} - } - printTree(nodes, parentID, level, func(node TreeNode, indent string) { - task := node.(TaskNode).Task - fmt.Printf("%sDescription: %s\n", indent, task.Description) - fmt.Printf( - "%sDue Date: %s, Completed: %v, Priority: %s\n", - indent, - utils.FormatDate(task.DueDate), - task.TaskCompleted, - utils.GetPriorityString(task.Priority), - ) - }) -} From b2defa5149e6d8fd77e7c377cb63a4a0535d5e50 Mon Sep 17 00:00:00 2001 From: Lian Drake Date: Mon, 9 Sep 2024 19:44:54 -0400 Subject: [PATCH 55/81] Refactored remove.go to use cobra's built in commands instead of fmt --- cmd/remove.go | 88 ++++++++++++++++++++++++++------------------------- 1 file changed, 45 insertions(+), 43 deletions(-) diff --git a/cmd/remove.go b/cmd/remove.go index 79fb35a..53db397 100644 --- a/cmd/remove.go +++ b/cmd/remove.go @@ -1,90 +1,92 @@ package cmd import ( - "fmt" "strconv" "github.com/d4r1us-drk/clido/pkg/repository" "github.com/spf13/cobra" ) -var removeCmd = &cobra.Command{ - Use: "remove [project|task] ", - Short: "Remove a project or task along with all its subprojects or subtasks", - Long: `Remove an existing project or task identified by its ID. This will also remove all associated subprojects +// NewRemoveCmd creates and returns the remove command. +func NewRemoveCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "remove [project|task] ", + Short: "Remove a project or task along with all its subprojects or subtasks", + Long: `Remove an existing project or task identified by its ID. This will also remove all associated subprojects or subtasks.`, - Run: func(cmd *cobra.Command, args []string) { - if len(args) < MinArgsLength { - fmt.Println("Insufficient arguments. Use 'remove project ' or 'remove task '.") - return - } + Run: func(cmd *cobra.Command, args []string) { + if len(args) < MinArgsLength { + cmd.Println( + "Insufficient arguments. Use 'remove project ' or 'remove task '.", + ) + return + } - repo, err := repository.NewRepository() - if err != nil { - fmt.Printf("Error initializing repository: %v\n", err) - return - } - defer repo.Close() + repo, err := repository.NewRepository() + if err != nil { + cmd.Printf("Error initializing repository: %v\n", err) + return + } + defer repo.Close() - id, err := strconv.Atoi(args[1]) - if err != nil { - fmt.Println("Invalid ID. Please provide a numeric ID.") - return - } + id, err := strconv.Atoi(args[1]) + if err != nil { + cmd.Println("Invalid ID. Please provide a numeric ID.") + return + } - switch args[0] { - case "project": - removeProject(repo, id) - case "task": - removeTask(repo, id) - default: - fmt.Println("Invalid option. Use 'remove project ' or 'remove task '.") - } - }, -} + switch args[0] { + case "project": + removeProject(cmd, repo, id) + case "task": + removeTask(cmd, repo, id) + default: + cmd.Println("Invalid option. Use 'remove project ' or 'remove task '.") + } + }, + } -func init() { - rootCmd.AddCommand(removeCmd) + return cmd } -func removeProject(repo *repository.Repository, id int) { +func removeProject(cmd *cobra.Command, repo *repository.Repository, id int) { // First remove all subprojects subprojects, err := repo.GetSubprojects(id) if err != nil { - fmt.Printf("Error retrieving subprojects: %v\n", err) + cmd.Printf("Error retrieving subprojects: %v\n", err) return } for _, subproject := range subprojects { - removeProject(repo, subproject.ID) + removeProject(cmd, repo, subproject.ID) } // Now remove the parent project err = repo.DeleteProject(id) if err != nil { - fmt.Printf("Error removing project: %v\n", err) + cmd.Printf("Error removing project: %v\n", err) return } - fmt.Printf("Project (ID: %d) and all its subprojects removed successfully.\n", id) + cmd.Printf("Project (ID: %d) and all its subprojects removed successfully.\n", id) } -func removeTask(repo *repository.Repository, id int) { +func removeTask(cmd *cobra.Command, repo *repository.Repository, id int) { // First remove all subtasks subtasks, err := repo.GetSubtasks(id) if err != nil { - fmt.Printf("Error retrieving subtasks: %v\n", err) + cmd.Printf("Error retrieving subtasks: %v\n", err) return } for _, subtask := range subtasks { - removeTask(repo, subtask.ID) + removeTask(cmd, repo, subtask.ID) } // Now remove the parent task err = repo.DeleteTask(id) if err != nil { - fmt.Printf("Error removing task: %v\n", err) + cmd.Printf("Error removing task: %v\n", err) return } - fmt.Printf("Task (ID: %d) and all its subtasks removed successfully.\n", id) + cmd.Printf("Task (ID: %d) and all its subtasks removed successfully.\n", id) } From 6287dd41e16d5aaa80821947227fce7ea5a6d4b7 Mon Sep 17 00:00:00 2001 From: Lian Drake Date: Mon, 9 Sep 2024 19:45:05 -0400 Subject: [PATCH 56/81] Refactored toggle.go to use cobra's built in commands instead of fmt --- cmd/toggle.go | 68 ++++++++++++++++++++++++++------------------------- 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/cmd/toggle.go b/cmd/toggle.go index b1198cd..18f1992 100644 --- a/cmd/toggle.go +++ b/cmd/toggle.go @@ -1,7 +1,6 @@ package cmd import ( - "fmt" "strconv" "time" @@ -9,43 +8,46 @@ import ( "github.com/spf13/cobra" ) -var toggleCmd = &cobra.Command{ - Use: "toggle ", - Short: "Toggle task completion status", - Long: `Toggle the completion status of a task identified by its ID.`, - Run: func(cmd *cobra.Command, args []string) { - if len(args) < 1 { - fmt.Println("Insufficient arguments. Use 'toggle '.") - return - } +// NewToggleCmd creates and returns the toggle command. +func NewToggleCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "toggle ", + Short: "Toggle task completion status", + Long: `Toggle the completion status of a task identified by its ID.`, + Run: func(cmd *cobra.Command, args []string) { + if len(args) < 1 { + cmd.Println("Insufficient arguments. Use 'toggle '.") + return + } - repo, err := repository.NewRepository() - if err != nil { - fmt.Printf("Error initializing repository: %v\n", err) - return - } - defer repo.Close() + repo, err := repository.NewRepository() + if err != nil { + cmd.Printf("Error initializing repository: %v\n", err) + return + } + defer repo.Close() - id, err := strconv.Atoi(args[0]) - if err != nil { - fmt.Println("Invalid task ID. Please provide a numeric ID.") - return - } + id, err := strconv.Atoi(args[0]) + if err != nil { + cmd.Println("Invalid task ID. Please provide a numeric ID.") + return + } - recursive, _ := cmd.Flags().GetBool("recursive") - toggleTask(repo, id, recursive) - }, -} + recursive, _ := cmd.Flags().GetBool("recursive") + toggleTask(cmd, repo, id, recursive) + }, + } + + // Add flag for recursive toggle + cmd.Flags().BoolP("recursive", "r", false, "Recursively toggle subtasks") -func init() { - rootCmd.AddCommand(toggleCmd) - toggleCmd.Flags().BoolP("recursive", "r", false, "Recursively toggle subtasks") + return cmd } -func toggleTask(repo *repository.Repository, id int, recursive bool) { +func toggleTask(cmd *cobra.Command, repo *repository.Repository, id int, recursive bool) { task, err := repo.GetTaskByID(id) if err != nil { - fmt.Printf("Error retrieving task: %v\n", err) + cmd.Printf("Error retrieving task: %v\n", err) return } @@ -60,7 +62,7 @@ func toggleTask(repo *repository.Repository, id int, recursive bool) { err = repo.UpdateTask(task) if err != nil { - fmt.Printf("Error updating task: %v\n", err) + cmd.Printf("Error updating task: %v\n", err) return } @@ -69,12 +71,12 @@ func toggleTask(repo *repository.Repository, id int, recursive bool) { status = "uncompleted" } - fmt.Printf("Task '%s' (ID: %d) marked as %s.\n", task.Name, id, status) + cmd.Printf("Task '%s' (ID: %d) marked as %s.\n", task.Name, id, status) if recursive { subtasks, _ := repo.GetSubtasks(id) for _, subtask := range subtasks { - toggleTask(repo, subtask.ID, recursive) + toggleTask(cmd, repo, subtask.ID, recursive) } } } From 6114c9c5ca4e8af24f10b41bbc5b2e3ebff3631f Mon Sep 17 00:00:00 2001 From: Lian Drake Date: Mon, 9 Sep 2024 19:45:23 -0400 Subject: [PATCH 57/81] Refactored new.go to use cobra's built in commands instead of fmt --- cmd/new.go | 94 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 48 insertions(+), 46 deletions(-) diff --git a/cmd/new.go b/cmd/new.go index 30f2f1c..7414f06 100644 --- a/cmd/new.go +++ b/cmd/new.go @@ -2,7 +2,6 @@ package cmd import ( "errors" - "fmt" "strconv" "time" @@ -17,43 +16,46 @@ var ( ErrNoDueDate = errors.New("no due date specified") ) -var newCmd = &cobra.Command{ - Use: "new [project|task]", - Short: "Create a new project or task", - Long: `Create a new project or task with the specified details.`, - Run: func(cmd *cobra.Command, args []string) { - if len(args) < 1 { - fmt.Println("Insufficient arguments. Use 'new project' or 'new task'.") - return - } +// NewNewCmd creates and returns the new command. +func NewNewCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "new [project|task]", + Short: "Create a new project or task", + Long: `Create a new project or task with the specified details.`, + Run: func(cmd *cobra.Command, args []string) { + if len(args) < 1 { + cmd.Println("Insufficient arguments. Use 'new project' or 'new task'.") + return + } - repo, err := repository.NewRepository() - if err != nil { - fmt.Printf("Error initializing repository: %v\n", err) - return - } - defer repo.Close() - - switch args[0] { - case "project": - createProject(cmd, repo) - case "task": - createTask(cmd, repo) - default: - fmt.Println("Invalid option. Use 'new project' or 'new task'.") - } - }, -} + repo, err := repository.NewRepository() + if err != nil { + cmd.Printf("Error initializing repository: %v\n", err) + return + } + defer repo.Close() + + switch args[0] { + case "project": + createProject(cmd, repo) + case "task": + createTask(cmd, repo) + default: + cmd.Println("Invalid option. Use 'new project' or 'new task'.") + } + }, + } -func init() { - rootCmd.AddCommand(newCmd) + // Define flags for the new command + cmd.Flags().StringP("name", "n", "", "Name of the project or task") + cmd.Flags().StringP("description", "d", "", "Description of the project or task") + cmd.Flags().StringP("project", "p", "", "Parent project name or ID for subprojects or tasks") + cmd.Flags().StringP("task", "t", "", "Parent task ID for subtasks") + cmd.Flags().StringP("due", "D", "", "Due date for the task (format: YYYY-MM-DD HH:MM)") + cmd.Flags(). + IntP("priority", "P", 0, "Priority of the task (1: High, 2: Medium, 3: Low, 4: None)") - newCmd.Flags().StringP("name", "n", "", "Name of the project or task") - newCmd.Flags().StringP("description", "d", "", "Description of the project or task") - newCmd.Flags().StringP("project", "p", "", "Parent project name or ID for subprojects or tasks") - newCmd.Flags().StringP("task", "t", "", "Parent task ID for subtasks") - newCmd.Flags().StringP("due", "D", "", "Due date for the task (format: YYYY-MM-DD HH:MM)") - newCmd.Flags().IntP("priority", "P", 0, "Priority of the task (1: High, 2: Medium, 3: Low, 4: None)") + return cmd } func createProject(cmd *cobra.Command, repo *repository.Repository) { @@ -62,7 +64,7 @@ func createProject(cmd *cobra.Command, repo *repository.Repository) { parentProjectIdentifier, _ := cmd.Flags().GetString("project") if name == "" { - fmt.Println("Project name is required.") + cmd.Println("Project name is required.") return } @@ -74,7 +76,7 @@ func createProject(cmd *cobra.Command, repo *repository.Repository) { } else { parentProject, err := repo.GetProjectByName(parentProjectIdentifier) if err != nil || parentProject == nil { - fmt.Printf("Parent project '%s' not found.\n", parentProjectIdentifier) + cmd.Printf("Parent project '%s' not found.\n", parentProjectIdentifier) return } parentProjectID = &parentProject.ID @@ -89,11 +91,11 @@ func createProject(cmd *cobra.Command, repo *repository.Repository) { err := repo.CreateProject(project) if err != nil { - fmt.Printf("Error creating project: %v\n", err) + cmd.Printf("Error creating project: %v\n", err) return } - fmt.Printf("Project '%s' created successfully.\n", name) + cmd.Printf("Project '%s' created successfully.\n", name) } func createTask(cmd *cobra.Command, repo *repository.Repository) { @@ -105,25 +107,25 @@ func createTask(cmd *cobra.Command, repo *repository.Repository) { priority, _ := cmd.Flags().GetInt("priority") if name == "" { - fmt.Println("Task name is required.") + cmd.Println("Task name is required.") return } projectID, err := getProjectID(projectIdentifier, repo) if err != nil { - fmt.Println(err) + cmd.Println(err) return } parentTaskID, err := getParentTaskID(parentTaskIdentifier) if err != nil && !errors.Is(err, ErrNoParentTask) { - fmt.Println(err) + cmd.Println(err) return } dueDate, err := parseDueDate(dueDateStr) if err != nil && !errors.Is(err, ErrNoDueDate) { - fmt.Println("Invalid date format. Using no due date.") + cmd.Println("Invalid date format. Using no due date.") } task := &models.Task{ @@ -136,11 +138,11 @@ func createTask(cmd *cobra.Command, repo *repository.Repository) { } if err = repo.CreateTask(task); err != nil { - fmt.Printf("Error creating task: %v\n", err) + cmd.Printf("Error creating task: %v\n", err) return } - fmt.Printf( + cmd.Printf( "Task '%s' created successfully with priority %s.\n", name, utils.GetPriorityString(utils.Priority(priority)), @@ -158,7 +160,7 @@ func getProjectID(identifier string, repo *repository.Repository) (int, error) { project, err := repo.GetProjectByName(identifier) if err != nil || project == nil { - return 0, fmt.Errorf("project '%s' not found", identifier) + return 0, errors.New("project '" + identifier + "' not found") } return project.ID, nil From 02af2bb64467a7c09be746b257b14bc834b2128a Mon Sep 17 00:00:00 2001 From: Lian Drake Date: Mon, 9 Sep 2024 19:47:39 -0400 Subject: [PATCH 58/81] Completed refactoring of cmd package, looking foward to fix #10 --- cmd/root.go | 50 ++++++++++++++++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 42a5f3f..640cb48 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,7 +1,6 @@ package cmd import ( - "fmt" "os" "github.com/d4r1us-drk/clido/internal/version" @@ -17,28 +16,43 @@ const ( MinArgsLength = 2 ) -var rootCmd = &cobra.Command{ - Use: "clido", - Short: "Clido is an awesome CLI to-do list management application", - Long: `Clido is a simple yet powerful CLI tool designed to help you manage +// NewRootCmd creates and returns the root command. +func NewRootCmd() *cobra.Command { + rootCmd := &cobra.Command{ + Use: "clido", + Short: "Clido is an awesome CLI to-do list management application", + Long: `Clido is a simple yet powerful CLI tool designed to help you manage your projects and tasks effectively from the terminal.`, + } + + // Add subcommands to rootCmd here + rootCmd.AddCommand(NewVersionCmd()) + rootCmd.AddCommand(NewCompletionCmd()) + rootCmd.AddCommand(NewNewCmd()) + rootCmd.AddCommand(NewEditCmd()) + rootCmd.AddCommand(NewListCmd()) + rootCmd.AddCommand(NewRemoveCmd()) + rootCmd.AddCommand(NewToggleCmd()) + + return rootCmd +} + +// NewVersionCmd creates and returns the version command. +func NewVersionCmd() *cobra.Command { + return &cobra.Command{ + Use: "version", + Short: "Print the version number of Clido", + Run: func(cmd *cobra.Command, _ []string) { + cmd.Println(version.FullVersion()) + }, + } } +// Execute runs the root command, which includes all subcommands. func Execute() { + rootCmd := NewRootCmd() if err := rootCmd.Execute(); err != nil { - fmt.Println(err) + rootCmd.Println(err) os.Exit(1) } } - -var versionCmd = &cobra.Command{ - Use: "version", - Short: "Print the version number of Clido", - Run: func(cmd *cobra.Command, args []string) { - fmt.Println(version.FullVersion()) - }, -} - -func init() { - rootCmd.AddCommand(versionCmd) -} From 5675a3747cd37ce0c357fd7384fa187d649f0e88 Mon Sep 17 00:00:00 2001 From: Lian Drake Date: Mon, 9 Sep 2024 19:48:46 -0400 Subject: [PATCH 59/81] The linter made some code formating here --- internal/version/version.go | 2 +- pkg/repository/repository.go | 30 +++++++++++++++--------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/internal/version/version.go b/internal/version/version.go index 71319b2..9686fd3 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -30,4 +30,4 @@ func FullVersion() string { runtime.GOOS, runtime.GOARCH, ) -} \ No newline at end of file +} diff --git a/pkg/repository/repository.go b/pkg/repository/repository.go index ffd9279..b6a19c3 100644 --- a/pkg/repository/repository.go +++ b/pkg/repository/repository.go @@ -3,15 +3,15 @@ package repository import ( "errors" "fmt" - "log" + "log" "os" "path/filepath" "runtime" - "time" + "time" "gorm.io/driver/sqlite" - "gorm.io/gorm/logger" "gorm.io/gorm" + "gorm.io/gorm/logger" ) type Repository struct { @@ -25,20 +25,20 @@ func NewRepository() (*Repository, error) { return nil, err } - // Custom logger for GORM, we use this to disable GORM's verbose messages - newLogger := logger.New( - log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer - logger.Config{ - SlowThreshold: time.Second, // Slow SQL threshold - LogLevel: logger.Silent, // Log level - IgnoreRecordNotFoundError: true, // Ignore ErrRecordNotFound error for logger - Colorful: false, // Disable color - }, - ) + // Custom logger for GORM, we use this to disable GORM's verbose messages + newLogger := logger.New( + log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer + logger.Config{ + SlowThreshold: time.Second, // Slow SQL threshold + LogLevel: logger.Silent, // Log level + IgnoreRecordNotFoundError: true, // Ignore ErrRecordNotFound error for logger + Colorful: false, // Disable color + }, + ) db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{ - Logger: newLogger, - }) + Logger: newLogger, + }) if err != nil { return nil, fmt.Errorf("failed to connect database: %w", err) } From 8c4b2264d967b9f0999ba430b059c3d4915160b4 Mon Sep 17 00:00:00 2001 From: Lian Drake Date: Wed, 11 Sep 2024 11:28:32 -0400 Subject: [PATCH 60/81] Added documentation for cmd/root.go --- cmd/root.go | 46 +++++++++++++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 640cb48..10aa5da 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -7,16 +7,21 @@ import ( "github.com/spf13/cobra" ) +// Constants for table printing. These are not user constraints const ( - MaxProjectNameLength = 30 - MaxProjectDescLength = 50 - MaxTaskNameLength = 20 - MaxTaskDescLength = 30 - MaxProjectNameWrapLength = 20 - MinArgsLength = 2 + MaxProjectNameLength = 30 // Maximum length for project names + MaxProjectDescLength = 50 // Maximum length for project descriptions + MaxTaskNameLength = 20 // Maximum length for task names + MaxTaskDescLength = 30 // Maximum length for task descriptions + MaxProjectNameWrapLength = 20 // Maximum length for wrapping project names in the UI + MinArgsLength = 2 // Minimum required arguments for certain commands ) -// NewRootCmd creates and returns the root command. +// NewRootCmd creates and returns the root command for the CLI application. +// This is the entry point of the application, which is responsible for managing +// various subcommands like version, new, edit, list, remove, and toggle commands. +// +// Returns a *cobra.Command which acts as the root command for all subcommands. func NewRootCmd() *cobra.Command { rootCmd := &cobra.Command{ Use: "clido", @@ -25,34 +30,41 @@ func NewRootCmd() *cobra.Command { your projects and tasks effectively from the terminal.`, } - // Add subcommands to rootCmd here - rootCmd.AddCommand(NewVersionCmd()) - rootCmd.AddCommand(NewCompletionCmd()) - rootCmd.AddCommand(NewNewCmd()) - rootCmd.AddCommand(NewEditCmd()) - rootCmd.AddCommand(NewListCmd()) - rootCmd.AddCommand(NewRemoveCmd()) - rootCmd.AddCommand(NewToggleCmd()) + // Adding subcommands to rootCmd + rootCmd.AddCommand(NewVersionCmd()) // Version command to display the app version + rootCmd.AddCommand(NewCompletionCmd()) // Completion command to generate shell autocompletion scripts + rootCmd.AddCommand(NewNewCmd()) // New command to add a new project or task + rootCmd.AddCommand(NewEditCmd()) // Edit command to modify an existing project or task + rootCmd.AddCommand(NewListCmd()) // List command to display projects or tasks + rootCmd.AddCommand(NewRemoveCmd()) // Remove command to delete a project or task + rootCmd.AddCommand(NewToggleCmd()) // Toggle command to change the status of a task return rootCmd } // NewVersionCmd creates and returns the version command. +// This command prints the current version of clido, using the version package. +// It helps users check which version of the tool they are running. func NewVersionCmd() *cobra.Command { return &cobra.Command{ Use: "version", Short: "Print the version number of Clido", Run: func(cmd *cobra.Command, _ []string) { + // Print the full version of the application, stored in the version package. cmd.Println(version.FullVersion()) }, } } -// Execute runs the root command, which includes all subcommands. +// Execute runs the root command of the application, which triggers +// the appropriate subcommand based on user input. +// +// If an error occurs during execution (such as invalid command usage), +// the application prints the error message and exits with a non-zero status code. func Execute() { rootCmd := NewRootCmd() if err := rootCmd.Execute(); err != nil { rootCmd.Println(err) - os.Exit(1) + os.Exit(1) // Exit the application with an error status code if the command fails } } From cd756df83878a69ab812d973c46a44106cc080d4 Mon Sep 17 00:00:00 2001 From: Lian Drake Date: Wed, 11 Sep 2024 11:28:44 -0400 Subject: [PATCH 61/81] Added documentation for cmd/completion.go --- cmd/completion.go | 39 ++++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/cmd/completion.go b/cmd/completion.go index f980ca3..d8998e7 100644 --- a/cmd/completion.go +++ b/cmd/completion.go @@ -6,10 +6,21 @@ import ( "github.com/spf13/cobra" ) +// NewCompletionCmd creates and returns the 'completion' command, +// which generates shell completion scripts for Bash, Zsh, Fish, and PowerShell. +// +// The command allows users to generate and load completion scripts for their preferred shell. +// Completion scripts help users auto-complete command-line inputs for 'clido'. +// +// This command supports the following shells: +// - Bash +// - Zsh +// - Fish +// - PowerShell func NewCompletionCmd() *cobra.Command { return &cobra.Command{ - Use: "completion [bash|zsh|fish|powershell]", - Short: "Generate completion script", + Use: "completion [bash|zsh|fish|powershell]", // Defines the valid subcommands for shell types + Short: "Generate completion script", // Brief description of the command Long: `To load completions: Bash: @@ -48,44 +59,50 @@ PowerShell: # To load completions for every new session, run: PS> clido completion powershell > clido.ps1 # and source this file from your PowerShell profile. -`, +`, // Detailed usage instructions for each shell - DisableFlagsInUseLine: true, - ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, + DisableFlagsInUseLine: true, // Disables flag usage display in the command usage line + ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, // Specifies valid arguments for shell types Args: func(cmd *cobra.Command, args []string) error { + // Ensures exactly one argument (shell type) is provided if len(args) != 1 { cmd.PrintErrln( "Error: requires exactly one argument: bash, zsh, fish, or powershell", ) - return cobra.NoArgs(cmd, args) + return cobra.NoArgs(cmd, args) // Returns an error if no arguments are provided } - return cobra.OnlyValidArgs(cmd, args) + return cobra.OnlyValidArgs(cmd, args) // Validates the argument }, Run: func(cmd *cobra.Command, args []string) { + // Switch case to handle shell type provided as argument switch args[0] { case "bash": + // Generate Bash completion script and output it to stdout if err := cmd.Root().GenBashCompletion(os.Stdout); err != nil { cmd.PrintErrf("Error generating bash completion: %v\n", err) - os.Exit(1) + os.Exit(1) // Exit with error code 1 if there is a failure } case "zsh": + // Generate Zsh completion script and output it to stdout if err := cmd.Root().GenZshCompletion(os.Stdout); err != nil { cmd.PrintErrf("Error generating zsh completion: %v\n", err) - os.Exit(1) + os.Exit(1) // Exit with error code 1 if there is a failure } case "fish": + // Generate Fish completion script and output it to stdout if err := cmd.Root().GenFishCompletion(os.Stdout, true); err != nil { cmd.PrintErrf("Error generating fish completion: %v\n", err) - os.Exit(1) + os.Exit(1) // Exit with error code 1 if there is a failure } case "powershell": + // Generate PowerShell completion script and output it to stdout if err := cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout); err != nil { cmd.PrintErrf("Error generating PowerShell completion: %v\n", err) - os.Exit(1) + os.Exit(1) // Exit with error code 1 if there is a failure } } }, From 158ce5bb15e3ffd63714517ca7ec8264ceb1a3d0 Mon Sep 17 00:00:00 2001 From: Lian Drake Date: Wed, 11 Sep 2024 11:28:58 -0400 Subject: [PATCH 62/81] Added documentation for cmd/edit.go --- cmd/edit.go | 56 +++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 44 insertions(+), 12 deletions(-) diff --git a/cmd/edit.go b/cmd/edit.go index b1a1b4b..d0ae047 100644 --- a/cmd/edit.go +++ b/cmd/edit.go @@ -10,18 +10,27 @@ import ( "github.com/spf13/cobra" ) -// NewEditCmd creates and returns the edit command. +// NewEditCmd creates and returns the 'edit' command for editing projects or tasks. +// +// The command allows users to modify existing projects or tasks by their unique ID. +// It supports editing the name, description, parent project, parent task, due date, and priority of a task. +// +// Usage: +// clido edit project # Edit a project by ID +// clido edit task # Edit a task by ID func NewEditCmd() *cobra.Command { cmd := &cobra.Command{ - Use: "edit [project|task] ", - Short: "Edit an existing project or task", - Long: `Edit the details of an existing project or task identified by its ID.`, + Use: "edit [project|task] ", // Specifies the valid options: project or task followed by an ID + Short: "Edit an existing project or task", // Short description of the command + Long: `Edit the details of an existing project or task identified by its ID.`, // Extended description Run: func(cmd *cobra.Command, args []string) { + // Ensure the command receives sufficient arguments (either "project" or "task" followed by an ID) if len(args) < MinArgsLength { cmd.Println("Insufficient arguments. Use 'edit project ' or 'edit task '.") return } + // Initialize the repository for database operations repo, err := repository.NewRepository() if err != nil { cmd.Printf("Error initializing repository: %v\n", err) @@ -29,12 +38,14 @@ func NewEditCmd() *cobra.Command { } defer repo.Close() + // Parse the ID argument into an integer id, err := strconv.Atoi(args[1]) if err != nil { cmd.Println("Invalid ID. Please provide a numeric ID.") return } + // Determine whether the user wants to edit a project or a task switch args[0] { case "project": editProject(cmd, repo, id) @@ -46,29 +57,36 @@ func NewEditCmd() *cobra.Command { }, } - // Define flags for the edit command - cmd.Flags().StringP("name", "n", "", "New name") - cmd.Flags().StringP("description", "d", "", "New description") - cmd.Flags().StringP("project", "p", "", "New parent project name or ID") - cmd.Flags().StringP("task", "t", "", "New parent task ID for subtasks") - cmd.Flags().StringP("due", "D", "", "New due date for task (format: YYYY-MM-DD HH:MM)") - cmd.Flags(). - IntP("priority", "P", 0, "New priority for task (1: High, 2: Medium, 3: Low, 4: None)") + // Define flags for the edit command, allowing users to specify what fields they want to update + cmd.Flags().StringP("name", "n", "", "New name") // Option to change the name of the project/task + cmd.Flags().StringP("description", "d", "", "New description") // Option to change the description + cmd.Flags().StringP("project", "p", "", "New parent project name or ID") // Option to change the parent project (for projects) + cmd.Flags().StringP("task", "t", "", "New parent task ID for subtasks") // Option to change the parent task (for tasks) + cmd.Flags().StringP("due", "D", "", "New due date for task (format: YYYY-MM-DD HH:MM)") // Option to set a new due date + cmd.Flags().IntP("priority", "P", 0, "New priority for task (1: High, 2: Medium, 3: Low, 4: None)") // Option to set a new priority level return cmd } +// editProject handles updating an existing project by its ID. +// The function retrieves the project from the repository, applies updates (name, description, parent project), +// and saves the changes back to the database. +// +// If the user provides a new parent project, it validates whether the project exists by name or ID. func editProject(cmd *cobra.Command, repo *repository.Repository, id int) { + // Retrieve the project by its ID project, err := repo.GetProjectByID(id) if err != nil { cmd.Printf("Error retrieving project: %v\n", err) return } + // Get the new values from the command flags name, _ := cmd.Flags().GetString("name") description, _ := cmd.Flags().GetString("description") parentProjectIdentifier, _ := cmd.Flags().GetString("project") + // Apply the updates to the project, if specified by the user if name != "" { project.Name = name } @@ -80,6 +98,7 @@ func editProject(cmd *cobra.Command, repo *repository.Repository, id int) { parentID, _ := strconv.Atoi(parentProjectIdentifier) project.ParentProjectID = &parentID } else { + // If the parent project is provided by name, fetch it by name var parentProject *models.Project parentProject, err = repo.GetProjectByName(parentProjectIdentifier) if err != nil || parentProject == nil { @@ -90,6 +109,7 @@ func editProject(cmd *cobra.Command, repo *repository.Repository, id int) { } } + // Save the updated project to the database err = repo.UpdateProject(project) if err != nil { cmd.Printf("Error updating project: %v\n", err) @@ -99,19 +119,27 @@ func editProject(cmd *cobra.Command, repo *repository.Repository, id int) { cmd.Printf("Project '%s' updated successfully.\n", project.Name) } +// editTask handles updating an existing task by its ID. +// The function retrieves the task from the repository, applies updates (name, description, due date, priority, parent task), +// and saves the changes back to the database. +// +// If the user provides a new parent task, it validates whether the task exists by ID. func editTask(cmd *cobra.Command, repo *repository.Repository, id int) { + // Retrieve the task by its ID task, err := repo.GetTaskByID(id) if err != nil { cmd.Printf("Error retrieving task: %v\n", err) return } + // Get the new values from the command flags name, _ := cmd.Flags().GetString("name") description, _ := cmd.Flags().GetString("description") dueDateStr, _ := cmd.Flags().GetString("due") priority, _ := cmd.Flags().GetInt("priority") parentTaskIdentifier, _ := cmd.Flags().GetString("task") + // Apply the updates to the task, if specified by the user if name != "" { task.Name = name } @@ -119,6 +147,7 @@ func editTask(cmd *cobra.Command, repo *repository.Repository, id int) { task.Description = description } if dueDateStr != "" { + // Parse the new due date var parsedDate time.Time parsedDate, err = time.Parse("2006-01-02 15:04", dueDateStr) if err == nil { @@ -128,6 +157,7 @@ func editTask(cmd *cobra.Command, repo *repository.Repository, id int) { } } if priority != 0 { + // Validate the priority (must be between 1 and 4) if priority >= 1 && priority <= 4 { task.Priority = utils.Priority(priority) } else { @@ -135,6 +165,7 @@ func editTask(cmd *cobra.Command, repo *repository.Repository, id int) { } } if parentTaskIdentifier != "" { + // Validate the parent task ID (must be numeric) if utils.IsNumeric(parentTaskIdentifier) { parentID, _ := strconv.Atoi(parentTaskIdentifier) task.ParentTaskID = &parentID @@ -144,6 +175,7 @@ func editTask(cmd *cobra.Command, repo *repository.Repository, id int) { } } + // Save the updated task to the database err = repo.UpdateTask(task) if err != nil { cmd.Printf("Error updating task: %v\n", err) From e88387c8646c1f1dc750b5b2a0937722ff4b3226 Mon Sep 17 00:00:00 2001 From: Lian Drake Date: Wed, 11 Sep 2024 11:29:20 -0400 Subject: [PATCH 63/81] Added documentation for cmd/new.go --- cmd/new.go | 60 ++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 47 insertions(+), 13 deletions(-) diff --git a/cmd/new.go b/cmd/new.go index 7414f06..584ad38 100644 --- a/cmd/new.go +++ b/cmd/new.go @@ -12,22 +12,34 @@ import ( ) var ( + // ErrNoParentTask is returned when no parent task is provided during task creation. ErrNoParentTask = errors.New("no parent task specified") - ErrNoDueDate = errors.New("no due date specified") + + // ErrNoDueDate is returned when no due date is provided during task creation. + ErrNoDueDate = errors.New("no due date specified") ) -// NewNewCmd creates and returns the new command. +// NewNewCmd creates and returns the 'new' command for creating projects or tasks. +// +// The command allows users to create new projects or tasks with the specified details, +// such as name, description, parent project, parent task, due date, and priority. +// +// Usage: +// clido new project # Create a new project +// clido new task # Create a new task func NewNewCmd() *cobra.Command { cmd := &cobra.Command{ - Use: "new [project|task]", - Short: "Create a new project or task", - Long: `Create a new project or task with the specified details.`, + Use: "new [project|task]", // Specifies valid options: 'project' or 'task' + Short: "Create a new project or task", // Short description of the command + Long: `Create a new project or task with the specified details.`, // Extended description Run: func(cmd *cobra.Command, args []string) { + // Ensure sufficient arguments (either 'project' or 'task') if len(args) < 1 { cmd.Println("Insufficient arguments. Use 'new project' or 'new task'.") return } + // Initialize the repository for database operations repo, err := repository.NewRepository() if err != nil { cmd.Printf("Error initializing repository: %v\n", err) @@ -35,6 +47,7 @@ func NewNewCmd() *cobra.Command { } defer repo.Close() + // Determine whether the user wants to create a project or a task switch args[0] { case "project": createProject(cmd, repo) @@ -46,23 +59,26 @@ func NewNewCmd() *cobra.Command { }, } - // Define flags for the new command - cmd.Flags().StringP("name", "n", "", "Name of the project or task") - cmd.Flags().StringP("description", "d", "", "Description of the project or task") - cmd.Flags().StringP("project", "p", "", "Parent project name or ID for subprojects or tasks") - cmd.Flags().StringP("task", "t", "", "Parent task ID for subtasks") - cmd.Flags().StringP("due", "D", "", "Due date for the task (format: YYYY-MM-DD HH:MM)") - cmd.Flags(). - IntP("priority", "P", 0, "Priority of the task (1: High, 2: Medium, 3: Low, 4: None)") + // Define flags for the new command, allowing users to specify details + cmd.Flags().StringP("name", "n", "", "Name of the project or task") // Name of the project/task (required) + cmd.Flags().StringP("description", "d", "", "Description of the project or task") // Description + cmd.Flags().StringP("project", "p", "", "Parent project name or ID for subprojects or tasks") // Parent project + cmd.Flags().StringP("task", "t", "", "Parent task ID for subtasks") // Parent task for subtasks + cmd.Flags().StringP("due", "D", "", "Due date for the task (format: YYYY-MM-DD HH:MM)") // Due date + cmd.Flags().IntP("priority", "P", 0, "Priority of the task (1: High, 2: Medium, 3: Low, 4: None)") // Task priority return cmd } +// createProject handles the creation of a new project. +// It retrieves input from flags (name, description, and parent project), validates them, +// and saves the new project to the database. func createProject(cmd *cobra.Command, repo *repository.Repository) { name, _ := cmd.Flags().GetString("name") description, _ := cmd.Flags().GetString("description") parentProjectIdentifier, _ := cmd.Flags().GetString("project") + // Ensure the project name is provided if name == "" { cmd.Println("Project name is required.") return @@ -70,6 +86,7 @@ func createProject(cmd *cobra.Command, repo *repository.Repository) { var parentProjectID *int if parentProjectIdentifier != "" { + // Determine whether the parent project is specified by ID or name if utils.IsNumeric(parentProjectIdentifier) { id, _ := strconv.Atoi(parentProjectIdentifier) parentProjectID = &id @@ -83,12 +100,14 @@ func createProject(cmd *cobra.Command, repo *repository.Repository) { } } + // Create the new project project := &models.Project{ Name: name, Description: description, ParentProjectID: parentProjectID, } + // Save the new project to the database err := repo.CreateProject(project) if err != nil { cmd.Printf("Error creating project: %v\n", err) @@ -98,6 +117,9 @@ func createProject(cmd *cobra.Command, repo *repository.Repository) { cmd.Printf("Project '%s' created successfully.\n", name) } +// createTask handles the creation of a new task. +// It retrieves input from flags (name, description, project, parent task, due date, and priority), +// validates them, and saves the new task to the database. func createTask(cmd *cobra.Command, repo *repository.Repository) { name, _ := cmd.Flags().GetString("name") description, _ := cmd.Flags().GetString("description") @@ -106,28 +128,33 @@ func createTask(cmd *cobra.Command, repo *repository.Repository) { dueDateStr, _ := cmd.Flags().GetString("due") priority, _ := cmd.Flags().GetInt("priority") + // Ensure the task name is provided if name == "" { cmd.Println("Task name is required.") return } + // Validate and retrieve the project ID projectID, err := getProjectID(projectIdentifier, repo) if err != nil { cmd.Println(err) return } + // Validate and retrieve the parent task ID, if provided parentTaskID, err := getParentTaskID(parentTaskIdentifier) if err != nil && !errors.Is(err, ErrNoParentTask) { cmd.Println(err) return } + // Validate and parse the due date, if provided dueDate, err := parseDueDate(dueDateStr) if err != nil && !errors.Is(err, ErrNoDueDate) { cmd.Println("Invalid date format. Using no due date.") } + // Create the new task task := &models.Task{ Name: name, Description: description, @@ -137,6 +164,7 @@ func createTask(cmd *cobra.Command, repo *repository.Repository) { ParentTaskID: parentTaskID, } + // Save the new task to the database if err = repo.CreateTask(task); err != nil { cmd.Printf("Error creating task: %v\n", err) return @@ -149,6 +177,8 @@ func createTask(cmd *cobra.Command, repo *repository.Repository) { ) } +// getProjectID retrieves the project ID based on the identifier (either name or ID). +// It returns an error if the project does not exist. func getProjectID(identifier string, repo *repository.Repository) (int, error) { if identifier == "" { return 0, errors.New("task must be associated with a project") @@ -166,6 +196,8 @@ func getProjectID(identifier string, repo *repository.Repository) (int, error) { return project.ID, nil } +// getParentTaskID retrieves the parent task ID based on the identifier. +// It returns an error if the identifier is not a numeric ID. func getParentTaskID(identifier string) (*int, error) { if identifier == "" { return nil, ErrNoParentTask @@ -179,6 +211,8 @@ func getParentTaskID(identifier string) (*int, error) { return &id, nil } +// parseDueDate parses a string into a time.Time object. +// It returns an error if the string is not in the expected format. func parseDueDate(dateStr string) (*time.Time, error) { if dateStr == "" { return nil, ErrNoDueDate From 2e28881fff12f3e18518add4ef0a34c801e3e2ea Mon Sep 17 00:00:00 2001 From: Lian Drake Date: Wed, 11 Sep 2024 11:29:29 -0400 Subject: [PATCH 64/81] Added documentation for cmd/remove.go --- cmd/remove.go | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/cmd/remove.go b/cmd/remove.go index 53db397..a4fb49e 100644 --- a/cmd/remove.go +++ b/cmd/remove.go @@ -7,14 +7,22 @@ import ( "github.com/spf13/cobra" ) -// NewRemoveCmd creates and returns the remove command. +// NewRemoveCmd creates and returns the 'remove' command for deleting projects or tasks. +// +// The command allows users to remove an existing project or task by its ID. +// It also ensures that all associated subprojects or subtasks are recursively removed. +// +// Usage: +// clido remove project # Remove a project and its subprojects +// clido remove task # Remove a task and its subtasks func NewRemoveCmd() *cobra.Command { cmd := &cobra.Command{ - Use: "remove [project|task] ", - Short: "Remove a project or task along with all its subprojects or subtasks", + Use: "remove [project|task] ", // Specifies valid options: 'project' or 'task' followed by an ID + Short: "Remove a project or task along with all its subprojects or subtasks", // Short description of the command Long: `Remove an existing project or task identified by its ID. This will also remove all associated subprojects - or subtasks.`, + or subtasks.`, // Extended description with clarification on subprojects and subtasks being removed recursively Run: func(cmd *cobra.Command, args []string) { + // Ensure sufficient arguments (either 'project' or 'task' followed by an ID) if len(args) < MinArgsLength { cmd.Println( "Insufficient arguments. Use 'remove project ' or 'remove task '.", @@ -22,6 +30,7 @@ func NewRemoveCmd() *cobra.Command { return } + // Initialize the repository for database operations repo, err := repository.NewRepository() if err != nil { cmd.Printf("Error initializing repository: %v\n", err) @@ -29,12 +38,14 @@ func NewRemoveCmd() *cobra.Command { } defer repo.Close() + // Parse the ID argument into an integer id, err := strconv.Atoi(args[1]) if err != nil { cmd.Println("Invalid ID. Please provide a numeric ID.") return } + // Determine whether the user wants to remove a project or a task switch args[0] { case "project": removeProject(cmd, repo, id) @@ -49,18 +60,22 @@ func NewRemoveCmd() *cobra.Command { return cmd } +// removeProject handles the recursive removal of a project and all its subprojects. +// It first retrieves and removes all subprojects, and then deletes the parent project. func removeProject(cmd *cobra.Command, repo *repository.Repository, id int) { - // First remove all subprojects + // Retrieve all subprojects associated with the project subprojects, err := repo.GetSubprojects(id) if err != nil { cmd.Printf("Error retrieving subprojects: %v\n", err) return } + + // Recursively remove all subprojects for _, subproject := range subprojects { removeProject(cmd, repo, subproject.ID) } - // Now remove the parent project + // Remove the parent project after all subprojects have been removed err = repo.DeleteProject(id) if err != nil { cmd.Printf("Error removing project: %v\n", err) @@ -70,18 +85,22 @@ func removeProject(cmd *cobra.Command, repo *repository.Repository, id int) { cmd.Printf("Project (ID: %d) and all its subprojects removed successfully.\n", id) } +// removeTask handles the recursive removal of a task and all its subtasks. +// It first retrieves and removes all subtasks, and then deletes the parent task. func removeTask(cmd *cobra.Command, repo *repository.Repository, id int) { - // First remove all subtasks + // Retrieve all subtasks associated with the task subtasks, err := repo.GetSubtasks(id) if err != nil { cmd.Printf("Error retrieving subtasks: %v\n", err) return } + + // Recursively remove all subtasks for _, subtask := range subtasks { removeTask(cmd, repo, subtask.ID) } - // Now remove the parent task + // Remove the parent task after all subtasks have been removed err = repo.DeleteTask(id) if err != nil { cmd.Printf("Error removing task: %v\n", err) From 48371093e98806670f330006f19b5282aa2947e0 Mon Sep 17 00:00:00 2001 From: Lian Drake Date: Wed, 11 Sep 2024 11:29:36 -0400 Subject: [PATCH 65/81] Added documentation for cmd/toggle.go --- cmd/toggle.go | 38 +++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/cmd/toggle.go b/cmd/toggle.go index 18f1992..ddc98ae 100644 --- a/cmd/toggle.go +++ b/cmd/toggle.go @@ -8,18 +8,28 @@ import ( "github.com/spf13/cobra" ) -// NewToggleCmd creates and returns the toggle command. +// NewToggleCmd creates and returns the 'toggle' command for marking tasks as completed or uncompleted. +// +// The command allows users to toggle the completion status of a task by its ID. +// If the task is marked as completed, it updates the completion date. +// The command also supports recursively toggling the status of all subtasks. +// +// Usage: +// clido toggle # Toggle the completion status of a task +// clido toggle -r # Recursively toggle the completion status of the task and its subtasks func NewToggleCmd() *cobra.Command { cmd := &cobra.Command{ - Use: "toggle ", - Short: "Toggle task completion status", - Long: `Toggle the completion status of a task identified by its ID.`, + Use: "toggle ", // Specifies the required task ID as the argument + Short: "Toggle task completion status", // Short description of the command + Long: `Toggle the completion status of a task identified by its ID.`, // Extended description Run: func(cmd *cobra.Command, args []string) { + // Ensure at least one argument (the task ID) is provided if len(args) < 1 { cmd.Println("Insufficient arguments. Use 'toggle '.") return } + // Initialize the repository for database operations repo, err := repository.NewRepository() if err != nil { cmd.Printf("Error initializing repository: %v\n", err) @@ -27,56 +37,70 @@ func NewToggleCmd() *cobra.Command { } defer repo.Close() + // Parse the task ID argument into an integer id, err := strconv.Atoi(args[0]) if err != nil { cmd.Println("Invalid task ID. Please provide a numeric ID.") return } + // Check if the recursive flag was provided recursive, _ := cmd.Flags().GetBool("recursive") toggleTask(cmd, repo, id, recursive) }, } - // Add flag for recursive toggle + // Add flag for recursive toggle, allowing users to recursively toggle all subtasks cmd.Flags().BoolP("recursive", "r", false, "Recursively toggle subtasks") return cmd } +// toggleTask toggles the completion status of a task and optionally its subtasks. +// +// If the task is currently marked as incomplete, it will be marked as completed, +// and the completion date will be updated. If the task is marked as completed, +// it will be toggled to incomplete and the completion date will be cleared. +// +// If the recursive flag is set to true, this function will also toggle the completion status of all subtasks. func toggleTask(cmd *cobra.Command, repo *repository.Repository, id int, recursive bool) { + // Retrieve the task by its ID task, err := repo.GetTaskByID(id) if err != nil { cmd.Printf("Error retrieving task: %v\n", err) return } + // Toggle the task's completion status task.TaskCompleted = !task.TaskCompleted task.LastUpdatedDate = time.Now() + // Set the completion date if the task is marked as completed, or clear it if uncompleted if task.TaskCompleted { task.CompletionDate = &task.LastUpdatedDate } else { task.CompletionDate = nil } + // Update the task in the repository err = repo.UpdateTask(task) if err != nil { cmd.Printf("Error updating task: %v\n", err) return } + // Display the updated status of the task status := "completed" if !task.TaskCompleted { status = "uncompleted" } - cmd.Printf("Task '%s' (ID: %d) marked as %s.\n", task.Name, id, status) + // If the recursive flag is set, retrieve and toggle the completion status of all subtasks if recursive { subtasks, _ := repo.GetSubtasks(id) for _, subtask := range subtasks { - toggleTask(cmd, repo, subtask.ID, recursive) + toggleTask(cmd, repo, subtask.ID, recursive) // Recursively toggle subtasks } } } From 3ced492a79bb3d8ad532358be52fb90a00aa61c5 Mon Sep 17 00:00:00 2001 From: Lian Drake Date: Wed, 11 Sep 2024 11:29:47 -0400 Subject: [PATCH 66/81] Added documentation for cmd/list.go --- cmd/list.go | 62 +++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 55 insertions(+), 7 deletions(-) diff --git a/cmd/list.go b/cmd/list.go index 4fda5f0..6272a4b 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -13,18 +13,31 @@ import ( "github.com/spf13/cobra" ) -// NewListCmd creates and returns the list command. +// NewListCmd creates and returns the 'list' command for displaying projects or tasks. +// +// The command allows users to list all projects or tasks. Tasks can be optionally filtered by project. +// Users can display the output in a table, tree view, or JSON format. +// +// Usage: +// clido list projects # List all projects +// clido list tasks # List all tasks +// clido list tasks -p 1 # List tasks filtered by project ID +// clido list tasks -p MyProject # List tasks filtered by project name +// clido list tasks -t # Display tasks in a tree-like structure +// clido list projects -j # Output projects in JSON format func NewListCmd() *cobra.Command { cmd := &cobra.Command{ - Use: "list [projects|tasks]", - Short: "List projects or tasks", - Long: `List all projects or tasks, optionally filtered by project for tasks.`, + Use: "list [projects|tasks]", // Specifies the options: 'projects' or 'tasks' + Short: "List projects or tasks", // Short description of the command + Long: `List all projects or tasks, optionally filtered by project for tasks.`, // Extended description Run: func(cmd *cobra.Command, args []string) { + // Ensure at least one argument (either 'projects' or 'tasks') is provided if len(args) < 1 { cmd.Println("Insufficient arguments. Use 'list projects' or 'list tasks'.") return } + // Initialize the repository for database operations repo, err := repository.NewRepository() if err != nil { cmd.Printf("Error initializing repository: %v\n", err) @@ -32,9 +45,11 @@ func NewListCmd() *cobra.Command { } defer repo.Close() + // Retrieve flags for output format outputJSON, _ := cmd.Flags().GetBool("json") treeView, _ := cmd.Flags().GetBool("tree") + // Determine whether to list projects or tasks switch args[0] { case "projects": listProjects(cmd, repo, outputJSON, treeView) @@ -48,13 +63,14 @@ func NewListCmd() *cobra.Command { } // Define flags for the list command - cmd.Flags().StringP("project", "p", "", "Filter tasks by project name or ID") - cmd.Flags().BoolP("json", "j", false, "Output list in JSON format") - cmd.Flags().BoolP("tree", "t", false, "Display projects or tasks in a tree-like structure") + cmd.Flags().StringP("project", "p", "", "Filter tasks by project name or ID") // Filter tasks by project + cmd.Flags().BoolP("json", "j", false, "Output list in JSON format") // Output as JSON + cmd.Flags().BoolP("tree", "t", false, "Display projects or tasks in a tree-like structure") // Display in tree view return cmd } +// listProjects lists all projects in either table, tree view, or JSON format. func listProjects(cmd *cobra.Command, repo *repository.Repository, outputJSON bool, treeView bool) { projects, err := repo.GetAllProjects() if err != nil { @@ -64,6 +80,7 @@ func listProjects(cmd *cobra.Command, repo *repository.Repository, outputJSON bo switch { case outputJSON: + // Output projects in JSON format var jsonData []byte jsonData, err = json.MarshalIndent(projects, "", " ") if err != nil { @@ -73,13 +90,16 @@ func listProjects(cmd *cobra.Command, repo *repository.Repository, outputJSON bo cmd.Println(string(jsonData)) case treeView: + // Display projects in tree view printProjectTree(cmd, projects, nil, 0) default: + // Display projects in a table printProjectTable(cmd, repo, projects) } } +// listTasks lists tasks, optionally filtered by a project, in table, tree view, or JSON format. func listTasks( cmd *cobra.Command, repo *repository.Repository, @@ -94,28 +114,35 @@ func listTasks( } if !outputJSON { + // Print header for tasks printTaskHeader(cmd, project) } switch { case outputJSON: + // Output tasks in JSON format printTasksJSON(cmd, tasks) case treeView: + // Display tasks in tree view printTaskTree(cmd, tasks, nil, 0) default: + // Display tasks in a table printTaskTable(repo, tasks) } } +// getTasks retrieves tasks filtered by a project or returns all tasks if no filter is provided. func getTasks( repo *repository.Repository, projectFilter string, ) ([]*models.Task, *models.Project, error) { if projectFilter == "" { + // Return all tasks if no project filter is specified tasks, err := repo.GetAllTasks() return tasks, nil, err } + // Get tasks filtered by project project, err := getProject(repo, projectFilter) if err != nil { return nil, nil, err @@ -125,14 +152,17 @@ func getTasks( return tasks, project, err } +// getProject retrieves a project by its name or ID, based on the projectFilter input. func getProject(repo *repository.Repository, projectFilter string) (*models.Project, error) { var project *models.Project var err error if utils.IsNumeric(projectFilter) { + // Retrieve project by ID projectID, _ := strconv.Atoi(projectFilter) project, err = repo.GetProjectByID(projectID) } else { + // Retrieve project by name project, err = repo.GetProjectByName(projectFilter) } @@ -143,6 +173,7 @@ func getProject(repo *repository.Repository, projectFilter string) (*models.Proj return project, nil } +// printTaskHeader prints the header for the task list, either all tasks or tasks within a specific project. func printTaskHeader(cmd *cobra.Command, project *models.Project) { if project != nil { cmd.Printf("Tasks in project '%s':\n", project.Name) @@ -151,6 +182,7 @@ func printTaskHeader(cmd *cobra.Command, project *models.Project) { } } +// printTasksJSON outputs the tasks in JSON format. func printTasksJSON(cmd *cobra.Command, tasks []*models.Task) { jsonData, err := json.MarshalIndent(tasks, "", " ") if err != nil { @@ -160,6 +192,7 @@ func printTasksJSON(cmd *cobra.Command, tasks []*models.Task) { cmd.Println(string(jsonData)) } +// printProjectTable displays the list of projects in a table format. func printProjectTable( cmd *cobra.Command, repo *repository.Repository, @@ -170,6 +203,7 @@ func printProjectTable( table.SetRowLine(true) for _, project := range projects { + // Determine whether the project is a parent or child project typeField := "Parent" parentChildField := "None" if project.ParentProjectID != nil { @@ -185,6 +219,7 @@ func printProjectTable( } } + // Add project details to the table table.Append([]string{ strconv.Itoa(project.ID), utils.WrapText(project.Name, MaxProjectNameLength), @@ -198,6 +233,7 @@ func printProjectTable( table.Render() } +// printTaskTable displays the list of tasks in a table format. func printTaskTable(repo *repository.Repository, tasks []*models.Task) { table := tablewriter.NewWriter(os.Stdout) table.SetHeader([]string{ @@ -206,6 +242,7 @@ func printTaskTable(repo *repository.Repository, tasks []*models.Task) { table.SetRowLine(true) for _, task := range tasks { + // Determine whether the task is a parent or child task typeField := "Parent" parentChildField := "None" if task.ParentTaskID != nil { @@ -221,12 +258,14 @@ func printTaskTable(repo *repository.Repository, tasks []*models.Task) { } } + // Get project name for the task project, _ := repo.GetProjectByID(task.ProjectID) projectName := "" if project != nil { projectName = project.Name } + // Add task details to the table table.Append([]string{ strconv.Itoa(task.ID), utils.WrapText(task.Name, MaxTaskNameLength), @@ -244,6 +283,7 @@ func printTaskTable(repo *repository.Repository, tasks []*models.Task) { table.Render() } +// printProjectTree displays projects in a tree view. func printProjectTree(cmd *cobra.Command, projects []*models.Project, parentID *int, level int) { nodes := make([]TreeNode, len(projects)) for i, p := range projects { @@ -252,12 +292,14 @@ func printProjectTree(cmd *cobra.Command, projects []*models.Project, parentID * printTree(cmd, nodes, parentID, level, nil) } +// printTaskTree displays tasks in a tree view. func printTaskTree(cmd *cobra.Command, tasks []*models.Task, parentID *int, level int) { nodes := make([]TreeNode, len(tasks)) for i, t := range tasks { nodes[i] = TaskNode{t} } printTree(cmd, nodes, parentID, level, func(node TreeNode, indent string) { + // Print task details in the tree view task := node.(TaskNode).Task cmd.Printf("%sDescription: %s\n", indent, task.Description) cmd.Printf( @@ -270,12 +312,14 @@ func printTaskTree(cmd *cobra.Command, tasks []*models.Task, parentID *int, leve }) } +// TreeNode represents a node in a tree structure (for both projects and tasks). type TreeNode interface { GetID() int GetParentID() *int GetName() string } +// ProjectNode represents a project in the tree view. type ProjectNode struct { *models.Project } @@ -284,6 +328,7 @@ func (p ProjectNode) GetID() int { return p.ID } func (p ProjectNode) GetParentID() *int { return p.ParentProjectID } func (p ProjectNode) GetName() string { return p.Name } +// TaskNode represents a task in the tree view. type TaskNode struct { *models.Task } @@ -292,6 +337,7 @@ func (t TaskNode) GetID() int { return t.ID } func (t TaskNode) GetParentID() *int { return t.ParentTaskID } func (t TaskNode) GetName() string { return t.Name } +// printTree prints the tree structure for projects or tasks. func printTree( cmd *cobra.Command, nodes []TreeNode, @@ -303,6 +349,7 @@ func printTree( for i, node := range nodes { if (parentID == nil && node.GetParentID() == nil) || (parentID != nil && node.GetParentID() != nil && *node.GetParentID() == *parentID) { + // Use appropriate tree symbols for formatting prefix := "├──" if i == len(nodes)-1 { prefix = "└──" @@ -312,6 +359,7 @@ func printTree( printDetails(node, indent+" ") } nodeID := node.GetID() + // Recursively print child nodes printTree(cmd, nodes, &nodeID, level+1, printDetails) } } From 7ed213aea6ff61993a65fc4e80af1250fc394b27 Mon Sep 17 00:00:00 2001 From: Lian Drake Date: Wed, 11 Sep 2024 11:30:10 -0400 Subject: [PATCH 67/81] Added documentation for internal/version/version.go --- internal/version/version.go | 54 +++++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/internal/version/version.go b/internal/version/version.go index 9686fd3..683943f 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -1,33 +1,47 @@ package version import ( - "fmt" - "runtime" + "fmt" + "runtime" ) +// Info represents the build information for the application. +// It includes fields for the version, build date, and Git commit. type Info struct { - Version string - BuildDate string - GitCommit string + Version string // The version of the application + BuildDate string // The date when the application was built + GitCommit string // The Git commit hash corresponding to the build } +// Get returns an Info struct containing the current build information. +// By default, it returns "dev" for the version, and "unknown" for the build date and Git commit. +// These values can be replaced at build time using linker flags. func Get() Info { - return Info{ - Version: "dev", - BuildDate: "unknown", - GitCommit: "unknown", - } + return Info{ + Version: "dev", + BuildDate: "unknown", + GitCommit: "unknown", + } } +// FullVersion returns a formatted string with full version details, including the application version, +// build date, Git commit hash, the Go runtime version, and the operating system and architecture details. +// +// Example output: +// Clido version dev +// Build date: unknown +// Git commit: unknown +// Go version: go1.16.4 +// OS/Arch: linux/amd64 func FullVersion() string { - info := Get() - return fmt.Sprintf( - "Clido version %s\nBuild date: %s\nGit commit: %s\nGo version: %s\nOS/Arch: %s/%s", - info.Version, - info.BuildDate, - info.GitCommit, - runtime.Version(), - runtime.GOOS, - runtime.GOARCH, - ) + info := Get() + return fmt.Sprintf( + "Clido version %s\nBuild date: %s\nGit commit: %s\nGo version: %s\nOS/Arch: %s/%s", + info.Version, + info.BuildDate, + info.GitCommit, + runtime.Version(), // The Go runtime version + runtime.GOOS, // The operating system + runtime.GOARCH, // The system architecture (e.g., amd64, arm) + ) } From ecfa6f105b07cbb5a78e618380eb0b6605bbfa2a Mon Sep 17 00:00:00 2001 From: Lian Drake Date: Wed, 11 Sep 2024 11:30:37 -0400 Subject: [PATCH 68/81] Added documentation for pkg/models/project.go --- pkg/models/project.go | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/pkg/models/project.go b/pkg/models/project.go index 4eec0cc..d96338e 100644 --- a/pkg/models/project.go +++ b/pkg/models/project.go @@ -6,6 +6,20 @@ import ( "gorm.io/gorm" ) +// Project represents a project entity in the to-do list system. +// It supports hierarchical relationships, where a project can have a parent project and multiple subprojects. +// Each project can also have associated tasks. +// +// Fields: +// - ID: The unique identifier for the project. +// - Name: The name of the project, which must be unique and non-null. +// - Description: A description of the project (optional). +// - CreationDate: The date and time when the project was created (automatically set). +// - LastModifiedDate: The date and time when the project was last updated (automatically set). +// - ParentProjectID: The ID of the parent project, if this project is a subproject (optional). +// - ParentProject: A reference to the parent project (not serialized to JSON). +// - SubProjects: A list of subprojects belonging to this project (not serialized to JSON). +// - Tasks: A list of tasks associated with this project (not serialized to JSON). type Project struct { ID int `gorm:"primaryKey" json:"id"` Name string `gorm:"unique;not null" json:"name"` @@ -13,17 +27,21 @@ type Project struct { CreationDate time.Time `gorm:"not null" json:"creation_date"` LastModifiedDate time.Time `gorm:"not null" json:"last_modified_date"` ParentProjectID *int ` json:"parent_project_id,omitempty"` - ParentProject *Project `gorm:"foreignKey:ParentProjectID" json:"-"` - SubProjects []Project `gorm:"foreignKey:ParentProjectID" json:"-"` - Tasks []Task `gorm:"foreignKey:ProjectID" json:"-"` + ParentProject *Project `gorm:"foreignKey:ParentProjectID" json:"-"` // Foreign key for the parent project (ignored in JSON) + SubProjects []Project `gorm:"foreignKey:ParentProjectID" json:"-"` // List of subprojects (ignored in JSON) + Tasks []Task `gorm:"foreignKey:ProjectID" json:"-"` // List of tasks associated with the project (ignored in JSON) } +// BeforeCreate is a GORM hook that sets the CreationDate and LastModifiedDate fields +// to the current time before a new project is inserted into the database. func (p *Project) BeforeCreate(_ *gorm.DB) error { p.CreationDate = time.Now() p.LastModifiedDate = time.Now() return nil } +// BeforeUpdate is a GORM hook that updates the LastModifiedDate field to the current time +// before an existing project is updated in the database. func (p *Project) BeforeUpdate(_ *gorm.DB) error { p.LastModifiedDate = time.Now() return nil From f4b37df0028db16fbd06e3ff061d4209bf1deaf0 Mon Sep 17 00:00:00 2001 From: Lian Drake Date: Wed, 11 Sep 2024 11:30:44 -0400 Subject: [PATCH 69/81] Added documentation for pkg/models/task.go --- pkg/models/task.go | 39 +++++++++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/pkg/models/task.go b/pkg/models/task.go index ace33fd..3e3b88c 100644 --- a/pkg/models/task.go +++ b/pkg/models/task.go @@ -7,29 +7,52 @@ import ( "gorm.io/gorm" ) +// Task represents a task entity in the to-do list system. +// Each task belongs to a project and can have a parent task (for subtasks) and multiple subtasks. +// The task tracks its completion status, priority, and relevant timestamps. +// +// Fields: +// - ID: The unique identifier for the task. +// - Name: The name of the task, which is required. +// - Description: A description of the task (optional). +// - ProjectID: The ID of the project to which the task belongs (required). +// - Project: A reference to the project this task belongs to (not serialized to JSON). +// - TaskCompleted: A boolean indicating whether the task is completed (required). +// - DueDate: The due date for the task (optional). +// - CompletionDate: The date when the task was completed (optional, set when TaskCompleted is true). +// - CreationDate: The date and time when the task was created (automatically set). +// - LastUpdatedDate: The date and time when the task was last updated (automatically set). +// - Priority: The priority level of the task, represented by an integer (1: High, 2: Medium, 3: Low, 4: None). +// - ParentTaskID: The ID of the parent task, if this task is a subtask (optional). +// - ParentTask: A reference to the parent task (not serialized to JSON). +// - SubTasks: A list of subtasks belonging to this task (not serialized to JSON). type Task struct { ID int `gorm:"primaryKey" json:"id"` Name string `gorm:"not null" json:"name"` Description string ` json:"description"` ProjectID int `gorm:"not null" json:"project_id"` - Project Project `gorm:"foreignKey:ProjectID" json:"-"` - TaskCompleted bool `gorm:"not null" json:"task_completed"` - DueDate *time.Time ` json:"due_date,omitempty"` - CompletionDate *time.Time ` json:"completion_date,omitempty"` + Project Project `gorm:"foreignKey:ProjectID" json:"-"` // Foreign key for the project (ignored in JSON) + TaskCompleted bool `gorm:"not null" json:"task_completed"` // Tracks completion status + DueDate *time.Time ` json:"due_date,omitempty"` // Optional due date + CompletionDate *time.Time ` json:"completion_date,omitempty"` // Optional completion date CreationDate time.Time `gorm:"not null" json:"creation_date"` LastUpdatedDate time.Time `gorm:"not null" json:"last_updated_date"` - Priority utils.Priority `gorm:"not null;default:4" json:"priority"` - ParentTaskID *int ` json:"parent_task_id,omitempty"` - ParentTask *Task `gorm:"foreignKey:ParentTaskID" json:"-"` - SubTasks []Task `gorm:"foreignKey:ParentTaskID" json:"-"` + Priority utils.Priority `gorm:"not null;default:4" json:"priority"` // Default priority set to "None" + ParentTaskID *int ` json:"parent_task_id,omitempty"` // Optional parent task + ParentTask *Task `gorm:"foreignKey:ParentTaskID" json:"-"` // Foreign key for the parent task (ignored in JSON) + SubTasks []Task `gorm:"foreignKey:ParentTaskID" json:"-"` // List of subtasks (ignored in JSON) } +// BeforeCreate is a GORM hook that sets the CreationDate and LastUpdatedDate fields +// to the current time before a new task is inserted into the database. func (t *Task) BeforeCreate(_ *gorm.DB) error { t.CreationDate = time.Now() t.LastUpdatedDate = time.Now() return nil } +// BeforeUpdate is a GORM hook that updates the LastUpdatedDate field to the current time +// before an existing task is updated in the database. func (t *Task) BeforeUpdate(_ *gorm.DB) error { t.LastUpdatedDate = time.Now() return nil From 367289775cd1ce7df3b293315444e4e5e913fdc2 Mon Sep 17 00:00:00 2001 From: Lian Drake Date: Wed, 11 Sep 2024 11:31:00 -0400 Subject: [PATCH 70/81] Added documentation for pkg/utils/helpers.go --- pkg/utils/helpers.go | 54 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/pkg/utils/helpers.go b/pkg/utils/helpers.go index e0c7a74..ff96eaf 100644 --- a/pkg/utils/helpers.go +++ b/pkg/utils/helpers.go @@ -8,21 +8,37 @@ import ( "github.com/fatih/color" ) +// IsNumeric checks whether a given string is numeric. +// It attempts to convert the string into an integer and returns true if successful, otherwise false. +// +// Example: +// IsNumeric("123") // returns true +// IsNumeric("abc") // returns false func IsNumeric(s string) bool { _, err := strconv.Atoi(s) return err == nil } +// WrapText wraps a given text to a specified maximum line length. +// It breaks the text into words and ensures that no line exceeds the specified maxLength. +// +// If the text fits within maxLength, it is returned unchanged. Otherwise, the text is split into multiple lines. +// +// Example: +// WrapText("This is a very long sentence that needs to be wrapped.", 20) +// // returns: +// "This is a very long\nsentence that needs\nto be wrapped." func WrapText(text string, maxLength int) string { if len(text) <= maxLength { return text } var result string - words := strings.Fields(text) + words := strings.Fields(text) // Split text into words line := "" for _, word := range words { + // Check if adding the word would exceed the max length if len(line)+len(word)+1 > maxLength { if len(result) > 0 { result += "\n" @@ -37,6 +53,7 @@ func WrapText(text string, maxLength int) string { } } + // Add the remaining line if len(line) > 0 { if len(result) > 0 { result += "\n" @@ -47,14 +64,22 @@ func WrapText(text string, maxLength int) string { return result } +// Priority represents the priority level of a task. +// Priority levels include High (1), Medium (2), and Low (3). type Priority int const ( - PriorityHigh Priority = 1 - PriorityMedium Priority = 2 - PriorityLow Priority = 3 + PriorityHigh Priority = 1 // High priority + PriorityMedium Priority = 2 // Medium priority + PriorityLow Priority = 3 // Low priority ) +// GetPriorityString returns the string representation of a Priority value. +// The possible values are "High", "Medium", "Low", or "None" (for undefined priority). +// +// Example: +// GetPriorityString(PriorityHigh) // returns "High" +// GetPriorityString(4) // returns "None" func GetPriorityString(priority Priority) string { switch priority { case PriorityHigh: @@ -68,6 +93,12 @@ func GetPriorityString(priority Priority) string { } } +// FormatDate formats a time.Time object into a human-readable string in the format "YYYY-MM-DD HH:MM". +// If the time is nil, it returns "None". +// +// Example: +// FormatDate(time.Now()) // returns "2024-09-11 14:30" +// FormatDate(nil) // returns "None" func FormatDate(t *time.Time) string { if t == nil { return "None" @@ -75,6 +106,16 @@ func FormatDate(t *time.Time) string { return t.Format("2006-01-02 15:04") } +// ColoredPastDue determines if a task is past due and returns a colored string indicating the result. +// If the task is past due and not completed, it returns "yes" in red. If it's completed or not past due, it returns "no" in green. +// +// It converts the given due date to the local time zone for comparison. +// If no due date is provided, it assumes the task is not past due. +// +// Example: +// ColoredPastDue(&time.Now(), false) // returns "no" in green if task is not past due +// ColoredPastDue(&pastTime, false) // returns "yes" in red if task is past due and incomplete +// ColoredPastDue(&pastTime, true) // returns "yes" in green if task is completed func ColoredPastDue(dueDate *time.Time, completed bool) string { if dueDate == nil { return color.GreenString("no") @@ -84,7 +125,7 @@ func ColoredPastDue(dueDate *time.Time, completed bool) string { now := time.Now() localLocation := now.Location() - // Grab dueDate and interpret it as local time + // Convert due date to local time zone dueDateAsLocalTime := time.Date( dueDate.Year(), dueDate.Month(), @@ -93,9 +134,10 @@ func ColoredPastDue(dueDate *time.Time, completed bool) string { dueDate.Minute(), dueDate.Second(), dueDate.Nanosecond(), - localLocation, // Use local timezone for interpretation + localLocation, // Use local timezone ) + // Compare current time with the due date if now.After(dueDateAsLocalTime) { if completed { return color.GreenString("yes") From 68a75db2ade3b3812ff20305029d23ef3f0fde87 Mon Sep 17 00:00:00 2001 From: Lian Drake Date: Wed, 11 Sep 2024 11:31:19 -0400 Subject: [PATCH 71/81] Added documentation for pkg/repository/repository.go --- pkg/repository/repository.go | 50 +++++++++++++++++++++++++++++------- 1 file changed, 41 insertions(+), 9 deletions(-) diff --git a/pkg/repository/repository.go b/pkg/repository/repository.go index b6a19c3..247cfc6 100644 --- a/pkg/repository/repository.go +++ b/pkg/repository/repository.go @@ -14,42 +14,59 @@ import ( "gorm.io/gorm/logger" ) +// Repository manages the database connection and migrations for the application. +// It encapsulates the GORM database instance and a migrator responsible for applying database migrations. type Repository struct { - db *gorm.DB - migrator *Migrator + db *gorm.DB // The GORM database instance + migrator *Migrator // The migrator responsible for handling database migrations } +// NewRepository initializes a new Repository instance, setting up the SQLite database connection. +// It also configures a custom GORM logger and applies any pending migrations. +// +// Returns: +// - A pointer to the initialized Repository. +// - An error if there was an issue with database connection or migration. +// +// The database path is determined based on the operating system: +// - On Windows, the path is inside the APPDATA directory. +// - On Unix-based systems, it is located under ~/.local/share/clido/data.db. func NewRepository() (*Repository, error) { + // Determine the database path dbPath, err := getDBPath() if err != nil { return nil, err } - // Custom logger for GORM, we use this to disable GORM's verbose messages + // Custom logger for GORM, disabling verbose logging newLogger := logger.New( - log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer + log.New(os.Stdout, "\r\n", log.LstdFlags), // Output logger with timestamp logger.Config{ - SlowThreshold: time.Second, // Slow SQL threshold - LogLevel: logger.Silent, // Log level - IgnoreRecordNotFoundError: true, // Ignore ErrRecordNotFound error for logger - Colorful: false, // Disable color + SlowThreshold: time.Second, // Log slow SQL queries taking longer than 1 second + LogLevel: logger.Silent, // Disable all log output (silent mode) + IgnoreRecordNotFoundError: true, // Ignore record not found errors in logs + Colorful: false, // Disable colored output in logs }, ) + // Open the SQLite database using GORM db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{ - Logger: newLogger, + Logger: newLogger, // Use the custom logger }) if err != nil { return nil, fmt.Errorf("failed to connect database: %w", err) } + // Initialize the migrator migrator := NewMigrator() + // Create the repository instance repo := &Repository{ db: db, migrator: migrator, } + // Run database migrations err = repo.migrator.Migrate(repo.db) if err != nil { return nil, fmt.Errorf("failed to run migrations: %w", err) @@ -58,9 +75,19 @@ func NewRepository() (*Repository, error) { return repo, nil } +// getDBPath determines the path for the SQLite database based on the operating system. +// +// On Windows, the path is in the APPDATA directory. +// On Unix-based systems, the path is in the ~/.local/share/clido directory. +// +// Returns: +// - The database file path as a string. +// - An error if the environment variables required for path construction are not set or +// if there was an issue creating the database directory. func getDBPath() (string, error) { var dbPath string + // Determine the correct path based on the operating system if runtime.GOOS == "windows" { appDataPath := os.Getenv("APPDATA") if appDataPath == "" { @@ -75,6 +102,7 @@ func getDBPath() (string, error) { dbPath = filepath.Join(homePath, ".local", "share", "clido", "data.db") } + // Ensure the database directory exists, creating it if necessary dbDir := filepath.Dir(dbPath) if err := os.MkdirAll(dbDir, 0o755); err != nil { return "", fmt.Errorf("error creating database directory: %w", err) @@ -83,6 +111,10 @@ func getDBPath() (string, error) { return dbPath, nil } +// Close closes the database connection gracefully. +// It retrieves the underlying SQL database object from GORM and calls its Close method. +// +// Returns an error if the database connection could not be closed. func (r *Repository) Close() error { sqlDB, err := r.db.DB() if err != nil { From 7bad8995f38e84942ffd38eb0ed0f2fecdc65745 Mon Sep 17 00:00:00 2001 From: Lian Drake Date: Wed, 11 Sep 2024 11:31:27 -0400 Subject: [PATCH 72/81] Added documentation for pkg/repository/migrations.go --- pkg/repository/migrations.go | 58 +++++++++++++++++++++++++++++------- 1 file changed, 48 insertions(+), 10 deletions(-) diff --git a/pkg/repository/migrations.go b/pkg/repository/migrations.go index 84c3eaf..d5fe67b 100644 --- a/pkg/repository/migrations.go +++ b/pkg/repository/migrations.go @@ -5,18 +5,38 @@ import ( "gorm.io/gorm" ) +// Migration represents a database migration entry. +// Each migration is uniquely identified by a version string. +// +// Fields: +// - ID: The unique identifier for the migration (primary key). +// - Version: The version of the migration, which is unique in the database. type Migration struct { - ID uint `gorm:"primaryKey"` - Version string `gorm:"uniqueIndex"` + ID uint `gorm:"primaryKey"` // Primary key for the migration + Version string `gorm:"uniqueIndex"` // Unique version identifier for each migration } +// Migrator is responsible for applying database migrations. +// It holds a list of migrations, each associated with a version and a function that applies the migration. type Migrator struct { migrations []struct { - version string - migrate func(*gorm.DB) error + version string // The version of the migration + migrate func(*gorm.DB) error // The function that applies the migration } } +// NewMigrator initializes a new Migrator with a list of migrations. +// +// Each migration is represented by a version string and a function that performs the migration. +// In this case, the initial migration (version "1.0") creates the `Project` and `Task` tables. +// +// Example Migration: +// { +// version: "1.0", +// migrate: func(db *gorm.DB) error { +// return db.AutoMigrate(&models.Project{}, &models.Task{}) +// }, +// } func NewMigrator() *Migrator { return &Migrator{ migrations: []struct { @@ -24,38 +44,56 @@ func NewMigrator() *Migrator { migrate func(*gorm.DB) error }{ { - version: "1.0", + version: "1.0", // The first version of the database schema migrate: func(db *gorm.DB) error { + // Automatically migrates the schema for the Project and Task models return db.AutoMigrate(&models.Project{}, &models.Task{}) }, }, - // Example migration for reference: + // Example of how to add a new migration: // { - // version: "1.1", - // migrate: func(db *gorm.DB) error { - // return db.Exec("ALTER TABLE projects ADD COLUMN status VARCHAR(50)").Error - // }, + // version: "1.1", + // migrate: func(db *gorm.DB) error { + // // SQL or schema changes for version 1.1 + // return db.Exec("ALTER TABLE projects ADD COLUMN status VARCHAR(50)").Error + // }, // }, }, } } +// Migrate applies any pending migrations to the database. +// +// It first ensures that the `Migration` table exists, then checks the latest applied migration. +// Migrations that have a version greater than the last applied one are executed sequentially. +// After each migration is applied, a record is inserted into the `Migration` table. +// +// Parameters: +// - db: The GORM database connection. +// +// Returns: +// - An error if any migration fails, or nil if all migrations succeed. func (m *Migrator) Migrate(db *gorm.DB) error { + // Ensure the Migration table exists err := db.AutoMigrate(&Migration{}) if err != nil { return err } + // Retrieve the latest migration version from the database var lastMigration Migration db.Order("version desc").First(&lastMigration) + // Apply pending migrations for _, migration := range m.migrations { if migration.version > lastMigration.Version { + // Execute the migration function err = migration.migrate(db) if err != nil { return err } + // Record the applied migration version err = db.Create(&Migration{Version: migration.version}).Error if err != nil { return err From 554351490bf1e1b30834b1ec72102f4202164434 Mon Sep 17 00:00:00 2001 From: Lian Drake Date: Wed, 11 Sep 2024 11:31:36 -0400 Subject: [PATCH 73/81] Added documentation for pkg/repository/project_repo.go --- pkg/repository/project_repo.go | 56 ++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/pkg/repository/project_repo.go b/pkg/repository/project_repo.go index 06cb5fd..003ce01 100644 --- a/pkg/repository/project_repo.go +++ b/pkg/repository/project_repo.go @@ -4,10 +4,25 @@ import ( "github.com/d4r1us-drk/clido/pkg/models" ) +// CreateProject inserts a new project into the database. +// +// Parameters: +// - project: A pointer to the project model to be created. +// +// Returns: +// - An error if the operation fails; nil if successful. func (r *Repository) CreateProject(project *models.Project) error { return r.db.Create(project).Error } +// GetProjectByID retrieves a project from the database by its ID. +// +// Parameters: +// - id: The unique ID of the project to retrieve. +// +// Returns: +// - A pointer to the retrieved project if found. +// - An error if the project could not be found or another issue occurred. func (r *Repository) GetProjectByID(id int) (*models.Project, error) { var project models.Project err := r.db.First(&project, id).Error @@ -17,6 +32,14 @@ func (r *Repository) GetProjectByID(id int) (*models.Project, error) { return &project, nil } +// GetProjectByName retrieves a project from the database by its name. +// +// Parameters: +// - name: The unique name of the project to retrieve. +// +// Returns: +// - A pointer to the retrieved project if found. +// - An error if the project could not be found or another issue occurred. func (r *Repository) GetProjectByName(name string) (*models.Project, error) { var project models.Project err := r.db.Where("name = ?", name).First(&project).Error @@ -26,26 +49,59 @@ func (r *Repository) GetProjectByName(name string) (*models.Project, error) { return &project, nil } +// GetAllProjects retrieves all projects from the database. +// +// Returns: +// - A slice of pointers to all retrieved projects. +// - An error if the operation fails. func (r *Repository) GetAllProjects() ([]*models.Project, error) { var projects []*models.Project err := r.db.Find(&projects).Error return projects, err } +// GetSubprojects retrieves all subprojects that have the given parent project ID. +// +// Parameters: +// - parentProjectID: The ID of the parent project to retrieve subprojects for. +// +// Returns: +// - A slice of pointers to all subprojects under the specified parent project. +// - An error if the operation fails. func (r *Repository) GetSubprojects(parentProjectID int) ([]*models.Project, error) { var projects []*models.Project err := r.db.Where("parent_project_id = ?", parentProjectID).Find(&projects).Error return projects, err } +// UpdateProject updates an existing project in the database. +// +// Parameters: +// - project: A pointer to the project model to be updated. +// +// Returns: +// - An error if the update operation fails; nil if successful. func (r *Repository) UpdateProject(project *models.Project) error { return r.db.Save(project).Error } +// DeleteProject removes a project from the database by its ID. +// +// Parameters: +// - id: The unique ID of the project to be deleted. +// +// Returns: +// - An error if the deletion fails; nil if successful. func (r *Repository) DeleteProject(id int) error { return r.db.Delete(&models.Project{}, id).Error } +// GetNextProjectID retrieves the next available project ID in the database. +// It selects the maximum project ID and adds 1 to determine the next available ID. +// +// Returns: +// - The next available project ID as an integer. +// - An error if the operation fails. func (r *Repository) GetNextProjectID() (int, error) { var maxID int err := r.db.Model(&models.Project{}).Select("COALESCE(MAX(id), 0) + 1").Scan(&maxID).Error From 056020c5fa2374e2f9ae2817e8f202ec3bed4475 Mon Sep 17 00:00:00 2001 From: Lian Drake Date: Wed, 11 Sep 2024 11:31:45 -0400 Subject: [PATCH 74/81] Added documentation for pkg/repository/task_repo.go --- pkg/repository/task_repo.go | 56 +++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/pkg/repository/task_repo.go b/pkg/repository/task_repo.go index 3369fdb..267f00f 100644 --- a/pkg/repository/task_repo.go +++ b/pkg/repository/task_repo.go @@ -4,10 +4,25 @@ import ( "github.com/d4r1us-drk/clido/pkg/models" ) +// CreateTask inserts a new task into the database. +// +// Parameters: +// - task: A pointer to the task model to be created. +// +// Returns: +// - An error if the operation fails; nil if successful. func (r *Repository) CreateTask(task *models.Task) error { return r.db.Create(task).Error } +// GetTaskByID retrieves a task from the database by its ID. +// +// Parameters: +// - id: The unique ID of the task to retrieve. +// +// Returns: +// - A pointer to the retrieved task if found. +// - An error if the task could not be found or another issue occurred. func (r *Repository) GetTaskByID(id int) (*models.Task, error) { var task models.Task err := r.db.First(&task, id).Error @@ -17,32 +32,73 @@ func (r *Repository) GetTaskByID(id int) (*models.Task, error) { return &task, nil } +// GetAllTasks retrieves all tasks from the database. +// +// Returns: +// - A slice of pointers to all retrieved tasks. +// - An error if the operation fails. func (r *Repository) GetAllTasks() ([]*models.Task, error) { var tasks []*models.Task err := r.db.Find(&tasks).Error return tasks, err } +// GetTasksByProjectID retrieves all tasks associated with a specific project by the project's ID. +// +// Parameters: +// - projectID: The ID of the project to retrieve tasks for. +// +// Returns: +// - A slice of pointers to all tasks associated with the specified project. +// - An error if the operation fails. func (r *Repository) GetTasksByProjectID(projectID int) ([]*models.Task, error) { var tasks []*models.Task err := r.db.Where("project_id = ?", projectID).Find(&tasks).Error return tasks, err } +// GetSubtasks retrieves all subtasks that have the given parent task ID. +// +// Parameters: +// - parentTaskID: The ID of the parent task to retrieve subtasks for. +// +// Returns: +// - A slice of pointers to all subtasks under the specified parent task. +// - An error if the operation fails. func (r *Repository) GetSubtasks(parentTaskID int) ([]*models.Task, error) { var tasks []*models.Task err := r.db.Where("parent_task_id = ?", parentTaskID).Find(&tasks).Error return tasks, err } +// UpdateTask updates an existing task in the database. +// +// Parameters: +// - task: A pointer to the task model to be updated. +// +// Returns: +// - An error if the update operation fails; nil if successful. func (r *Repository) UpdateTask(task *models.Task) error { return r.db.Save(task).Error } +// DeleteTask removes a task from the database by its ID. +// +// Parameters: +// - id: The unique ID of the task to be deleted. +// +// Returns: +// - An error if the deletion fails; nil if successful. func (r *Repository) DeleteTask(id int) error { return r.db.Delete(&models.Task{}, id).Error } +// GetNextTaskID retrieves the next available task ID in the database. +// It selects the maximum task ID and adds 1 to determine the next available ID. +// +// Returns: +// - The next available task ID as an integer. +// - An error if the operation fails. func (r *Repository) GetNextTaskID() (int, error) { var maxID int err := r.db.Model(&models.Task{}).Select("COALESCE(MAX(id), 0) + 1").Scan(&maxID).Error From 6c5d03faa29b6f66ed11c6c3cbd5e55bfd38ba38 Mon Sep 17 00:00:00 2001 From: Lian Drake Date: Sat, 21 Sep 2024 21:29:44 -0400 Subject: [PATCH 75/81] Gigantic update, now we're using MVC, sorry code reviewers --- Makefile | 2 +- cmd/completion.go | 31 +- cmd/edit.go | 187 ++++------- cmd/list.go | 294 ++++++------------ cmd/new.go | 200 +++--------- cmd/remove.go | 71 +---- cmd/root.go | 64 ++-- cmd/toggle.go | 94 ++---- controllers/project_controller.go | 132 ++++++++ controllers/task_controller.go | 275 ++++++++++++++++ go.mod | 15 +- go.sum | 15 +- internal/version/version.go | 44 +-- main.go | 29 +- {pkg/models => models}/project.go | 6 +- {pkg/models => models}/task.go | 29 +- pkg/utils/helpers.go | 149 --------- {pkg/repository => repository}/migrations.go | 26 +- .../repository => repository}/project_repo.go | 49 +-- {pkg/repository => repository}/repository.go | 17 +- {pkg/repository => repository}/task_repo.go | 49 +-- utils/helpers.go | 122 ++++++++ 22 files changed, 910 insertions(+), 990 deletions(-) create mode 100644 controllers/project_controller.go create mode 100644 controllers/task_controller.go rename {pkg/models => models}/project.go (92%) rename {pkg/models => models}/task.go (56%) delete mode 100644 pkg/utils/helpers.go rename {pkg/repository => repository}/migrations.go (76%) rename {pkg/repository => repository}/project_repo.go (58%) rename {pkg/repository => repository}/repository.go (82%) rename {pkg/repository => repository}/task_repo.go (57%) create mode 100644 utils/helpers.go diff --git a/Makefile b/Makefile index c0f16fb..8f17f87 100644 --- a/Makefile +++ b/Makefile @@ -97,7 +97,7 @@ install: # Uninstall the application uninstall: - @rm $(GOPATH)/bin/$(BINARY_NAME) + @rm $(GOPATH)/bin/$(BINARY_NAME) # Installation help help: diff --git a/cmd/completion.go b/cmd/completion.go index d8998e7..0b6856e 100644 --- a/cmd/completion.go +++ b/cmd/completion.go @@ -11,16 +11,10 @@ import ( // // The command allows users to generate and load completion scripts for their preferred shell. // Completion scripts help users auto-complete command-line inputs for 'clido'. -// -// This command supports the following shells: -// - Bash -// - Zsh -// - Fish -// - PowerShell func NewCompletionCmd() *cobra.Command { return &cobra.Command{ - Use: "completion [bash|zsh|fish|powershell]", // Defines the valid subcommands for shell types - Short: "Generate completion script", // Brief description of the command + Use: "completion [bash|zsh|fish|powershell]", // Defines the valid subcommands for shell types + Short: "Generate completion script", // Brief description of the command Long: `To load completions: Bash: @@ -61,17 +55,22 @@ PowerShell: # and source this file from your PowerShell profile. `, // Detailed usage instructions for each shell - DisableFlagsInUseLine: true, // Disables flag usage display in the command usage line - ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, // Specifies valid arguments for shell types + DisableFlagsInUseLine: true, // Disables flag usage display in the command usage line + ValidArgs: []string{ + "bash", + "zsh", + "fish", + "powershell", + }, // Specifies valid arguments for shell types Args: func(cmd *cobra.Command, args []string) error { // Ensures exactly one argument (shell type) is provided if len(args) != 1 { cmd.PrintErrln( "Error: requires exactly one argument: bash, zsh, fish, or powershell", ) - return cobra.NoArgs(cmd, args) // Returns an error if no arguments are provided + return cobra.NoArgs(cmd, args) // Returns an error if no arguments are provided } - return cobra.OnlyValidArgs(cmd, args) // Validates the argument + return cobra.OnlyValidArgs(cmd, args) // Validates the argument }, Run: func(cmd *cobra.Command, args []string) { @@ -81,28 +80,28 @@ PowerShell: // Generate Bash completion script and output it to stdout if err := cmd.Root().GenBashCompletion(os.Stdout); err != nil { cmd.PrintErrf("Error generating bash completion: %v\n", err) - os.Exit(1) // Exit with error code 1 if there is a failure + os.Exit(1) // Exit with error code 1 if there is a failure } case "zsh": // Generate Zsh completion script and output it to stdout if err := cmd.Root().GenZshCompletion(os.Stdout); err != nil { cmd.PrintErrf("Error generating zsh completion: %v\n", err) - os.Exit(1) // Exit with error code 1 if there is a failure + os.Exit(1) // Exit with error code 1 if there is a failure } case "fish": // Generate Fish completion script and output it to stdout if err := cmd.Root().GenFishCompletion(os.Stdout, true); err != nil { cmd.PrintErrf("Error generating fish completion: %v\n", err) - os.Exit(1) // Exit with error code 1 if there is a failure + os.Exit(1) // Exit with error code 1 if there is a failure } case "powershell": // Generate PowerShell completion script and output it to stdout if err := cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout); err != nil { cmd.PrintErrf("Error generating PowerShell completion: %v\n", err) - os.Exit(1) // Exit with error code 1 if there is a failure + os.Exit(1) // Exit with error code 1 if there is a failure } } }, diff --git a/cmd/edit.go b/cmd/edit.go index d0ae047..4b50a71 100644 --- a/cmd/edit.go +++ b/cmd/edit.go @@ -2,27 +2,21 @@ package cmd import ( "strconv" - "time" - "github.com/d4r1us-drk/clido/pkg/models" - "github.com/d4r1us-drk/clido/pkg/repository" - "github.com/d4r1us-drk/clido/pkg/utils" + "github.com/d4r1us-drk/clido/controllers" + "github.com/d4r1us-drk/clido/utils" "github.com/spf13/cobra" ) // NewEditCmd creates and returns the 'edit' command for editing projects or tasks. -// -// The command allows users to modify existing projects or tasks by their unique ID. -// It supports editing the name, description, parent project, parent task, due date, and priority of a task. -// -// Usage: -// clido edit project # Edit a project by ID -// clido edit task # Edit a task by ID -func NewEditCmd() *cobra.Command { +func NewEditCmd( + projectController *controllers.ProjectController, + taskController *controllers.TaskController, +) *cobra.Command { cmd := &cobra.Command{ - Use: "edit [project|task] ", // Specifies the valid options: project or task followed by an ID - Short: "Edit an existing project or task", // Short description of the command - Long: `Edit the details of an existing project or task identified by its ID.`, // Extended description + Use: "edit [project|task] ", + Short: "Edit an existing project or task", + Long: `Edit the details of an existing project or task identified by its ID.`, Run: func(cmd *cobra.Command, args []string) { // Ensure the command receives sufficient arguments (either "project" or "task" followed by an ID) if len(args) < MinArgsLength { @@ -30,14 +24,6 @@ func NewEditCmd() *cobra.Command { return } - // Initialize the repository for database operations - repo, err := repository.NewRepository() - if err != nil { - cmd.Printf("Error initializing repository: %v\n", err) - return - } - defer repo.Close() - // Parse the ID argument into an integer id, err := strconv.Atoi(args[1]) if err != nil { @@ -48,9 +34,9 @@ func NewEditCmd() *cobra.Command { // Determine whether the user wants to edit a project or a task switch args[0] { case "project": - editProject(cmd, repo, id) + editProject(cmd, projectController, id) case "task": - editTask(cmd, repo, id) + editTask(cmd, taskController, id) default: cmd.Println("Invalid option. Use 'edit project ' or 'edit task '.") } @@ -58,132 +44,93 @@ func NewEditCmd() *cobra.Command { } // Define flags for the edit command, allowing users to specify what fields they want to update - cmd.Flags().StringP("name", "n", "", "New name") // Option to change the name of the project/task - cmd.Flags().StringP("description", "d", "", "New description") // Option to change the description - cmd.Flags().StringP("project", "p", "", "New parent project name or ID") // Option to change the parent project (for projects) - cmd.Flags().StringP("task", "t", "", "New parent task ID for subtasks") // Option to change the parent task (for tasks) - cmd.Flags().StringP("due", "D", "", "New due date for task (format: YYYY-MM-DD HH:MM)") // Option to set a new due date - cmd.Flags().IntP("priority", "P", 0, "New priority for task (1: High, 2: Medium, 3: Low, 4: None)") // Option to set a new priority level + cmd.Flags(). + StringP("name", "n", "", "New name") + cmd.Flags(). + StringP("description", "d", "", "New description") + cmd.Flags(). + StringP("project", "p", "", "New parent project name or ID") + cmd.Flags(). + StringP("task", "t", "", "New parent task ID for subtasks") + cmd.Flags(). + StringP("due", "D", "", "New due date for task (format: YYYY-MM-DD HH:MM)") + cmd.Flags(). + IntP("priority", "P", 0, "New priority for task (1: High, 2: Medium, 3: Low, 4: None)") return cmd } // editProject handles updating an existing project by its ID. -// The function retrieves the project from the repository, applies updates (name, description, parent project), -// and saves the changes back to the database. -// -// If the user provides a new parent project, it validates whether the project exists by name or ID. -func editProject(cmd *cobra.Command, repo *repository.Repository, id int) { - // Retrieve the project by its ID - project, err := repo.GetProjectByID(id) - if err != nil { - cmd.Printf("Error retrieving project: %v\n", err) - return - } - - // Get the new values from the command flags +// It retrieves input from flags (name, description, and parent project), and uses the ProjectController. +func editProject(cmd *cobra.Command, projectController *controllers.ProjectController, id int) { name, _ := cmd.Flags().GetString("name") description, _ := cmd.Flags().GetString("description") parentProjectIdentifier, _ := cmd.Flags().GetString("project") - // Apply the updates to the project, if specified by the user - if name != "" { - project.Name = name - } - if description != "" { - project.Description = description - } - if parentProjectIdentifier != "" { - if utils.IsNumeric(parentProjectIdentifier) { - parentID, _ := strconv.Atoi(parentProjectIdentifier) - project.ParentProjectID = &parentID - } else { - // If the parent project is provided by name, fetch it by name - var parentProject *models.Project - parentProject, err = repo.GetProjectByName(parentProjectIdentifier) - if err != nil || parentProject == nil { - cmd.Printf("Parent project '%s' not found.\n", parentProjectIdentifier) - return - } - project.ParentProjectID = &parentProject.ID - } + // Check if any fields are provided for update + if name == "" && description == "" && parentProjectIdentifier == "" { + cmd.Println( + "No fields provided for update. Use flags to update the name, description, or parent project.", + ) + return } - // Save the updated project to the database - err = repo.UpdateProject(project) + // Call the controller to edit the project + err := projectController.EditProject(id, name, description, parentProjectIdentifier) if err != nil { cmd.Printf("Error updating project: %v\n", err) return } - cmd.Printf("Project '%s' updated successfully.\n", project.Name) + cmd.Printf("Project with ID '%d' updated successfully.\n", id) } // editTask handles updating an existing task by its ID. -// The function retrieves the task from the repository, applies updates (name, description, due date, priority, parent task), -// and saves the changes back to the database. -// -// If the user provides a new parent task, it validates whether the task exists by ID. -func editTask(cmd *cobra.Command, repo *repository.Repository, id int) { - // Retrieve the task by its ID - task, err := repo.GetTaskByID(id) - if err != nil { - cmd.Printf("Error retrieving task: %v\n", err) - return - } - - // Get the new values from the command flags +// It retrieves input from flags (name, description, due date, priority, and parent task) and uses the TaskController. +func editTask(cmd *cobra.Command, taskController *controllers.TaskController, id int) { name, _ := cmd.Flags().GetString("name") description, _ := cmd.Flags().GetString("description") dueDateStr, _ := cmd.Flags().GetString("due") priority, _ := cmd.Flags().GetInt("priority") parentTaskIdentifier, _ := cmd.Flags().GetString("task") - // Apply the updates to the task, if specified by the user - if name != "" { - task.Name = name - } - if description != "" { - task.Description = description - } - if dueDateStr != "" { - // Parse the new due date - var parsedDate time.Time - parsedDate, err = time.Parse("2006-01-02 15:04", dueDateStr) - if err == nil { - task.DueDate = &parsedDate - } else { - cmd.Println("Invalid date format. Keeping the existing due date.") - } - } - if priority != 0 { - // Validate the priority (must be between 1 and 4) - if priority >= 1 && priority <= 4 { - task.Priority = utils.Priority(priority) - } else { - cmd.Println("Invalid priority. Keeping the existing priority.") - } + // Validate priority if provided + if priority != 0 && (priority < PriorityHigh || priority > PriorityNone) { + cmd.Println("Invalid priority. Use 1 for High, 2 for Medium, 3 for Low, or 4 for None.") + return } - if parentTaskIdentifier != "" { - // Validate the parent task ID (must be numeric) - if utils.IsNumeric(parentTaskIdentifier) { - parentID, _ := strconv.Atoi(parentTaskIdentifier) - task.ParentTaskID = &parentID - } else { - cmd.Println("Parent task must be identified by a numeric ID.") - return - } + + // Check if any fields are provided for update + if name == "" && description == "" && dueDateStr == "" && priority == 0 && + parentTaskIdentifier == "" { + cmd.Println( + "No fields provided for update. Use flags to update the name, description, due date, priority, or parent task.", + ) + return } - // Save the updated task to the database - err = repo.UpdateTask(task) + // Call the controller to edit the task + err := taskController.EditTask( + id, + name, + description, + dueDateStr, + priority, + parentTaskIdentifier, + ) if err != nil { cmd.Printf("Error updating task: %v\n", err) return } - cmd.Printf("Task '%s' updated successfully.\n", task.Name) - cmd.Printf("New details: Priority: %s, Due Date: %s\n", - utils.GetPriorityString(task.Priority), - utils.FormatDate(task.DueDate)) + // Format and display the new details + priorityStr := utils.GetPriorityString(priority) + formattedDueDate := "None" + if dueDateStr != "" { + parsedDueDate, _ := utils.ParseDueDate(dueDateStr) + formattedDueDate = utils.FormatDate(parsedDueDate) + } + + cmd.Printf("Task with ID '%d' updated successfully.\n", id) + cmd.Printf("New details: Priority: %s, Due Date: %s\n", priorityStr, formattedDueDate) } diff --git a/cmd/list.go b/cmd/list.go index 6272a4b..7c813d7 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -4,75 +4,68 @@ import ( "encoding/json" "os" "strconv" - "strings" - "github.com/d4r1us-drk/clido/pkg/models" - "github.com/d4r1us-drk/clido/pkg/repository" - "github.com/d4r1us-drk/clido/pkg/utils" + "github.com/d4r1us-drk/clido/controllers" + "github.com/d4r1us-drk/clido/models" + "github.com/d4r1us-drk/clido/utils" "github.com/olekukonko/tablewriter" "github.com/spf13/cobra" + "github.com/xlab/treeprint" ) // NewListCmd creates and returns the 'list' command for displaying projects or tasks. -// -// The command allows users to list all projects or tasks. Tasks can be optionally filtered by project. -// Users can display the output in a table, tree view, or JSON format. -// -// Usage: -// clido list projects # List all projects -// clido list tasks # List all tasks -// clido list tasks -p 1 # List tasks filtered by project ID -// clido list tasks -p MyProject # List tasks filtered by project name -// clido list tasks -t # Display tasks in a tree-like structure -// clido list projects -j # Output projects in JSON format -func NewListCmd() *cobra.Command { +func NewListCmd( + projectController *controllers.ProjectController, + taskController *controllers.TaskController, +) *cobra.Command { cmd := &cobra.Command{ - Use: "list [projects|tasks]", // Specifies the options: 'projects' or 'tasks' - Short: "List projects or tasks", // Short description of the command - Long: `List all projects or tasks, optionally filtered by project for tasks.`, // Extended description + Use: "list [projects|tasks]", + Short: "List projects or tasks", + Long: `List all projects or tasks, optionally filtered by project for tasks.`, Run: func(cmd *cobra.Command, args []string) { - // Ensure at least one argument (either 'projects' or 'tasks') is provided if len(args) < 1 { cmd.Println("Insufficient arguments. Use 'list projects' or 'list tasks'.") return } - // Initialize the repository for database operations - repo, err := repository.NewRepository() - if err != nil { - cmd.Printf("Error initializing repository: %v\n", err) - return - } - defer repo.Close() - // Retrieve flags for output format outputJSON, _ := cmd.Flags().GetBool("json") treeView, _ := cmd.Flags().GetBool("tree") - // Determine whether to list projects or tasks switch args[0] { case "projects": - listProjects(cmd, repo, outputJSON, treeView) + listProjects(cmd, projectController, outputJSON, treeView) case "tasks": projectFilter, _ := cmd.Flags().GetString("project") - listTasks(cmd, repo, projectFilter, outputJSON, treeView) + listTasks( + cmd, + taskController, + projectController, + projectFilter, + outputJSON, + treeView, + ) default: cmd.Println("Invalid option. Use 'list projects' or 'list tasks'.") } }, } - // Define flags for the list command - cmd.Flags().StringP("project", "p", "", "Filter tasks by project name or ID") // Filter tasks by project - cmd.Flags().BoolP("json", "j", false, "Output list in JSON format") // Output as JSON - cmd.Flags().BoolP("tree", "t", false, "Display projects or tasks in a tree-like structure") // Display in tree view + cmd.Flags().StringP("project", "p", "", "Filter tasks by project name or ID") + cmd.Flags().BoolP("json", "j", false, "Output list in JSON format") + cmd.Flags().BoolP("tree", "t", false, "Display projects or tasks in a tree-like structure") return cmd } // listProjects lists all projects in either table, tree view, or JSON format. -func listProjects(cmd *cobra.Command, repo *repository.Repository, outputJSON bool, treeView bool) { - projects, err := repo.GetAllProjects() +func listProjects( + cmd *cobra.Command, + projectController *controllers.ProjectController, + outputJSON bool, + treeView bool, +) { + projects, err := projectController.ListProjects() if err != nil { cmd.Printf("Error listing projects: %v\n", err) return @@ -80,99 +73,43 @@ func listProjects(cmd *cobra.Command, repo *repository.Repository, outputJSON bo switch { case outputJSON: - // Output projects in JSON format - var jsonData []byte - jsonData, err = json.MarshalIndent(projects, "", " ") - if err != nil { - cmd.Printf("Error marshalling projects to JSON: %v\n", err) - return - } - cmd.Println(string(jsonData)) - + printProjectsJSON(cmd, projects) case treeView: - // Display projects in tree view - printProjectTree(cmd, projects, nil, 0) - + printProjectTree(cmd, projects) default: - // Display projects in a table - printProjectTable(cmd, repo, projects) + printProjectTable(cmd, projects) } } // listTasks lists tasks, optionally filtered by a project, in table, tree view, or JSON format. func listTasks( cmd *cobra.Command, - repo *repository.Repository, + taskController *controllers.TaskController, + projectController *controllers.ProjectController, projectFilter string, outputJSON bool, treeView bool, ) { - tasks, project, err := getTasks(repo, projectFilter) + tasks, project, err := taskController.ListTasksByProjectFilter(projectFilter) if err != nil { - cmd.Println(err) + cmd.Printf("Error listing tasks: %v\n", err) return } if !outputJSON { - // Print header for tasks printTaskHeader(cmd, project) } switch { case outputJSON: - // Output tasks in JSON format printTasksJSON(cmd, tasks) case treeView: - // Display tasks in tree view - printTaskTree(cmd, tasks, nil, 0) + printTaskTree(cmd, tasks) default: - // Display tasks in a table - printTaskTable(repo, tasks) + printTaskTable(taskController, projectController, tasks) } } -// getTasks retrieves tasks filtered by a project or returns all tasks if no filter is provided. -func getTasks( - repo *repository.Repository, - projectFilter string, -) ([]*models.Task, *models.Project, error) { - if projectFilter == "" { - // Return all tasks if no project filter is specified - tasks, err := repo.GetAllTasks() - return tasks, nil, err - } - - // Get tasks filtered by project - project, err := getProject(repo, projectFilter) - if err != nil { - return nil, nil, err - } - - tasks, err := repo.GetTasksByProjectID(project.ID) - return tasks, project, err -} - -// getProject retrieves a project by its name or ID, based on the projectFilter input. -func getProject(repo *repository.Repository, projectFilter string) (*models.Project, error) { - var project *models.Project - var err error - - if utils.IsNumeric(projectFilter) { - // Retrieve project by ID - projectID, _ := strconv.Atoi(projectFilter) - project, err = repo.GetProjectByID(projectID) - } else { - // Retrieve project by name - project, err = repo.GetProjectByName(projectFilter) - } - - if err != nil || project == nil { - return nil, err - } - - return project, nil -} - // printTaskHeader prints the header for the task list, either all tasks or tasks within a specific project. func printTaskHeader(cmd *cobra.Command, project *models.Project) { if project != nil { @@ -182,6 +119,15 @@ func printTaskHeader(cmd *cobra.Command, project *models.Project) { } } +func printProjectsJSON(cmd *cobra.Command, projects []*models.Project) { + jsonData, jsonErr := json.MarshalIndent(projects, "", " ") + if jsonErr != nil { + cmd.Printf("Error marshalling projects to JSON: %v\n", jsonErr) + return + } + cmd.Println(string(jsonData)) +} + // printTasksJSON outputs the tasks in JSON format. func printTasksJSON(cmd *cobra.Command, tasks []*models.Task) { jsonData, err := json.MarshalIndent(tasks, "", " ") @@ -193,33 +139,17 @@ func printTasksJSON(cmd *cobra.Command, tasks []*models.Task) { } // printProjectTable displays the list of projects in a table format. -func printProjectTable( - cmd *cobra.Command, - repo *repository.Repository, - projects []*models.Project, -) { - table := tablewriter.NewWriter(os.Stdout) +func printProjectTable(cmd *cobra.Command, projects []*models.Project) { + table := tablewriter.NewWriter(cmd.OutOrStdout()) table.SetHeader([]string{"ID", "Name", "Description", "Type", "Child Of"}) table.SetRowLine(true) for _, project := range projects { - // Determine whether the project is a parent or child project typeField := "Parent" parentChildField := "None" if project.ParentProjectID != nil { typeField = "Child" - parentProject, _ := repo.GetProjectByID(*project.ParentProjectID) - if parentProject != nil { - parentChildField = parentProject.Name - } - } else { - subprojects, _ := repo.GetSubprojects(project.ID) - if len(subprojects) > 0 { - typeField = "Parent" - } } - - // Add project details to the table table.Append([]string{ strconv.Itoa(project.ID), utils.WrapText(project.Name, MaxProjectNameLength), @@ -234,7 +164,11 @@ func printProjectTable( } // printTaskTable displays the list of tasks in a table format. -func printTaskTable(repo *repository.Repository, tasks []*models.Task) { +func printTaskTable( + taskController *controllers.TaskController, + projectController *controllers.ProjectController, + tasks []*models.Task, +) { table := tablewriter.NewWriter(os.Stdout) table.SetHeader([]string{ "ID", "Name", "Description", "Due Date", "Completed", "Past Due", "Priority", "Project", "Type", "Parent/Child Of", @@ -242,30 +176,28 @@ func printTaskTable(repo *repository.Repository, tasks []*models.Task) { table.SetRowLine(true) for _, task := range tasks { - // Determine whether the task is a parent or child task typeField := "Parent" parentChildField := "None" + if task.ParentTaskID != nil { typeField = "Child" - parentTask, _ := repo.GetTaskByID(*task.ParentTaskID) + parentTask, _ := taskController.GetTaskByID(*task.ParentTaskID) if parentTask != nil { parentChildField = parentTask.Name } } else { - subtasks, _ := repo.GetSubtasks(task.ID) + subtasks, _ := taskController.ListSubtasks(task.ID) if len(subtasks) > 0 { typeField = "Parent" } } - // Get project name for the task - project, _ := repo.GetProjectByID(task.ProjectID) + project, _ := projectController.GetProjectByID(task.ProjectID) projectName := "" if project != nil { projectName = project.Name } - // Add task details to the table table.Append([]string{ strconv.Itoa(task.ID), utils.WrapText(task.Name, MaxTaskNameLength), @@ -283,84 +215,52 @@ func printTaskTable(repo *repository.Repository, tasks []*models.Task) { table.Render() } -// printProjectTree displays projects in a tree view. -func printProjectTree(cmd *cobra.Command, projects []*models.Project, parentID *int, level int) { - nodes := make([]TreeNode, len(projects)) - for i, p := range projects { - nodes[i] = ProjectNode{p} - } - printTree(cmd, nodes, parentID, level, nil) -} +// printProjectTree displays projects in a tree view using treeprint. +func printProjectTree(cmd *cobra.Command, projects []*models.Project) { + tree := treeprint.New() + projectMap := make(map[int]treeprint.Tree) -// printTaskTree displays tasks in a tree view. -func printTaskTree(cmd *cobra.Command, tasks []*models.Task, parentID *int, level int) { - nodes := make([]TreeNode, len(tasks)) - for i, t := range tasks { - nodes[i] = TaskNode{t} + for _, project := range projects { + projectLabel := formatProjectLabel(project) + if project.ParentProjectID != nil { + parentNode, exists := projectMap[*project.ParentProjectID] + if exists { + projectMap[project.ID] = parentNode.AddBranch(projectLabel) + } + } else { + projectMap[project.ID] = tree.AddBranch(projectLabel) + } } - printTree(cmd, nodes, parentID, level, func(node TreeNode, indent string) { - // Print task details in the tree view - task := node.(TaskNode).Task - cmd.Printf("%sDescription: %s\n", indent, task.Description) - cmd.Printf( - "%sDue Date: %s, Completed: %v, Priority: %s\n", - indent, - utils.FormatDate(task.DueDate), - task.TaskCompleted, - utils.GetPriorityString(task.Priority), - ) - }) -} -// TreeNode represents a node in a tree structure (for both projects and tasks). -type TreeNode interface { - GetID() int - GetParentID() *int - GetName() string + cmd.Println(tree.String()) } -// ProjectNode represents a project in the tree view. -type ProjectNode struct { - *models.Project -} +// printTaskTree displays tasks in a tree view using treeprint. +func printTaskTree(cmd *cobra.Command, tasks []*models.Task) { + tree := treeprint.New() + taskMap := make(map[int]treeprint.Tree) -func (p ProjectNode) GetID() int { return p.ID } -func (p ProjectNode) GetParentID() *int { return p.ParentProjectID } -func (p ProjectNode) GetName() string { return p.Name } + for _, task := range tasks { + taskLabel := formatTaskLabel(task) + if task.ParentTaskID != nil { + parentNode, exists := taskMap[*task.ParentTaskID] + if exists { + taskMap[task.ID] = parentNode.AddBranch(taskLabel) + } + } else { + taskMap[task.ID] = tree.AddBranch(taskLabel) + } + } -// TaskNode represents a task in the tree view. -type TaskNode struct { - *models.Task + cmd.Println(tree.String()) } -func (t TaskNode) GetID() int { return t.ID } -func (t TaskNode) GetParentID() *int { return t.ParentTaskID } -func (t TaskNode) GetName() string { return t.Name } +// formatProjectLabel creates a label for each project node. +func formatProjectLabel(project *models.Project) string { + return project.Name + " (ID: " + strconv.Itoa(project.ID) + ")" +} -// printTree prints the tree structure for projects or tasks. -func printTree( - cmd *cobra.Command, - nodes []TreeNode, - parentID *int, - level int, - printDetails func(TreeNode, string), -) { - indent := strings.Repeat("│ ", level) - for i, node := range nodes { - if (parentID == nil && node.GetParentID() == nil) || - (parentID != nil && node.GetParentID() != nil && *node.GetParentID() == *parentID) { - // Use appropriate tree symbols for formatting - prefix := "├──" - if i == len(nodes)-1 { - prefix = "└──" - } - cmd.Printf("%s%s %s (ID: %d)\n", indent, prefix, node.GetName(), node.GetID()) - if printDetails != nil { - printDetails(node, indent+" ") - } - nodeID := node.GetID() - // Recursively print child nodes - printTree(cmd, nodes, &nodeID, level+1, printDetails) - } - } +// formatTaskLabel creates a label for each task node. +func formatTaskLabel(task *models.Task) string { + return task.Name + " (ID: " + strconv.Itoa(task.ID) + ")" } diff --git a/cmd/new.go b/cmd/new.go index 584ad38..2d0ed58 100644 --- a/cmd/new.go +++ b/cmd/new.go @@ -1,114 +1,62 @@ package cmd import ( - "errors" - "strconv" - "time" - - "github.com/d4r1us-drk/clido/pkg/models" - "github.com/d4r1us-drk/clido/pkg/repository" - "github.com/d4r1us-drk/clido/pkg/utils" + "github.com/d4r1us-drk/clido/controllers" "github.com/spf13/cobra" ) -var ( - // ErrNoParentTask is returned when no parent task is provided during task creation. - ErrNoParentTask = errors.New("no parent task specified") - - // ErrNoDueDate is returned when no due date is provided during task creation. - ErrNoDueDate = errors.New("no due date specified") -) - // NewNewCmd creates and returns the 'new' command for creating projects or tasks. -// -// The command allows users to create new projects or tasks with the specified details, -// such as name, description, parent project, parent task, due date, and priority. -// -// Usage: -// clido new project # Create a new project -// clido new task # Create a new task -func NewNewCmd() *cobra.Command { +func NewNewCmd( + projectController *controllers.ProjectController, + taskController *controllers.TaskController, +) *cobra.Command { cmd := &cobra.Command{ - Use: "new [project|task]", // Specifies valid options: 'project' or 'task' - Short: "Create a new project or task", // Short description of the command - Long: `Create a new project or task with the specified details.`, // Extended description + Use: "new [project|task]", + Short: "Create a new project or task", + Long: `Create a new project or task with the specified details.`, Run: func(cmd *cobra.Command, args []string) { - // Ensure sufficient arguments (either 'project' or 'task') if len(args) < 1 { cmd.Println("Insufficient arguments. Use 'new project' or 'new task'.") return } - // Initialize the repository for database operations - repo, err := repository.NewRepository() - if err != nil { - cmd.Printf("Error initializing repository: %v\n", err) - return - } - defer repo.Close() - - // Determine whether the user wants to create a project or a task + // Create project or task switch args[0] { case "project": - createProject(cmd, repo) + createProject(cmd, projectController) case "task": - createTask(cmd, repo) + createTask(cmd, taskController) default: cmd.Println("Invalid option. Use 'new project' or 'new task'.") } }, } - // Define flags for the new command, allowing users to specify details - cmd.Flags().StringP("name", "n", "", "Name of the project or task") // Name of the project/task (required) - cmd.Flags().StringP("description", "d", "", "Description of the project or task") // Description - cmd.Flags().StringP("project", "p", "", "Parent project name or ID for subprojects or tasks") // Parent project - cmd.Flags().StringP("task", "t", "", "Parent task ID for subtasks") // Parent task for subtasks - cmd.Flags().StringP("due", "D", "", "Due date for the task (format: YYYY-MM-DD HH:MM)") // Due date - cmd.Flags().IntP("priority", "P", 0, "Priority of the task (1: High, 2: Medium, 3: Low, 4: None)") // Task priority + // Define flags for project and task creation + cmd.Flags().StringP("name", "n", "", "Name of the project or task") + cmd.Flags().StringP("description", "d", "", "Description of the project or task") + cmd.Flags().StringP("project", "p", "", "Parent project name or ID for subprojects or tasks") + cmd.Flags().StringP("task", "t", "", "Parent task ID for subtasks") + cmd.Flags().StringP("due", "D", "", "Due date for the task (format: YYYY-MM-DD HH:MM)") + cmd.Flags(). + IntP("priority", "P", PriorityEmpty, "Priority of the task (1: High, 2: Medium, 3: Low, 4: None)") return cmd } -// createProject handles the creation of a new project. -// It retrieves input from flags (name, description, and parent project), validates them, -// and saves the new project to the database. -func createProject(cmd *cobra.Command, repo *repository.Repository) { +func createProject(cmd *cobra.Command, projectController *controllers.ProjectController) { name, _ := cmd.Flags().GetString("name") description, _ := cmd.Flags().GetString("description") parentProjectIdentifier, _ := cmd.Flags().GetString("project") - // Ensure the project name is provided + // Ensure project name is provided if name == "" { cmd.Println("Project name is required.") return } - var parentProjectID *int - if parentProjectIdentifier != "" { - // Determine whether the parent project is specified by ID or name - if utils.IsNumeric(parentProjectIdentifier) { - id, _ := strconv.Atoi(parentProjectIdentifier) - parentProjectID = &id - } else { - parentProject, err := repo.GetProjectByName(parentProjectIdentifier) - if err != nil || parentProject == nil { - cmd.Printf("Parent project '%s' not found.\n", parentProjectIdentifier) - return - } - parentProjectID = &parentProject.ID - } - } - - // Create the new project - project := &models.Project{ - Name: name, - Description: description, - ParentProjectID: parentProjectID, - } - - // Save the new project to the database - err := repo.CreateProject(project) + // Call the controller to create the project + err := projectController.CreateProject(name, description, parentProjectIdentifier) if err != nil { cmd.Printf("Error creating project: %v\n", err) return @@ -117,10 +65,7 @@ func createProject(cmd *cobra.Command, repo *repository.Repository) { cmd.Printf("Project '%s' created successfully.\n", name) } -// createTask handles the creation of a new task. -// It retrieves input from flags (name, description, project, parent task, due date, and priority), -// validates them, and saves the new task to the database. -func createTask(cmd *cobra.Command, repo *repository.Repository) { +func createTask(cmd *cobra.Command, taskController *controllers.TaskController) { name, _ := cmd.Flags().GetString("name") description, _ := cmd.Flags().GetString("description") projectIdentifier, _ := cmd.Flags().GetString("project") @@ -128,100 +73,31 @@ func createTask(cmd *cobra.Command, repo *repository.Repository) { dueDateStr, _ := cmd.Flags().GetString("due") priority, _ := cmd.Flags().GetInt("priority") - // Ensure the task name is provided + // Ensure task name is provided if name == "" { cmd.Println("Task name is required.") return } - // Validate and retrieve the project ID - projectID, err := getProjectID(projectIdentifier, repo) - if err != nil { - cmd.Println(err) - return - } - - // Validate and retrieve the parent task ID, if provided - parentTaskID, err := getParentTaskID(parentTaskIdentifier) - if err != nil && !errors.Is(err, ErrNoParentTask) { - cmd.Println(err) + // Add validation for priority + if priority != 0 && (priority < 1 || priority > 4) { + cmd.Println("Invalid priority. Use 1 for High, 2 for Medium, 3 for Low, or 4 for None.") return } - // Validate and parse the due date, if provided - dueDate, err := parseDueDate(dueDateStr) - if err != nil && !errors.Is(err, ErrNoDueDate) { - cmd.Println("Invalid date format. Using no due date.") - } - - // Create the new task - task := &models.Task{ - Name: name, - Description: description, - ProjectID: projectID, - DueDate: dueDate, - Priority: utils.Priority(priority), - ParentTaskID: parentTaskID, - } - - // Save the new task to the database - if err = repo.CreateTask(task); err != nil { - cmd.Printf("Error creating task: %v\n", err) - return - } - - cmd.Printf( - "Task '%s' created successfully with priority %s.\n", + // Call the controller to create the task + err := taskController.CreateTask( name, - utils.GetPriorityString(utils.Priority(priority)), + description, + projectIdentifier, + parentTaskIdentifier, + dueDateStr, + priority, ) -} - -// getProjectID retrieves the project ID based on the identifier (either name or ID). -// It returns an error if the project does not exist. -func getProjectID(identifier string, repo *repository.Repository) (int, error) { - if identifier == "" { - return 0, errors.New("task must be associated with a project") - } - - if utils.IsNumeric(identifier) { - return strconv.Atoi(identifier) - } - - project, err := repo.GetProjectByName(identifier) - if err != nil || project == nil { - return 0, errors.New("project '" + identifier + "' not found") - } - - return project.ID, nil -} - -// getParentTaskID retrieves the parent task ID based on the identifier. -// It returns an error if the identifier is not a numeric ID. -func getParentTaskID(identifier string) (*int, error) { - if identifier == "" { - return nil, ErrNoParentTask - } - - if !utils.IsNumeric(identifier) { - return nil, errors.New("parent task must be identified by a numeric ID") - } - - id, _ := strconv.Atoi(identifier) - return &id, nil -} - -// parseDueDate parses a string into a time.Time object. -// It returns an error if the string is not in the expected format. -func parseDueDate(dateStr string) (*time.Time, error) { - if dateStr == "" { - return nil, ErrNoDueDate - } - - parsedDate, err := time.Parse("2006-01-02 15:04", dateStr) if err != nil { - return nil, err + cmd.Printf("Error creating task: %v\n", err) + return } - return &parsedDate, nil + cmd.Printf("Task '%s' created successfully.\n", name) } diff --git a/cmd/remove.go b/cmd/remove.go index a4fb49e..2d90a76 100644 --- a/cmd/remove.go +++ b/cmd/remove.go @@ -3,24 +3,19 @@ package cmd import ( "strconv" - "github.com/d4r1us-drk/clido/pkg/repository" + "github.com/d4r1us-drk/clido/controllers" "github.com/spf13/cobra" ) // NewRemoveCmd creates and returns the 'remove' command for deleting projects or tasks. -// -// The command allows users to remove an existing project or task by its ID. -// It also ensures that all associated subprojects or subtasks are recursively removed. -// -// Usage: -// clido remove project # Remove a project and its subprojects -// clido remove task # Remove a task and its subtasks -func NewRemoveCmd() *cobra.Command { +func NewRemoveCmd( + projectController *controllers.ProjectController, + taskController *controllers.TaskController, +) *cobra.Command { cmd := &cobra.Command{ - Use: "remove [project|task] ", // Specifies valid options: 'project' or 'task' followed by an ID - Short: "Remove a project or task along with all its subprojects or subtasks", // Short description of the command - Long: `Remove an existing project or task identified by its ID. This will also remove all associated subprojects - or subtasks.`, // Extended description with clarification on subprojects and subtasks being removed recursively + Use: "remove [project|task] ", + Short: "Remove a project or task along with all its subprojects or subtasks", + Long: "Remove a project or task by ID, along with all its sub-items.", Run: func(cmd *cobra.Command, args []string) { // Ensure sufficient arguments (either 'project' or 'task' followed by an ID) if len(args) < MinArgsLength { @@ -30,14 +25,6 @@ func NewRemoveCmd() *cobra.Command { return } - // Initialize the repository for database operations - repo, err := repository.NewRepository() - if err != nil { - cmd.Printf("Error initializing repository: %v\n", err) - return - } - defer repo.Close() - // Parse the ID argument into an integer id, err := strconv.Atoi(args[1]) if err != nil { @@ -48,9 +35,9 @@ func NewRemoveCmd() *cobra.Command { // Determine whether the user wants to remove a project or a task switch args[0] { case "project": - removeProject(cmd, repo, id) + removeProject(cmd, projectController, id) case "task": - removeTask(cmd, repo, id) + removeTask(cmd, taskController, id) default: cmd.Println("Invalid option. Use 'remove project ' or 'remove task '.") } @@ -61,22 +48,9 @@ func NewRemoveCmd() *cobra.Command { } // removeProject handles the recursive removal of a project and all its subprojects. -// It first retrieves and removes all subprojects, and then deletes the parent project. -func removeProject(cmd *cobra.Command, repo *repository.Repository, id int) { - // Retrieve all subprojects associated with the project - subprojects, err := repo.GetSubprojects(id) - if err != nil { - cmd.Printf("Error retrieving subprojects: %v\n", err) - return - } - - // Recursively remove all subprojects - for _, subproject := range subprojects { - removeProject(cmd, repo, subproject.ID) - } - - // Remove the parent project after all subprojects have been removed - err = repo.DeleteProject(id) +// It uses the ProjectController to handle the deletion. +func removeProject(cmd *cobra.Command, projectController *controllers.ProjectController, id int) { + err := projectController.RemoveProject(id) if err != nil { cmd.Printf("Error removing project: %v\n", err) return @@ -86,22 +60,9 @@ func removeProject(cmd *cobra.Command, repo *repository.Repository, id int) { } // removeTask handles the recursive removal of a task and all its subtasks. -// It first retrieves and removes all subtasks, and then deletes the parent task. -func removeTask(cmd *cobra.Command, repo *repository.Repository, id int) { - // Retrieve all subtasks associated with the task - subtasks, err := repo.GetSubtasks(id) - if err != nil { - cmd.Printf("Error retrieving subtasks: %v\n", err) - return - } - - // Recursively remove all subtasks - for _, subtask := range subtasks { - removeTask(cmd, repo, subtask.ID) - } - - // Remove the parent task after all subtasks have been removed - err = repo.DeleteTask(id) +// It uses the TaskController to handle the deletion. +func removeTask(cmd *cobra.Command, taskController *controllers.TaskController, id int) { + err := taskController.RemoveTask(id) if err != nil { cmd.Printf("Error removing task: %v\n", err) return diff --git a/cmd/root.go b/cmd/root.go index 10aa5da..a25da44 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -3,68 +3,66 @@ package cmd import ( "os" + "github.com/d4r1us-drk/clido/controllers" "github.com/d4r1us-drk/clido/internal/version" "github.com/spf13/cobra" ) -// Constants for table printing. These are not user constraints +// Constants for table printing. These are not user constraints. const ( - MaxProjectNameLength = 30 // Maximum length for project names - MaxProjectDescLength = 50 // Maximum length for project descriptions - MaxTaskNameLength = 20 // Maximum length for task names - MaxTaskDescLength = 30 // Maximum length for task descriptions - MaxProjectNameWrapLength = 20 // Maximum length for wrapping project names in the UI - MinArgsLength = 2 // Minimum required arguments for certain commands + MaxProjectNameLength = 30 // Maximum length for project names + MaxProjectDescLength = 50 // Maximum length for project descriptions + MaxTaskNameLength = 20 // Maximum length for task names + MaxTaskDescLength = 30 // Maximum length for task descriptions + MaxProjectNameWrapLength = 20 // Maximum length for wrapping project names in the UI + MinArgsLength = 2 // Minimum required arguments for certain commands + PriorityHigh = 1 + PriorityMedium = 2 + PriorityLow = 3 + PriorityNone = 4 + PriorityEmpty = 0 ) // NewRootCmd creates and returns the root command for the CLI application. -// This is the entry point of the application, which is responsible for managing -// various subcommands like version, new, edit, list, remove, and toggle commands. -// -// Returns a *cobra.Command which acts as the root command for all subcommands. -func NewRootCmd() *cobra.Command { +func NewRootCmd( + projectController *controllers.ProjectController, + taskController *controllers.TaskController, +) *cobra.Command { rootCmd := &cobra.Command{ Use: "clido", Short: "Clido is an awesome CLI to-do list management application", - Long: `Clido is a simple yet powerful CLI tool designed to help you manage - your projects and tasks effectively from the terminal.`, + Long: `Clido is a simple yet powerful CLI tool designed to help you manage + your projects and tasks effectively from the terminal.`, } - // Adding subcommands to rootCmd - rootCmd.AddCommand(NewVersionCmd()) // Version command to display the app version - rootCmd.AddCommand(NewCompletionCmd()) // Completion command to generate shell autocompletion scripts - rootCmd.AddCommand(NewNewCmd()) // New command to add a new project or task - rootCmd.AddCommand(NewEditCmd()) // Edit command to modify an existing project or task - rootCmd.AddCommand(NewListCmd()) // List command to display projects or tasks - rootCmd.AddCommand(NewRemoveCmd()) // Remove command to delete a project or task - rootCmd.AddCommand(NewToggleCmd()) // Toggle command to change the status of a task + // Add subcommands and pass the controllers + rootCmd.AddCommand(NewVersionCmd()) // Version command to display the app version + rootCmd.AddCommand(NewCompletionCmd()) + rootCmd.AddCommand(NewNewCmd(projectController, taskController)) + rootCmd.AddCommand(NewEditCmd(projectController, taskController)) + rootCmd.AddCommand(NewListCmd(projectController, taskController)) + rootCmd.AddCommand(NewRemoveCmd(projectController, taskController)) + rootCmd.AddCommand(NewToggleCmd(taskController)) return rootCmd } -// NewVersionCmd creates and returns the version command. -// This command prints the current version of clido, using the version package. -// It helps users check which version of the tool they are running. +// NewVersionCmd creates the version command. func NewVersionCmd() *cobra.Command { return &cobra.Command{ Use: "version", Short: "Print the version number of Clido", Run: func(cmd *cobra.Command, _ []string) { - // Print the full version of the application, stored in the version package. cmd.Println(version.FullVersion()) }, } } -// Execute runs the root command of the application, which triggers -// the appropriate subcommand based on user input. -// -// If an error occurs during execution (such as invalid command usage), -// the application prints the error message and exits with a non-zero status code. +// Execute runs the root command. func Execute() { - rootCmd := NewRootCmd() + rootCmd := NewRootCmd(nil, nil) if err := rootCmd.Execute(); err != nil { rootCmd.Println(err) - os.Exit(1) // Exit the application with an error status code if the command fails + os.Exit(1) } } diff --git a/cmd/toggle.go b/cmd/toggle.go index ddc98ae..9171675 100644 --- a/cmd/toggle.go +++ b/cmd/toggle.go @@ -2,26 +2,17 @@ package cmd import ( "strconv" - "time" - "github.com/d4r1us-drk/clido/pkg/repository" + "github.com/d4r1us-drk/clido/controllers" "github.com/spf13/cobra" ) // NewToggleCmd creates and returns the 'toggle' command for marking tasks as completed or uncompleted. -// -// The command allows users to toggle the completion status of a task by its ID. -// If the task is marked as completed, it updates the completion date. -// The command also supports recursively toggling the status of all subtasks. -// -// Usage: -// clido toggle # Toggle the completion status of a task -// clido toggle -r # Recursively toggle the completion status of the task and its subtasks -func NewToggleCmd() *cobra.Command { +func NewToggleCmd(taskController *controllers.TaskController) *cobra.Command { cmd := &cobra.Command{ - Use: "toggle ", // Specifies the required task ID as the argument - Short: "Toggle task completion status", // Short description of the command - Long: `Toggle the completion status of a task identified by its ID.`, // Extended description + Use: "toggle ", + Short: "Toggle task completion status", + Long: `Toggle the completion status of a task identified by its ID.`, Run: func(cmd *cobra.Command, args []string) { // Ensure at least one argument (the task ID) is provided if len(args) < 1 { @@ -29,14 +20,6 @@ func NewToggleCmd() *cobra.Command { return } - // Initialize the repository for database operations - repo, err := repository.NewRepository() - if err != nil { - cmd.Printf("Error initializing repository: %v\n", err) - return - } - defer repo.Close() - // Parse the task ID argument into an integer id, err := strconv.Atoi(args[0]) if err != nil { @@ -46,7 +29,23 @@ func NewToggleCmd() *cobra.Command { // Check if the recursive flag was provided recursive, _ := cmd.Flags().GetBool("recursive") - toggleTask(cmd, repo, id, recursive) + + // Toggle task completion status using the controller + completionStatus, toggleErr := taskController.ToggleTaskCompletion(id, recursive) + if toggleErr != nil { + cmd.Printf("Error toggling task: %v\n", toggleErr) + return + } + + if recursive { + cmd.Printf( + "Task (ID: %d) and its subtasks (if any) have been set as %s.\n", + id, + completionStatus, + ) + } else { + cmd.Printf("Task (ID: %d) has been set as %s.\n", id, completionStatus) + } }, } @@ -55,52 +54,3 @@ func NewToggleCmd() *cobra.Command { return cmd } - -// toggleTask toggles the completion status of a task and optionally its subtasks. -// -// If the task is currently marked as incomplete, it will be marked as completed, -// and the completion date will be updated. If the task is marked as completed, -// it will be toggled to incomplete and the completion date will be cleared. -// -// If the recursive flag is set to true, this function will also toggle the completion status of all subtasks. -func toggleTask(cmd *cobra.Command, repo *repository.Repository, id int, recursive bool) { - // Retrieve the task by its ID - task, err := repo.GetTaskByID(id) - if err != nil { - cmd.Printf("Error retrieving task: %v\n", err) - return - } - - // Toggle the task's completion status - task.TaskCompleted = !task.TaskCompleted - task.LastUpdatedDate = time.Now() - - // Set the completion date if the task is marked as completed, or clear it if uncompleted - if task.TaskCompleted { - task.CompletionDate = &task.LastUpdatedDate - } else { - task.CompletionDate = nil - } - - // Update the task in the repository - err = repo.UpdateTask(task) - if err != nil { - cmd.Printf("Error updating task: %v\n", err) - return - } - - // Display the updated status of the task - status := "completed" - if !task.TaskCompleted { - status = "uncompleted" - } - cmd.Printf("Task '%s' (ID: %d) marked as %s.\n", task.Name, id, status) - - // If the recursive flag is set, retrieve and toggle the completion status of all subtasks - if recursive { - subtasks, _ := repo.GetSubtasks(id) - for _, subtask := range subtasks { - toggleTask(cmd, repo, subtask.ID, recursive) // Recursively toggle subtasks - } - } -} diff --git a/controllers/project_controller.go b/controllers/project_controller.go new file mode 100644 index 0000000..fcff8b7 --- /dev/null +++ b/controllers/project_controller.go @@ -0,0 +1,132 @@ +package controllers + +import ( + "errors" + + "github.com/d4r1us-drk/clido/models" + "github.com/d4r1us-drk/clido/repository" + "github.com/d4r1us-drk/clido/utils" +) + +// Error constants for project operations. +var ( + ErrNoProjectName = errors.New("project name is required") + ErrParentProjectNotFound = errors.New("parent project not found") +) + +// ProjectController manages the project-related business logic. +type ProjectController struct { + repo *repository.Repository +} + +// NewProjectController creates and returns a new instance of ProjectController. +func NewProjectController(repo *repository.Repository) *ProjectController { + return &ProjectController{repo: repo} +} + +// CreateProject handles the creation of a new project. +func (pc *ProjectController) CreateProject( + name, description, parentProjectIdentifier string, +) error { + // Validate project name + if name == "" { + return ErrNoProjectName + } + + // Retrieve the parent project ID (if any) + var parentProjectID *int + if parentProjectIdentifier != "" { + parentID, projectErr := utils.ParseIntOrError(parentProjectIdentifier) + if projectErr != nil { + project, lookupErr := pc.repo.GetProjectByName(parentProjectIdentifier) + if lookupErr != nil { + return ErrParentProjectNotFound + } + parentID = project.ID + } + parentProjectID = &parentID + } + + // Create a new project + project := models.Project{ + Name: name, + Description: description, + ParentProjectID: parentProjectID, + } + + // Store the project in the repository + return pc.repo.CreateProject(&project) +} + +// EditProject handles updating an existing project by its ID. +func (pc *ProjectController) EditProject( + id int, + name, description, parentProjectIdentifier string, +) error { + // Retrieve the existing project + project, getProjectErr := pc.repo.GetProjectByID(id) + if getProjectErr != nil { + return getProjectErr + } + + // Apply updates + if name != "" { + project.Name = name + } + if description != "" { + project.Description = description + } + if parentProjectIdentifier != "" { + parentID, projectErr := utils.ParseIntOrError(parentProjectIdentifier) + if projectErr != nil { + projectByName, lookupErr := pc.repo.GetProjectByName(parentProjectIdentifier) + if lookupErr != nil { + return ErrParentProjectNotFound + } + parentID = projectByName.ID + } + project.ParentProjectID = &parentID + } + + // Update the project in the repository + return pc.repo.UpdateProject(project) +} + +// ListProjects returns all projects stored in the repository. +func (pc *ProjectController) ListProjects() ([]*models.Project, error) { + return pc.repo.GetAllProjects() +} + +// GetProjectByID returns a project by its ID. +func (pc *ProjectController) GetProjectByID(id int) (*models.Project, error) { + return pc.repo.GetProjectByID(id) +} + +// GetProjectByName returns a project by its name. +func (pc *ProjectController) GetProjectByName(name string) (*models.Project, error) { + return pc.repo.GetProjectByName(name) +} + +// ListSubprojects returns subprojects for a specific parent project. +func (pc *ProjectController) ListSubprojects(parentID int) ([]*models.Project, error) { + return pc.repo.GetSubprojects(parentID) +} + +// RemoveProject handles the recursive removal of a project and all its subprojects. +func (pc *ProjectController) RemoveProject(id int) error { + // Retrieve all subprojects of the project + subprojects, getSubprojectsErr := pc.repo.GetSubprojects(id) + if getSubprojectsErr != nil { + return getSubprojectsErr + } + + // Recursively remove subprojects + for _, subproject := range subprojects { + if removeErr := pc.RemoveProject(subproject.ID); removeErr != nil { + return removeErr + } + } + + // Remove the parent project + return pc.repo.DeleteProject(id) +} diff --git a/controllers/task_controller.go b/controllers/task_controller.go new file mode 100644 index 0000000..ec9dec7 --- /dev/null +++ b/controllers/task_controller.go @@ -0,0 +1,275 @@ +package controllers + +import ( + "errors" + "time" + + "github.com/d4r1us-drk/clido/models" + "github.com/d4r1us-drk/clido/repository" + "github.com/d4r1us-drk/clido/utils" +) + +// Error constants for task operations. +var ( + ErrNoTaskName = errors.New("task name is required") + ErrNoProject = errors.New("project name or numeric ID is required") + ErrNoProjectFound = errors.New("project not found") + ErrInvalidParentTask = errors.New("parent task must be identified by a numeric ID") + ErrInvalidDueDate = errors.New("invalid due date format") + ErrTaskNotFound = errors.New("task not found") + ErrParentTaskNotFound = errors.New("parent task not found") +) + +// TaskController manages the task-related business logic. +type TaskController struct { + repo *repository.Repository +} + +// NewTaskController creates and returns a new instance of TaskController. +func NewTaskController(repo *repository.Repository) *TaskController { + return &TaskController{repo: repo} +} + +// CreateTask handles the creation of a new task. +func (tc *TaskController) CreateTask( + name, description, projectIdentifier, parentTaskIdentifier, dueDateStr string, + priority int, +) error { + // Validate mandatory arguments + if name == "" { + return ErrNoTaskName + } + if projectIdentifier == "" { + return ErrNoProject + } + + // Try to parse projectIdentifier as an integer (ID), otherwise get the project by name + projectID, projectErr := utils.ParseIntOrError(projectIdentifier) + if projectErr != nil { + project, lookupErr := tc.repo.GetProjectByName(projectIdentifier) + if lookupErr != nil { + return ErrNoProjectFound + } + projectID = project.ID + } + + // Get parent task ID (optional) + var parentTaskID *int + if parentTaskIdentifier != "" { + id, parentErr := utils.ParseIntOrError(parentTaskIdentifier) + if parentErr != nil { + return ErrInvalidParentTask + } + parentTaskID = &id + } + + // Parse due date (optional) + var dueDate *time.Time + if dueDateStr != "" { + parsedDate, dueDateErr := utils.ParseDueDate(dueDateStr) + if dueDateErr != nil { + return ErrInvalidDueDate + } + dueDate = parsedDate + } + + // Create a new task + task := &models.Task{ + Name: name, + Description: description, + ProjectID: projectID, + DueDate: dueDate, + Priority: priority, + ParentTaskID: parentTaskID, + } + + // Store the task in the repository + if createErr := tc.repo.CreateTask(task); createErr != nil { + return createErr + } + + return nil +} + +// EditTask handles updating an existing task by its ID. +func (tc *TaskController) EditTask( + id int, + name, description, dueDateStr string, + priority int, + parentTaskIdentifier string, +) error { + task, getTaskErr := tc.repo.GetTaskByID(id) + if getTaskErr != nil { + return ErrTaskNotFound + } + + // Apply updates + if name != "" { + task.Name = name + } + if description != "" { + task.Description = description + } + if dueDateStr != "" { + dueDate, dueDateErr := utils.ParseDueDate(dueDateStr) + if dueDateErr != nil { + return ErrInvalidDueDate + } + task.DueDate = dueDate + } + if priority != 0 { + task.Priority = priority + } + if parentTaskIdentifier != "" { + parentTaskID, parentErr := utils.ParseIntOrError(parentTaskIdentifier) + if parentErr != nil { + return ErrInvalidParentTask + } + task.ParentTaskID = &parentTaskID + } + + // Update the task in the repository + if updateErr := tc.repo.UpdateTask(task); updateErr != nil { + return updateErr + } + + return nil +} + +// ListTasks returns all tasks stored in the repository. +func (tc *TaskController) ListTasks() ([]*models.Task, error) { + tasks, getAllErr := tc.repo.GetAllTasks() + if getAllErr != nil { + return nil, getAllErr + } + return tasks, nil +} + +// ListTasksByProjectFilter returns tasks filtered by project. +func (tc *TaskController) ListTasksByProjectFilter( + projectFilter string, +) ([]*models.Task, *models.Project, error) { + if projectFilter == "" { + // Return all tasks if no filter is provided + tasks, getAllErr := tc.repo.GetAllTasks() + return tasks, nil, getAllErr + } + + // Try to parse the projectFilter as a numeric ID first + projectID, projectErr := utils.ParseIntOrError(projectFilter) + if projectErr != nil { + // If parsing fails, assume it's a project name and get the project by name + project, lookupErr := tc.repo.GetProjectByName(projectFilter) + if lookupErr != nil || project == nil { + return nil, nil, ErrNoProjectFound + } + projectID = project.ID + } + + // Retrieve the project by ID + project, getProjectErr := tc.repo.GetProjectByID(projectID) + if getProjectErr != nil || project == nil { + return nil, nil, ErrNoProjectFound + } + + // Get tasks by project ID + tasks, getTasksErr := tc.repo.GetTasksByProjectID(project.ID) + if getTasksErr != nil { + return nil, nil, getTasksErr + } + + return tasks, project, nil +} + +// ToggleTaskCompletion toggles the completion status of a task. +// If recursive is true, it also toggles the completion status of all subtasks. +func (tc *TaskController) ToggleTaskCompletion(id int, recursive bool) (string, error) { + // Retrieve the task by its ID + task, getTaskErr := tc.repo.GetTaskByID(id) + if getTaskErr != nil { + return "", ErrTaskNotFound + } + + // Toggle the completion status + completion := "not completed" + if task.TaskCompleted { + task.TaskCompleted = false + task.CompletionDate = nil + } else { + task.TaskCompleted = true + now := time.Now() + task.CompletionDate = &now + completion = "completed" + } + + // Update the task in the repository + updateErr := tc.repo.UpdateTask(task) + if updateErr != nil { + return "", updateErr + } + + // If recursive flag is set, toggle the completion status of all subtasks + if recursive { + subtasks, getSubtasksErr := tc.ListSubtasks(id) + if getSubtasksErr != nil { + return "", getSubtasksErr + } + + for _, subtask := range subtasks { + if _, toggleErr := tc.ToggleTaskCompletion(subtask.ID, true); toggleErr != nil { + return "", toggleErr + } + } + } + + return completion, nil +} + +// RemoveTask handles the recursive removal of a task and all its subtasks. +func (tc *TaskController) RemoveTask(id int) error { + // Get all subtasks for the given task + subtasks, getSubtasksErr := tc.repo.GetSubtasks(id) + if getSubtasksErr != nil { + return getSubtasksErr + } + + // Recursively remove subtasks + for _, subtask := range subtasks { + if removeErr := tc.RemoveTask(subtask.ID); removeErr != nil { + return removeErr + } + } + + // Remove the parent task + if deleteErr := tc.repo.DeleteTask(id); deleteErr != nil { + return deleteErr + } + + return nil +} + +// GetTaskByID returns the task details for a given task ID. +func (tc *TaskController) GetTaskByID(id int) (*models.Task, error) { + task, getTaskErr := tc.repo.GetTaskByID(id) + if getTaskErr != nil { + return nil, ErrTaskNotFound + } + return task, nil +} + +func (tc *TaskController) GetTaskProjectName(id int) (*string, error) { + task, getTaskErr := tc.GetTaskByID(id) + if getTaskErr != nil { + return nil, getTaskErr + } + return &task.Project.Name, nil +} + +// ListSubtasks returns the subtasks for a given task ID. +func (tc *TaskController) ListSubtasks(taskID int) ([]*models.Task, error) { + subtasks, getSubtasksErr := tc.repo.GetSubtasks(taskID) + if getSubtasksErr != nil { + return nil, getSubtasksErr + } + return subtasks, nil +} diff --git a/go.mod b/go.mod index 4b3e5f0..455118f 100644 --- a/go.mod +++ b/go.mod @@ -5,11 +5,19 @@ go 1.22.5 require ( github.com/olekukonko/tablewriter v0.0.5 github.com/spf13/cobra v1.8.1 + github.com/xlab/treeprint v1.2.0 gorm.io/driver/sqlite v1.5.6 gorm.io/gorm v1.25.11 ) require ( + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + golang.org/x/sys v0.18.0 // indirect +) + +require ( + github.com/fatih/color v1.17.0 github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect @@ -18,10 +26,3 @@ require ( github.com/spf13/pflag v1.0.5 // indirect golang.org/x/text v0.14.0 // indirect ) - -require ( - github.com/fatih/color v1.17.0 - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - golang.org/x/sys v0.24.0 // indirect -) diff --git a/go.sum b/go.sum index 6c35e37..59dbe94 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,6 @@ github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -18,18 +20,27 @@ github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= +github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= -golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/sqlite v1.5.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE= gorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4= diff --git a/internal/version/version.go b/internal/version/version.go index 683943f..fa1a3a3 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -1,30 +1,30 @@ package version import ( - "fmt" - "runtime" + "fmt" + "runtime" ) // Info represents the build information for the application. // It includes fields for the version, build date, and Git commit. type Info struct { - Version string // The version of the application - BuildDate string // The date when the application was built - GitCommit string // The Git commit hash corresponding to the build + Version string // The version of the application + BuildDate string // The date when the application was built + GitCommit string // The Git commit hash corresponding to the build } // Get returns an Info struct containing the current build information. // By default, it returns "dev" for the version, and "unknown" for the build date and Git commit. // These values can be replaced at build time using linker flags. func Get() Info { - return Info{ - Version: "dev", - BuildDate: "unknown", - GitCommit: "unknown", - } + return Info{ + Version: "dev", + BuildDate: "unknown", + GitCommit: "unknown", + } } -// FullVersion returns a formatted string with full version details, including the application version, +// FullVersion returns a formatted string with full version details, including the application version, // build date, Git commit hash, the Go runtime version, and the operating system and architecture details. // // Example output: @@ -32,16 +32,16 @@ func Get() Info { // Build date: unknown // Git commit: unknown // Go version: go1.16.4 -// OS/Arch: linux/amd64 +// OS/Arch: linux/amd64. func FullVersion() string { - info := Get() - return fmt.Sprintf( - "Clido version %s\nBuild date: %s\nGit commit: %s\nGo version: %s\nOS/Arch: %s/%s", - info.Version, - info.BuildDate, - info.GitCommit, - runtime.Version(), // The Go runtime version - runtime.GOOS, // The operating system - runtime.GOARCH, // The system architecture (e.g., amd64, arm) - ) + info := Get() + return fmt.Sprintf( + "Clido version %s\nBuild date: %s\nGit commit: %s\nGo version: %s\nOS/Arch: %s/%s", + info.Version, + info.BuildDate, + info.GitCommit, + runtime.Version(), // The Go runtime version + runtime.GOOS, // The operating system + runtime.GOARCH, // The system architecture (e.g., amd64, arm) + ) } diff --git a/main.go b/main.go index 67290d9..a0e26cf 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,32 @@ package main -import "github.com/d4r1us-drk/clido/cmd" +import ( + "log" + + "github.com/d4r1us-drk/clido/cmd" + "github.com/d4r1us-drk/clido/controllers" + "github.com/d4r1us-drk/clido/repository" +) func main() { - cmd.Execute() + // Initialize the repository + repo, repoErr := repository.NewRepository() + if repoErr != nil { + log.Printf("Error initializing repository: %v", repoErr) + return + } + defer repo.Close() + + // Initialize controllers + projectController := controllers.NewProjectController(repo) + taskController := controllers.NewTaskController(repo) + + // Initialize the root command with controllers + rootCmd := cmd.NewRootCmd(projectController, taskController) + + // Execute the root command + if err := rootCmd.Execute(); err != nil { + log.Printf("Error executing command: %v", err) + repo.Close() + } } diff --git a/pkg/models/project.go b/models/project.go similarity index 92% rename from pkg/models/project.go rename to models/project.go index d96338e..5407bcf 100644 --- a/pkg/models/project.go +++ b/models/project.go @@ -27,9 +27,9 @@ type Project struct { CreationDate time.Time `gorm:"not null" json:"creation_date"` LastModifiedDate time.Time `gorm:"not null" json:"last_modified_date"` ParentProjectID *int ` json:"parent_project_id,omitempty"` - ParentProject *Project `gorm:"foreignKey:ParentProjectID" json:"-"` // Foreign key for the parent project (ignored in JSON) - SubProjects []Project `gorm:"foreignKey:ParentProjectID" json:"-"` // List of subprojects (ignored in JSON) - Tasks []Task `gorm:"foreignKey:ProjectID" json:"-"` // List of tasks associated with the project (ignored in JSON) + ParentProject *Project `gorm:"foreignKey:ParentProjectID" json:"-"` + SubProjects []Project `gorm:"foreignKey:ParentProjectID" json:"-"` + Tasks []Task `gorm:"foreignKey:ProjectID" json:"-"` } // BeforeCreate is a GORM hook that sets the CreationDate and LastModifiedDate fields diff --git a/pkg/models/task.go b/models/task.go similarity index 56% rename from pkg/models/task.go rename to models/task.go index 3e3b88c..267305b 100644 --- a/pkg/models/task.go +++ b/models/task.go @@ -3,7 +3,6 @@ package models import ( "time" - "github.com/d4r1us-drk/clido/pkg/utils" "gorm.io/gorm" ) @@ -27,20 +26,20 @@ import ( // - ParentTask: A reference to the parent task (not serialized to JSON). // - SubTasks: A list of subtasks belonging to this task (not serialized to JSON). type Task struct { - ID int `gorm:"primaryKey" json:"id"` - Name string `gorm:"not null" json:"name"` - Description string ` json:"description"` - ProjectID int `gorm:"not null" json:"project_id"` - Project Project `gorm:"foreignKey:ProjectID" json:"-"` // Foreign key for the project (ignored in JSON) - TaskCompleted bool `gorm:"not null" json:"task_completed"` // Tracks completion status - DueDate *time.Time ` json:"due_date,omitempty"` // Optional due date - CompletionDate *time.Time ` json:"completion_date,omitempty"` // Optional completion date - CreationDate time.Time `gorm:"not null" json:"creation_date"` - LastUpdatedDate time.Time `gorm:"not null" json:"last_updated_date"` - Priority utils.Priority `gorm:"not null;default:4" json:"priority"` // Default priority set to "None" - ParentTaskID *int ` json:"parent_task_id,omitempty"` // Optional parent task - ParentTask *Task `gorm:"foreignKey:ParentTaskID" json:"-"` // Foreign key for the parent task (ignored in JSON) - SubTasks []Task `gorm:"foreignKey:ParentTaskID" json:"-"` // List of subtasks (ignored in JSON) + ID int `gorm:"primaryKey" json:"id"` + Name string `gorm:"not null" json:"name"` + Description string ` json:"description"` + ProjectID int `gorm:"not null" json:"project_id"` + Project Project `gorm:"foreignKey:ProjectID" json:"-"` + TaskCompleted bool `gorm:"not null" json:"task_completed"` + DueDate *time.Time ` json:"due_date,omitempty"` + CompletionDate *time.Time ` json:"completion_date,omitempty"` + CreationDate time.Time `gorm:"not null" json:"creation_date"` + LastUpdatedDate time.Time `gorm:"not null" json:"last_updated_date"` + Priority int `gorm:"not null;default:4" json:"priority"` + ParentTaskID *int ` json:"parent_task_id,omitempty"` + ParentTask *Task `gorm:"foreignKey:ParentTaskID" json:"-"` + SubTasks []Task `gorm:"foreignKey:ParentTaskID" json:"-"` } // BeforeCreate is a GORM hook that sets the CreationDate and LastUpdatedDate fields diff --git a/pkg/utils/helpers.go b/pkg/utils/helpers.go deleted file mode 100644 index ff96eaf..0000000 --- a/pkg/utils/helpers.go +++ /dev/null @@ -1,149 +0,0 @@ -package utils - -import ( - "strconv" - "strings" - "time" - - "github.com/fatih/color" -) - -// IsNumeric checks whether a given string is numeric. -// It attempts to convert the string into an integer and returns true if successful, otherwise false. -// -// Example: -// IsNumeric("123") // returns true -// IsNumeric("abc") // returns false -func IsNumeric(s string) bool { - _, err := strconv.Atoi(s) - return err == nil -} - -// WrapText wraps a given text to a specified maximum line length. -// It breaks the text into words and ensures that no line exceeds the specified maxLength. -// -// If the text fits within maxLength, it is returned unchanged. Otherwise, the text is split into multiple lines. -// -// Example: -// WrapText("This is a very long sentence that needs to be wrapped.", 20) -// // returns: -// "This is a very long\nsentence that needs\nto be wrapped." -func WrapText(text string, maxLength int) string { - if len(text) <= maxLength { - return text - } - - var result string - words := strings.Fields(text) // Split text into words - line := "" - - for _, word := range words { - // Check if adding the word would exceed the max length - if len(line)+len(word)+1 > maxLength { - if len(result) > 0 { - result += "\n" - } - result += line - line = word - } else { - if len(line) > 0 { - line += " " - } - line += word - } - } - - // Add the remaining line - if len(line) > 0 { - if len(result) > 0 { - result += "\n" - } - result += line - } - - return result -} - -// Priority represents the priority level of a task. -// Priority levels include High (1), Medium (2), and Low (3). -type Priority int - -const ( - PriorityHigh Priority = 1 // High priority - PriorityMedium Priority = 2 // Medium priority - PriorityLow Priority = 3 // Low priority -) - -// GetPriorityString returns the string representation of a Priority value. -// The possible values are "High", "Medium", "Low", or "None" (for undefined priority). -// -// Example: -// GetPriorityString(PriorityHigh) // returns "High" -// GetPriorityString(4) // returns "None" -func GetPriorityString(priority Priority) string { - switch priority { - case PriorityHigh: - return "High" - case PriorityMedium: - return "Medium" - case PriorityLow: - return "Low" - default: - return "None" - } -} - -// FormatDate formats a time.Time object into a human-readable string in the format "YYYY-MM-DD HH:MM". -// If the time is nil, it returns "None". -// -// Example: -// FormatDate(time.Now()) // returns "2024-09-11 14:30" -// FormatDate(nil) // returns "None" -func FormatDate(t *time.Time) string { - if t == nil { - return "None" - } - return t.Format("2006-01-02 15:04") -} - -// ColoredPastDue determines if a task is past due and returns a colored string indicating the result. -// If the task is past due and not completed, it returns "yes" in red. If it's completed or not past due, it returns "no" in green. -// -// It converts the given due date to the local time zone for comparison. -// If no due date is provided, it assumes the task is not past due. -// -// Example: -// ColoredPastDue(&time.Now(), false) // returns "no" in green if task is not past due -// ColoredPastDue(&pastTime, false) // returns "yes" in red if task is past due and incomplete -// ColoredPastDue(&pastTime, true) // returns "yes" in green if task is completed -func ColoredPastDue(dueDate *time.Time, completed bool) string { - if dueDate == nil { - return color.GreenString("no") - } - - // Ensure the current time is in the local time zone - now := time.Now() - localLocation := now.Location() - - // Convert due date to local time zone - dueDateAsLocalTime := time.Date( - dueDate.Year(), - dueDate.Month(), - dueDate.Day(), - dueDate.Hour(), - dueDate.Minute(), - dueDate.Second(), - dueDate.Nanosecond(), - localLocation, // Use local timezone - ) - - // Compare current time with the due date - if now.After(dueDateAsLocalTime) { - if completed { - return color.GreenString("yes") - } - return color.RedString("yes") - } - - return color.GreenString("no") -} diff --git a/pkg/repository/migrations.go b/repository/migrations.go similarity index 76% rename from pkg/repository/migrations.go rename to repository/migrations.go index d5fe67b..9968db2 100644 --- a/pkg/repository/migrations.go +++ b/repository/migrations.go @@ -1,19 +1,15 @@ package repository import ( - "github.com/d4r1us-drk/clido/pkg/models" + "github.com/d4r1us-drk/clido/models" "gorm.io/gorm" ) // Migration represents a database migration entry. // Each migration is uniquely identified by a version string. -// -// Fields: -// - ID: The unique identifier for the migration (primary key). -// - Version: The version of the migration, which is unique in the database. type Migration struct { - ID uint `gorm:"primaryKey"` // Primary key for the migration - Version string `gorm:"uniqueIndex"` // Unique version identifier for each migration + ID uint `gorm:"primaryKey"` // Primary key for the migration + Version string `gorm:"uniqueIndex"` // Unique version identifier for each migration } // Migrator is responsible for applying database migrations. @@ -29,14 +25,6 @@ type Migrator struct { // // Each migration is represented by a version string and a function that performs the migration. // In this case, the initial migration (version "1.0") creates the `Project` and `Task` tables. -// -// Example Migration: -// { -// version: "1.0", -// migrate: func(db *gorm.DB) error { -// return db.AutoMigrate(&models.Project{}, &models.Task{}) -// }, -// } func NewMigrator() *Migrator { return &Migrator{ migrations: []struct { @@ -44,7 +32,7 @@ func NewMigrator() *Migrator { migrate func(*gorm.DB) error }{ { - version: "1.0", // The first version of the database schema + version: "1.0", // The first version of the database schema migrate: func(db *gorm.DB) error { // Automatically migrates the schema for the Project and Task models return db.AutoMigrate(&models.Project{}, &models.Task{}) @@ -67,12 +55,6 @@ func NewMigrator() *Migrator { // It first ensures that the `Migration` table exists, then checks the latest applied migration. // Migrations that have a version greater than the last applied one are executed sequentially. // After each migration is applied, a record is inserted into the `Migration` table. -// -// Parameters: -// - db: The GORM database connection. -// -// Returns: -// - An error if any migration fails, or nil if all migrations succeed. func (m *Migrator) Migrate(db *gorm.DB) error { // Ensure the Migration table exists err := db.AutoMigrate(&Migration{}) diff --git a/pkg/repository/project_repo.go b/repository/project_repo.go similarity index 58% rename from pkg/repository/project_repo.go rename to repository/project_repo.go index 003ce01..e999a77 100644 --- a/pkg/repository/project_repo.go +++ b/repository/project_repo.go @@ -1,28 +1,15 @@ package repository import ( - "github.com/d4r1us-drk/clido/pkg/models" + "github.com/d4r1us-drk/clido/models" ) // CreateProject inserts a new project into the database. -// -// Parameters: -// - project: A pointer to the project model to be created. -// -// Returns: -// - An error if the operation fails; nil if successful. func (r *Repository) CreateProject(project *models.Project) error { return r.db.Create(project).Error } // GetProjectByID retrieves a project from the database by its ID. -// -// Parameters: -// - id: The unique ID of the project to retrieve. -// -// Returns: -// - A pointer to the retrieved project if found. -// - An error if the project could not be found or another issue occurred. func (r *Repository) GetProjectByID(id int) (*models.Project, error) { var project models.Project err := r.db.First(&project, id).Error @@ -33,13 +20,6 @@ func (r *Repository) GetProjectByID(id int) (*models.Project, error) { } // GetProjectByName retrieves a project from the database by its name. -// -// Parameters: -// - name: The unique name of the project to retrieve. -// -// Returns: -// - A pointer to the retrieved project if found. -// - An error if the project could not be found or another issue occurred. func (r *Repository) GetProjectByName(name string) (*models.Project, error) { var project models.Project err := r.db.Where("name = ?", name).First(&project).Error @@ -50,10 +30,6 @@ func (r *Repository) GetProjectByName(name string) (*models.Project, error) { } // GetAllProjects retrieves all projects from the database. -// -// Returns: -// - A slice of pointers to all retrieved projects. -// - An error if the operation fails. func (r *Repository) GetAllProjects() ([]*models.Project, error) { var projects []*models.Project err := r.db.Find(&projects).Error @@ -61,13 +37,6 @@ func (r *Repository) GetAllProjects() ([]*models.Project, error) { } // GetSubprojects retrieves all subprojects that have the given parent project ID. -// -// Parameters: -// - parentProjectID: The ID of the parent project to retrieve subprojects for. -// -// Returns: -// - A slice of pointers to all subprojects under the specified parent project. -// - An error if the operation fails. func (r *Repository) GetSubprojects(parentProjectID int) ([]*models.Project, error) { var projects []*models.Project err := r.db.Where("parent_project_id = ?", parentProjectID).Find(&projects).Error @@ -75,33 +44,17 @@ func (r *Repository) GetSubprojects(parentProjectID int) ([]*models.Project, err } // UpdateProject updates an existing project in the database. -// -// Parameters: -// - project: A pointer to the project model to be updated. -// -// Returns: -// - An error if the update operation fails; nil if successful. func (r *Repository) UpdateProject(project *models.Project) error { return r.db.Save(project).Error } // DeleteProject removes a project from the database by its ID. -// -// Parameters: -// - id: The unique ID of the project to be deleted. -// -// Returns: -// - An error if the deletion fails; nil if successful. func (r *Repository) DeleteProject(id int) error { return r.db.Delete(&models.Project{}, id).Error } // GetNextProjectID retrieves the next available project ID in the database. // It selects the maximum project ID and adds 1 to determine the next available ID. -// -// Returns: -// - The next available project ID as an integer. -// - An error if the operation fails. func (r *Repository) GetNextProjectID() (int, error) { var maxID int err := r.db.Model(&models.Project{}).Select("COALESCE(MAX(id), 0) + 1").Scan(&maxID).Error diff --git a/pkg/repository/repository.go b/repository/repository.go similarity index 82% rename from pkg/repository/repository.go rename to repository/repository.go index 247cfc6..0dae9b3 100644 --- a/pkg/repository/repository.go +++ b/repository/repository.go @@ -23,14 +23,6 @@ type Repository struct { // NewRepository initializes a new Repository instance, setting up the SQLite database connection. // It also configures a custom GORM logger and applies any pending migrations. -// -// Returns: -// - A pointer to the initialized Repository. -// - An error if there was an issue with database connection or migration. -// -// The database path is determined based on the operating system: -// - On Windows, the path is inside the APPDATA directory. -// - On Unix-based systems, it is located under ~/.local/share/clido/data.db. func NewRepository() (*Repository, error) { // Determine the database path dbPath, err := getDBPath() @@ -51,7 +43,7 @@ func NewRepository() (*Repository, error) { // Open the SQLite database using GORM db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{ - Logger: newLogger, // Use the custom logger + Logger: newLogger, // Use the custom logger }) if err != nil { return nil, fmt.Errorf("failed to connect database: %w", err) @@ -79,11 +71,6 @@ func NewRepository() (*Repository, error) { // // On Windows, the path is in the APPDATA directory. // On Unix-based systems, the path is in the ~/.local/share/clido directory. -// -// Returns: -// - The database file path as a string. -// - An error if the environment variables required for path construction are not set or -// if there was an issue creating the database directory. func getDBPath() (string, error) { var dbPath string @@ -113,8 +100,6 @@ func getDBPath() (string, error) { // Close closes the database connection gracefully. // It retrieves the underlying SQL database object from GORM and calls its Close method. -// -// Returns an error if the database connection could not be closed. func (r *Repository) Close() error { sqlDB, err := r.db.DB() if err != nil { diff --git a/pkg/repository/task_repo.go b/repository/task_repo.go similarity index 57% rename from pkg/repository/task_repo.go rename to repository/task_repo.go index 267f00f..306bf3d 100644 --- a/pkg/repository/task_repo.go +++ b/repository/task_repo.go @@ -1,28 +1,15 @@ package repository import ( - "github.com/d4r1us-drk/clido/pkg/models" + "github.com/d4r1us-drk/clido/models" ) // CreateTask inserts a new task into the database. -// -// Parameters: -// - task: A pointer to the task model to be created. -// -// Returns: -// - An error if the operation fails; nil if successful. func (r *Repository) CreateTask(task *models.Task) error { return r.db.Create(task).Error } // GetTaskByID retrieves a task from the database by its ID. -// -// Parameters: -// - id: The unique ID of the task to retrieve. -// -// Returns: -// - A pointer to the retrieved task if found. -// - An error if the task could not be found or another issue occurred. func (r *Repository) GetTaskByID(id int) (*models.Task, error) { var task models.Task err := r.db.First(&task, id).Error @@ -33,10 +20,6 @@ func (r *Repository) GetTaskByID(id int) (*models.Task, error) { } // GetAllTasks retrieves all tasks from the database. -// -// Returns: -// - A slice of pointers to all retrieved tasks. -// - An error if the operation fails. func (r *Repository) GetAllTasks() ([]*models.Task, error) { var tasks []*models.Task err := r.db.Find(&tasks).Error @@ -44,13 +27,6 @@ func (r *Repository) GetAllTasks() ([]*models.Task, error) { } // GetTasksByProjectID retrieves all tasks associated with a specific project by the project's ID. -// -// Parameters: -// - projectID: The ID of the project to retrieve tasks for. -// -// Returns: -// - A slice of pointers to all tasks associated with the specified project. -// - An error if the operation fails. func (r *Repository) GetTasksByProjectID(projectID int) ([]*models.Task, error) { var tasks []*models.Task err := r.db.Where("project_id = ?", projectID).Find(&tasks).Error @@ -58,13 +34,6 @@ func (r *Repository) GetTasksByProjectID(projectID int) ([]*models.Task, error) } // GetSubtasks retrieves all subtasks that have the given parent task ID. -// -// Parameters: -// - parentTaskID: The ID of the parent task to retrieve subtasks for. -// -// Returns: -// - A slice of pointers to all subtasks under the specified parent task. -// - An error if the operation fails. func (r *Repository) GetSubtasks(parentTaskID int) ([]*models.Task, error) { var tasks []*models.Task err := r.db.Where("parent_task_id = ?", parentTaskID).Find(&tasks).Error @@ -72,33 +41,17 @@ func (r *Repository) GetSubtasks(parentTaskID int) ([]*models.Task, error) { } // UpdateTask updates an existing task in the database. -// -// Parameters: -// - task: A pointer to the task model to be updated. -// -// Returns: -// - An error if the update operation fails; nil if successful. func (r *Repository) UpdateTask(task *models.Task) error { return r.db.Save(task).Error } // DeleteTask removes a task from the database by its ID. -// -// Parameters: -// - id: The unique ID of the task to be deleted. -// -// Returns: -// - An error if the deletion fails; nil if successful. func (r *Repository) DeleteTask(id int) error { return r.db.Delete(&models.Task{}, id).Error } // GetNextTaskID retrieves the next available task ID in the database. // It selects the maximum task ID and adds 1 to determine the next available ID. -// -// Returns: -// - The next available task ID as an integer. -// - An error if the operation fails. func (r *Repository) GetNextTaskID() (int, error) { var maxID int err := r.db.Model(&models.Task{}).Select("COALESCE(MAX(id), 0) + 1").Scan(&maxID).Error diff --git a/utils/helpers.go b/utils/helpers.go new file mode 100644 index 0000000..801b9a3 --- /dev/null +++ b/utils/helpers.go @@ -0,0 +1,122 @@ +package utils + +import ( + "strconv" + "strings" + "time" + + "github.com/fatih/color" +) + +// Constants for wrapping text and priority values. +const ( + PriorityHigh = 1 + PriorityMedium = 2 + PriorityLow = 3 + PriorityNone = 4 +) + +// ParseIntOrError tries to parse a string as an integer and returns an error if the parsing fails. +func ParseIntOrError(value string) (int, error) { + return strconv.Atoi(value) +} + +// WrapText wraps a given text to a specified maximum line length. +func WrapText(text string, maxLength int) string { + if len(text) <= maxLength { + return text + } + + var result string + words := strings.Fields(text) // Split text into words + line := "" + + for _, word := range words { + // Check if adding the word would exceed the max length + if len(line)+len(word)+1 > maxLength { + if len(result) > 0 { + result += "\n" + } + result += line + line = word + } else { + if len(line) > 0 { + line += " " + } + line += word + } + } + + // Add the remaining line + if len(line) > 0 { + if len(result) > 0 { + result += "\n" + } + result += line + } + + return result +} + +// GetPriorityString returns the string representation of a Priority value. +func GetPriorityString(priority int) string { + switch priority { + case PriorityHigh: + return "High" + case PriorityMedium: + return "Medium" + case PriorityLow: + return "Low" + default: + return "None" + } +} + +// ColoredPastDue returns a colored string depending on the due date. +func ColoredPastDue(dueDate *time.Time, completed bool) string { + if dueDate == nil { + return color.GreenString("no") + } + + // Ensure the current time is in the local time zone + now := time.Now() + localLocation := now.Location() + + // Grab dueDate and interpret it as local time + dueDateAsLocalTime := time.Date( + dueDate.Year(), + dueDate.Month(), + dueDate.Day(), + dueDate.Hour(), + dueDate.Minute(), + dueDate.Second(), + dueDate.Nanosecond(), + localLocation, // Use local timezone for interpretation + ) + + if now.After(dueDateAsLocalTime) { + if completed { + return color.GreenString("yes") + } + return color.RedString("yes") + } + + return color.GreenString("no") +} + +// FormatDate formats a time.Time object into a human-readable string in the format "YYYY-MM-DD HH:MM". +func FormatDate(t *time.Time) string { + if t == nil { + return "None" + } + return t.Format("2006-01-02 15:04") +} + +// ParseDueDate parses a date string in the format "2006-01-02 15:04" and returns a pointer to time.Time. +func ParseDueDate(dueDateStr string) (*time.Time, error) { + date, err := time.Parse("2006-01-02 15:04", dueDateStr) + if err != nil { + return nil, err + } + return &date, nil +} From ceb32a66c205e8d56d7b6c0ff93bc794be5c26a7 Mon Sep 17 00:00:00 2001 From: Lian Drake Date: Sat, 21 Sep 2024 22:27:22 -0400 Subject: [PATCH 76/81] Fixed sqlite3 driver compilations, now we're using a pure Go driver, we now support all platforms --- Makefile | 6 ++---- cmd/completion.go | 7 ++----- cmd/edit.go | 2 +- cmd/list.go | 2 +- cmd/new.go | 2 +- cmd/root.go | 6 +++--- go.mod | 11 +++++++++-- go.sum | 25 +++++++++++++++++++++---- repository/repository.go | 2 +- 9 files changed, 41 insertions(+), 22 deletions(-) diff --git a/Makefile b/Makefile index 8f17f87..8f3a223 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ BUILD_DATE=$(shell date -u +"%Y-%m-%dT%H:%M:%SZ") LDFLAGS=-ldflags "-X '$(PACKAGE)/internal/version.Get().Version=$(VERSION)' -X '$(PACKAGE)/internal/version.Get().BuildDate=$(BUILD_DATE)' -X '$(PACKAGE)/internal/version.Get().GitCommit=$(GIT_COMMIT)'" # Platforms to build for -PLATFORMS=windows/amd64 darwin/amd64 darwin/arm64 linux/amd64 linux/arm64 +PLATFORMS=windows/amd64 windows/arm64 darwin/amd64 darwin/arm64 linux/amd64 linux/arm64 # Tools GOLANGCI_LINT := $(shell command -v golangci-lint 2> /dev/null) @@ -81,9 +81,7 @@ build-all: $(eval GOOS=$(word 1,$(subst /, ,$(PLATFORM))))\ $(eval GOARCH=$(word 2,$(subst /, ,$(PLATFORM))))\ $(eval EXTENSION=$(if $(filter $(GOOS),windows),.exe,))\ - $(eval CGO_ENABLED=$(if $(filter $(GOOS),windows),1,0))\ - $(eval CC=$(if $(filter $(GOOS),windows),x86_64-w64-mingw32-gcc,))\ - GOOS=$(GOOS) GOARCH=$(GOARCH) CGO_ENABLED=$(CGO_ENABLED) CC=$(CC) $(GOBUILD) $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-$(GOOS)-$(GOARCH)$(EXTENSION) .;\ + GOOS=$(GOOS) GOARCH=$(GOARCH) CGO_ENABLED=0 $(GOBUILD) $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-$(GOOS)-$(GOARCH)$(EXTENSION) .;\ ) # Version information diff --git a/cmd/completion.go b/cmd/completion.go index 0b6856e..f638d75 100644 --- a/cmd/completion.go +++ b/cmd/completion.go @@ -8,13 +8,10 @@ import ( // NewCompletionCmd creates and returns the 'completion' command, // which generates shell completion scripts for Bash, Zsh, Fish, and PowerShell. -// -// The command allows users to generate and load completion scripts for their preferred shell. -// Completion scripts help users auto-complete command-line inputs for 'clido'. func NewCompletionCmd() *cobra.Command { return &cobra.Command{ - Use: "completion [bash|zsh|fish|powershell]", // Defines the valid subcommands for shell types - Short: "Generate completion script", // Brief description of the command + Use: "completion [bash|zsh|fish|powershell]", + Short: "Generate completion script", Long: `To load completions: Bash: diff --git a/cmd/edit.go b/cmd/edit.go index 4b50a71..e5d52f9 100644 --- a/cmd/edit.go +++ b/cmd/edit.go @@ -16,7 +16,7 @@ func NewEditCmd( cmd := &cobra.Command{ Use: "edit [project|task] ", Short: "Edit an existing project or task", - Long: `Edit the details of an existing project or task identified by its ID.`, + Long: "Edit the details of an existing project or task identified by its ID.", Run: func(cmd *cobra.Command, args []string) { // Ensure the command receives sufficient arguments (either "project" or "task" followed by an ID) if len(args) < MinArgsLength { diff --git a/cmd/list.go b/cmd/list.go index 7c813d7..7a3356d 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -21,7 +21,7 @@ func NewListCmd( cmd := &cobra.Command{ Use: "list [projects|tasks]", Short: "List projects or tasks", - Long: `List all projects or tasks, optionally filtered by project for tasks.`, + Long: "List all projects or tasks, optionally filtered by project for tasks.", Run: func(cmd *cobra.Command, args []string) { if len(args) < 1 { cmd.Println("Insufficient arguments. Use 'list projects' or 'list tasks'.") diff --git a/cmd/new.go b/cmd/new.go index 2d0ed58..14f12a7 100644 --- a/cmd/new.go +++ b/cmd/new.go @@ -13,7 +13,7 @@ func NewNewCmd( cmd := &cobra.Command{ Use: "new [project|task]", Short: "Create a new project or task", - Long: `Create a new project or task with the specified details.`, + Long: "Create a new project or task with the specified details.", Run: func(cmd *cobra.Command, args []string) { if len(args) < 1 { cmd.Println("Insufficient arguments. Use 'new project' or 'new task'.") diff --git a/cmd/root.go b/cmd/root.go index a25da44..3014687 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -30,9 +30,9 @@ func NewRootCmd( ) *cobra.Command { rootCmd := &cobra.Command{ Use: "clido", - Short: "Clido is an awesome CLI to-do list management application", - Long: `Clido is a simple yet powerful CLI tool designed to help you manage - your projects and tasks effectively from the terminal.`, + Short: "clido is an awesome CLI to-do list management application", + Long: "clido is a simple yet powerful CLI tool designed to help you manage " + + "your projects and tasks effectively from the terminal.", } // Add subcommands and pass the controllers diff --git a/go.mod b/go.mod index 455118f..22bdbdb 100644 --- a/go.mod +++ b/go.mod @@ -6,23 +6,30 @@ require ( github.com/olekukonko/tablewriter v0.0.5 github.com/spf13/cobra v1.8.1 github.com/xlab/treeprint v1.2.0 - gorm.io/driver/sqlite v1.5.6 gorm.io/gorm v1.25.11 ) require ( + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/glebarez/go-sqlite v1.21.2 // indirect + github.com/google/uuid v1.3.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect golang.org/x/sys v0.18.0 // indirect + modernc.org/libc v1.22.5 // indirect + modernc.org/mathutil v1.5.0 // indirect + modernc.org/memory v1.5.0 // indirect + modernc.org/sqlite v1.23.1 // indirect ) require ( github.com/fatih/color v1.17.0 + github.com/glebarez/sqlite v1.11.0 github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/mattn/go-runewidth v0.0.9 // indirect - github.com/mattn/go-sqlite3 v1.14.22 // indirect github.com/spf13/pflag v1.0.5 // indirect golang.org/x/text v0.14.0 // indirect ) diff --git a/go.sum b/go.sum index 59dbe94..d79cd94 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,18 @@ github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= +github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= +github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= +github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= @@ -16,12 +26,13 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= -github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= @@ -42,7 +53,13 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gorm.io/driver/sqlite v1.5.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE= -gorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4= gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg= gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= +modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= +modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= +modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= +modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= +modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM= +modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk= diff --git a/repository/repository.go b/repository/repository.go index 0dae9b3..38686cd 100644 --- a/repository/repository.go +++ b/repository/repository.go @@ -9,7 +9,7 @@ import ( "runtime" "time" - "gorm.io/driver/sqlite" + "github.com/glebarez/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" ) From 230d2675ebb0d2e78468135070dcd59c5354bb2c Mon Sep 17 00:00:00 2001 From: Lian Drake Date: Sat, 21 Sep 2024 22:28:35 -0400 Subject: [PATCH 77/81] Who told the formater to use string literals --- cmd/toggle.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/toggle.go b/cmd/toggle.go index 9171675..bb20454 100644 --- a/cmd/toggle.go +++ b/cmd/toggle.go @@ -12,7 +12,7 @@ func NewToggleCmd(taskController *controllers.TaskController) *cobra.Command { cmd := &cobra.Command{ Use: "toggle ", Short: "Toggle task completion status", - Long: `Toggle the completion status of a task identified by its ID.`, + Long: "Toggle the completion status of a task identified by its ID.", Run: func(cmd *cobra.Command, args []string) { // Ensure at least one argument (the task ID) is provided if len(args) < 1 { From 8ad425acd7cd4d3c7d4646bf2685cd6da4d781b6 Mon Sep 17 00:00:00 2001 From: Lian Drake Date: Sat, 21 Sep 2024 23:44:56 -0400 Subject: [PATCH 78/81] Updated README.md --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b772b6d..98a5070 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ Clido is a simple yet powerful CLI tool designed to help you manage your project - [Cobra](https://github.com/spf13/cobra) - For building powerful modern CLI applications - [SQLite](https://www.sqlite.org/index.html) - [Tablewriter](https://github.com/olekukonko/tablewriter) - For table formatting in terminal +- [Treeprint](https://github.com/xlab/treeprint) - For tree formating in terminal ## Getting Started @@ -120,11 +121,12 @@ clido help - [x] Add priority levels for tasks - [x] Implement Cobra framework for improved CLI structure - [x] Add shell completion support -- [ ] Add sub-tasks and sub-projects +- [X] Add sub-tasks and sub-projects +- [X] Add a JSON output option to facilitate scripting +- [X] Use MVC Architecture and dependency injection +- [ ] Add a TUI interface - [ ] Add a config file with customizable options, like database path, date-time format, etc. -- [ ] Add a JSON output option to facilitate scripting - [ ] Add reminders and notifications (this would require a daemon) -- [ ] Add a TUI interface See the [open issues](https://github.com/d4r1us-drk/clido/issues) for a full list of proposed features (and known issues). From fdc7777196efa1ea87c68055267fcd93152dea74 Mon Sep 17 00:00:00 2001 From: Lian Drake Date: Sun, 22 Sep 2024 01:05:59 -0400 Subject: [PATCH 79/81] Fixed error propagation and return status code --- cmd/completion.go | 52 +++++++++++++++------------------ cmd/edit.go | 73 ++++++++++++++++++++++------------------------- cmd/list.go | 25 ++++++++-------- cmd/new.go | 44 ++++++++++++++-------------- cmd/remove.go | 39 ++++++++++++++----------- cmd/root.go | 8 ++---- cmd/toggle.go | 25 ++++++++-------- main.go | 15 +++++++--- 8 files changed, 141 insertions(+), 140 deletions(-) diff --git a/cmd/completion.go b/cmd/completion.go index f638d75..6a61a97 100644 --- a/cmd/completion.go +++ b/cmd/completion.go @@ -16,40 +16,40 @@ func NewCompletionCmd() *cobra.Command { Bash: - $ source <(clido completion bash) + $ source <(clido completion bash) - # To load completions for each session, execute once: - # Linux: - $ clido completion bash > /etc/bash_completion.d/clido - # macOS: - $ clido completion bash > /usr/local/etc/bash_completion.d/clido + To load completions for each session, execute once: + Linux: + $ clido completion bash > /etc/bash_completion.d/clido + macOS: + $ clido completion bash > /usr/local/etc/bash_completion.d/clido Zsh: - # If shell completion is not already enabled in your environment, - # you will need to enable it. You can execute the following once: + If shell completion is not already enabled in your environment, + you will need to enable it. You can execute the following once: - $ echo "autoload -U compinit; compinit" >> ~/.zshrc + $ echo "autoload -U compinit; compinit" >> ~/.zshrc - # To load completions for each session, execute once: - $ clido completion zsh > "${fpath[1]}/_clido" + To load completions for each session, execute once: + $ clido completion zsh > "${fpath[1]}/_clido" - # You will need to start a new shell for this setup to take effect. + You will need to start a new shell for this setup to take effect. fish: - $ clido completion fish | source + $ clido completion fish | source - # To load completions for each session, execute once: - $ clido completion fish > ~/.config/fish/completions/clido.fish + To load completions for each session, execute once: + $ clido completion fish > ~/.config/fish/completions/clido.fish PowerShell: - PS> clido completion powershell | Out-String | Invoke-Expression + PS> clido completion powershell | Out-String | Invoke-Expression - # To load completions for every new session, run: - PS> clido completion powershell > clido.ps1 - # and source this file from your PowerShell profile. + To load completions for every new session, run: + PS> clido completion powershell > clido.ps1 + and source this file from your PowerShell profile. `, // Detailed usage instructions for each shell DisableFlagsInUseLine: true, // Disables flag usage display in the command usage line @@ -59,16 +59,10 @@ PowerShell: "fish", "powershell", }, // Specifies valid arguments for shell types - Args: func(cmd *cobra.Command, args []string) error { - // Ensures exactly one argument (shell type) is provided - if len(args) != 1 { - cmd.PrintErrln( - "Error: requires exactly one argument: bash, zsh, fish, or powershell", - ) - return cobra.NoArgs(cmd, args) // Returns an error if no arguments are provided - } - return cobra.OnlyValidArgs(cmd, args) // Validates the argument - }, + Args: cobra.MatchAll( + cobra.ExactArgs(1), + cobra.OnlyValidArgs, + ), // Use MatchAll to enforce both conditions Run: func(cmd *cobra.Command, args []string) { // Switch case to handle shell type provided as argument diff --git a/cmd/edit.go b/cmd/edit.go index e5d52f9..eb87f8d 100644 --- a/cmd/edit.go +++ b/cmd/edit.go @@ -1,6 +1,7 @@ package cmd import ( + "errors" "strconv" "github.com/d4r1us-drk/clido/controllers" @@ -17,43 +18,38 @@ func NewEditCmd( Use: "edit [project|task] ", Short: "Edit an existing project or task", Long: "Edit the details of an existing project or task identified by its ID.", - Run: func(cmd *cobra.Command, args []string) { + RunE: func(cmd *cobra.Command, args []string) error { // Ensure the command receives sufficient arguments (either "project" or "task" followed by an ID) if len(args) < MinArgsLength { - cmd.Println("Insufficient arguments. Use 'edit project ' or 'edit task '.") - return + return errors.New( + "insufficient arguments. Use 'edit project ' or 'edit task '", + ) } // Parse the ID argument into an integer id, err := strconv.Atoi(args[1]) if err != nil { - cmd.Println("Invalid ID. Please provide a numeric ID.") - return + return errors.New("invalid ID. Please provide a numeric ID") } // Determine whether the user wants to edit a project or a task switch args[0] { case "project": - editProject(cmd, projectController, id) + return editProject(cmd, projectController, id) case "task": - editTask(cmd, taskController, id) + return editTask(cmd, taskController, id) default: - cmd.Println("Invalid option. Use 'edit project ' or 'edit task '.") + return errors.New("invalid option. Use 'edit project ' or 'edit task '") } }, } // Define flags for the edit command, allowing users to specify what fields they want to update - cmd.Flags(). - StringP("name", "n", "", "New name") - cmd.Flags(). - StringP("description", "d", "", "New description") - cmd.Flags(). - StringP("project", "p", "", "New parent project name or ID") - cmd.Flags(). - StringP("task", "t", "", "New parent task ID for subtasks") - cmd.Flags(). - StringP("due", "D", "", "New due date for task (format: YYYY-MM-DD HH:MM)") + cmd.Flags().StringP("name", "n", "", "New name") + cmd.Flags().StringP("description", "d", "", "New description") + cmd.Flags().StringP("project", "p", "", "New parent project name or ID") + cmd.Flags().StringP("task", "t", "", "New parent task ID for subtasks") + cmd.Flags().StringP("due", "D", "", "New due date for task (format: YYYY-MM-DD HH:MM)") cmd.Flags(). IntP("priority", "P", 0, "New priority for task (1: High, 2: Medium, 3: Low, 4: None)") @@ -61,33 +57,33 @@ func NewEditCmd( } // editProject handles updating an existing project by its ID. -// It retrieves input from flags (name, description, and parent project), and uses the ProjectController. -func editProject(cmd *cobra.Command, projectController *controllers.ProjectController, id int) { +func editProject( + cmd *cobra.Command, + projectController *controllers.ProjectController, + id int, +) error { name, _ := cmd.Flags().GetString("name") description, _ := cmd.Flags().GetString("description") parentProjectIdentifier, _ := cmd.Flags().GetString("project") // Check if any fields are provided for update if name == "" && description == "" && parentProjectIdentifier == "" { - cmd.Println( - "No fields provided for update. Use flags to update the name, description, or parent project.", - ) - return + return errors.New("no fields provided for update. " + + "Use flags to update the name, description, or parent project") } // Call the controller to edit the project err := projectController.EditProject(id, name, description, parentProjectIdentifier) if err != nil { - cmd.Printf("Error updating project: %v\n", err) - return + return errors.New("error updating project: " + err.Error()) } - cmd.Printf("Project with ID '%d' updated successfully.\n", id) + cmd.Println("Project with ID '" + strconv.Itoa(id) + "' updated successfully.") + return nil } // editTask handles updating an existing task by its ID. -// It retrieves input from flags (name, description, due date, priority, and parent task) and uses the TaskController. -func editTask(cmd *cobra.Command, taskController *controllers.TaskController, id int) { +func editTask(cmd *cobra.Command, taskController *controllers.TaskController, id int) error { name, _ := cmd.Flags().GetString("name") description, _ := cmd.Flags().GetString("description") dueDateStr, _ := cmd.Flags().GetString("due") @@ -96,17 +92,16 @@ func editTask(cmd *cobra.Command, taskController *controllers.TaskController, id // Validate priority if provided if priority != 0 && (priority < PriorityHigh || priority > PriorityNone) { - cmd.Println("Invalid priority. Use 1 for High, 2 for Medium, 3 for Low, or 4 for None.") - return + return errors.New( + "invalid priority. Use 1 for High, 2 for Medium, 3 for Low, or 4 for None", + ) } // Check if any fields are provided for update if name == "" && description == "" && dueDateStr == "" && priority == 0 && parentTaskIdentifier == "" { - cmd.Println( - "No fields provided for update. Use flags to update the name, description, due date, priority, or parent task.", - ) - return + return errors.New("no fields provided for update. " + + "Use flags to update the name, description, due date, priority, or parent task") } // Call the controller to edit the task @@ -119,8 +114,7 @@ func editTask(cmd *cobra.Command, taskController *controllers.TaskController, id parentTaskIdentifier, ) if err != nil { - cmd.Printf("Error updating task: %v\n", err) - return + return errors.New("error updating task: " + err.Error()) } // Format and display the new details @@ -131,6 +125,7 @@ func editTask(cmd *cobra.Command, taskController *controllers.TaskController, id formattedDueDate = utils.FormatDate(parsedDueDate) } - cmd.Printf("Task with ID '%d' updated successfully.\n", id) - cmd.Printf("New details: Priority: %s, Due Date: %s\n", priorityStr, formattedDueDate) + cmd.Println("Task with ID '" + strconv.Itoa(id) + "' updated successfully.") + cmd.Println("New details: Priority: " + priorityStr + ", Due Date: " + formattedDueDate) + return nil } diff --git a/cmd/list.go b/cmd/list.go index 7a3356d..3d47e4e 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -2,6 +2,7 @@ package cmd import ( "encoding/json" + "errors" "os" "strconv" @@ -22,10 +23,9 @@ func NewListCmd( Use: "list [projects|tasks]", Short: "List projects or tasks", Long: "List all projects or tasks, optionally filtered by project for tasks.", - Run: func(cmd *cobra.Command, args []string) { + RunE: func(cmd *cobra.Command, args []string) error { if len(args) < 1 { - cmd.Println("Insufficient arguments. Use 'list projects' or 'list tasks'.") - return + return errors.New("insufficient arguments. Use 'list projects' or 'list tasks'") } // Retrieve flags for output format @@ -34,10 +34,10 @@ func NewListCmd( switch args[0] { case "projects": - listProjects(cmd, projectController, outputJSON, treeView) + return listProjects(cmd, projectController, outputJSON, treeView) case "tasks": projectFilter, _ := cmd.Flags().GetString("project") - listTasks( + return listTasks( cmd, taskController, projectController, @@ -46,7 +46,7 @@ func NewListCmd( treeView, ) default: - cmd.Println("Invalid option. Use 'list projects' or 'list tasks'.") + return errors.New("invalid option. Use 'list projects' or 'list tasks'") } }, } @@ -64,11 +64,10 @@ func listProjects( projectController *controllers.ProjectController, outputJSON bool, treeView bool, -) { +) error { projects, err := projectController.ListProjects() if err != nil { - cmd.Printf("Error listing projects: %v\n", err) - return + return errors.New("error listing projects: " + err.Error()) } switch { @@ -79,6 +78,7 @@ func listProjects( default: printProjectTable(cmd, projects) } + return nil } // listTasks lists tasks, optionally filtered by a project, in table, tree view, or JSON format. @@ -89,11 +89,10 @@ func listTasks( projectFilter string, outputJSON bool, treeView bool, -) { +) error { tasks, project, err := taskController.ListTasksByProjectFilter(projectFilter) if err != nil { - cmd.Printf("Error listing tasks: %v\n", err) - return + return errors.New("error listing tasks: " + err.Error()) } if !outputJSON { @@ -108,6 +107,7 @@ func listTasks( default: printTaskTable(taskController, projectController, tasks) } + return nil } // printTaskHeader prints the header for the task list, either all tasks or tasks within a specific project. @@ -119,6 +119,7 @@ func printTaskHeader(cmd *cobra.Command, project *models.Project) { } } +// printProjectsJSON outputs the projects in JSON format. func printProjectsJSON(cmd *cobra.Command, projects []*models.Project) { jsonData, jsonErr := json.MarshalIndent(projects, "", " ") if jsonErr != nil { diff --git a/cmd/new.go b/cmd/new.go index 14f12a7..3a7e86d 100644 --- a/cmd/new.go +++ b/cmd/new.go @@ -1,6 +1,8 @@ package cmd import ( + "errors" + "github.com/d4r1us-drk/clido/controllers" "github.com/spf13/cobra" ) @@ -14,20 +16,19 @@ func NewNewCmd( Use: "new [project|task]", Short: "Create a new project or task", Long: "Create a new project or task with the specified details.", - Run: func(cmd *cobra.Command, args []string) { + RunE: func(cmd *cobra.Command, args []string) error { if len(args) < 1 { - cmd.Println("Insufficient arguments. Use 'new project' or 'new task'.") - return + return errors.New("insufficient arguments. Use 'new project' or 'new task'") } - // Create project or task + // Create project or task based on the argument switch args[0] { case "project": - createProject(cmd, projectController) + return createProject(cmd, projectController) case "task": - createTask(cmd, taskController) + return createTask(cmd, taskController) default: - cmd.Println("Invalid option. Use 'new project' or 'new task'.") + return errors.New("invalid option. Use 'new project' or 'new task'") } }, } @@ -44,28 +45,27 @@ func NewNewCmd( return cmd } -func createProject(cmd *cobra.Command, projectController *controllers.ProjectController) { +func createProject(cmd *cobra.Command, projectController *controllers.ProjectController) error { name, _ := cmd.Flags().GetString("name") description, _ := cmd.Flags().GetString("description") parentProjectIdentifier, _ := cmd.Flags().GetString("project") // Ensure project name is provided if name == "" { - cmd.Println("Project name is required.") - return + return errors.New("project name is required") } // Call the controller to create the project err := projectController.CreateProject(name, description, parentProjectIdentifier) if err != nil { - cmd.Printf("Error creating project: %v\n", err) - return + return errors.New("error creating project: " + err.Error()) } - cmd.Printf("Project '%s' created successfully.\n", name) + cmd.Println("Project '" + name + "' created successfully.") + return nil } -func createTask(cmd *cobra.Command, taskController *controllers.TaskController) { +func createTask(cmd *cobra.Command, taskController *controllers.TaskController) error { name, _ := cmd.Flags().GetString("name") description, _ := cmd.Flags().GetString("description") projectIdentifier, _ := cmd.Flags().GetString("project") @@ -75,14 +75,14 @@ func createTask(cmd *cobra.Command, taskController *controllers.TaskController) // Ensure task name is provided if name == "" { - cmd.Println("Task name is required.") - return + return errors.New("task name is required") } - // Add validation for priority + // Validate priority if priority != 0 && (priority < 1 || priority > 4) { - cmd.Println("Invalid priority. Use 1 for High, 2 for Medium, 3 for Low, or 4 for None.") - return + return errors.New( + "invalid priority. Use 1 for High, 2 for Medium, 3 for Low, or 4 for None", + ) } // Call the controller to create the task @@ -95,9 +95,9 @@ func createTask(cmd *cobra.Command, taskController *controllers.TaskController) priority, ) if err != nil { - cmd.Printf("Error creating task: %v\n", err) - return + return errors.New("error creating task: " + err.Error()) } - cmd.Printf("Task '%s' created successfully.\n", name) + cmd.Println("Task '" + name + "' created successfully.") + return nil } diff --git a/cmd/remove.go b/cmd/remove.go index 2d90a76..0e90b1f 100644 --- a/cmd/remove.go +++ b/cmd/remove.go @@ -1,6 +1,7 @@ package cmd import ( + "errors" "strconv" "github.com/d4r1us-drk/clido/controllers" @@ -16,30 +17,28 @@ func NewRemoveCmd( Use: "remove [project|task] ", Short: "Remove a project or task along with all its subprojects or subtasks", Long: "Remove a project or task by ID, along with all its sub-items.", - Run: func(cmd *cobra.Command, args []string) { + RunE: func(cmd *cobra.Command, args []string) error { // Ensure sufficient arguments (either 'project' or 'task' followed by an ID) if len(args) < MinArgsLength { - cmd.Println( - "Insufficient arguments. Use 'remove project ' or 'remove task '.", + return errors.New( + "insufficient arguments. Use 'remove project ' or 'remove task '", ) - return } // Parse the ID argument into an integer id, err := strconv.Atoi(args[1]) if err != nil { - cmd.Println("Invalid ID. Please provide a numeric ID.") - return + return errors.New("invalid ID. Please provide a numeric ID") } // Determine whether the user wants to remove a project or a task switch args[0] { case "project": - removeProject(cmd, projectController, id) + return removeProject(cmd, projectController, id) case "task": - removeTask(cmd, taskController, id) + return removeTask(cmd, taskController, id) default: - cmd.Println("Invalid option. Use 'remove project ' or 'remove task '.") + return errors.New("invalid option. Use 'remove project ' or 'remove task '") } }, } @@ -49,24 +48,30 @@ func NewRemoveCmd( // removeProject handles the recursive removal of a project and all its subprojects. // It uses the ProjectController to handle the deletion. -func removeProject(cmd *cobra.Command, projectController *controllers.ProjectController, id int) { +func removeProject( + cmd *cobra.Command, + projectController *controllers.ProjectController, + id int, +) error { err := projectController.RemoveProject(id) if err != nil { - cmd.Printf("Error removing project: %v\n", err) - return + return errors.New("error removing project: " + err.Error()) } - cmd.Printf("Project (ID: %d) and all its subprojects removed successfully.\n", id) + cmd.Println( + "Project (ID: " + strconv.Itoa(id) + ") and all its subprojects removed successfully.", + ) + return nil } // removeTask handles the recursive removal of a task and all its subtasks. // It uses the TaskController to handle the deletion. -func removeTask(cmd *cobra.Command, taskController *controllers.TaskController, id int) { +func removeTask(cmd *cobra.Command, taskController *controllers.TaskController, id int) error { err := taskController.RemoveTask(id) if err != nil { - cmd.Printf("Error removing task: %v\n", err) - return + return errors.New("error removing task: " + err.Error()) } - cmd.Printf("Task (ID: %d) and all its subtasks removed successfully.\n", id) + cmd.Println("Task (ID: " + strconv.Itoa(id) + ") and all its subtasks removed successfully.") + return nil } diff --git a/cmd/root.go b/cmd/root.go index 3014687..1b3f860 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,8 +1,6 @@ package cmd import ( - "os" - "github.com/d4r1us-drk/clido/controllers" "github.com/d4r1us-drk/clido/internal/version" "github.com/spf13/cobra" @@ -59,10 +57,10 @@ func NewVersionCmd() *cobra.Command { } // Execute runs the root command. -func Execute() { +func Execute() error { rootCmd := NewRootCmd(nil, nil) if err := rootCmd.Execute(); err != nil { - rootCmd.Println(err) - os.Exit(1) + return err } + return nil } diff --git a/cmd/toggle.go b/cmd/toggle.go index bb20454..4f43611 100644 --- a/cmd/toggle.go +++ b/cmd/toggle.go @@ -1,6 +1,7 @@ package cmd import ( + "errors" "strconv" "github.com/d4r1us-drk/clido/controllers" @@ -13,18 +14,16 @@ func NewToggleCmd(taskController *controllers.TaskController) *cobra.Command { Use: "toggle ", Short: "Toggle task completion status", Long: "Toggle the completion status of a task identified by its ID.", - Run: func(cmd *cobra.Command, args []string) { + RunE: func(cmd *cobra.Command, args []string) error { // Ensure at least one argument (the task ID) is provided if len(args) < 1 { - cmd.Println("Insufficient arguments. Use 'toggle '.") - return + return errors.New("insufficient arguments. Use 'toggle '") } // Parse the task ID argument into an integer id, err := strconv.Atoi(args[0]) if err != nil { - cmd.Println("Invalid task ID. Please provide a numeric ID.") - return + return errors.New("invalid task ID. Please provide a numeric ID") } // Check if the recursive flag was provided @@ -33,19 +32,21 @@ func NewToggleCmd(taskController *controllers.TaskController) *cobra.Command { // Toggle task completion status using the controller completionStatus, toggleErr := taskController.ToggleTaskCompletion(id, recursive) if toggleErr != nil { - cmd.Printf("Error toggling task: %v\n", toggleErr) - return + return errors.New("error toggling task: " + toggleErr.Error()) } + // Print the result based on the recursive flag if recursive { - cmd.Printf( - "Task (ID: %d) and its subtasks (if any) have been set as %s.\n", - id, - completionStatus, + cmd.Println( + "Task (ID: " + strconv.Itoa( + id, + ) + ") and its subtasks (if any) have been set as " + completionStatus + ".", ) } else { - cmd.Printf("Task (ID: %d) has been set as %s.\n", id, completionStatus) + cmd.Println("Task (ID: " + strconv.Itoa(id) + ") has been set as " + completionStatus + ".") } + + return nil }, } diff --git a/main.go b/main.go index a0e26cf..abbea79 100644 --- a/main.go +++ b/main.go @@ -2,18 +2,19 @@ package main import ( "log" + "os" "github.com/d4r1us-drk/clido/cmd" "github.com/d4r1us-drk/clido/controllers" "github.com/d4r1us-drk/clido/repository" ) -func main() { +func run() int { // Initialize the repository repo, repoErr := repository.NewRepository() if repoErr != nil { log.Printf("Error initializing repository: %v", repoErr) - return + return 1 // Exit code 1 indicates failure } defer repo.Close() @@ -26,7 +27,13 @@ func main() { // Execute the root command if err := rootCmd.Execute(); err != nil { - log.Printf("Error executing command: %v", err) - repo.Close() + return 1 // Return 1 on error } + + return 0 // Exit code 0 indicates success +} + +func main() { + // Call the run function and exit with the appropriate code + os.Exit(run()) } From 2b529541bfca5aec087f925eee8a6ff61b35afb8 Mon Sep 17 00:00:00 2001 From: Lian Drake Date: Sun, 22 Sep 2024 01:06:47 -0400 Subject: [PATCH 80/81] Fixed bugs that allowed tasks or project to be assigned to a non existing parent --- controllers/project_controller.go | 63 ++++++++++++++++++++----------- controllers/task_controller.go | 8 +++- 2 files changed, 47 insertions(+), 24 deletions(-) diff --git a/controllers/project_controller.go b/controllers/project_controller.go index fcff8b7..a55f615 100644 --- a/controllers/project_controller.go +++ b/controllers/project_controller.go @@ -10,8 +10,9 @@ import ( // Error constants for project operations. var ( - ErrNoProjectName = errors.New("project name is required") - ErrParentProjectNotFound = errors.New("parent project not found") + ErrNoProjectName = errors.New("project name is required") + ErrParentProjectNotFound = errors.New("parent project not found") + ErrNoParentProjectProvided = errors.New("no parent project provided") ) // ProjectController manages the project-related business logic. @@ -34,17 +35,9 @@ func (pc *ProjectController) CreateProject( } // Retrieve the parent project ID (if any) - var parentProjectID *int - if parentProjectIdentifier != "" { - parentID, projectErr := utils.ParseIntOrError(parentProjectIdentifier) - if projectErr != nil { - project, lookupErr := pc.repo.GetProjectByName(parentProjectIdentifier) - if lookupErr != nil { - return ErrParentProjectNotFound - } - parentID = project.ID - } - parentProjectID = &parentID + parentProjectID, err := pc.getParentProjectID(parentProjectIdentifier) + if err != nil && !errors.Is(err, ErrNoParentProjectProvided) { + return err } // Create a new project @@ -76,17 +69,13 @@ func (pc *ProjectController) EditProject( if description != "" { project.Description = description } - if parentProjectIdentifier != "" { - parentID, projectErr := utils.ParseIntOrError(parentProjectIdentifier) - if projectErr != nil { - projectByName, lookupErr := pc.repo.GetProjectByName(parentProjectIdentifier) - if lookupErr != nil { - return ErrParentProjectNotFound - } - parentID = projectByName.ID - } - project.ParentProjectID = &parentID + + // Retrieve and apply the parent project ID (if any) + parentProjectID, err := pc.getParentProjectID(parentProjectIdentifier) + if err != nil && !errors.Is(err, ErrNoParentProjectProvided) { + return err } + project.ParentProjectID = parentProjectID // Update the project in the repository return pc.repo.UpdateProject(project) @@ -130,3 +119,31 @@ func (pc *ProjectController) RemoveProject(id int) error { // Remove the parent project return pc.repo.DeleteProject(id) } + +// getParentProjectID checks and retrieves the parent project ID based on the identifier (name or ID). +func (pc *ProjectController) getParentProjectID(parentProjectIdentifier string) (*int, error) { + if parentProjectIdentifier == "" { + // No parent project identifier provided, so no parent project ID is needed + return nil, ErrNoParentProjectProvided + } + + // Try to parse the parent project identifier as an integer ID + parentID, err := utils.ParseIntOrError(parentProjectIdentifier) + if err == nil { + // Successfully parsed as ID, now check if the project exists by ID + project, getProjectErr := pc.repo.GetProjectByID(parentID) + if getProjectErr != nil || project == nil { + return nil, ErrParentProjectNotFound + } + return &parentID, nil + } + + // If parsing failed, treat it as a project name and search by name + project, lookupErr := pc.repo.GetProjectByName(parentProjectIdentifier) + if lookupErr != nil || project == nil { + return nil, ErrParentProjectNotFound + } + + // Return the found project ID + return &project.ID, nil +} diff --git a/controllers/task_controller.go b/controllers/task_controller.go index ec9dec7..7ade423 100644 --- a/controllers/task_controller.go +++ b/controllers/task_controller.go @@ -47,10 +47,16 @@ func (tc *TaskController) CreateTask( projectID, projectErr := utils.ParseIntOrError(projectIdentifier) if projectErr != nil { project, lookupErr := tc.repo.GetProjectByName(projectIdentifier) - if lookupErr != nil { + if lookupErr != nil || project == nil { return ErrNoProjectFound } projectID = project.ID + } else { + // Check if the project exists using the ID + project, getProjectErr := tc.repo.GetProjectByID(projectID) + if getProjectErr != nil || project == nil { + return ErrNoProjectFound + } } // Get parent task ID (optional) From ef966d32311080928e7b9ca8b2a870816a530bdf Mon Sep 17 00:00:00 2001 From: Lian Drake Date: Thu, 26 Sep 2024 17:28:17 -0400 Subject: [PATCH 81/81] Updated project architecture, just moved some code where it belongs in the MVC pattern --- main.go | 2 +- {cmd => views/cmd}/completion.go | 0 {cmd => views/cmd}/edit.go | 0 {cmd => views/cmd}/list.go | 0 {cmd => views/cmd}/new.go | 0 {cmd => views/cmd}/remove.go | 0 {cmd => views/cmd}/root.go | 0 {cmd => views/cmd}/toggle.go | 0 8 files changed, 1 insertion(+), 1 deletion(-) rename {cmd => views/cmd}/completion.go (100%) rename {cmd => views/cmd}/edit.go (100%) rename {cmd => views/cmd}/list.go (100%) rename {cmd => views/cmd}/new.go (100%) rename {cmd => views/cmd}/remove.go (100%) rename {cmd => views/cmd}/root.go (100%) rename {cmd => views/cmd}/toggle.go (100%) diff --git a/main.go b/main.go index abbea79..cb8ea7d 100644 --- a/main.go +++ b/main.go @@ -4,9 +4,9 @@ import ( "log" "os" - "github.com/d4r1us-drk/clido/cmd" "github.com/d4r1us-drk/clido/controllers" "github.com/d4r1us-drk/clido/repository" + "github.com/d4r1us-drk/clido/views/cmd" ) func run() int { diff --git a/cmd/completion.go b/views/cmd/completion.go similarity index 100% rename from cmd/completion.go rename to views/cmd/completion.go diff --git a/cmd/edit.go b/views/cmd/edit.go similarity index 100% rename from cmd/edit.go rename to views/cmd/edit.go diff --git a/cmd/list.go b/views/cmd/list.go similarity index 100% rename from cmd/list.go rename to views/cmd/list.go diff --git a/cmd/new.go b/views/cmd/new.go similarity index 100% rename from cmd/new.go rename to views/cmd/new.go diff --git a/cmd/remove.go b/views/cmd/remove.go similarity index 100% rename from cmd/remove.go rename to views/cmd/remove.go diff --git a/cmd/root.go b/views/cmd/root.go similarity index 100% rename from cmd/root.go rename to views/cmd/root.go diff --git a/cmd/toggle.go b/views/cmd/toggle.go similarity index 100% rename from cmd/toggle.go rename to views/cmd/toggle.go