Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
0372ad8
chore(project): Establish world-class baseline for code and documenta…
karolswdev Aug 29, 2025
9bca199
feat(pull): Implement conflict resolution strategies
karolswdev Aug 29, 2025
85a72ef
feat(automation): Implement webhook listener and GitHub Action
karolswdev Aug 29, 2025
0aae1c8
feat(analytics): Complete STORY-403 - stats command and Getting Start…
karolswdev Aug 29, 2025
f5336aa
docs: Complete PHASE-4 - The Elite Engine
karolswdev Aug 29, 2025
e67382f
feat(dry-run) added dry run and updates docs
karolswdev Aug 30, 2025
0025b0c
Merge branch 'feat/cleanup'
karolswdev Aug 30, 2025
a007897
merge: resolve conflict in ticket_service.go by keeping DryRun behavi…
karolswdev Aug 30, 2025
7da3bb6
feat(docs-overhaul): add community health files, CI, issue/PR templat…
karolswdev Aug 30, 2025
0568a4d
docs: align docs with code behavior (push state usage note), fix ARCH…
karolswdev Aug 30, 2025
87b4b5c
feat(push): wire push to state-aware PushService with dry-run; chore(…
karolswdev Aug 30, 2025
3024723
docs(readme): reflect state-aware push and dry-run behavior
karolswdev Aug 30, 2025
b4fb058
merge: resolve conflicts with origin/main (keep push state-aware, kee…
karolswdev Aug 30, 2025
2430a81
docs(readme): fix install path and API docs link to ticketr after merge
karolswdev Aug 30, 2025
87a7263
fix(lint): check Help/MarkHidden errors; check http.ResponseWriter wr…
karolswdev Aug 30, 2025
6efd2e0
fix(lint): replace deprecated ioutil; check errors in tests; remove e…
karolswdev Aug 30, 2025
452adad
fix(lint): remove empty branch in hash writer helper (SA9003)
karolswdev Aug 30, 2025
7ad63ce
chore(dev): add Makefile and .golangci.yml; document local lint and s…
karolswdev Aug 30, 2025
ad3eb18
security(jira): prevent cross-origin redirects and strip Authorizatio…
karolswdev Aug 30, 2025
0ce44fc
fix(lint): satisfy errcheck across code and tests (defer close, check…
karolswdev Aug 30, 2025
56f53e8
chore(toolchain): enforce Go toolchain via go.mod and make CI use go-…
karolswdev Aug 30, 2025
d082f62
refactor: reduce cyclomatic complexity
karolswdev Aug 30, 2025
b319c11
Merge origin/main into feat/reduce-cyclo: resolve conflicts (keep cyc…
karolswdev Aug 30, 2025
46faf2f
fix: close writeProgress() function block to resolve go vet syntax er…
karolswdev Aug 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ tools:
$(GO) install golang.org/x/vuln/cmd/govulncheck@latest
@echo "Tools installed to \"$$(go env GOPATH)/bin\". Ensure it is on your PATH."

.PHONY: cyclo
cyclo:
@command -v gocyclo >/dev/null 2>&1 || { echo "Installing gocyclo..."; $(GO) install github.com/fzipp/gocyclo/cmd/gocyclo@latest; }
gocyclo -over 15 ./...

check: fmt vet lint test

ci: tidy check vuln
8 changes: 2 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
A powerful command-line tool that bridges the gap between local Markdown files and Jira, enabling seamless story and task management with bidirectional synchronization.

[![CI](https://github.com/karolswdev/ticketr/actions/workflows/ci.yml/badge.svg)](https://github.com/karolswdev/ticketr/actions/workflows/ci.yml)
[![Go Version](https://img.shields.io/badge/Go-1.22%2B-00ADD8?style=flat&logo=go)](https://go.dev)
[![Go Version](https://img.shields.io/badge/Go-1.24%2B-00ADD8?style=flat&logo=go)](https://go.dev)
[![PkgGoDev](https://pkg.go.dev/badge/github.com/karolswdev/ticketr)](https://pkg.go.dev/github.com/karolswdev/ticketr)
[![Go Report Card](https://goreportcard.com/badge/github.com/karolswdev/ticketr)](https://goreportcard.com/report/github.com/karolswdev/ticketr)
[![Go Report Card](https://goreportcard.com/badge/github.com/karolswdev/ticketr?refresh=1)](https://goreportcard.com/report/github.com/karolswdev/ticketr)
[![License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
[![Docker](https://img.shields.io/badge/Docker-Ready-2496ED?style=flat&logo=docker)](Dockerfile)

Expand Down Expand Up @@ -317,11 +317,7 @@ Ticketr maintains a `.ticketr.state` file to track content hashes and support in
ticketr pull --strategy=local-wins
```

<<<<<<< HEAD
Push is now state-aware by default. The CLI uses the `PushService` so unchanged tickets are skipped. In `--dry-run` mode, Ticketr shows all intended operations without writing to JIRA or the file/state.
=======
Note: State-aware skipping for `push` exists in the `PushService`, but the default CLI path currently uses `TicketService` (always processes all tickets). A future release will wire `push` to the state-aware flow. Until then, all tickets are processed during `push`.
>>>>>>> origin/main

The `.ticketr.state` file is environment-specific and ignored by default via `.gitignore`.

Expand Down
228 changes: 84 additions & 144 deletions internal/adapters/filesystem/file_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,148 +28,88 @@ func (r *FileRepository) GetTickets(filepath string) ([]domain.Ticket, error) {

// SaveTickets writes tickets to a file in the new TICKET format
func (r *FileRepository) SaveTickets(filepath string, tickets []domain.Ticket) error {
file, err := os.Create(filepath)
if err != nil {
return fmt.Errorf("failed to create file: %w", err)
}
defer func() { _ = file.Close() }()

writer := bufio.NewWriter(file)
w := func(n int, err error) error {
if err != nil {
return err
}
return nil
}

for i, ticket := range tickets {
// Write ticket heading with Jira ID if present
if ticket.JiraID != "" {
if err := w(fmt.Fprintf(writer, "# TICKET: [%s] %s\n", ticket.JiraID, ticket.Title)); err != nil {
return err
}
} else {
if err := w(fmt.Fprintf(writer, "# TICKET: %s\n", ticket.Title)); err != nil {
return err
}
}
if err := w(fmt.Fprintln(writer)); err != nil {
return err
}

// Write description
if ticket.Description != "" {
if err := w(fmt.Fprintln(writer, "## Description")); err != nil {
return err
}
if err := w(fmt.Fprintln(writer, ticket.Description)); err != nil {
return err
}
if err := w(fmt.Fprintln(writer)); err != nil {
return err
}
}

// Write fields
if len(ticket.CustomFields) > 0 {
if err := w(fmt.Fprintln(writer, "## Fields")); err != nil {
return err
}
for key, value := range ticket.CustomFields {
if err := w(fmt.Fprintf(writer, "%s: %s\n", key, value)); err != nil {
return err
}
}
if err := w(fmt.Fprintln(writer)); err != nil {
return err
}
}

// Write acceptance criteria
if len(ticket.AcceptanceCriteria) > 0 {
if err := w(fmt.Fprintln(writer, "## Acceptance Criteria")); err != nil {
return err
}
for _, ac := range ticket.AcceptanceCriteria {
if err := w(fmt.Fprintf(writer, "- %s\n", ac)); err != nil {
return err
}
}
if err := w(fmt.Fprintln(writer)); err != nil {
return err
}
}

// Write tasks
if len(ticket.Tasks) > 0 {
if err := w(fmt.Fprintln(writer, "## Tasks")); err != nil {
return err
}
for _, task := range ticket.Tasks {
// Write task with Jira ID if present
if task.JiraID != "" {
if err := w(fmt.Fprintf(writer, "- [%s] %s\n", task.JiraID, task.Title)); err != nil {
return err
}
} else {
if err := w(fmt.Fprintf(writer, "- %s\n", task.Title)); err != nil {
return err
}
}

// Write task description (indented)
if task.Description != "" {
if err := w(fmt.Fprintln(writer, " ## Description")); err != nil {
return err
}
// Indent description lines
if err := w(fmt.Fprintf(writer, " %s\n", task.Description)); err != nil {
return err
}
if err := w(fmt.Fprintln(writer)); err != nil {
return err
}
}

// Write task fields (indented)
if len(task.CustomFields) > 0 {
if err := w(fmt.Fprintln(writer, " ## Fields")); err != nil {
return err
}
for key, value := range task.CustomFields {
if err := w(fmt.Fprintf(writer, " %s: %s\n", key, value)); err != nil {
return err
}
}
if err := w(fmt.Fprintln(writer)); err != nil {
return err
}
}

// Write task acceptance criteria (indented)
if len(task.AcceptanceCriteria) > 0 {
if err := w(fmt.Fprintln(writer, " ## Acceptance Criteria")); err != nil {
return err
}
for _, ac := range task.AcceptanceCriteria {
if err := w(fmt.Fprintf(writer, " - %s\n", ac)); err != nil {
return err
}
}
if err := w(fmt.Fprintln(writer)); err != nil {
return err
}
}
}
}

// Add spacing between tickets
if i < len(tickets)-1 {
if err := w(fmt.Fprintln(writer)); err != nil {
return err
}
}
}

return writer.Flush()
file, err := os.Create(filepath)
if err != nil { return fmt.Errorf("failed to create file: %w", err) }
defer func() { _ = file.Close() }()

writer := bufio.NewWriter(file)
w := func(n int, err error) error { if err != nil { return err }; return nil }

writeHeader := func(t domain.Ticket) error {
if t.JiraID != "" {
return w(fmt.Fprintf(writer, "# TICKET: [%s] %s\n", t.JiraID, t.Title))
}
return w(fmt.Fprintf(writer, "# TICKET: %s\n", t.Title))
}
writeDescription := func(desc string) error {
if desc == "" { return nil }
if err := w(fmt.Fprintln(writer, "## Description")); err != nil { return err }
if err := w(fmt.Fprintln(writer, desc)); err != nil { return err }
return w(fmt.Fprintln(writer))
}
writeFields := func(fields map[string]string) error {
if len(fields) == 0 { return nil }
if err := w(fmt.Fprintln(writer, "## Fields")); err != nil { return err }
for k, v := range fields {
if err := w(fmt.Fprintf(writer, "%s: %s\n", k, v)); err != nil { return err }
}
return w(fmt.Fprintln(writer))
}
writeAcceptance := func(criteria []string) error {
if len(criteria) == 0 { return nil }
if err := w(fmt.Fprintln(writer, "## Acceptance Criteria")); err != nil { return err }
for _, ac := range criteria {
if err := w(fmt.Fprintf(writer, "- %s\n", ac)); err != nil { return err }
}
return w(fmt.Fprintln(writer))
}
writeTask := func(t domain.Task) error {
if t.JiraID != "" {
if err := w(fmt.Fprintf(writer, "- [%s] %s\n", t.JiraID, t.Title)); err != nil { return err }
} else {
if err := w(fmt.Fprintf(writer, "- %s\n", t.Title)); err != nil { return err }
}
if t.Description != "" {
if err := w(fmt.Fprintln(writer, " ## Description")); err != nil { return err }
if err := w(fmt.Fprintf(writer, " %s\n", t.Description)); err != nil { return err }
if err := w(fmt.Fprintln(writer)); err != nil { return err }
}
if len(t.CustomFields) > 0 {
if err := w(fmt.Fprintln(writer, " ## Fields")); err != nil { return err }
for k, v := range t.CustomFields {
if err := w(fmt.Fprintf(writer, " %s: %s\n", k, v)); err != nil { return err }
}
if err := w(fmt.Fprintln(writer)); err != nil { return err }
}
if len(t.AcceptanceCriteria) > 0 {
if err := w(fmt.Fprintln(writer, " ## Acceptance Criteria")); err != nil { return err }
for _, ac := range t.AcceptanceCriteria {
if err := w(fmt.Fprintf(writer, " - %s\n", ac)); err != nil { return err }
}
if err := w(fmt.Fprintln(writer)); err != nil { return err }
}
return nil
}
writeTasks := func(tasks []domain.Task) error {
if len(tasks) == 0 { return nil }
if err := w(fmt.Fprintln(writer, "## Tasks")); err != nil { return err }
for _, t := range tasks {
if err := writeTask(t); err != nil { return err }
}
return nil
}

for i, ticket := range tickets {
if err := writeHeader(ticket); err != nil { return err }
if err := w(fmt.Fprintln(writer)); err != nil { return err }
if err := writeDescription(ticket.Description); err != nil { return err }
if err := writeFields(ticket.CustomFields); err != nil { return err }
if err := writeAcceptance(ticket.AcceptanceCriteria); err != nil { return err }
if err := writeTasks(ticket.Tasks); err != nil { return err }

if i < len(tickets)-1 {
if err := w(fmt.Fprintln(writer)); err != nil { return err }
}
}
return writer.Flush()
}
Loading
Loading