app

package
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 5, 2026 License: Apache-2.0 Imports: 27 Imported by: 0

Documentation

Overview

Package app resolves the selected project and names into ids and computes minimal writes for the curated commands. It talks to Taiga only through the narrow API interface.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func DeleteNotConfirmed added in v0.3.0

func DeleteNotConfirmed(flag string) error

DeleteNotConfirmed refuses a replacement without --confirm-delete, before any request (also with --dry-run, so a script learns of the missing flag before the real run).

func FieldPath

func FieldPath(kind string) (string, error)

FieldPath is the definitions endpoint of a kind of resource.

func ID

func ID(v any) int64

ID returns v as a positive id, or 0 when it is not an integer.

func MergeIDs

func MergeIDs(current, add, remove []int64) []int64

MergeIDs returns current ∪ add − remove, keeping order and dropping duplicates. It never returns nil, so removing the last id sends [].

func MergeNames

func MergeNames(current, add, remove []string) []string

MergeNames returns current ∪ add − remove, keeping order and dropping duplicates.

func Names

func Names(v any) ([]string, error)

Names normalizes Taiga tags ([name, color] pairs or plain names) to names.

func OrderStatuses

func OrderStatuses(current []string, specs []StatusSpec) ([]string, error)

OrderStatuses returns the desired order of every status: current ones (in their order) plus the declared ones that do not exist yet, appended in file order, each declaration with After moved right behind its predecessor. Statuses after the same predecessor keep the file order. An unknown predecessor or a cycle is an error.

func ParseFieldValue

func ParseFieldValue(typ, raw string) (any, error)

ParseFieldValue converts raw to the JSON value of a field of type typ. Taiga does not validate values (docs/api-notes.md), so the CLI does. An assignment never unsets a field: "null" is text, and an empty date is refused; unsetting takes UnsetFieldValue.

func PreviewProject

func PreviewProject(plan ProjectPlan, statuses []Object, projectID any) ([]WritePlan, []Action, error)

PreviewProject materializes the requests of plan without sending anything. statuses is the read that produced plan. New statuses get an order after the current ones, in file order.

func Scrub added in v0.3.0

func Scrub(v any) any

Scrub returns a deep copy of v with the value of every token parameter hidden, in every string at any depth (taiga.RedactTokens). Keys and other values stay. Every curated output goes through it: user photos (owner_extra_info, assigned_to_extra_info, the history user), project logos and attachment links are signed media URLs that open without authentication.

func Snapshot

func Snapshot(o Object) (map[string]json.RawMessage, error)

Snapshot converts an object read from Taiga back to raw JSON, for WriteVersionedFrom.

func SortStatuses

func SortStatuses(statuses []Object)

SortStatuses orders statuses as Taiga does (order, then name), numerically.

func SystemComment

func SystemComment(o Object) bool

SystemComment reports a history entry written by Taiga's own GitLab integration for a push hook: the inactive gitlab-<hash> user and one of the hook's comment templates (taiga/hooks/event_hooks.py). Both must match. A service account, a human quoting the template, a diff without text or an unknown author are never system comments: agents publish human comments with integration accounts (docs/api-notes.md).

func TagName

func TagName(s string) string

TagName is how Taiga stores a tag: lower case (observed in the local probe).

func UnsetFieldValue

func UnsetFieldValue(typ, name string) error

UnsetFieldValue checks that a field of type typ can be cleared.

func Unsupported

func Unsupported(cause, recovery string) error

Unsupported reports a flag whose Taiga contract the CLI cannot honour safely.

func Usage

func Usage(cause string) error

func ValidateField

func ValidateField(name, typ string) error

ValidateField checks a definition before any request. Taiga limits names to 64 characters; "=" is refused because Name=value assignments end the name at the first "=".

func ValuePath

func ValuePath(kind string, id int64) (string, error)

ValuePath is the custom-attributes-values resource of a story or task. It has its own version, independent of the story's or task's.

func WriteApplied

func WriteApplied(method, path string, status int, readErr error) error

WriteApplied reports a write confirmed by its HTTP status whose result could not be read. It exits 1, not 7: scripts that retry network errors must not repeat an applied write.

Types

type API

type API interface {
	Do(context.Context, taiga.Request) (*taiga.Response, error)
	GetAll(context.Context, string, url.Values) ([]json.RawMessage, error)
	WriteVersioned(context.Context, string, string, map[string]any, bool) (*taiga.Response, error)
	WriteVersionedFrom(context.Context, string, string, map[string]any, map[string]json.RawMessage, bool) (*taiga.Response, error)
	Upload(ctx context.Context, path string, fields map[string]string, fileField, fileName string, r io.Reader, size int64) (*taiga.Response, error)
	Download(ctx context.Context, rawURL string, w io.Writer) (int64, error)
	BaseURL() string
}

API is the part of *taiga.Client the services use.

type Action

type Action struct {
	Kind string `json:"kind"`
	Name string `json:"name"`
	Body Object `json:"body"`
}

Action is one semantic step of a project plan; new statuses have no id or version yet.

type ApplyResult

type ApplyResult struct {
	Plan      ProjectPlan `json:"plan"`
	Applied   []Action    `json:"applied"`
	Remaining []Action    `json:"remaining"`
	Complete  bool        `json:"complete"`
	Requests  []WritePlan `json:"requests"`
	Deferred  []Action    `json:"deferred"`
}

ApplyResult is what project apply did. Applied and Remaining describe a partial run; Complete is set only after a new read finds nothing left to do. Requests is the dry-run preview.

func Apply

func Apply(ctx context.Context, writer StatusWriter, spec ProjectSpec, dry bool) (ApplyResult, error)

Apply validates everything (permission, drift, reorder support) before the first write, then runs the plan in order. A failure stops it: nothing is undone and nothing is repeated, and the result says what was applied and what remains. Running it again re-reads and re-plans.

type FieldSpec

type FieldSpec struct {
	Name        string `toml:"name"`
	Type        string `toml:"type"`
	Description string `toml:"description"`
}

FieldSpec declares a story custom field; an absent description means "".

type LinkPlan added in v0.3.0

type LinkPlan struct {
	DryRun   bool        `json:"dry_run"`
	Requests []WritePlan `json:"requests"`
}

LinkPlan is what --dry-run prints for a write made of several requests.

type Object

type Object map[string]any

Object is a decoded Taiga object; numbers stay json.Number and unknown keys are kept.

func AssignmentPatch

func AssignmentPatch(before Object, p Patch) (Object, error)

AssignmentPatch returns the assignee and block fields that differ from before.

Taiga 6.7 answers assigned_users as the stored list plus assigned_to (docs/api-notes.md), so: removing the owner without changing assigned_to is refused, because Taiga would keep showing them; and any change of assigned_to also sends the full list, because the previous owner may not be in the stored list and would silently disappear.

func BuildPatch

func BuildPatch(before Object, p Patch) (Object, error)

BuildPatch returns only the fields that differ from before.

func Decode

func Decode(b []byte) (Object, error)

func FieldsView

func FieldsView(values Object, defs []Object) []Object

FieldsView binds the values to their definitions: each definition gets the stored value under "value" when there is one. Ids without a definition stay in attributes_values.

func MergeValues

func MergeValues(current Object, updates Object) Object

MergeValues returns current with updates applied; keys not in updates are kept as read.

func Projects added in v0.3.0

func Projects(ctx context.Context, api API, search string) ([]Object, error)

Projects lists the projects the account is a member of. Without member=, Taiga lists every project the account can see: public ones too, and the whole server for a superuser. The server's q= is a full-text search, not a substring, so search is applied here only.

func Read

func Read(ctx context.Context, api API, path string, q url.Values) (Object, error)

func Resolve

func Resolve(items []Object, selector, nameKey string) (Object, error)

Resolve finds exactly one entry whose nameKey or id equals selector.

type Patch

type Patch struct {
	Set                           Object
	AddTags, RemoveTags           []string
	Append                        *string
	AddAssignees, RemoveAssignees []int64
	Owner                         *int64
	ClearOwner                    bool
	Block                         *string
	Unblock                       bool
}

Patch records which changes were asked for; absent fields stay untouched.

type ProjectPlan

type ProjectPlan struct {
	Actions   []Action `json:"actions"`
	Unmanaged []Object `json:"unmanaged"`
	Drift     []Object `json:"drift"`
}

ProjectPlan is what apply would do. Unmanaged lists what exists and the file does not declare (kept as is); Drift lists declared definitions that exist with other values, which apply refuses instead of updating.

func BuildProjectPlan

func BuildProjectPlan(spec ProjectSpec, statuses, fields []Object) (ProjectPlan, error)

BuildProjectPlan compares spec with the statuses (already sorted by SortStatuses) and story field definitions read from Taiga. It never plans an update or a removal.

type ProjectSpec

type ProjectSpec struct {
	StoryStatus []StatusSpec `toml:"story_status"`
	StoryField  []FieldSpec  `toml:"story_field"`
}

ProjectSpec is the project TOML: only what it declares is managed, nothing is removed.

func ParseProjectSpec

func ParseProjectSpec(r io.Reader) (ProjectSpec, error)

ParseProjectSpec decodes the project TOML strictly (unknown keys are errors) and validates every declaration before anything is read from Taiga.

type Service

type Service struct {
	API     API
	Project Object
	// contains filtered or unexported fields
}

Service is created per command run; catalogs are cached only for that run.

func New

func New(ctx context.Context, api API, project string) (*Service, error)

New resolves project (slug, or numeric id) and returns a service bound to it.

func (*Service) ApplyProject

func (s *Service) ApplyProject(ctx context.Context, spec ProjectSpec, dry bool) (ApplyResult, error)

ApplyProject runs Apply against Taiga.

func (*Service) Attachments added in v0.3.0

func (s *Service) Attachments(ctx context.Context, kind string, owner Object) ([]Object, error)

Attachments lists the attachments of a story or task (kind "story" or "task").

func (*Service) Catalog

func (s *Service) Catalog(ctx context.Context, path string) ([]Object, error)

Catalog lists path for the project once per run. Entries that declare another project are dropped: a query parameter the server ignores must not widen the scope.

func (*Service) CloseStory

func (s *Service) CloseStory(ctx context.Context, ref, selector string, dry, force bool) (any, error)

CloseStory moves the story to a closed status: the given one, or the project's only closed status. A story already closed is left alone unless another closed status is named.

func (*Service) CloseTask added in v0.3.0

func (s *Service) CloseTask(ctx context.Context, ref, selector string, dry, force bool) (any, error)

CloseTask moves the task to a closed status, like CloseStory. It only changes the status: the MCP's archived tag is never added.

func (*Service) Comment

func (s *Service) Comment(ctx context.Context, name, ref, body string, dry bool) (any, error)

Comment publishes body on a story or task (name) with PATCH {comment, version} and returns it read again. Taiga's OCC never refuses a comment (it is not a field, so an old version is accepted; docs/api-notes.md), so the version protects nothing and the PATCH is sent once, never repeated. When its outcome is unknown (network error after the connection opened, 5xx or 3xx), the result is always comment_unconfirmed, exit 1, naming the new comments of ours with this exact text that the history shows (findComment).

func (*Service) Comments

func (s *Service) Comments(ctx context.Context, name, ref string, includeSystem bool) ([]Object, error)

Comments lists the comments of a story or task (name), newest first as Taiga sends them. Comments of the GitLab integration (SystemComment) are left out unless includeSystem; every entry is marked with is_system and its story or task (story_id/story_ref or task_id/task_ref). Edited and deleted comments stay, with Taiga's edit_comment_date and delete_comment_date.

func (*Service) CreateField

func (s *Service) CreateField(ctx context.Context, kind, name, typ string, description *string, dry bool) (any, error)

CreateField creates a definition unless one with the same name (case-sensitive, as Taiga compares) already exists: a compatible one is returned unchanged, a different one is an error. Existing definitions are never modified. A nil description is not compared.

func (*Service) CreateStory

func (s *Service) CreateStory(ctx context.Context, body Object, dry bool) (any, error)

CreateStory posts body to the project, once and without version. When the answer says nothing conclusive (network after the connection opened, 5xx, 3xx), the result is always story_create_unconfirmed, naming the candidates found in the project (unconfirmedCreate). A re-run is never offered: it would create a second story.

func (*Service) CreateStoryWithEpic added in v0.3.0

func (s *Service) CreateStoryWithEpic(ctx context.Context, body Object, epic Object, dry bool) (any, error)

CreateStoryWithEpic creates the story and then links it to epic. The story exists once the POST succeeds: a failed link is story_created_link_failed, naming the story, never an invitation to run the command again (that would create a second story).

func (*Service) CreateTask added in v0.3.0

func (s *Service) CreateTask(ctx context.Context, story Object, body Object, dry bool) (any, error)

CreateTask posts body as a task of story, sent once and without version. When the answer says nothing conclusive (network after the connection opened, 5xx, 3xx), the result is always task_create_unconfirmed, naming the candidates found in the story (findTask). A re-run is never offered: it would create a second task.

func (*Service) DownloadAttachment added in v0.3.0

func (s *Service) DownloadAttachment(ctx context.Context, kind string, owner Object, id int64, dest string, overwrite bool, out io.Writer) (Object, error)

DownloadAttachment saves attachment id of owner to dest: a file, an existing directory (the server's name, made safe), "" for the working directory, or "-" for out. The attachment must be in the owner's list, which is filtered by kind (the detail endpoint is not). An existing file is replaced only with overwrite. The bytes go to a temporary file in the same directory and become dest only after their size and sha1 match the attachment.

func (*Service) Epic

func (s *Service) Epic(ctx context.Context, ref string) (Object, error)

Epic resolves an epic of the project by ref only: a ref is never an id.

func (*Service) EpicDetail added in v0.3.0

func (s *Service) EpicDetail(ctx context.Context, ref string) (Object, error)

EpicDetail reads an epic by ref and adds user_stories: its linked stories in link order, each with id, ref, subject and project (plus project_slug when the story is in another project, which Taiga allows). A linked story the account cannot read keeps only its id.

func (*Service) Epics added in v0.3.0

func (s *Service) Epics(ctx context.Context, search string, closed *bool) ([]Object, error)

Epics lists the epics of the project; closed goes to Taiga as status__is_closed and is checked again here, search matches the subject (Taiga's q= is a full-text search).

func (*Service) FieldValues

func (s *Service) FieldValues(ctx context.Context, kind string, id int64) (Object, error)

FieldValues reads the values of the story or task id, after checking that it belongs to the selected project. A read error is returned as is, never as empty values.

func (*Service) Fields

func (s *Service) Fields(ctx context.Context, kind string) ([]Object, error)

Fields lists the custom field definitions of the project for kind.

func (*Service) Item added in v0.3.0

func (s *Service) Item(ctx context.Context, name, ref string) (Object, error)

Item reads a story or task (name) of the selected project by ref.

func (*Service) LinkEpic added in v0.3.0

func (s *Service) LinkEpic(ctx context.Context, story, epic Object, replace, dry bool) (any, error)

LinkEpic makes epic one of the story's epics; with replace, the only one. story is the read the change is computed from. The new link is created before any old one is removed, so the story never ends without an epic; no request is ever repeated. The caller checks --confirm-delete.

func (*Service) LinkableEpic added in v0.3.0

func (s *Service) LinkableEpic(ctx context.Context, ref string) (Object, error)

LinkableEpic resolves an epic of the project for a link: by ref, in a project whose epics module is on. Taiga links an epic of any project (probe); resolving here keeps it in this one.

func (*Service) Member

func (s *Service) Member(ctx context.Context, selector string) (Object, error)

Member resolves a project member by exact username, id or "me": the id must appear in the project's memberships.

func (*Service) Milestones added in v0.3.0

func (s *Service) Milestones(ctx context.Context, search string, closed *bool) ([]Object, error)

Milestones lists the sprints of the project. closed, when set, goes to Taiga (it honours it) and is checked again here, with search on the name.

func (*Service) ProjectPlan

func (s *Service) ProjectPlan(ctx context.Context, spec ProjectSpec) (ProjectPlan, error)

ProjectPlan reads the project's statuses and story fields and compares them with spec. It only reads: no permission is needed beyond seeing the project.

func (*Service) ProjectView added in v0.3.0

func (s *Service) ProjectView() Object

ProjectView is the selected project as read, without its credentials and signed URLs.

func (*Service) SetFieldValues

func (s *Service) SetFieldValues(ctx context.Context, kind string, id int64, entries, unsets []string, dry, force bool) (any, error)

SetFieldValues merges the Name=value entries into the values of the story or task id, clears the fields named in unsets, and writes the whole dictionary with the version of the values resource. A cleared field keeps its key with null: Taiga refuses an empty dictionary, so the key cannot be dropped. Clearing a field without a stored value adds nothing, because an absent key and null both mean no value. Every entry is resolved and parsed before anything is read or written; an unchanged dictionary writes nothing.

func (*Service) Stories

func (s *Service) Stories(ctx context.Context, filters url.Values) ([]Object, error)

Stories lists every story of the project matching filters (keys: ref, status, closed, search, assignee, epic, swimlane, tag; ids already resolved, swimlane "null" for none). Proven filters go to Taiga to shorten the list; every filter is then checked locally, so an ignored parameter never widens it.

func (*Service) Story

func (s *Service) Story(ctx context.Context, ref string, id int64) (Object, error)

Story reads a story of the selected project by ref (userstories/by_ref) or by id. Tags stay as Taiga sent them, because the object is the baseline of a versioned write.

func (*Service) StoryView

func (s *Service) StoryView(o Object) (Object, error)

StoryView is the output form: tag names instead of pairs, plus the web URL.

func (*Service) Swimlanes added in v0.3.0

func (s *Service) Swimlanes(ctx context.Context) ([]Object, error)

Swimlanes lists the swimlanes of the project in the order Taiga returns them, which is the board order (order, then name in the database collation: re-sorting here could break a tie differently). Each carries is_default; the catalog entries are copied, not changed.

func (*Service) Task added in v0.3.0

func (s *Service) Task(ctx context.Context, ref string, id int64) (Object, error)

Task reads a task of the selected project by ref (tasks/by_ref) or by id.

func (*Service) TaskView added in v0.3.0

func (s *Service) TaskView(o Object) (Object, error)

TaskView is the output form of a task: tag names instead of pairs, plus the web URL.

func (*Service) Tasks added in v0.3.0

func (s *Service) Tasks(ctx context.Context, filters url.Values) ([]Object, error)

Tasks lists every task of the project matching filters (keys: story, status, assignee, closed, search, tag, ref; ids already resolved). Every filter is checked locally too.

func (*Service) UpdateStory

func (s *Service) UpdateStory(ctx context.Context, ref string, p Patch, dry, force bool) (any, error)

func (*Service) UpdateStoryWithEpic added in v0.3.0

func (s *Service) UpdateStoryWithEpic(ctx context.Context, ref string, p Patch, epic Object, replace, dry, force bool) (any, error)

UpdateStoryWithEpic writes the field changes (if any) and then links epic, with replace the only one. A failed link after an applied PATCH keeps its code and says the fields are saved.

func (*Service) UpdateTask added in v0.3.0

func (s *Service) UpdateTask(ctx context.Context, ref string, p Patch, dry, force bool) (any, error)

UpdateTask applies p to the task ref. assigned_to is a plain versioned field of a task (docs/api-notes.md), so it takes the guarded retry like any other.

func (*Service) Upload added in v0.3.0

func (s *Service) Upload(ctx context.Context, kind string, owner Object, path, description string, dry bool) (any, error)

Upload attaches the file at path to owner. An attachment with the same name and sha1 already there is returned without sending (created: false). The POST is sent once and never repeated; its outcome is checked by the answer (sha1, size, object_id), and when the answer is lost by a new id with the same name and sha1 in the list.

func (*Service) User

func (s *Service) User(ctx context.Context, selector string) (Object, error)

User resolves a user by exact username, id or "me" in users?project=, which also lists non-members (probe). Use it only where membership does not matter, like removing an assignee.

func (*Service) Users added in v0.3.0

func (s *Service) Users(ctx context.Context, search string) ([]Object, error)

Users lists the members of the project: users?project= also lists non-members, and memberships has no username, so both are read. Pending invitations (no user) are left out. Each user carries is_admin, role and role_name from its membership.

func (*Service) View added in v0.3.0

func (s *Service) View(name string, o Object) (Object, error)

View is the output form of a story or task (name).

func (*Service) Write

func (s *Service) Write(ctx context.Context, k kind, path string, before, patch Object, dry, force bool) (any, error)

Write sends patch against before (the read used to compute it) and re-reads the resource. An empty patch writes nothing and returns before.

type StatusSpec

type StatusSpec struct {
	Name   string `toml:"name"`
	Color  string `toml:"color"`
	Closed bool   `toml:"closed"`
	After  string `toml:"after"`
}

StatusSpec declares a story status. Closed defaults to false; After names the status it follows (existing or declared in the same file).

type StatusWriter

type StatusWriter interface {
	CheckPermission(context.Context) error
	ProjectID() any
	Load(context.Context) (statuses, fields []Object, err error)
	CheckReorderSupport(context.Context) error
	CreateStatus(context.Context, Object) error
	CreateField(context.Context, Object) error
	Reorder(context.Context, []string) error
}

StatusWriter is the Taiga side of apply; its contracts are in docs/api-notes.md.

type UploadFile added in v0.3.0

type UploadFile struct {
	Name string `json:"name"`
	Size int64  `json:"size"`
	SHA1 string `json:"sha1"`
}

type UploadPlan added in v0.3.0

type UploadPlan struct {
	DryRun bool              `json:"dry_run"`
	Method string            `json:"method"`
	Path   string            `json:"path"`
	Fields map[string]string `json:"fields"`
	File   UploadFile        `json:"file"`
}

UploadPlan is what --dry-run prints for an upload: the form fields and the file's metadata, never its content.

type WritePlan

type WritePlan struct {
	DryRun bool   `json:"dry_run"`
	Method string `json:"method"`
	Path   string `json:"path"`
	Body   Object `json:"body"`
}

WritePlan is what --dry-run prints instead of sending.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL