diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..96c7186 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,27 @@ +name: CI + +on: + pull_request: + push: + branches: + - master + +permissions: + contents: read + +jobs: + check: + name: Check + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version-file: go.mod + cache: true + + - name: Run checks + run: make check diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..68648d0 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,71 @@ +name: Release + +on: + push: + tags: + - "v*" + +permissions: + contents: write + +jobs: + release: + name: Build and release + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version-file: go.mod + cache: true + + - name: Verify + run: make check + + - name: Build archives + env: + VERSION: ${{ github.ref_name }} + run: | + set -euo pipefail + mkdir -p dist + for target in \ + linux/amd64 \ + linux/arm64 \ + darwin/amd64 \ + darwin/arm64 \ + windows/amd64 \ + windows/arm64 + do + GOOS="${target%/*}" + GOARCH="${target#*/}" + name="redmine_${VERSION}_${GOOS}_${GOARCH}" + binary="redmine" + if [ "$GOOS" = "windows" ]; then + binary="redmine.exe" + fi + mkdir -p "dist/$name" + CGO_ENABLED=0 GOOS="$GOOS" GOARCH="$GOARCH" go build \ + -trimpath \ + -ldflags "-s -w -X main.version=${VERSION}" \ + -o "dist/$name/$binary" \ + ./cmd/redmine + cp README.md LICENSE "dist/$name/" + if [ "$GOOS" = "windows" ]; then + (cd dist && zip -qr "$name.zip" "$name") + else + tar -C dist -czf "dist/$name.tar.gz" "$name" + fi + rm -rf "dist/$name" + done + (cd dist && sha256sum * > checksums.txt) + + - name: Publish GitHub Release + env: + GH_TOKEN: ${{ github.token }} + VERSION: ${{ github.ref_name }} + run: gh release create "$VERSION" dist/* --title "$VERSION" --generate-notes diff --git a/.gitignore b/.gitignore index aaadf73..7721441 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore # # Binaries for programs and plugins +bin/ +dist/ *.exe *.exe~ *.dll @@ -18,7 +20,7 @@ coverage.* profile.cov # Dependency directories (remove the comment below to include it) -# vendor/ +vendor/ # Go workspace file go.work @@ -28,5 +30,5 @@ go.work.sum .env # Editor/IDE -# .idea/ -# .vscode/ +.idea/ +.vscode/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..2dd9370 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,26 @@ +# Redmine CLI + +## Development + +* Cover implementation changes with tests +* Add and generate documentation when adding or changing functionality +* At the end of implementation, run `make fix test gen-docs` + +## Running Individual Checks + +```bash +make build # compile +make lint # golangci-lint +make fix # auto-fix lint issues (gofmt + goimports) +make test # all unit tests +make gen # regenerate OpenAPI command metadata and docs +make gen-docs # regenerate docs +make check # lint, test, and generated-file checks +make check-docs # verify generated docs are current +``` + +## Documentation conventions + +CLI documentation is generated from `openapi/openapi.yaml` by `make gen-docs`. + +Use English as the primary language for documentation, commits, comments, etc. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..03582ba --- /dev/null +++ b/Makefile @@ -0,0 +1,84 @@ +OPENAPI_SPEC_URL ?= https://raw.githubusercontent.com/d-yoshi/redmine-openapi/main/openapi.yaml +OPENAPI_SPEC_DIR ?= openapi +OPENAPI_SPEC ?= $(OPENAPI_SPEC_DIR)/openapi.yaml +GO ?= go +BINARY ?= redmine +BIN_DIR ?= bin +VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo dev) +LDFLAGS ?= -s -w -X main.version=$(VERSION) +GOFILES := $(shell find . -name '*.go' -not -path './vendor/*') + +.DEFAULT_GOAL := help + +.PHONY: help download-openapi-spec build install lint fix test gen gen-docs check-docs check-gen check clean +help: + @echo "Targets:" + @echo " download-openapi-spec Download latest Redmine OpenAPI spec to $(OPENAPI_SPEC)" + @echo " build Compile $(BINARY)" + @echo " install Install $(BINARY)" + @echo " lint Run golangci-lint when available, otherwise go vet" + @echo " fix Run gofmt and goimports" + @echo " test Run unit tests" + @echo " gen Regenerate OpenAPI command metadata and docs" + @echo " gen-docs Regenerate docs/usage.md and generated metadata" + @echo " check-docs Verify docs/usage.md is current" + @echo " check-gen Verify generated files are current" + @echo " check Run lint, tests, and generated-file checks" + +download-openapi-spec: + @mkdir -p "$(dir $(OPENAPI_SPEC))" + @tmp=$$(mktemp); \ + trap 'rm -f "$$tmp"' EXIT; \ + curl -fsSL "$(OPENAPI_SPEC_URL)" -o "$$tmp"; \ + grep -q '^openapi: 3\.' "$$tmp"; \ + mv "$$tmp" "$(OPENAPI_SPEC)"; \ + echo "Downloaded $(OPENAPI_SPEC)" + +build: + @mkdir -p "$(BIN_DIR)" + $(GO) build -trimpath -ldflags "$(LDFLAGS)" -o "$(BIN_DIR)/$(BINARY)" ./cmd/redmine + +install: + $(GO) install -trimpath -ldflags "$(LDFLAGS)" ./cmd/redmine + +lint: + @if command -v golangci-lint >/dev/null 2>&1; then \ + golangci-lint run; \ + else \ + echo "golangci-lint not found; running go vet"; \ + $(GO) vet ./...; \ + fi + +fix: + @gofmt -w $(GOFILES) + @if command -v goimports >/dev/null 2>&1; then \ + goimports -w $(GOFILES); \ + else \ + $(GO) run golang.org/x/tools/cmd/goimports@latest -w $(GOFILES); \ + fi + +test: + $(GO) test ./... + +gen: + $(GO) run ./cmd/redmine-openapi-gen -spec "$(OPENAPI_SPEC)" -out internal/openapi/generated.go -docs docs/usage.md + +gen-docs: gen + +check-docs: + @tmp=$$(mktemp -d); \ + trap 'rm -rf "$$tmp"' EXIT; \ + $(GO) run ./cmd/redmine-openapi-gen -spec "$(OPENAPI_SPEC)" -out "$$tmp/generated.go" -docs "$$tmp/usage.md"; \ + diff -u docs/usage.md "$$tmp/usage.md" + +check-gen: + @tmp=$$(mktemp -d); \ + trap 'rm -rf "$$tmp"' EXIT; \ + $(GO) run ./cmd/redmine-openapi-gen -spec "$(OPENAPI_SPEC)" -out "$$tmp/generated.go" -docs "$$tmp/usage.md"; \ + diff -u internal/openapi/generated.go "$$tmp/generated.go"; \ + diff -u docs/usage.md "$$tmp/usage.md" + +check: lint test check-gen + +clean: + rm -rf "$(BIN_DIR)" dist diff --git a/README.md b/README.md index a08ee25..4896b9a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,26 @@ # redmine-cli -A Redmine CLI tool bringing Redmine to your command line + +A Redmine CLI tool bringing Redmine to your command line. + +Built based on https://d-yoshi.github.io/redmine-openapi/ ([repo](https://github.com/d-yoshi/redmine-openapi)). + +## Usage + +```bash +redmine auth login --host https://redmine.example.com --api-key "$REDMINE_API_KEY" +redmine issue list --limit 20 +redmine issue show 123 --include journals +redmine issue create --project-id my-project --subject "Fix checkout" +``` + +Full generated usage documentation is in [docs/usage.md](docs/usage.md). + +## Development + +The command metadata and usage docs are generated from `openapi/openapi.yaml`. + +```bash +make download-openapi-spec +make gen +make check +``` diff --git a/cmd/redmine-openapi-gen/main.go b/cmd/redmine-openapi-gen/main.go new file mode 100644 index 0000000..57b923b --- /dev/null +++ b/cmd/redmine-openapi-gen/main.go @@ -0,0 +1,881 @@ +package main + +import ( + "bytes" + "errors" + "flag" + "fmt" + "go/format" + "os" + "path/filepath" + "regexp" + "sort" + "strings" + + "github.com/muxx/redmine-cli/internal/openapi" + "gopkg.in/yaml.v3" +) + +var pathParamPattern = regexp.MustCompile(`\{([^}]+)\}`) + +func main() { + specPath := flag.String("spec", "openapi/openapi.yaml", "OpenAPI YAML file") + outPath := flag.String("out", "internal/openapi/generated.go", "generated Go output") + docsPath := flag.String("docs", "docs/usage.md", "generated usage docs output") + flag.Parse() + + if err := run(*specPath, *outPath, *docsPath); err != nil { + fmt.Fprintf(os.Stderr, "redmine-openapi-gen: %v\n", err) + os.Exit(1) + } +} + +func run(specPath, outPath, docsPath string) error { + data, err := os.ReadFile(specPath) + if err != nil { + return err + } + + var doc document + if err := yaml.Unmarshal(data, &doc); err != nil { + return err + } + if doc.OpenAPI == "" { + return errors.New("input does not look like an OpenAPI document") + } + + ops, err := buildOperations(&doc) + if err != nil { + return err + } + + if outPath != "" { + source, err := renderGo(&doc, ops) + if err != nil { + return err + } + if err := writeFile(outPath, source); err != nil { + return err + } + } + + if docsPath != "" { + if err := writeFile(docsPath, renderDocs(&doc, ops)); err != nil { + return err + } + } + + return nil +} + +func writeFile(path string, data []byte) error { + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + return err + } + return os.WriteFile(path, data, 0o644) +} + +type document struct { + OpenAPI string `yaml:"openapi"` + Info info `yaml:"info"` + Tags []tag `yaml:"tags"` + Paths map[string]pathItem `yaml:"paths"` + Components components `yaml:"components"` +} + +type info struct { + Title string `yaml:"title"` + Description string `yaml:"description"` + Version string `yaml:"version"` +} + +type tag struct { + Name string `yaml:"name"` + Description string `yaml:"description"` +} + +type components struct { + Parameters map[string]parameter `yaml:"parameters"` +} + +type pathItem struct { + Get *operation `yaml:"get"` + Post *operation `yaml:"post"` + Put *operation `yaml:"put"` + Patch *operation `yaml:"patch"` + Delete *operation `yaml:"delete"` +} + +func (p pathItem) operations() []methodOperation { + return []methodOperation{ + {method: "GET", operation: p.Get}, + {method: "POST", operation: p.Post}, + {method: "PUT", operation: p.Put}, + {method: "PATCH", operation: p.Patch}, + {method: "DELETE", operation: p.Delete}, + } +} + +type methodOperation struct { + method string + operation *operation +} + +type operation struct { + Tags []string `yaml:"tags"` + Summary string `yaml:"summary"` + Description string `yaml:"description"` + OperationID string `yaml:"operationId"` + Parameters []parameter `yaml:"parameters"` + RequestBody requestBody `yaml:"requestBody"` + Responses map[string]response `yaml:"responses"` +} + +type parameter struct { + Ref string `yaml:"$ref"` + Name string `yaml:"name"` + In string `yaml:"in"` + Required bool `yaml:"required"` + Description string `yaml:"description"` + Schema *schema `yaml:"schema"` +} + +type requestBody struct { + Content map[string]mediaType `yaml:"content"` +} + +type response struct { + Ref string `yaml:"$ref"` + Content map[string]mediaType `yaml:"content"` +} + +type mediaType struct { + Schema *schema `yaml:"schema"` +} + +type schema struct { + Ref string `yaml:"$ref"` + Type string `yaml:"type"` + Format string `yaml:"format"` + Description string `yaml:"description"` + Nullable bool `yaml:"nullable"` + Required []string `yaml:"required"` + Properties map[string]*schema `yaml:"properties"` + Items *schema `yaml:"items"` + OneOf []*schema `yaml:"oneOf"` + AllOf []*schema `yaml:"allOf"` + Enum []any `yaml:"enum"` +} + +func buildOperations(doc *document) ([]openapi.Operation, error) { + tagOrder := make(map[string]int, len(doc.Tags)) + for i, tag := range doc.Tags { + tagOrder[tag.Name] = i + } + + var paths []string + for path := range doc.Paths { + paths = append(paths, path) + } + sort.Strings(paths) + + type indexedOperation struct { + index int + op openapi.Operation + } + var indexed []indexedOperation + index := 0 + seenCommands := map[string]map[string]int{} + + for _, path := range paths { + for _, methodOp := range doc.Paths[path].operations() { + src := methodOp.operation + if src == nil { + continue + } + tagName := firstTag(src.Tags) + group, aliases := groupName(tagName) + command := commandName(src.Summary, group, aliases) + if command == "" { + command = kebab(src.OperationID) + } + if _, ok := seenCommands[group]; !ok { + seenCommands[group] = map[string]int{} + } + seenCommands[group][command]++ + if seenCommands[group][command] > 1 { + command = command + "-" + kebab(src.OperationID) + } + + params := resolveParameters(doc, src.Parameters) + op := openapi.Operation{ + ID: src.OperationID, + Group: group, + GroupAlias: aliases, + Command: command, + Method: methodOp.method, + Path: path, + Summary: clean(src.Summary), + Description: clean(src.Description), + PathParams: pathParameters(path, params), + QueryParams: queryParameters(params), + HeaderParams: headerParameters(params), + Body: body(src.RequestBody), + ResponseBinary: responseBinary(src.Responses), + } + indexed = append(indexed, indexedOperation{ + index: sortIndex(tagOrder, tagName, index), + op: op, + }) + index++ + } + } + + sort.SliceStable(indexed, func(i, j int) bool { + if indexed[i].index != indexed[j].index { + return indexed[i].index < indexed[j].index + } + if indexed[i].op.Group != indexed[j].op.Group { + return indexed[i].op.Group < indexed[j].op.Group + } + if indexed[i].op.Command != indexed[j].op.Command { + return indexed[i].op.Command < indexed[j].op.Command + } + return indexed[i].op.ID < indexed[j].op.ID + }) + + ops := make([]openapi.Operation, 0, len(indexed)) + for _, entry := range indexed { + ops = append(ops, entry.op) + } + return ops, nil +} + +func sortIndex(tagOrder map[string]int, tag string, fallback int) int { + if idx, ok := tagOrder[tag]; ok { + return idx*1000 + fallback + } + return 100000 + fallback +} + +func firstTag(tags []string) string { + if len(tags) == 0 { + return "API" + } + return tags[0] +} + +func resolveParameters(doc *document, params []parameter) []parameter { + resolved := make([]parameter, 0, len(params)) + for _, param := range params { + if param.Ref != "" { + name := strings.TrimPrefix(param.Ref, "#/components/parameters/") + if component, ok := doc.Components.Parameters[name]; ok { + resolved = append(resolved, component) + } + continue + } + resolved = append(resolved, param) + } + return resolved +} + +func pathParameters(path string, params []parameter) []openapi.Parameter { + byName := map[string]parameter{} + for _, param := range params { + if param.In == "path" { + byName[param.Name] = param + } + } + + matches := pathParamPattern.FindAllStringSubmatch(path, -1) + result := make([]openapi.Parameter, 0, len(matches)) + for _, match := range matches { + name := match[1] + if name == "format" { + continue + } + param := byName[name] + if param.Name == "" { + param = parameter{Name: name, In: "path", Required: true} + } + result = append(result, convertParameter(param)) + } + return result +} + +func queryParameters(params []parameter) []openapi.Parameter { + var result []openapi.Parameter + for _, param := range params { + if param.In != "query" { + continue + } + result = append(result, convertParameter(param)) + } + return result +} + +func headerParameters(params []parameter) []openapi.Parameter { + var result []openapi.Parameter + for _, param := range params { + if param.In != "header" { + continue + } + if strings.EqualFold(param.Name, "X-Redmine-Switch-User") { + continue + } + result = append(result, convertParameter(param)) + } + return result +} + +func convertParameter(param parameter) openapi.Parameter { + typ, _ := schemaType(param.Schema) + return openapi.Parameter{ + Name: param.Name, + Flag: kebab(param.Name), + Placeholder: strings.ToUpper(strings.ReplaceAll(param.Name, "-", "_")), + Required: param.Required, + Type: typ, + Description: clean(param.Description), + Enum: enumStrings(param.Schema), + } +} + +func body(req requestBody) *openapi.Body { + if req.Content == nil { + return nil + } + if mt, ok := req.Content["application/octet-stream"]; ok { + _ = mt + return &openapi.Body{ContentType: "application/octet-stream", Binary: true} + } + + mt, ok := req.Content["application/json"] + if !ok || mt.Schema == nil { + return nil + } + + root := "" + bodySchema := mt.Schema + required := requiredSet(bodySchema.Required) + if bodySchema.Type == "object" && len(bodySchema.Properties) == 1 { + for name, child := range bodySchema.Properties { + if required[name] && child != nil && (child.Type == "object" || len(child.Properties) > 0) { + root = name + bodySchema = child + } + } + } + + fields := make([]openapi.BodyField, 0, len(bodySchema.Properties)) + required = requiredSet(bodySchema.Required) + var names []string + for name := range bodySchema.Properties { + names = append(names, name) + } + sort.Strings(names) + for _, name := range names { + fieldSchema := bodySchema.Properties[name] + typ, array := schemaType(fieldSchema) + fields = append(fields, openapi.BodyField{ + Name: name, + Flag: kebab(name), + Required: required[name], + Type: typ, + Array: array, + Nullable: fieldSchema != nil && fieldSchema.Nullable, + Description: clean(schemaDescription(fieldSchema)), + Enum: enumStrings(fieldSchema), + }) + } + + return &openapi.Body{ + ContentType: "application/json", + Root: root, + Fields: fields, + } +} + +func schemaType(s *schema) (string, bool) { + if s == nil { + return "string", false + } + if s.Type == "array" { + itemType, _ := schemaType(s.Items) + return itemType, true + } + if s.Type != "" { + if s.Format != "" { + return s.Type + ":" + s.Format, false + } + return s.Type, false + } + if len(s.OneOf) > 0 { + types := make([]string, 0, len(s.OneOf)) + for _, child := range s.OneOf { + typ, _ := schemaType(child) + if typ != "" { + types = append(types, typ) + } + } + return strings.Join(types, "|"), false + } + if s.Ref != "" { + return "object", false + } + return "string", false +} + +func schemaDescription(s *schema) string { + if s == nil { + return "" + } + return s.Description +} + +func enumStrings(s *schema) []string { + if s == nil || len(s.Enum) == 0 { + return nil + } + values := make([]string, 0, len(s.Enum)) + for _, value := range s.Enum { + values = append(values, fmt.Sprint(value)) + } + return values +} + +func responseBinary(responses map[string]response) bool { + for code, resp := range responses { + if !strings.HasPrefix(code, "2") { + continue + } + for contentType, mt := range resp.Content { + if contentType != "application/json" { + return true + } + if mt.Schema != nil && mt.Schema.Format == "binary" { + return true + } + } + } + return false +} + +func requiredSet(values []string) map[string]bool { + set := make(map[string]bool, len(values)) + for _, value := range values { + set[value] = true + } + return set +} + +func groupName(tag string) (string, []string) { + plural := kebab(tag) + parts := strings.Split(plural, "-") + if len(parts) == 0 { + return "api", nil + } + lastPlural := parts[len(parts)-1] + lastSingular := singular(lastPlural) + parts[len(parts)-1] = lastSingular + group := strings.Join(parts, "-") + aliases := uniqueNonEmpty(plural, lastSingular, lastPlural) + aliases = removeValue(aliases, group) + return group, aliases +} + +func uniqueNonEmpty(values ...string) []string { + seen := map[string]bool{} + var result []string + for _, value := range values { + if value == "" || seen[value] { + continue + } + seen[value] = true + result = append(result, value) + } + return result +} + +func removeValue(values []string, remove string) []string { + result := values[:0] + for _, value := range values { + if value == remove { + continue + } + result = append(result, value) + } + if len(result) == 0 { + return nil + } + return result +} + +func singular(value string) string { + switch value { + case "statuses": + return "status" + case "categories": + return "category" + case "queries": + return "query" + case "entries": + return "entry" + case "memberships": + return "membership" + case "repositories": + return "repository" + case "fields": + return "field" + case "news": + return "news" + } + if strings.HasSuffix(value, "ies") { + return strings.TrimSuffix(value, "ies") + "y" + } + if strings.HasSuffix(value, "ses") { + return strings.TrimSuffix(value, "es") + } + if strings.HasSuffix(value, "s") && len(value) > 1 { + return strings.TrimSuffix(value, "s") + } + return value +} + +func commandName(summary, group string, aliases []string) string { + action := kebab(summary) + if action == "" { + return "" + } + allAliases := append([]string{group}, aliases...) + for _, alias := range allAliases { + if action == alias { + return "run" + } + } + + parts := strings.SplitN(action, "-", 2) + if len(parts) == 1 { + return action + } + + verb := parts[0] + rest := parts[1] + switch verb { + case "list", "show", "create", "update", "delete", "add", "remove", "archive", "unarchive", "close", "reopen", "download", "upload": + default: + return action + } + + for _, alias := range allAliases { + if rest == alias { + return verb + } + if strings.HasPrefix(rest, alias+"-") { + return verb + "-" + strings.TrimPrefix(rest, alias+"-") + } + if strings.HasSuffix(rest, "-"+alias) { + prefix := strings.TrimSuffix(rest, "-"+alias) + if prefix != "" { + return verb + "-" + prefix + } + return verb + } + } + return action +} + +func kebab(value string) string { + value = strings.TrimSpace(value) + var b strings.Builder + lastDash := false + for _, r := range value { + switch { + case r >= 'a' && r <= 'z': + b.WriteRune(r) + lastDash = false + case r >= 'A' && r <= 'Z': + b.WriteRune(r + ('a' - 'A')) + lastDash = false + case r >= '0' && r <= '9': + b.WriteRune(r) + lastDash = false + default: + if !lastDash && b.Len() > 0 { + b.WriteByte('-') + lastDash = true + } + } + } + return strings.Trim(b.String(), "-") +} + +func clean(value string) string { + lines := strings.Split(strings.TrimSpace(value), "\n") + for i, line := range lines { + lines[i] = strings.TrimSpace(line) + } + return strings.Join(lines, "\n") +} + +func renderGo(doc *document, ops []openapi.Operation) ([]byte, error) { + var buf bytes.Buffer + buf.WriteString("// Code generated by redmine-openapi-gen; DO NOT EDIT.\n\n") + buf.WriteString("package openapi\n\n") + fmt.Fprintf(&buf, "const SpecTitle = %q\n", doc.Info.Title) + fmt.Fprintf(&buf, "const SpecVersion = %q\n\n", doc.Info.Version) + buf.WriteString("var Operations = []Operation{\n") + for _, op := range ops { + writeOperation(&buf, op) + } + buf.WriteString("}\n") + + source, err := format.Source(buf.Bytes()) + if err != nil { + return nil, err + } + return source, nil +} + +func writeOperation(buf *bytes.Buffer, op openapi.Operation) { + buf.WriteString("{\n") + fmt.Fprintf(buf, "ID:%q,\n", op.ID) + fmt.Fprintf(buf, "Group:%q,\n", op.Group) + fmt.Fprintf(buf, "GroupAlias:%s,\n", stringSlice(op.GroupAlias)) + fmt.Fprintf(buf, "Command:%q,\n", op.Command) + fmt.Fprintf(buf, "Method:%q,\n", op.Method) + fmt.Fprintf(buf, "Path:%q,\n", op.Path) + fmt.Fprintf(buf, "Summary:%q,\n", op.Summary) + fmt.Fprintf(buf, "Description:%q,\n", op.Description) + fmt.Fprintf(buf, "PathParams:%s,\n", parameters(op.PathParams)) + fmt.Fprintf(buf, "QueryParams:%s,\n", parameters(op.QueryParams)) + fmt.Fprintf(buf, "HeaderParams:%s,\n", parameters(op.HeaderParams)) + fmt.Fprintf(buf, "Body:%s,\n", bodyLiteral(op.Body)) + fmt.Fprintf(buf, "ResponseBinary:%t,\n", op.ResponseBinary) + buf.WriteString("},\n") +} + +func stringSlice(values []string) string { + if len(values) == 0 { + return "nil" + } + var parts []string + for _, value := range values { + parts = append(parts, fmt.Sprintf("%q", value)) + } + return "[]" + "string{" + strings.Join(parts, ",") + "}" +} + +func parameters(params []openapi.Parameter) string { + if len(params) == 0 { + return "nil" + } + var buf strings.Builder + buf.WriteString("[]Parameter{\n") + for _, param := range params { + fmt.Fprintf(&buf, "{Name:%q,Flag:%q,Placeholder:%q,Required:%t,Type:%q,Description:%q,Enum:%s},\n", + param.Name, + param.Flag, + param.Placeholder, + param.Required, + param.Type, + param.Description, + stringSlice(param.Enum), + ) + } + buf.WriteString("}") + return buf.String() +} + +func bodyLiteral(body *openapi.Body) string { + if body == nil { + return "nil" + } + return fmt.Sprintf("&Body{ContentType:%q,Root:%q,Binary:%t,Fields:%s}", body.ContentType, body.Root, body.Binary, bodyFields(body.Fields)) +} + +func bodyFields(fields []openapi.BodyField) string { + if len(fields) == 0 { + return "nil" + } + var buf strings.Builder + buf.WriteString("[]BodyField{\n") + for _, field := range fields { + fmt.Fprintf(&buf, "{Name:%q,Flag:%q,Required:%t,Type:%q,Array:%t,Nullable:%t,Description:%q,Enum:%s},\n", + field.Name, + field.Flag, + field.Required, + field.Type, + field.Array, + field.Nullable, + field.Description, + stringSlice(field.Enum), + ) + } + buf.WriteString("}") + return buf.String() +} + +func renderDocs(doc *document, ops []openapi.Operation) []byte { + var buf bytes.Buffer + fmt.Fprintf(&buf, "# redmine CLI usage\n\n") + fmt.Fprintf(&buf, "\n\n") + fmt.Fprintf(&buf, "Generated from `%s` `%s`.\n\n", doc.Info.Title, doc.Info.Version) + buf.WriteString("## Global usage\n\n") + buf.WriteString("```bash\n") + buf.WriteString("redmine [command] [subcommand] [flags]\n") + buf.WriteString("```\n\n") + buf.WriteString("Set `REDMINE_HOST` and `REDMINE_API_KEY`, or run `redmine auth login --host --api-key `. Login validates credentials with `GET /users/current.json` before saving them.\n\n") + buf.WriteString("Common flags: `--host`, `--api-key`, `--username`, `--password`, `--switch-user`, `--output json|yaml|raw`, `--config`.\n\n") + buf.WriteString("Request body commands accept generated body flags, repeated `--field key=value`, or `--body @file.json`.\n\n") + renderAuthDocs(&buf) + buf.WriteString("## Examples\n\n") + buf.WriteString("```bash\n") + buf.WriteString("redmine auth login --host https://redmine.example.com --api-key \"$REDMINE_API_KEY\"\n") + buf.WriteString("redmine auth status\n") + buf.WriteString("redmine issue list --limit 20\n") + buf.WriteString("redmine issue show 123 --include journals\n") + buf.WriteString("redmine issue create --project-id my-project --subject \"Fix checkout\"\n") + buf.WriteString("redmine project list\n") + buf.WriteString("```\n\n") + + grouped := map[string][]openapi.Operation{} + var groups []string + for _, op := range ops { + if _, ok := grouped[op.Group]; !ok { + groups = append(groups, op.Group) + } + grouped[op.Group] = append(grouped[op.Group], op) + } + + buf.WriteString("## Commands\n\n") + for _, group := range groups { + fmt.Fprintf(&buf, "### `%s`\n\n", group) + for _, op := range grouped[group] { + fmt.Fprintf(&buf, "- `redmine %s %s%s` - %s\n", op.Group, op.Command, usageArgs(op.PathParams), op.Summary) + } + buf.WriteString("\n") + } + + buf.WriteString("## Command reference\n\n") + for _, group := range groups { + for _, op := range grouped[group] { + renderOperationDocs(&buf, op) + } + } + return buf.Bytes() +} + +func renderAuthDocs(buf *bytes.Buffer) { + buf.WriteString("## Authentication\n\n") + buf.WriteString("### `redmine auth login --host --api-key `\n\n") + buf.WriteString("Checks the credentials with `GET /users/current.json` and saves them to the config file only after a successful response. The API key can also be read from stdin with `--stdin`.\n\n") + buf.WriteString("### `redmine auth status`\n\n") + buf.WriteString("Loads authentication from flags, environment, or config, calls `GET /users/current.json`, and prints the resolved host, auth method, authenticated user, and status.\n\n") + buf.WriteString("### `redmine auth logout`\n\n") + buf.WriteString("Removes the saved config file.\n\n") +} + +func renderOperationDocs(buf *bytes.Buffer, op openapi.Operation) { + fmt.Fprintf(buf, "### `redmine %s %s%s`\n\n", op.Group, op.Command, usageArgs(op.PathParams)) + fmt.Fprintf(buf, "%s\n\n", op.Summary) + if op.Description != "" { + fmt.Fprintf(buf, "%s\n\n", op.Description) + } + fmt.Fprintf(buf, "- Operation: `%s %s` (`%s`)\n", op.Method, op.Path, op.ID) + if len(op.PathParams) > 0 { + buf.WriteString("- Arguments:\n") + for _, param := range op.PathParams { + fmt.Fprintf(buf, " - `%s`", strings.ToLower(param.Placeholder)) + if param.Description != "" { + fmt.Fprintf(buf, ": %s", oneLine(param.Description)) + } + buf.WriteString("\n") + } + } + if len(op.QueryParams) > 0 || len(op.HeaderParams) > 0 { + buf.WriteString("- Flags:\n") + for _, param := range op.QueryParams { + usage := fmt.Sprintf("--%s <%s>", param.Flag, flagType(param.Type)) + if reservedFlagName(param.Flag) { + usage = fmt.Sprintf("--param %s=<%s>", param.Name, flagType(param.Type)) + } + fmt.Fprintf(buf, " - `%s`", usage) + if param.Description != "" { + fmt.Fprintf(buf, ": %s", oneLine(param.Description)) + } + buf.WriteString("\n") + } + for _, param := range op.HeaderParams { + usage := fmt.Sprintf("--%s <%s>", param.Flag, flagType(param.Type)) + if reservedFlagName(param.Flag) { + usage = fmt.Sprintf("--header %s=<%s>", param.Name, flagType(param.Type)) + } + fmt.Fprintf(buf, " - `%s`", usage) + if param.Description != "" { + fmt.Fprintf(buf, ": %s", oneLine(param.Description)) + } + buf.WriteString("\n") + } + } + if op.Body != nil { + if op.Body.Binary { + buf.WriteString("- Body: `--input ` sends `application/octet-stream`.\n") + } else { + if op.Body.Root != "" { + fmt.Fprintf(buf, "- Body root: `%s`\n", op.Body.Root) + } + if len(op.Body.Fields) > 0 { + buf.WriteString("- Body flags:\n") + for _, field := range op.Body.Fields { + required := "" + if field.Required { + required = " required" + } + usage := fmt.Sprintf("--%s <%s>", field.Flag, flagType(field.Type)) + if reservedFlagName(field.Flag) { + usage = fmt.Sprintf("--field %s=<%s>", field.Name, flagType(field.Type)) + } + fmt.Fprintf(buf, " - `%s`%s", usage, required) + if field.Description != "" { + fmt.Fprintf(buf, ": %s", oneLine(field.Description)) + } + buf.WriteString("\n") + } + } + } + } + buf.WriteString("\n") +} + +func usageArgs(params []openapi.Parameter) string { + if len(params) == 0 { + return "" + } + var args []string + for _, param := range params { + args = append(args, "<"+strings.ToLower(param.Placeholder)+">") + } + return " " + strings.Join(args, " ") +} + +func flagType(typ string) string { + if typ == "" { + return "value" + } + return typ +} + +func oneLine(value string) string { + return strings.Join(strings.Fields(value), " ") +} + +func reservedFlagName(name string) bool { + switch name { + case "api-key", "body", "config", "field", "header", "help", "host", "input", "insecure", "output", "param", "password", "switch-user", "timeout", "username", "version": + return true + default: + return false + } +} diff --git a/cmd/redmine/main.go b/cmd/redmine/main.go new file mode 100644 index 0000000..48789e0 --- /dev/null +++ b/cmd/redmine/main.go @@ -0,0 +1,18 @@ +package main + +import ( + "fmt" + "os" + + "github.com/muxx/redmine-cli/internal/cli" +) + +var version = "dev" + +func main() { + cmd := cli.New(version) + if err := cmd.Execute(); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} diff --git a/docs/usage.md b/docs/usage.md new file mode 100644 index 0000000..922abf3 --- /dev/null +++ b/docs/usage.md @@ -0,0 +1,1391 @@ +# redmine CLI usage + + + +Generated from `Redmine API` `1.7.1+redmine.6.1`. + +## Global usage + +```bash +redmine [command] [subcommand] [flags] +``` + +Set `REDMINE_HOST` and `REDMINE_API_KEY`, or run `redmine auth login --host --api-key `. Login validates credentials with `GET /users/current.json` before saving them. + +Common flags: `--host`, `--api-key`, `--username`, `--password`, `--switch-user`, `--output json|yaml|raw`, `--config`. + +Request body commands accept generated body flags, repeated `--field key=value`, or `--body @file.json`. + +## Authentication + +### `redmine auth login --host --api-key ` + +Checks the credentials with `GET /users/current.json` and saves them to the config file only after a successful response. The API key can also be read from stdin with `--stdin`. + +### `redmine auth status` + +Loads authentication from flags, environment, or config, calls `GET /users/current.json`, and prints the resolved host, auth method, authenticated user, and status. + +### `redmine auth logout` + +Removes the saved config file. + +## Examples + +```bash +redmine auth login --host https://redmine.example.com --api-key "$REDMINE_API_KEY" +redmine auth status +redmine issue list --limit 20 +redmine issue show 123 --include journals +redmine issue create --project-id my-project --subject "Fix checkout" +redmine project list +``` + +## Commands + +### `issue` + +- `redmine issue list` - List issues +- `redmine issue create` - Create issue +- `redmine issue show ` - Show issue +- `redmine issue update ` - Update issue +- `redmine issue delete ` - Delete issue +- `redmine issue add-watcher ` - Add watcher +- `redmine issue remove-watcher ` - Remove watcher +- `redmine issue list-by-project ` - List issues by project +- `redmine issue create-under-project ` - Create issue under project + +### `project` + +- `redmine project list` - List projects +- `redmine project create` - Create project +- `redmine project show ` - Show project +- `redmine project update ` - Update project +- `redmine project delete ` - Delete project +- `redmine project archive ` - Archive project +- `redmine project close ` - Close project +- `redmine project reopen ` - Reopen project +- `redmine project unarchive ` - Unarchive project + +### `project-membership` + +- `redmine project-membership show ` - Show membership +- `redmine project-membership update ` - Update membership +- `redmine project-membership delete ` - Delete membership +- `redmine project-membership list ` - List memberships +- `redmine project-membership create ` - Create membership + +### `user` + +- `redmine user list` - List users +- `redmine user create` - Create user +- `redmine user show-current` - Show current user +- `redmine user show ` - Show user +- `redmine user update ` - Update user +- `redmine user delete ` - Delete user + +### `time-entry` + +- `redmine time-entry create-under-issue ` - Create time entry under issue +- `redmine time-entry list-by-project ` - List time entries by project +- `redmine time-entry create-under-project ` - Create time entry under project +- `redmine time-entry list` - List time entries +- `redmine time-entry create` - Create time entry +- `redmine time-entry show ` - Show time entry +- `redmine time-entry update ` - Update time entry +- `redmine time-entry delete ` - Delete time entry + +### `news` + +- `redmine news list` - List news +- `redmine news show ` - Show news +- `redmine news update ` - Update news +- `redmine news delete ` - Delete news +- `redmine news list-by-project ` - List news by project +- `redmine news create ` - Create news + +### `issue-relation` + +- `redmine issue-relation list ` - List issue relations +- `redmine issue-relation create ` - Create issue relation +- `redmine issue-relation show ` - Show issue relation +- `redmine issue-relation delete ` - Delete issue relation + +### `version` + +- `redmine version list-by-project ` - List versions by project +- `redmine version create ` - Create version +- `redmine version show ` - Show version +- `redmine version update ` - Update version +- `redmine version delete ` - Delete version + +### `wiki-page` + +- `redmine wiki-page list ` - List wiki pages +- `redmine wiki-page show ` - Show wiki page +- `redmine wiki-page create-or-update ` - Create or update wiki page +- `redmine wiki-page delete ` - Delete wiki page +- `redmine wiki-page show-by-specific-version ` - Show wiki page by specific version + +### `query` + +- `redmine query list` - List queries + +### `attachment` + +- `redmine attachment download-file ` - Download attachment file +- `redmine attachment download-thumbnail ` - Download thumbnail +- `redmine attachment show ` - Show attachment +- `redmine attachment update ` - Update attachment +- `redmine attachment delete ` - Delete attachment +- `redmine attachment upload-file` - Upload attachment file + +### `issue-status` + +- `redmine issue-status list` - List issue statuses + +### `tracker` + +- `redmine tracker list` - List trackers + +### `enumeration` + +- `redmine enumeration list-document-categories` - List document categories +- `redmine enumeration list-issue-priorities` - List issue priorities +- `redmine enumeration list-time-entry-activities` - List time entry activities + +### `issue-category` + +- `redmine issue-category show ` - Show issue category +- `redmine issue-category update ` - Update issue category +- `redmine issue-category delete ` - Delete issue category +- `redmine issue-category list ` - List issue categories +- `redmine issue-category create ` - Create issue category + +### `role` + +- `redmine role list` - List roles +- `redmine role show ` - Show role + +### `group` + +- `redmine group list` - List groups +- `redmine group create` - Create group +- `redmine group show ` - Show group +- `redmine group update ` - Update group +- `redmine group delete ` - Delete group +- `redmine group add-user-to ` - Add user to group +- `redmine group remove-user-from ` - Remove user from group + +### `custom-field` + +- `redmine custom-field list` - List custom fields + +### `search` + +- `redmine search search-within-project ` - Search within project +- `redmine search run` - Search + +### `file` + +- `redmine file list ` - List files +- `redmine file create ` - Create file + +### `my-account` + +- `redmine my-account show` - Show my account +- `redmine my-account update` - Update my account + +### `journal` + +- `redmine journal update ` - Update journal + +### `repository` + +- `redmine repository add-related-issue ` - Add related issue +- `redmine repository remove-related-issue ` - Remove related issue + +## Command reference + +### `redmine issue list` + +List issues + +- Operation: `GET /issues.{format}` (`getIssues`) +- Flags: + - `--offset ` + - `--limit ` + - `--nometa ` + - `--sort `: Sort order. Comma-separated list of `field` or `field:desc`. Default direction is ascending. Examples: `id:desc`, `status:desc,id` + - `--include `: Comma-separated list of associated data to include for each issue. Values: `attachments` (file attachments), `relations` (issue relations) + - `--issue-id `: Filter by issue ID. Format: `[operator]value` Operators: `=` (default), `>=`, `<=`, `><` (between, pipe-separated), `*` (any), `!*` (none) Examples: `1`, `>=100`, `><1|100` + - `--project-id `: Filter by project. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal) Special values: `mine` (projects where current user is a member), `bookmarks` (bookmarked projects) Examples: `1|2`, `mine`, `!mine` + - `--subproject-id `: Filter by subproject. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal), `*` (any), `!*` (none) Examples: `1|2`, `*`, `!*` + - `--tracker-id `: Filter by tracker. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal), `ev` (has ever been), `!ev` (has never been), `cf` (changed from) Examples: `1|2`, `!3` + - `--status-id `: Filter by issue status. Format: `[operator]value[|value2|...]` Operators: `o` (open), `=` (default), `!` (not equal), `c` (closed), `*` (any), `ev` (has ever been), `!ev` (has never been), `cf` (changed from) Examples: `o`, `1|2`, `c`, `!3` + - `--priority-id `: Filter by priority. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal), `ev` (has ever been), `!ev` (has never been), `cf` (changed from) Examples: `1|2`, `!3` + - `--assigned-to-id `: Filter by assignee. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal), `*` (any), `!*` (none), `ev` (has ever been), `!ev` (has never been), `cf` (changed from) Special values: `me` (current user) Examples: `1`, `me`, `!*`, `!me` + - `--author-id `: Filter by author. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal) Special values: `me` (current user) Examples: `1`, `me`, `!me` + - `--author-group `: Filter by author's group. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal) Examples: `1|2` + - `--author-role `: Filter by author's role. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal) Examples: `1|2` + - `--member-of-group `: Filter by assignee's group membership. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal), `*` (any), `!*` (none) Examples: `1`, `*` + - `--assigned-to-role `: Filter by assignee's role. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal), `*` (any), `!*` (none) Examples: `1`, `*` + - `--fixed-version-id `: Filter by target version. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal), `*` (any), `!*` (none), `ev` (has ever been), `!ev` (has never been), `cf` (changed from) Examples: `1|2`, `!*` + - `--fixed-version-due-date `: Filter by target version's due date. Format: `[value]` Operators: `=` (on date), `>=` (on or after), `<=` (on or before), `><` (between, pipe-separated), `t` (today), `ld` (yesterday), `nd` (tomorrow), `w` (this week), `lw` (last week), `l2w` (last 2 weeks), `nw` (next week), `m` (this month), `lm` (last month), `nm` (next month), `y` (this year), `t-` (n days ago), `>t-` (less than n days ago), `t+` (in more than n days), `=2024-01-01`, `t`, `>t-7` + - `--fixed-version-status `: Filter by target version's status. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal) Values: `open`, `locked`, `closed` Examples: `open`, `open|locked`, `!closed` + - `--category-id `: Filter by category. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal), `*` (any), `!*` (none), `ev` (has ever been), `!ev` (has never been), `cf` (changed from) Examples: `1|2`, `!*` + - `--parent-id `: Filter by parent issue. Format: `[operator]value` Operators: `=` (exact parent ID), `~` (descendant of, subtree search), `*` (any, i.e. is a subtask), `!*` (none, i.e. is a root issue) Examples: `123`, `~123`, `*`, `!*` + - `--child-id `: Filter by child issue. Format: `[operator]value` Operators: `=` (exact child ID), `~` (ancestor of, subtree search), `*` (any, i.e. has subtasks), `!*` (none, i.e. is a leaf issue) Examples: `123`, `~123`, `*`, `!*` + - `--cf-x `: Filter by custom field value. Replace `x` with the custom field ID (e.g., `cf_1`, `cf_12`). Format and operators depend on the custom field type (string, int, date, list, etc.) and follow the same conventions as other filter parameters. Examples: `cf_1=~keyword`, `cf_5=1|2`, `cf_10=>=2024-01-01` + - `--subject `: Filter by subject. Format: `value` Operators: `~` (contains), `!~` (not contains), `*~` (contains any word), `^` (starts with), `$` (ends with), `*` (any), `!*` (none) Examples: `~keyword`, `^prefix` + - `--description `: Filter by description. Format: `value` Operators: `~` (contains), `!~` (not contains), `*~` (contains any word), `^` (starts with), `$` (ends with), `*` (any), `!*` (none) Examples: `~keyword`, `!~excluded` + - `--notes `: Filter by journal notes. Format: `value` Operators: `~` (contains), `!~` (not contains), `*~` (contains any word), `^` (starts with), `$` (ends with), `*` (any), `!*` (none) Examples: `~keyword` + - `--created-on `: Filter by creation date. Format: `[value]` Operators: `=` (on date), `>=` (on or after), `<=` (on or before), `><` (between, pipe-separated), `t` (today), `ld` (yesterday), `w` (this week), `lw` (last week), `l2w` (last 2 weeks), `m` (this month), `lm` (last month), `y` (this year), `t-` (n days ago), `>t-` (less than n days ago), `=2024-01-01`, `t`, `>t-7`, `lm` + - `--updated-on `: Filter by last updated date. Format: `[value]` Operators: `=` (on date), `>=` (on or after), `<=` (on or before), `><` (between, pipe-separated), `t` (today), `ld` (yesterday), `w` (this week), `lw` (last week), `l2w` (last 2 weeks), `m` (this month), `lm` (last month), `y` (this year), `t-` (n days ago), `>t-` (less than n days ago), `=2024-01-01`, `t`, `>t-7`, `lm` + - `--closed-on `: Filter by closed date. Format: `[value]` Operators: `=` (on date), `>=` (on or after), `<=` (on or before), `><` (between, pipe-separated), `t` (today), `ld` (yesterday), `w` (this week), `lw` (last week), `l2w` (last 2 weeks), `m` (this month), `lm` (last month), `y` (this year), `t-` (n days ago), `>t-` (less than n days ago), `=2024-01-01`, `t`, `>t-7`, `lm` + - `--start-date `: Filter by start date. Format: `[value]` Operators: `=` (on date), `>=` (on or after), `<=` (on or before), `><` (between, pipe-separated), `t` (today), `ld` (yesterday), `nd` (tomorrow), `w` (this week), `lw` (last week), `l2w` (last 2 weeks), `nw` (next week), `m` (this month), `lm` (last month), `nm` (next month), `y` (this year), `t-` (n days ago), `>t-` (less than n days ago), `t+` (in more than n days), `=2024-01-01`, `t`, `>t-7` + - `--due-date `: Filter by due date. Format: `[value]` Operators: `=` (on date), `>=` (on or after), `<=` (on or before), `><` (between, pipe-separated), `t` (today), `ld` (yesterday), `nd` (tomorrow), `w` (this week), `lw` (last week), `l2w` (last 2 weeks), `nw` (next week), `m` (this month), `lm` (last month), `nm` (next month), `y` (this year), `t-` (n days ago), `>t-` (less than n days ago), `t+` (in more than n days), `=2024-01-01`, `t`, `>t-7` + - `--estimated-hours `: Filter by estimated hours. Format: `[operator]value` Operators: `=` (default), `>=`, `<=`, `><` (between, pipe-separated), `*` (any), `!*` (none) Examples: `>=5`, `><1|10`, `!*` + - `--spent-time `: Filter by total spent time. Format: `[operator]value` Operators: `=` (default), `>=`, `<=`, `><` (between, pipe-separated), `*` (any), `!*` (none) Examples: `>=5`, `><1|10`, `!*` + - `--done-ratio `: Filter by done ratio (percentage). Format: `[operator]value` Operators: `=` (default), `>=`, `<=`, `><` (between, pipe-separated), `*` (any), `!*` (none) Examples: `>=50`, `><0|99`, `100` + - `--is-private `: Filter by private flag. Format: `[operator]value` Operators: `=` (default), `!` (not equal) Values: `1` (private), `0` (not private) Examples: `1`, `0` + - `--attachment `: Filter by attachment filename. Format: `value` Operators: `~` (contains), `!~` (not contains), `*~` (contains any word), `^` (starts with), `$` (ends with), `*` (any), `!*` (none) Examples: `~report`, `*.pdf` + - `--attachment-description `: Filter by attachment description. Format: `value` Operators: `~` (contains), `!~` (not contains), `*~` (contains any word), `^` (starts with), `$` (ends with), `*` (any), `!*` (none) Examples: `~screenshot` + - `--watcher-id `: Filter by watcher. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal) Special values: `me` (current user) Examples: `1`, `me`, `!me` + - `--updated-by `: Filter by user who updated the issue. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal) Special values: `me` (current user) Examples: `1`, `me` + - `--last-updated-by `: Filter by user who last updated the issue. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal) Special values: `me` (current user) Examples: `1`, `me` + - `--project-status `: Filter by project status. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal) Values: `1` (active), `5` (closed) Examples: `1`, `!5` + - `--relates `: Filter by "relates to" relation. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal), `=p` (same project), `=!p` (different project), `!p` (not in project), `*o` (related to open issue), `!o` (related to closed issue), `*` (any), `!*` (none) Examples: `123`, `=p`, `*o`, `*` + - `--duplicates `: Filter by "duplicates" relation. Format: `[operator]value[|value2|...]` Operators: `=`, `!`, `=p`, `=!p`, `!p`, `*o`, `!o`, `*`, `!*` Examples: `123`, `*` + - `--duplicated `: Filter by "duplicated by" relation. Format: `[operator]value[|value2|...]` Operators: `=`, `!`, `=p`, `=!p`, `!p`, `*o`, `!o`, `*`, `!*` Examples: `123`, `*` + - `--blocks `: Filter by "blocks" relation. Format: `[operator]value[|value2|...]` Operators: `=`, `!`, `=p`, `=!p`, `!p`, `*o`, `!o`, `*`, `!*` Examples: `123`, `*o` + - `--blocked `: Filter by "blocked by" relation. Format: `[operator]value[|value2|...]` Operators: `=`, `!`, `=p`, `=!p`, `!p`, `*o`, `!o`, `*`, `!*` Examples: `123`, `*o` + - `--precedes `: Filter by "precedes" relation. Format: `[operator]value[|value2|...]` Operators: `=`, `!`, `=p`, `=!p`, `!p`, `*o`, `!o`, `*`, `!*` Examples: `123`, `*` + - `--follows `: Filter by "follows" relation. Format: `[operator]value[|value2|...]` Operators: `=`, `!`, `=p`, `=!p`, `!p`, `*o`, `!o`, `*`, `!*` Examples: `123`, `*` + - `--copied-to `: Filter by "copied to" relation. Format: `[operator]value[|value2|...]` Operators: `=`, `!`, `=p`, `=!p`, `!p`, `*o`, `!o`, `*`, `!*` Examples: `123`, `*` + - `--copied-from `: Filter by "copied from" relation. Format: `[operator]value[|value2|...]` Operators: `=`, `!`, `=p`, `=!p`, `!p`, `*o`, `!o`, `*`, `!*` Examples: `123`, `*` + - `--any-searchable `: Full-text search across all searchable fields. Format: `value` Operators: `~` (contains all words), `*~` (contains any word), `!~` (not contains) Examples: `~keyword`, `*~foo bar` + - `--query-id `: ID of a saved query to apply. When specified, the saved query's filter criteria are used. Other filter parameters in the same request are ignored. Use GET /queries.json to retrieve available saved queries. + - `--x-redmine-nometa ` + +### `redmine issue create` + +Create issue + +- Operation: `POST /issues.{format}` (`createIssue`) +- Body root: `issue` +- Body flags: + - `--assigned-to-id ` + - `--category-id ` + - `--custom-field-values ` + - `--custom-fields ` + - `--description ` + - `--done-ratio ` + - `--due-date ` + - `--estimated-hours ` + - `--fixed-version-id ` + - `--is-private ` + - `--parent-issue-id ` + - `--priority-id ` + - `--project-id ` required + - `--start-date ` + - `--status-id ` + - `--subject ` required + - `--tracker-id ` + - `--uploads ` + - `--watcher-user-ids ` + +### `redmine issue show ` + +Show issue + +- Operation: `GET /issues/{issue_id}.{format}` (`getIssue`) +- Arguments: + - `issue_id` +- Flags: + - `--include `: Comma-separated list of associated data to include in the response. Values: `children` (child issues), `attachments` (file attachments), `relations` (issue relations), `changesets` (associated VCS changesets), `journals` (change history and notes), `watchers` (users watching the issue), `allowed_statuses` (status transitions available for the current user) + +### `redmine issue update ` + +Update issue + +- Operation: `PUT /issues/{issue_id}.{format}` (`updateIssue`) +- Arguments: + - `issue_id` +- Body flags: + - `--issue ` + +### `redmine issue delete ` + +Delete issue + +- Operation: `DELETE /issues/{issue_id}.{format}` (`deleteIssue`) +- Arguments: + - `issue_id` + +### `redmine issue add-watcher ` + +Add watcher + +- Operation: `POST /issues/{issue_id}/watchers.{format}` (`addWatcher`) +- Arguments: + - `issue_id` +- Body flags: + - `--user-id ` required + +### `redmine issue remove-watcher ` + +Remove watcher + +- Operation: `DELETE /issues/{issue_id}/watchers/{user_id}.{format}` (`removeWatcher`) +- Arguments: + - `issue_id` + - `user_id` + +### `redmine issue list-by-project ` + +List issues by project + +- Operation: `GET /projects/{project_id}/issues.{format}` (`getIssuesByProject`) +- Arguments: + - `project_id` +- Flags: + - `--offset ` + - `--limit ` + - `--nometa ` + - `--sort `: Sort order. Comma-separated list of `field` or `field:desc`. Default direction is ascending. Examples: `id:desc`, `status:desc,id` + - `--include `: Comma-separated list of associated data to include for each issue. Values: `attachments` (file attachments), `relations` (issue relations) + - `--issue-id `: Filter by issue ID. Format: `[operator]value` Operators: `=` (default), `>=`, `<=`, `><` (between, pipe-separated), `*` (any), `!*` (none) Examples: `1`, `>=100`, `><1|100` + - `--subproject-id `: Filter by subproject. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal), `*` (any), `!*` (none) Examples: `1|2`, `*`, `!*` + - `--tracker-id `: Filter by tracker. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal), `ev` (has ever been), `!ev` (has never been), `cf` (changed from) Examples: `1|2`, `!3` + - `--status-id `: Filter by issue status. Format: `[operator]value[|value2|...]` Operators: `o` (open), `=` (default), `!` (not equal), `c` (closed), `*` (any), `ev` (has ever been), `!ev` (has never been), `cf` (changed from) Examples: `o`, `1|2`, `c`, `!3` + - `--priority-id `: Filter by priority. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal), `ev` (has ever been), `!ev` (has never been), `cf` (changed from) Examples: `1|2`, `!3` + - `--assigned-to-id `: Filter by assignee. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal), `*` (any), `!*` (none), `ev` (has ever been), `!ev` (has never been), `cf` (changed from) Special values: `me` (current user) Examples: `1`, `me`, `!*`, `!me` + - `--author-id `: Filter by author. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal) Special values: `me` (current user) Examples: `1`, `me`, `!me` + - `--author-group `: Filter by author's group. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal) Examples: `1|2` + - `--author-role `: Filter by author's role. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal) Examples: `1|2` + - `--member-of-group `: Filter by assignee's group membership. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal), `*` (any), `!*` (none) Examples: `1`, `*` + - `--assigned-to-role `: Filter by assignee's role. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal), `*` (any), `!*` (none) Examples: `1`, `*` + - `--fixed-version-id `: Filter by target version. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal), `*` (any), `!*` (none), `ev` (has ever been), `!ev` (has never been), `cf` (changed from) Examples: `1|2`, `!*` + - `--fixed-version-due-date `: Filter by target version's due date. Format: `[value]` Operators: `=` (on date), `>=` (on or after), `<=` (on or before), `><` (between, pipe-separated), `t` (today), `ld` (yesterday), `nd` (tomorrow), `w` (this week), `lw` (last week), `l2w` (last 2 weeks), `nw` (next week), `m` (this month), `lm` (last month), `nm` (next month), `y` (this year), `t-` (n days ago), `>t-` (less than n days ago), `t+` (in more than n days), `=2024-01-01`, `t`, `>t-7` + - `--fixed-version-status `: Filter by target version's status. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal) Values: `open`, `locked`, `closed` Examples: `open`, `open|locked`, `!closed` + - `--category-id `: Filter by category. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal), `*` (any), `!*` (none), `ev` (has ever been), `!ev` (has never been), `cf` (changed from) Examples: `1|2`, `!*` + - `--parent-id `: Filter by parent issue. Format: `[operator]value` Operators: `=` (exact parent ID), `~` (descendant of, subtree search), `*` (any, i.e. is a subtask), `!*` (none, i.e. is a root issue) Examples: `123`, `~123`, `*`, `!*` + - `--child-id `: Filter by child issue. Format: `[operator]value` Operators: `=` (exact child ID), `~` (ancestor of, subtree search), `*` (any, i.e. has subtasks), `!*` (none, i.e. is a leaf issue) Examples: `123`, `~123`, `*`, `!*` + - `--cf-x `: Filter by custom field value. Replace `x` with the custom field ID (e.g., `cf_1`, `cf_12`). Format and operators depend on the custom field type (string, int, date, list, etc.) and follow the same conventions as other filter parameters. Examples: `cf_1=~keyword`, `cf_5=1|2`, `cf_10=>=2024-01-01` + - `--subject `: Filter by subject. Format: `value` Operators: `~` (contains), `!~` (not contains), `*~` (contains any word), `^` (starts with), `$` (ends with), `*` (any), `!*` (none) Examples: `~keyword`, `^prefix` + - `--description `: Filter by description. Format: `value` Operators: `~` (contains), `!~` (not contains), `*~` (contains any word), `^` (starts with), `$` (ends with), `*` (any), `!*` (none) Examples: `~keyword`, `!~excluded` + - `--notes `: Filter by journal notes. Format: `value` Operators: `~` (contains), `!~` (not contains), `*~` (contains any word), `^` (starts with), `$` (ends with), `*` (any), `!*` (none) Examples: `~keyword` + - `--created-on `: Filter by creation date. Format: `[value]` Operators: `=` (on date), `>=` (on or after), `<=` (on or before), `><` (between, pipe-separated), `t` (today), `ld` (yesterday), `w` (this week), `lw` (last week), `l2w` (last 2 weeks), `m` (this month), `lm` (last month), `y` (this year), `t-` (n days ago), `>t-` (less than n days ago), `=2024-01-01`, `t`, `>t-7`, `lm` + - `--updated-on `: Filter by last updated date. Format: `[value]` Operators: `=` (on date), `>=` (on or after), `<=` (on or before), `><` (between, pipe-separated), `t` (today), `ld` (yesterday), `w` (this week), `lw` (last week), `l2w` (last 2 weeks), `m` (this month), `lm` (last month), `y` (this year), `t-` (n days ago), `>t-` (less than n days ago), `=2024-01-01`, `t`, `>t-7`, `lm` + - `--closed-on `: Filter by closed date. Format: `[value]` Operators: `=` (on date), `>=` (on or after), `<=` (on or before), `><` (between, pipe-separated), `t` (today), `ld` (yesterday), `w` (this week), `lw` (last week), `l2w` (last 2 weeks), `m` (this month), `lm` (last month), `y` (this year), `t-` (n days ago), `>t-` (less than n days ago), `=2024-01-01`, `t`, `>t-7`, `lm` + - `--start-date `: Filter by start date. Format: `[value]` Operators: `=` (on date), `>=` (on or after), `<=` (on or before), `><` (between, pipe-separated), `t` (today), `ld` (yesterday), `nd` (tomorrow), `w` (this week), `lw` (last week), `l2w` (last 2 weeks), `nw` (next week), `m` (this month), `lm` (last month), `nm` (next month), `y` (this year), `t-` (n days ago), `>t-` (less than n days ago), `t+` (in more than n days), `=2024-01-01`, `t`, `>t-7` + - `--due-date `: Filter by due date. Format: `[value]` Operators: `=` (on date), `>=` (on or after), `<=` (on or before), `><` (between, pipe-separated), `t` (today), `ld` (yesterday), `nd` (tomorrow), `w` (this week), `lw` (last week), `l2w` (last 2 weeks), `nw` (next week), `m` (this month), `lm` (last month), `nm` (next month), `y` (this year), `t-` (n days ago), `>t-` (less than n days ago), `t+` (in more than n days), `=2024-01-01`, `t`, `>t-7` + - `--estimated-hours `: Filter by estimated hours. Format: `[operator]value` Operators: `=` (default), `>=`, `<=`, `><` (between, pipe-separated), `*` (any), `!*` (none) Examples: `>=5`, `><1|10`, `!*` + - `--spent-time `: Filter by total spent time. Format: `[operator]value` Operators: `=` (default), `>=`, `<=`, `><` (between, pipe-separated), `*` (any), `!*` (none) Examples: `>=5`, `><1|10`, `!*` + - `--done-ratio `: Filter by done ratio (percentage). Format: `[operator]value` Operators: `=` (default), `>=`, `<=`, `><` (between, pipe-separated), `*` (any), `!*` (none) Examples: `>=50`, `><0|99`, `100` + - `--is-private `: Filter by private flag. Format: `[operator]value` Operators: `=` (default), `!` (not equal) Values: `1` (private), `0` (not private) Examples: `1`, `0` + - `--attachment `: Filter by attachment filename. Format: `value` Operators: `~` (contains), `!~` (not contains), `*~` (contains any word), `^` (starts with), `$` (ends with), `*` (any), `!*` (none) Examples: `~report`, `*.pdf` + - `--attachment-description `: Filter by attachment description. Format: `value` Operators: `~` (contains), `!~` (not contains), `*~` (contains any word), `^` (starts with), `$` (ends with), `*` (any), `!*` (none) Examples: `~screenshot` + - `--watcher-id `: Filter by watcher. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal) Special values: `me` (current user) Examples: `1`, `me`, `!me` + - `--updated-by `: Filter by user who updated the issue. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal) Special values: `me` (current user) Examples: `1`, `me` + - `--last-updated-by `: Filter by user who last updated the issue. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal) Special values: `me` (current user) Examples: `1`, `me` + - `--project-status `: Filter by project status. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal) Values: `1` (active), `5` (closed) Examples: `1`, `!5` + - `--relates `: Filter by "relates to" relation. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal), `=p` (same project), `=!p` (different project), `!p` (not in project), `*o` (related to open issue), `!o` (related to closed issue), `*` (any), `!*` (none) Examples: `123`, `=p`, `*o`, `*` + - `--duplicates `: Filter by "duplicates" relation. Format: `[operator]value[|value2|...]` Operators: `=`, `!`, `=p`, `=!p`, `!p`, `*o`, `!o`, `*`, `!*` Examples: `123`, `*` + - `--duplicated `: Filter by "duplicated by" relation. Format: `[operator]value[|value2|...]` Operators: `=`, `!`, `=p`, `=!p`, `!p`, `*o`, `!o`, `*`, `!*` Examples: `123`, `*` + - `--blocks `: Filter by "blocks" relation. Format: `[operator]value[|value2|...]` Operators: `=`, `!`, `=p`, `=!p`, `!p`, `*o`, `!o`, `*`, `!*` Examples: `123`, `*o` + - `--blocked `: Filter by "blocked by" relation. Format: `[operator]value[|value2|...]` Operators: `=`, `!`, `=p`, `=!p`, `!p`, `*o`, `!o`, `*`, `!*` Examples: `123`, `*o` + - `--precedes `: Filter by "precedes" relation. Format: `[operator]value[|value2|...]` Operators: `=`, `!`, `=p`, `=!p`, `!p`, `*o`, `!o`, `*`, `!*` Examples: `123`, `*` + - `--follows `: Filter by "follows" relation. Format: `[operator]value[|value2|...]` Operators: `=`, `!`, `=p`, `=!p`, `!p`, `*o`, `!o`, `*`, `!*` Examples: `123`, `*` + - `--copied-to `: Filter by "copied to" relation. Format: `[operator]value[|value2|...]` Operators: `=`, `!`, `=p`, `=!p`, `!p`, `*o`, `!o`, `*`, `!*` Examples: `123`, `*` + - `--copied-from `: Filter by "copied from" relation. Format: `[operator]value[|value2|...]` Operators: `=`, `!`, `=p`, `=!p`, `!p`, `*o`, `!o`, `*`, `!*` Examples: `123`, `*` + - `--any-searchable `: Full-text search across all searchable fields. Format: `value` Operators: `~` (contains all words), `*~` (contains any word), `!~` (not contains) Examples: `~keyword`, `*~foo bar` + - `--query-id `: ID of a saved query to apply. When specified, the saved query's filter criteria are used. Other filter parameters in the same request are ignored. Use GET /queries.json to retrieve available saved queries. + - `--x-redmine-nometa ` + +### `redmine issue create-under-project ` + +Create issue under project + +- Operation: `POST /projects/{project_id}/issues.{format}` (`createIssueUnderProject`) +- Arguments: + - `project_id` +- Body root: `issue` +- Body flags: + - `--assigned-to-id ` + - `--category-id ` + - `--custom-field-values ` + - `--custom-fields ` + - `--description ` + - `--done-ratio ` + - `--due-date ` + - `--estimated-hours ` + - `--fixed-version-id ` + - `--is-private ` + - `--parent-issue-id ` + - `--priority-id ` + - `--start-date ` + - `--status-id ` + - `--subject ` required + - `--tracker-id ` + - `--uploads ` + - `--watcher-user-ids ` + +### `redmine project list` + +List projects + +- Operation: `GET /projects.{format}` (`getProjects`) +- Flags: + - `--offset ` + - `--limit ` + - `--nometa ` + - `--include `: Comma-separated list of associated data to include for each project. Values: `trackers` (available issue trackers), `issue_categories` (issue categories), `time_entry_activities` (time entry activity types), `enabled_modules` (enabled modules/features), `issue_custom_fields` (custom fields for issues) + - `--status `: Filter by project status. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal) Values: `1` (active), `5` (closed) Examples: `1`, `1|5`, `!1` + - `--id `: Filter by project ID. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal) Special values: `mine` (projects where current user is a member), `bookmarks` (bookmarked projects) Examples: `1|2|3`, `mine`, `!mine`, `mine|bookmarks` + - `--name `: Filter by project name. Format: `value` Operators: `~` (contains), `!~` (not contains), `*~` (contains any word), `^` (starts with), `$` (ends with), `*` (any), `!*` (none) Examples: `~keyword`, `^prefix` + - `--description `: Filter by project description. Format: `value` Operators: `~` (contains), `!~` (not contains), `*~` (contains any word), `^` (starts with), `$` (ends with), `*` (any), `!*` (none) Examples: `~keyword`, `!~excluded` + - `--parent-id `: Filter by parent project. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal), `*` (any, i.e. is a subproject), `!*` (none, i.e. is a root project) Special values: `mine` (projects where current user is a member), `bookmarks` (bookmarked projects) Examples: `1|2`, `mine`, `*`, `!*` + - `--is-public `: Filter by public/private status. Format: `[operator]value` Operators: `=` (default), `!` (not equal) Values: `1` (public), `0` (private) Examples: `1`, `!1` + - `--created-on `: Filter by creation date. Format: `[value]` Operators: `=` (on date), `>=` (on or after), `<=` (on or before), `><` (between, pipe-separated), `t` (today), `ld` (yesterday), `w` (this week), `lw` (last week), `l2w` (last 2 weeks), `m` (this month), `lm` (last month), `y` (this year), `t-` (n days ago), `>t-` (less than n days ago), `=2024-01-01`, `><2024-01-01|2024-12-31`, `t`, `>t-7`, `lm` + - `--updated-on `: Filter by last updated date. Format: `[value]` Operators: `=` (on date), `>=` (on or after), `<=` (on or before), `><` (between, pipe-separated), `t` (today), `ld` (yesterday), `w` (this week), `lw` (last week), `l2w` (last 2 weeks), `m` (this month), `lm` (last month), `y` (this year), `t-` (n days ago), `>t-` (less than n days ago), `=2024-01-01`, `><2024-01-01|2024-12-31`, `t`, `>t-7`, `lm` + - `--x-redmine-nometa ` + +### `redmine project create` + +Create project + +- Operation: `POST /projects.{format}` (`createProject`) +- Body root: `project` +- Body flags: + - `--custom-field-values ` + - `--custom-fields ` + - `--default-assigned-to-id `: Default assignee for new issues in this project. Set to null to clear. + - `--default-issue-query-id `: Default saved query for the project's issue list page. Set to null to clear. + - `--default-version-id `: Default target version for new issues in this project. Set to null to clear. + - `--description ` + - `--enabled-module-names `: Enabled module names for this project. Values: `issue_tracking`, `time_tracking`, `news`, `documents`, `files`, `wiki`, `repository`, `boards`, `calendar`, `gantt` + - `--homepage ` + - `--identifier ` required + - `--inherit-members `: Inherit members from the parent project. Only effective for subprojects (parent_id is set). + - `--is-public ` + - `--issue-custom-field-ids `: IDs of issue custom fields enabled for this project. Custom fields with `is_for_all=true` are always available regardless of this setting. + - `--name ` required + - `--parent-id ` + - `--tracker-ids `: IDs of trackers available in this project. + +### `redmine project show ` + +Show project + +- Operation: `GET /projects/{project_id}.{format}` (`getProject`) +- Arguments: + - `project_id` +- Flags: + - `--include `: Comma-separated list of associated data to include in the response. Values: `trackers` (available issue trackers), `issue_categories` (issue categories), `time_entry_activities` (time entry activity types), `enabled_modules` (enabled modules/features), `issue_custom_fields` (custom fields for issues) + +### `redmine project update ` + +Update project + +- Operation: `PUT /projects/{project_id}.{format}` (`updateProject`) +- Arguments: + - `project_id` +- Body flags: + - `--project ` + +### `redmine project delete ` + +Delete project + +- Operation: `DELETE /projects/{project_id}.{format}` (`deleteProject`) +- Arguments: + - `project_id` + +### `redmine project archive ` + +Archive project + +- Operation: `PUT /projects/{project_id}/archive.{format}` (`archiveProject`) +- Arguments: + - `project_id` + +### `redmine project close ` + +Close project + +Undocumented: This operation is not listed in the official documentation. + +- Operation: `PUT /projects/{project_id}/close.{format}` (`closeProject`) +- Arguments: + - `project_id` + +### `redmine project reopen ` + +Reopen project + +Undocumented: This operation is not listed in the official documentation. + +- Operation: `PUT /projects/{project_id}/reopen.{format}` (`reopenProject`) +- Arguments: + - `project_id` + +### `redmine project unarchive ` + +Unarchive project + +- Operation: `PUT /projects/{project_id}/unarchive.{format}` (`unarchiveProject`) +- Arguments: + - `project_id` + +### `redmine project-membership show ` + +Show membership + +- Operation: `GET /memberships/{membership_id}.{format}` (`getMembership`) +- Arguments: + - `membership_id` + +### `redmine project-membership update ` + +Update membership + +- Operation: `PUT /memberships/{membership_id}.{format}` (`updateMembership`) +- Arguments: + - `membership_id` +- Body flags: + - `--membership ` + +### `redmine project-membership delete ` + +Delete membership + +- Operation: `DELETE /memberships/{membership_id}.{format}` (`deleteMembership`) +- Arguments: + - `membership_id` + +### `redmine project-membership list ` + +List memberships + +- Operation: `GET /projects/{project_id}/memberships.{format}` (`getMemberships`) +- Arguments: + - `project_id` +- Flags: + - `--offset ` + - `--limit ` + - `--nometa ` + - `--x-redmine-nometa ` + +### `redmine project-membership create ` + +Create membership + +- Operation: `POST /projects/{project_id}/memberships.{format}` (`createMembership`) +- Arguments: + - `project_id` +- Body root: `membership` +- Body flags: + - `--role-ids ` required + - `--user-id ` required + +### `redmine user list` + +List users + +- Operation: `GET /users.{format}` (`getUsers`) +- Flags: + - `--offset ` + - `--limit ` + - `--nometa ` + - `--include `: Comma-separated list of associated data to include for each user. Values: `auth_source` (authentication source information) + - `--status `: Filter by user status. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal), `*` (any), `!*` (none) Values: `1` (active), `2` (registered), `3` (locked) Examples: `1`, `1|2`, `!3` + - `--name `: Filter by name, login, or email (contains search). The API always uses the `~` (contains) operator regardless of any operator prefix. Examples: `john`, `admin` + - `--group-id `: Filter by group membership (legacy parameter). Internally converted to `is_member_of_group` filter with `=` operator. Value is the group ID. + - `--auth-source-id `: Filter by authentication source. Undocumented Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal), `*` (any), `!*` (none) Examples: `1`, `!1` + - `--twofa-scheme `: Filter by two-factor authentication scheme. Undocumented Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal), `*` (any), `!*` (none) Only available when two-factor authentication is enabled in Redmine settings. + - `--login `: Filter by login. Undocumented Format: `[operator]value` Operators: `~` (contains, default), `*~` (contains any word), `=` (exact match), `!~` (does not contain), `!` (not equal), `^` (starts with), `$` (ends with), `*` (any), `!*` (none) Examples: `~admin`, `^john` + - `--firstname `: Filter by first name. Undocumented Format: `[operator]value` Operators: `~` (contains, default), `*~` (contains any word), `=` (exact match), `!~` (does not contain), `!` (not equal), `^` (starts with), `$` (ends with), `*` (any), `!*` (none) Examples: `~John`, `=John` + - `--lastname `: Filter by last name. Undocumented Format: `[operator]value` Operators: `~` (contains, default), `*~` (contains any word), `=` (exact match), `!~` (does not contain), `!` (not equal), `^` (starts with), `$` (ends with), `*` (any), `!*` (none) Examples: `~Doe`, `=Doe` + - `--mail `: Filter by email address. Undocumented Format: `[operator]value` Operators: `~` (contains, default), `*~` (contains any word), `=` (exact match), `!~` (does not contain), `!` (not equal), `^` (starts with), `$` (ends with), `*` (any), `!*` (none) Examples: `~example.com`, `$@example.com` + - `--created-on `: Filter by creation date. Undocumented Format: `[operator]value` Operators: `=` (on date), `>=`, `<=`, `><` (between, pipe-separated), `>t-` (more than N days ago), `=2024-01-01`, `>t-30`, `lm` + - `--last-login-on `: Filter by last login date. Undocumented Format: `[operator]value` Operators: `=` (on date), `>=`, `<=`, `><` (between, pipe-separated), `>t-` (more than N days ago), `=2024-01-01`, `>t-30`, `!*` + - `--admin `: Filter by admin privilege. Undocumented Format: `[operator]value` Operators: `=` (default), `!` (not equal) Values: `1` (admin), `0` (non-admin) Examples: `1`, `!1` + - `--x-redmine-nometa ` + +### `redmine user create` + +Create user + +- Operation: `POST /users.{format}` (`createUser`) +- Body flags: + - `--pref ` + - `--send-information `: Set to true to send an account information email to the user. Not stored; only triggers email delivery. + - `--user ` required + +### `redmine user show-current` + +Show current user + +- Operation: `GET /users/current.{format}` (`getCurrentUser`) +- Flags: + - `--include `: Comma-separated list of associated data to include in the response. Values: `memberships` (project memberships with roles), `groups` (groups the user belongs to), `auth_source` (authentication source information) + +### `redmine user show ` + +Show user + +- Operation: `GET /users/{user_id}.{format}` (`getUser`) +- Arguments: + - `user_id` +- Flags: + - `--include `: Comma-separated list of associated data to include in the response. Values: `memberships` (project memberships with roles), `groups` (groups the user belongs to), `auth_source` (authentication source information) + +### `redmine user update ` + +Update user + +- Operation: `PUT /users/{user_id}.{format}` (`updateUser`) +- Arguments: + - `user_id` +- Body flags: + - `--pref ` + - `--send-information `: Set to true to send an account information email to the user. Not stored; only triggers email delivery. + - `--user ` + +### `redmine user delete ` + +Delete user + +- Operation: `DELETE /users/{user_id}.{format}` (`deleteUser`) +- Arguments: + - `user_id` + +### `redmine time-entry create-under-issue ` + +Create time entry under issue + +- Operation: `POST /issues/{issue_id}/time_entries.{format}` (`createTimeEntryUnderIssue`) +- Arguments: + - `issue_id` +- Body root: `time_entry` +- Body flags: + - `--activity-id ` + - `--comments ` + - `--custom-field-values ` + - `--custom-fields ` + - `--hours ` required + - `--project-id ` + - `--spent-on ` + - `--user-id ` + +### `redmine time-entry list-by-project ` + +List time entries by project + +- Operation: `GET /projects/{project_id}/time_entries.{format}` (`getTimeEntriesByProject`) +- Arguments: + - `project_id` +- Flags: + - `--offset ` + - `--limit ` + - `--nometa ` + - `--user-id `: Filter by user. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal), `*` (any), `!*` (none) Special values: `me` (current user) Examples: `1`, `me`, `!*` + - `--spent-on `: Filter by date spent. Format: `[operator]value` Operators: `=` (on date), `>=`, `<=`, `><` (between, pipe-separated), `>t-` (more than N days ago), `=2024-01-01`, `><2024-01-01|2024-12-31`, `>t-7`, `t`, `lw` + - `--from `: Shortcut for `spent_on` filter. When used with `to`, filters as between (`>=from` and `<=to`). When used alone, filters as `>=from`. Format: `YYYY-MM-DD` + - `--to `: Shortcut for `spent_on` filter. When used with `from`, filters as between (`>=from` and `<=to`). When used alone, filters as `<=to`. Format: `YYYY-MM-DD` + - `--subproject-id `: Filter by subproject. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal), `*` (any), `!*` (none) Examples: `1|2`, `*`, `!*` + - `--issue-id `: Filter by issue. Format: `[operator]value` Operators: `=` (exact match), `~` (issue and its subtasks), `*` (any), `!*` (none) Examples: `=1`, `~1`, `*`, `!*` + - `--issue-tracker-id `: Filter by issue's tracker. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal) Examples: `1|2`, `!3` + - `--issue-status-id `: Filter by issue's status. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal) Examples: `1|2`, `!3` + - `--issue-fixed-version-id `: Filter by issue's target version. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal) Examples: `1|2`, `!3` + - `--issue-parent-id `: Filter by issue's parent. Format: `[operator]value` Operators: `=` (exact match), `~` (issue and its subtasks), `*` (any), `!*` (none) Examples: `=1`, `~1`, `*`, `!*` + - `--issue-category-id `: Filter by issue's category. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal), `*` (any), `!*` (none) Examples: `1|2`, `!*` + - `--issue-subject `: Filter by issue's subject. Format: `[operator]value` Operators: `~` (contains, default), `*~` (contains any word), `!~` (does not contain), `^` (starts with), `$` (ends with), `*` (any), `!*` (none) Examples: `~keyword`, `^prefix`, `$suffix` + - `--user-group `: Filter by user's group. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal), `*` (any), `!*` (none) Examples: `1|2`, `*` + - `--user-role `: Filter by user's role. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal), `*` (any), `!*` (none) Examples: `1|2`, `*` + - `--author-id `: Filter by author. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal), `*` (any), `!*` (none) Special values: `me` (current user) Examples: `1`, `me`, `!*` + - `--activity-id `: Filter by activity. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal) Examples: `1|2`, `!3` + - `--project-status `: Filter by project's status. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal) Values: `1` (active), `5` (closed) Examples: `1`, `!5` + - `--comments `: Filter by comments. Format: `[operator]value` Operators: `~` (contains, default), `*~` (contains any word), `!~` (does not contain), `^` (starts with), `$` (ends with), `*` (any), `!*` (none) Examples: `~keyword`, `^prefix` + - `--hours `: Filter by hours. Format: `[operator]value` Operators: `=` (default), `>=`, `<=`, `><` (between, pipe-separated), `*` (any), `!*` (none) Examples: `=8`, `>=4`, `><1|8` + - `--sort `: Sort order. Comma-separated list of `field` or `field:desc`. Default direction is ascending. Examples: `spent_on:desc`, `hours:desc,spent_on` + - `--x-redmine-nometa ` + +### `redmine time-entry create-under-project ` + +Create time entry under project + +- Operation: `POST /projects/{project_id}/time_entries.{format}` (`createTimeEntryUnderProject`) +- Arguments: + - `project_id` +- Body root: `time_entry` +- Body flags: + - `--activity-id ` + - `--comments ` + - `--custom-field-values ` + - `--custom-fields ` + - `--hours ` required + - `--issue-id ` + - `--spent-on ` + - `--user-id ` + +### `redmine time-entry list` + +List time entries + +- Operation: `GET /time_entries.{format}` (`getTimeEntries`) +- Flags: + - `--offset ` + - `--limit ` + - `--nometa ` + - `--user-id `: Filter by user. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal), `*` (any), `!*` (none) Special values: `me` (current user) Examples: `1`, `me`, `!*` + - `--project-id `: Filter by project. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal) Special values: `mine` (projects where current user is a member), `bookmarks` (bookmarked projects) Examples: `1|2`, `mine`, `!mine` + - `--spent-on `: Filter by date spent. Format: `[operator]value` Operators: `=` (on date), `>=`, `<=`, `><` (between, pipe-separated), `>t-` (more than N days ago), `=2024-01-01`, `><2024-01-01|2024-12-31`, `>t-7`, `t`, `lw` + - `--from `: Shortcut for `spent_on` filter. When used with `to`, filters as between (`>=from` and `<=to`). When used alone, filters as `>=from`. Format: `YYYY-MM-DD` + - `--to `: Shortcut for `spent_on` filter. When used with `from`, filters as between (`>=from` and `<=to`). When used alone, filters as `<=to`. Format: `YYYY-MM-DD` + - `--subproject-id `: Filter by subproject. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal), `*` (any), `!*` (none) Examples: `1|2`, `*`, `!*` + - `--issue-id `: Filter by issue. Format: `[operator]value` Operators: `=` (exact match), `~` (issue and its subtasks), `*` (any), `!*` (none) Examples: `=1`, `~1`, `*`, `!*` + - `--issue-tracker-id `: Filter by issue's tracker. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal) Examples: `1|2`, `!3` + - `--issue-status-id `: Filter by issue's status. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal) Examples: `1|2`, `!3` + - `--issue-fixed-version-id `: Filter by issue's target version. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal) Examples: `1|2`, `!3` + - `--issue-parent-id `: Filter by issue's parent. Format: `[operator]value` Operators: `=` (exact match), `~` (issue and its subtasks), `*` (any), `!*` (none) Examples: `=1`, `~1`, `*`, `!*` + - `--issue-category-id `: Filter by issue's category. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal), `*` (any), `!*` (none) Examples: `1|2`, `!*` + - `--issue-subject `: Filter by issue's subject. Format: `[operator]value` Operators: `~` (contains, default), `*~` (contains any word), `!~` (does not contain), `^` (starts with), `$` (ends with), `*` (any), `!*` (none) Examples: `~keyword`, `^prefix`, `$suffix` + - `--user-group `: Filter by user's group. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal), `*` (any), `!*` (none) Examples: `1|2`, `*` + - `--user-role `: Filter by user's role. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal), `*` (any), `!*` (none) Examples: `1|2`, `*` + - `--author-id `: Filter by author. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal), `*` (any), `!*` (none) Special values: `me` (current user) Examples: `1`, `me`, `!*` + - `--activity-id `: Filter by activity. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal) Examples: `1|2`, `!3` + - `--project-status `: Filter by project's status. Format: `[operator]value[|value2|...]` Operators: `=` (default), `!` (not equal) Values: `1` (active), `5` (closed) Examples: `1`, `!5` + - `--comments `: Filter by comments. Format: `[operator]value` Operators: `~` (contains, default), `*~` (contains any word), `!~` (does not contain), `^` (starts with), `$` (ends with), `*` (any), `!*` (none) Examples: `~keyword`, `^prefix` + - `--hours `: Filter by hours. Format: `[operator]value` Operators: `=` (default), `>=`, `<=`, `><` (between, pipe-separated), `*` (any), `!*` (none) Examples: `=8`, `>=4`, `><1|8` + - `--sort `: Sort order. Comma-separated list of `field` or `field:desc`. Default direction is ascending. Examples: `spent_on:desc`, `hours:desc,spent_on` + - `--x-redmine-nometa ` + +### `redmine time-entry create` + +Create time entry + +- Operation: `POST /time_entries.{format}` (`createTimeEntry`) +- Body root: `time_entry` +- Body flags: + - `--activity-id ` + - `--comments ` + - `--custom-field-values ` + - `--custom-fields ` + - `--hours ` required + - `--issue-id ` + - `--project-id ` + - `--spent-on ` + - `--user-id ` + +### `redmine time-entry show ` + +Show time entry + +- Operation: `GET /time_entries/{time_entry_id}.{format}` (`getTimeEntry`) +- Arguments: + - `time_entry_id` + +### `redmine time-entry update ` + +Update time entry + +- Operation: `PUT /time_entries/{time_entry_id}.{format}` (`updateTimeEntry`) +- Arguments: + - `time_entry_id` +- Body flags: + - `--time-entry ` + +### `redmine time-entry delete ` + +Delete time entry + +- Operation: `DELETE /time_entries/{time_entry_id}.{format}` (`deleteTimeEntry`) +- Arguments: + - `time_entry_id` + +### `redmine news list` + +List news + +- Operation: `GET /news.{format}` (`getNewsList`) +- Flags: + - `--offset ` + - `--limit ` + - `--nometa ` + - `--x-redmine-nometa ` + +### `redmine news show ` + +Show news + +Undocumented: This operation is not listed in the official documentation. + +- Operation: `GET /news/{news_id}.{format}` (`getNews`) +- Arguments: + - `news_id` +- Flags: + - `--include `: Comma-separated list of associated data to include in the response. Values: `attachments` (file attachments), `comments` (comments/replies on the news item) + +### `redmine news update ` + +Update news + +Undocumented: This operation is not listed in the official documentation. + +- Operation: `PUT /news/{news_id}.{format}` (`updateNews`) +- Arguments: + - `news_id` +- Body flags: + - `--news ` + +### `redmine news delete ` + +Delete news + +Undocumented: This operation is not listed in the official documentation. + +- Operation: `DELETE /news/{news_id}.{format}` (`deleteNews`) +- Arguments: + - `news_id` + +### `redmine news list-by-project ` + +List news by project + +- Operation: `GET /projects/{project_id}/news.{format}` (`getNewsListByProject`) +- Arguments: + - `project_id` +- Flags: + - `--offset ` + - `--limit ` + - `--nometa ` + - `--x-redmine-nometa ` + +### `redmine news create ` + +Create news + +Undocumented: This operation is not listed in the official documentation. + +- Operation: `POST /projects/{project_id}/news.{format}` (`createNews`) +- Arguments: + - `project_id` +- Body root: `news` +- Body flags: + - `--description ` required + - `--summary ` + - `--title ` required + - `--uploads ` + +### `redmine issue-relation list ` + +List issue relations + +- Operation: `GET /issues/{issue_id}/relations.{format}` (`getIssueRelations`) +- Arguments: + - `issue_id` + +### `redmine issue-relation create ` + +Create issue relation + +- Operation: `POST /issues/{issue_id}/relations.{format}` (`createIssueRelation`) +- Arguments: + - `issue_id` +- Body root: `relation` +- Body flags: + - `--delay `: Number of days of delay for `precedes`/`follows` relations. Ignored for other relation types. + - `--issue-to-id ` required + - `--relation-type ` required: Type of relation from this issue to the target issue. Values: `relates` (related to), `duplicates` (this duplicates target), `duplicated` (this is duplicated by target), `blocks` (this blocks target), `blocked` (this is blocked by target), `precedes` (this must finish before target starts), `follows` (this must start after target finishes), `copied_to` (this was copied to target), `copied_from` (this was copied from target) + +### `redmine issue-relation show ` + +Show issue relation + +- Operation: `GET /relations/{issue_relation_id}.{format}` (`getIssueRelation`) +- Arguments: + - `issue_relation_id` + +### `redmine issue-relation delete ` + +Delete issue relation + +- Operation: `DELETE /relations/{issue_relation_id}.{format}` (`deleteIssueRelation`) +- Arguments: + - `issue_relation_id` + +### `redmine version list-by-project ` + +List versions by project + +- Operation: `GET /projects/{project_id}/versions.{format}` (`getVersionsByProject`) +- Arguments: + - `project_id` +- Flags: + - `--nometa ` + - `--x-redmine-nometa ` + +### `redmine version create ` + +Create version + +- Operation: `POST /projects/{project_id}/versions.{format}` (`createVersion`) +- Arguments: + - `project_id` +- Body root: `version` +- Body flags: + - `--custom-field-values ` + - `--custom-fields ` + - `--default-project-version `: Set to true to make this the default target version for new issues in the project. + - `--description ` + - `--due-date ` + - `--name ` required + - `--sharing `: Version sharing scope. Values: `none` (not shared), `descendants` (with subprojects), `hierarchy` (with ancestors and descendants), `tree` (with entire project tree), `system` (with all projects) + - `--status ` + - `--wiki-page-title `: Title of a wiki page to associate with this version. Set to null to clear. + +### `redmine version show ` + +Show version + +- Operation: `GET /versions/{version_id}.{format}` (`getVersion`) +- Arguments: + - `version_id` + +### `redmine version update ` + +Update version + +- Operation: `PUT /versions/{version_id}.{format}` (`updateVersion`) +- Arguments: + - `version_id` +- Body flags: + - `--field version=` + +### `redmine version delete ` + +Delete version + +- Operation: `DELETE /versions/{version_id}.{format}` (`deleteVersion`) +- Arguments: + - `version_id` + +### `redmine wiki-page list ` + +List wiki pages + +- Operation: `GET /projects/{project_id}/wiki/index.{format}` (`getWikiPages`) +- Arguments: + - `project_id` + +### `redmine wiki-page show ` + +Show wiki page + +- Operation: `GET /projects/{project_id}/wiki/{wiki_page_title}.{format}` (`getWikiPage`) +- Arguments: + - `project_id` + - `wiki_page_title` +- Flags: + - `--include `: Comma-separated list of associated data to include in the response. Values: `attachments` (file attachments) + +### `redmine wiki-page create-or-update ` + +Create or update wiki page + +- Operation: `PUT /projects/{project_id}/wiki/{wiki_page_title}.{format}` (`updateWikiPage`) +- Arguments: + - `project_id` + - `wiki_page_title` +- Body flags: + - `--wiki-page ` + +### `redmine wiki-page delete ` + +Delete wiki page + +- Operation: `DELETE /projects/{project_id}/wiki/{wiki_page_title}.{format}` (`deleteWikiPage`) +- Arguments: + - `project_id` + - `wiki_page_title` + +### `redmine wiki-page show-by-specific-version ` + +Show wiki page by specific version + +- Operation: `GET /projects/{project_id}/wiki/{wiki_page_title}/{version_id}.{format}` (`getWikiPageByVersion`) +- Arguments: + - `project_id` + - `wiki_page_title` + - `version_id` +- Flags: + - `--include `: Comma-separated list of associated data to include in the response. Values: `attachments` (file attachments) + +### `redmine query list` + +List queries + +- Operation: `GET /queries.{format}` (`getQueries`) +- Flags: + - `--offset ` + - `--limit ` + - `--nometa ` + - `--x-redmine-nometa ` + +### `redmine attachment download-file ` + +Download attachment file + +Undocumented: This operation is not listed in the official documentation. + +- Operation: `GET /attachments/download/{attachment_id}/{filename}` (`downloadAttachmentFile`) +- Arguments: + - `attachment_id` + - `filename` + +### `redmine attachment download-thumbnail ` + +Download thumbnail + +Undocumented: This operation is not listed in the official documentation. + +- Operation: `GET /attachments/thumbnail/{attachment_id}` (`downloadThumbnail`) +- Arguments: + - `attachment_id` +- Flags: + - `--size `: Desired thumbnail size in pixels (max 800). Rounded up to the nearest 50px increment. If not specified, the configured default thumbnail size is used. + +### `redmine attachment show ` + +Show attachment + +- Operation: `GET /attachments/{attachment_id}.{format}` (`getAttachment`) +- Arguments: + - `attachment_id` + +### `redmine attachment update ` + +Update attachment + +- Operation: `PATCH /attachments/{attachment_id}.{format}` (`updateAttachment`) +- Arguments: + - `attachment_id` +- Body flags: + - `--attachment ` + +### `redmine attachment delete ` + +Delete attachment + +- Operation: `DELETE /attachments/{attachment_id}.{format}` (`deleteAttachment`) +- Arguments: + - `attachment_id` + +### `redmine attachment upload-file` + +Upload attachment file + +- Operation: `POST /uploads.{format}` (`uploadAttachmentFile`) +- Flags: + - `--filename `: Filename for the uploaded file. If not provided, a random hexadecimal name is generated. + - `--content-type `: MIME type of the file (e.g., `application/pdf`, `image/png`). If not provided, auto-detected from the filename. +- Body: `--input ` sends `application/octet-stream`. + +### `redmine issue-status list` + +List issue statuses + +- Operation: `GET /issue_statuses.{format}` (`getIssueStatuses`) + +### `redmine tracker list` + +List trackers + +- Operation: `GET /trackers.{format}` (`getTrackers`) + +### `redmine enumeration list-document-categories` + +List document categories + +- Operation: `GET /enumerations/document_categories.{format}` (`getDocumentCategories`) + +### `redmine enumeration list-issue-priorities` + +List issue priorities + +- Operation: `GET /enumerations/issue_priorities.{format}` (`getIssuePriorities`) + +### `redmine enumeration list-time-entry-activities` + +List time entry activities + +- Operation: `GET /enumerations/time_entry_activities.{format}` (`getTimeEntryActivities`) + +### `redmine issue-category show ` + +Show issue category + +- Operation: `GET /issue_categories/{issue_category_id}.{format}` (`getIssueCategory`) +- Arguments: + - `issue_category_id` + +### `redmine issue-category update ` + +Update issue category + +- Operation: `PUT /issue_categories/{issue_category_id}.{format}` (`updateIssueCategory`) +- Arguments: + - `issue_category_id` +- Body flags: + - `--issue-category ` + +### `redmine issue-category delete ` + +Delete issue category + +- Operation: `DELETE /issue_categories/{issue_category_id}.{format}` (`deleteIssueCategory`) +- Arguments: + - `issue_category_id` +- Flags: + - `--reassign-to-id `: ID of another issue category to reassign issues to before deleting this category. If the category has associated issues and this parameter is not provided, the issues will have their category unset. + +### `redmine issue-category list ` + +List issue categories + +- Operation: `GET /projects/{project_id}/issue_categories.{format}` (`getIssueCategories`) +- Arguments: + - `project_id` +- Flags: + - `--nometa ` + - `--x-redmine-nometa ` + +### `redmine issue-category create ` + +Create issue category + +- Operation: `POST /projects/{project_id}/issue_categories.{format}` (`createIssueCategory`) +- Arguments: + - `project_id` +- Body root: `issue_category` +- Body flags: + - `--assigned-to-id ` + - `--name ` required + +### `redmine role list` + +List roles + +- Operation: `GET /roles.{format}` (`getRoles`) + +### `redmine role show ` + +Show role + +- Operation: `GET /roles/{role_id}.{format}` (`getRole`) +- Arguments: + - `role_id` + +### `redmine group list` + +List groups + +- Operation: `GET /groups.{format}` (`getGroups`) +- Flags: + - `--builtin `: Include built-in groups (e.g., "Anonymous", "Non member") in the response. Set to `1` to include built-in groups. Without this parameter, only user-created groups are returned. + +### `redmine group create` + +Create group + +- Operation: `POST /groups.{format}` (`createGroup`) +- Body root: `group` +- Body flags: + - `--custom-field-values ` + - `--custom-fields ` + - `--name ` + - `--twofa-required `: Require two-factor authentication for all members of this group. + - `--user-ids `: User IDs to add as members of this group. + +### `redmine group show ` + +Show group + +- Operation: `GET /groups/{group_id}.{format}` (`getGroup`) +- Arguments: + - `group_id` +- Flags: + - `--include `: Comma-separated list of associated data to include in the response. Values: `users` (list of users in the group), `memberships` (project memberships with roles) + +### `redmine group update ` + +Update group + +- Operation: `PUT /groups/{group_id}.{format}` (`updateGroup`) +- Arguments: + - `group_id` +- Body flags: + - `--group ` + +### `redmine group delete ` + +Delete group + +- Operation: `DELETE /groups/{group_id}.{format}` (`deleteGroup`) +- Arguments: + - `group_id` + +### `redmine group add-user-to ` + +Add user to group + +- Operation: `POST /groups/{group_id}/users.{format}` (`addUserToGroup`) +- Arguments: + - `group_id` +- Body flags: + - `--user-id ` required + +### `redmine group remove-user-from ` + +Remove user from group + +- Operation: `DELETE /groups/{group_id}/users/{user_id}.{format}` (`removeUserFromGroup`) +- Arguments: + - `group_id` + - `user_id` + +### `redmine custom-field list` + +List custom fields + +- Operation: `GET /custom_fields.{format}` (`getCustomFields`) + +### `redmine search search-within-project ` + +Search within project + +- Operation: `GET /projects/{project_id}/search.{format}` (`getSearchByProject`) +- Arguments: + - `project_id` +- Flags: + - `--limit ` + - `--offset ` + - `--nometa ` + - `--q `: Search query string. Tokenized into words for matching. When `all_words` is enabled (default), all tokens must match. Otherwise, any token can match. + - `--scope `: Search scope. Values: `subprojects` (include subprojects of the specified project) When not specified, only the specified project is searched. + - `--all-words `: Require all words to match. Default is `true` (all words must match). Set to empty string to disable (any word matches). Any non-empty value enables this option. + - `--titles-only `: Search titles only. Default is `false`. Any non-empty value enables this option. + - `--issues `: Include issues in search results. Any non-empty value enables this search type. If no search type parameters are specified, all types are searched. + - `--news `: Include news in search results. Any non-empty value enables this search type. If no search type parameters are specified, all types are searched. + - `--wiki-pages `: Include wiki pages in search results. Any non-empty value enables this search type. If no search type parameters are specified, all types are searched. + - `--documents `: Include documents in search results. Any non-empty value enables this search type. If no search type parameters are specified, all types are searched. + - `--changesets `: Include changesets in search results. Any non-empty value enables this search type. If no search type parameters are specified, all types are searched. + - `--messages `: Include forum messages in search results. Any non-empty value enables this search type. If no search type parameters are specified, all types are searched. + - `--open-issues `: Only return open issues. Default is `false`. Any non-empty value enables this option. + - `--attachments `: Search attachments. Default is `0` (do not search attachments). Values: `0` (disabled), `1` (search attachments and content), `only` (search attachments only) + - `--x-redmine-nometa ` + +### `redmine search run` + +Search + +- Operation: `GET /search.{format}` (`getSearch`) +- Flags: + - `--limit ` + - `--offset ` + - `--nometa ` + - `--q `: Search query string. Tokenized into words for matching. When `all_words` is enabled (default), all tokens must match. Otherwise, any token can match. + - `--scope `: Search scope. Values: `all` (all projects), `my_projects` (projects where current user is a member), `bookmarks` (bookmarked projects), `subprojects` (include subprojects, only effective for project-scoped search) + - `--all-words `: Require all words to match. Default is `true` (all words must match). Set to empty string to disable (any word matches). Any non-empty value enables this option. + - `--titles-only `: Search titles only. Default is `false`. Any non-empty value enables this option. + - `--issues `: Include issues in search results. Any non-empty value enables this search type. If no search type parameters are specified, all types are searched. + - `--news `: Include news in search results. Any non-empty value enables this search type. If no search type parameters are specified, all types are searched. + - `--wiki-pages `: Include wiki pages in search results. Any non-empty value enables this search type. If no search type parameters are specified, all types are searched. + - `--projects `: Include projects in search results. Any non-empty value enables this search type. If no search type parameters are specified, all types are searched. Not available when searching within a specific project. + - `--documents `: Include documents in search results. Any non-empty value enables this search type. If no search type parameters are specified, all types are searched. + - `--changesets `: Include changesets in search results. Any non-empty value enables this search type. If no search type parameters are specified, all types are searched. + - `--messages `: Include forum messages in search results. Any non-empty value enables this search type. If no search type parameters are specified, all types are searched. + - `--open-issues `: Only return open issues. Default is `false`. Any non-empty value enables this option. + - `--attachments `: Search attachments. Default is `0` (do not search attachments). Values: `0` (disabled), `1` (search attachments and content), `only` (search attachments only) + - `--x-redmine-nometa ` + +### `redmine file list ` + +List files + +- Operation: `GET /projects/{project_id}/files.{format}` (`getFiles`) +- Arguments: + - `project_id` + +### `redmine file create ` + +Create file + +- Operation: `POST /projects/{project_id}/files.{format}` (`createFile`) +- Arguments: + - `project_id` +- Body root: `file` +- Body flags: + - `--content-type ` + - `--description ` + - `--filename ` + - `--token ` required + - `--version-id ` + +### `redmine my-account show` + +Show my account + +- Operation: `GET /my/account.{format}` (`getMyAccount`) + +### `redmine my-account update` + +Update my account + +- Operation: `PUT /my/account.{format}` (`updateMyAccount`) +- Body flags: + - `--pref ` + - `--user ` + +### `redmine journal update ` + +Update journal + +Undocumented: This operation is not listed in the official documentation. + +- Operation: `PUT /journals/{journal_id}.{format}` (`updateJournal`) +- Arguments: + - `journal_id` +- Body flags: + - `--journal ` + +### `redmine repository add-related-issue ` + +Add related issue + +Undocumented: This operation is not listed in the official documentation. + +- Operation: `POST /projects/{project_id}/repository/{repository_id}/revisions/{revision}/issues.{format}` (`addRelatedIssue`) +- Arguments: + - `project_id` + - `repository_id` + - `revision` +- Body flags: + - `--issue-id ` required + +### `redmine repository remove-related-issue ` + +Remove related issue + +Undocumented: This operation is not listed in the official documentation. + +- Operation: `DELETE /projects/{project_id}/repository/{repository_id}/revisions/{revision}/issues/{issue_id}.{format}` (`removeRelatedIssue`) +- Arguments: + - `project_id` + - `repository_id` + - `revision` + - `issue_id` + diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..dac51ae --- /dev/null +++ b/go.mod @@ -0,0 +1,13 @@ +module github.com/muxx/redmine-cli + +go 1.26 + +require ( + github.com/spf13/cobra v1.10.2 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/pflag v1.0.9 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..47edb24 --- /dev/null +++ b/go.sum @@ -0,0 +1,13 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/cli/auth.go b/internal/cli/auth.go new file mode 100644 index 0000000..0fe2a33 --- /dev/null +++ b/internal/cli/auth.go @@ -0,0 +1,208 @@ +package cli + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + + "github.com/muxx/redmine-cli/internal/config" + "github.com/muxx/redmine-cli/internal/openapi" + "github.com/muxx/redmine-cli/internal/redmine" + "github.com/spf13/cobra" +) + +func addAuthCommands(root *cobra.Command, opts *rootOptions) { + auth := &cobra.Command{ + Use: "auth", + Short: "Manage authentication", + } + root.AddCommand(auth) + + var login loginOptions + loginCmd := &cobra.Command{ + Use: "login", + Short: "Verify and save Redmine authentication", + RunE: func(cmd *cobra.Command, args []string) error { + return runLogin(cmd, opts, login) + }, + } + loginFlags := loginCmd.Flags() + loginFlags.StringVar(&login.host, "host", "", "Redmine base URL") + loginFlags.StringVar(&login.apiKey, "api-key", "", "Redmine API key") + loginFlags.StringVar(&login.username, "username", "", "Basic auth username") + loginFlags.StringVar(&login.password, "password", "", "Basic auth password") + loginFlags.BoolVar(&login.stdin, "stdin", false, "Read API key from stdin") + auth.AddCommand(loginCmd) + + auth.AddCommand(&cobra.Command{ + Use: "status", + Short: "Check configured authentication", + RunE: func(cmd *cobra.Command, args []string) error { + return runStatus(cmd, opts) + }, + }) + + auth.AddCommand(&cobra.Command{ + Use: "logout", + Short: "Remove saved authentication", + RunE: func(cmd *cobra.Command, args []string) error { + if err := config.Remove(opts.configPath); err != nil { + return err + } + _, err := fmt.Fprintln(opts.out, "Logged out") + return err + }, + }) +} + +type loginOptions struct { + host string + apiKey string + username string + password string + stdin bool +} + +func runLogin(cmd *cobra.Command, opts *rootOptions, login loginOptions) error { + host := firstNonEmpty(login.host, opts.host) + if host == "" { + return fmt.Errorf("--host is required") + } + apiKey := firstNonEmpty(login.apiKey, opts.apiKey) + if login.stdin { + data, err := io.ReadAll(cmd.InOrStdin()) + if err != nil { + return err + } + apiKey = strings.TrimSpace(string(data)) + } + username := firstNonEmpty(login.username, opts.username) + password := firstNonEmpty(login.password, opts.password) + if apiKey == "" && username == "" && password == "" { + return fmt.Errorf("provide --api-key or --username/--password") + } + + cfg := config.Config{ + Host: host, + APIKey: apiKey, + Username: username, + Password: password, + } + result, err := checkAuth(cmd, opts, resolvedFromConfig(cfg)) + if err != nil { + return err + } + if err := config.Save(opts.configPath, cfg); err != nil { + return err + } + _, err = fmt.Fprintf(opts.out, "Logged in to %s as %s\n", host, result.User) + return err +} + +func runStatus(cmd *cobra.Command, opts *rootOptions) error { + path := opts.configPath + if path == "" { + defaultPath, err := config.DefaultPath() + if err != nil { + return err + } + path = defaultPath + } + resolvedCfg, err := resolvedConfig(opts) + if err != nil { + return err + } + if resolvedCfg.Host == "" { + _, err := fmt.Fprintf(opts.out, "No Redmine authentication configured at %s\n", path) + return err + } + result, err := checkAuth(cmd, opts, resolvedCfg) + if err != nil { + return err + } + method := "none" + switch { + case resolvedCfg.APIKey != "": + method = "api-key" + case resolvedCfg.Username != "" || resolvedCfg.Password != "": + method = "basic" + } + _, err = fmt.Fprintf(opts.out, "Host: %s\nAuth: %s\nUser: %s\nStatus: ok\nConfig: %s\n", resolvedCfg.Host, method, result.User, path) + return err +} + +type authCheckResult struct { + User string +} + +func checkAuth(cmd *cobra.Command, opts *rootOptions, cfg resolved) (authCheckResult, error) { + if cfg.Host == "" { + return authCheckResult{}, fmt.Errorf("redmine host is not configured") + } + if cfg.APIKey == "" && cfg.Username == "" && cfg.Password == "" { + return authCheckResult{}, fmt.Errorf("redmine authentication is not configured") + } + + httpClient := opts.httpClient + if httpClient == nil { + httpClient = redmine.NewHTTPClient(opts.timeout, opts.insecure) + } + client := redmine.Client{ + BaseURL: cfg.Host, + APIKey: cfg.APIKey, + Username: cfg.Username, + Password: cfg.Password, + SwitchUser: cfg.SwitchUser, + HTTPClient: httpClient, + } + resp, err := client.Do(cmd.Context(), redmine.Request{ + Operation: currentUserOperation(), + }) + if err != nil { + return authCheckResult{}, fmt.Errorf("authentication check failed: %w", err) + } + return authCheckResult{User: currentUserLabel(resp.Body)}, nil +} + +func currentUserOperation() openapi.Operation { + for _, op := range openapi.Operations { + if op.ID == "getCurrentUser" { + return op + } + } + return openapi.Operation{ + ID: "getCurrentUser", + Method: http.MethodGet, + Path: "/users/current.{format}", + } +} + +func currentUserLabel(data []byte) string { + var payload struct { + User struct { + ID int `json:"id"` + Login string `json:"login"` + Firstname string `json:"firstname"` + Lastname string `json:"lastname"` + Mail string `json:"mail"` + } `json:"user"` + } + if err := json.Unmarshal(data, &payload); err != nil { + return "authenticated user" + } + user := payload.User + switch { + case user.Login != "": + return user.Login + case user.Firstname != "" || user.Lastname != "": + return strings.TrimSpace(user.Firstname + " " + user.Lastname) + case user.Mail != "": + return user.Mail + case user.ID > 0: + return fmt.Sprintf("user #%d", user.ID) + default: + return "authenticated user" + } +} diff --git a/internal/cli/root.go b/internal/cli/root.go new file mode 100644 index 0000000..5d7d9d9 --- /dev/null +++ b/internal/cli/root.go @@ -0,0 +1,514 @@ +package cli + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "strconv" + "strings" + "time" + + "github.com/muxx/redmine-cli/internal/config" + "github.com/muxx/redmine-cli/internal/openapi" + "github.com/muxx/redmine-cli/internal/redmine" + "github.com/spf13/cobra" +) + +// New returns the root CLI command. +func New(version string) *cobra.Command { + return NewWithIO(version, os.Stdin, os.Stdout, os.Stderr, nil) +} + +// NewWithIO returns the root CLI command with injectable streams and HTTP client. +func NewWithIO(version string, in io.Reader, out, errOut io.Writer, httpClient *http.Client) *cobra.Command { + opts := &rootOptions{ + version: version, + in: in, + out: out, + errOut: errOut, + httpClient: httpClient, + output: redmine.OutputJSON, + timeout: 30 * time.Second, + } + + root := &cobra.Command{ + Use: "redmine", + Short: "Work with Redmine from the command line", + Long: "Work with Redmine from the command line using commands generated from the Redmine OpenAPI specification.", + Version: version, + SilenceUsage: true, + SilenceErrors: true, + } + root.SetIn(in) + root.SetOut(out) + root.SetErr(errOut) + + flags := root.PersistentFlags() + flags.StringVar(&opts.configPath, "config", "", "Config file path") + flags.StringVar(&opts.host, "host", "", "Redmine base URL") + flags.StringVar(&opts.apiKey, "api-key", "", "Redmine API key") + flags.StringVar(&opts.username, "username", "", "Basic auth username") + flags.StringVar(&opts.password, "password", "", "Basic auth password") + flags.StringVar(&opts.switchUser, "switch-user", "", "Send X-Redmine-Switch-User") + flags.StringVarP(&opts.output, "output", "o", redmine.OutputJSON, "Output format: json, yaml, raw") + flags.DurationVar(&opts.timeout, "timeout", 30*time.Second, "HTTP timeout") + flags.BoolVar(&opts.insecure, "insecure", false, "Skip TLS certificate verification") + + addAuthCommands(root, opts) + addGeneratedCommands(root, opts, openapi.Operations) + return root +} + +type rootOptions struct { + version string + in io.Reader + out io.Writer + errOut io.Writer + httpClient *http.Client + + configPath string + host string + apiKey string + username string + password string + switchUser string + output string + timeout time.Duration + insecure bool +} + +func addGeneratedCommands(root *cobra.Command, opts *rootOptions, operations []openapi.Operation) { + groups := map[string]*cobra.Command{} + for _, op := range operations { + group := groups[op.Group] + if group == nil { + group = &cobra.Command{ + Use: op.Group, + Aliases: op.GroupAlias, + Short: "Manage " + strings.ReplaceAll(op.Group, "-", " "), + } + root.AddCommand(group) + groups[op.Group] = group + } + group.AddCommand(operationCommand(opts, op)) + } +} + +func operationCommand(opts *rootOptions, op openapi.Operation) *cobra.Command { + flags := &operationFlags{ + query: map[string]*parameterValues{}, + headers: map[string]*parameterValues{}, + body: map[string]*bodyValues{}, + } + + cmd := &cobra.Command{ + Use: op.Command + usageArgs(op.PathParams), + Short: op.Summary, + Long: operationLong(op), + Args: cobra.ExactArgs(len(op.PathParams)), + RunE: func(cmd *cobra.Command, args []string) error { + return runOperation(cmd, opts, op, flags, args) + }, + } + + cmdFlags := cmd.Flags() + cmdFlags.StringArrayVar(&flags.extraQuery, "param", nil, "Additional query parameter as key=value") + cmdFlags.StringArrayVar(&flags.extraHeaders, "header", nil, "Additional HTTP header as Name=value") + for _, param := range op.QueryParams { + if reservedFlag(param.Flag) { + continue + } + values := ¶meterValues{param: param} + flags.query[param.Name] = values + cmdFlags.StringArrayVar(&values.values, param.Flag, nil, flagDescription(param.Description, param.Type, param.Required, param.Enum)) + } + for _, param := range op.HeaderParams { + values := ¶meterValues{param: param} + flags.headers[param.Name] = values + if cmdFlags.Lookup(param.Flag) != nil || reservedFlag(param.Flag) { + continue + } + cmdFlags.StringArrayVar(&values.values, param.Flag, nil, flagDescription(param.Description, param.Type, param.Required, param.Enum)) + } + if op.Body != nil { + if op.Body.Binary { + cmdFlags.StringVar(&flags.input, "input", "", "File to send as request body; use - for stdin") + } else { + cmdFlags.StringVar(&flags.rawBody, "body", "", "Raw JSON request body or @file") + cmdFlags.StringArrayVar(&flags.extraFields, "field", nil, "Additional body field as key=value") + for _, field := range op.Body.Fields { + if cmdFlags.Lookup(field.Flag) != nil || reservedFlag(field.Flag) { + continue + } + values := &bodyValues{field: field} + flags.body[field.Name] = values + cmdFlags.StringArrayVar(&values.values, field.Flag, nil, flagDescription(field.Description, field.Type, field.Required, field.Enum)) + } + } + } + + return cmd +} + +func reservedFlag(name string) bool { + switch name { + case "api-key", "body", "config", "field", "header", "help", "host", "input", "insecure", "output", "param", "password", "switch-user", "timeout", "username", "version": + return true + default: + return false + } +} + +type operationFlags struct { + query map[string]*parameterValues + headers map[string]*parameterValues + body map[string]*bodyValues + extraQuery []string + extraHeaders []string + extraFields []string + rawBody string + input string +} + +type parameterValues struct { + param openapi.Parameter + values []string +} + +type bodyValues struct { + field openapi.BodyField + values []string +} + +func runOperation(cmd *cobra.Command, opts *rootOptions, op openapi.Operation, flags *operationFlags, args []string) error { + cfg, err := resolvedConfig(opts) + if err != nil { + return err + } + + path := map[string]string{} + for i, param := range op.PathParams { + path[param.Name] = args[i] + } + query, err := collectParameters(flags.query, flags.extraQuery) + if err != nil { + return err + } + headers, err := collectHeaders(flags.headers, flags.extraHeaders) + if err != nil { + return err + } + body, err := buildBody(cmd, op, flags) + if err != nil { + return err + } + + httpClient := opts.httpClient + if httpClient == nil { + httpClient = redmine.NewHTTPClient(opts.timeout, opts.insecure) + } + client := redmine.Client{ + BaseURL: cfg.Host, + APIKey: cfg.APIKey, + Username: cfg.Username, + Password: cfg.Password, + SwitchUser: cfg.SwitchUser, + HTTPClient: httpClient, + } + resp, err := client.Do(cmd.Context(), redmine.Request{ + Operation: op, + Path: path, + Query: query, + Headers: headers, + Body: body, + }) + if err != nil { + return err + } + return redmine.WriteResponse(opts.out, resp, opts.output) +} + +type resolved struct { + Host string + APIKey string + Username string + Password string + SwitchUser string +} + +func resolvedFromConfig(cfg config.Config) resolved { + return resolved{ + Host: cfg.Host, + APIKey: cfg.APIKey, + Username: cfg.Username, + Password: cfg.Password, + } +} + +func resolvedConfig(opts *rootOptions) (resolved, error) { + fileCfg, err := config.Load(opts.configPath) + if err != nil { + return resolved{}, err + } + return resolved{ + Host: firstNonEmpty(opts.host, os.Getenv("REDMINE_HOST"), fileCfg.Host), + APIKey: firstNonEmpty(opts.apiKey, os.Getenv("REDMINE_API_KEY"), fileCfg.APIKey), + Username: firstNonEmpty(opts.username, os.Getenv("REDMINE_USERNAME"), fileCfg.Username), + Password: firstNonEmpty(opts.password, os.Getenv("REDMINE_PASSWORD"), fileCfg.Password), + SwitchUser: firstNonEmpty(opts.switchUser, os.Getenv("REDMINE_SWITCH_USER")), + }, nil +} + +func collectParameters(generated map[string]*parameterValues, extra []string) (map[string][]string, error) { + result := map[string][]string{} + for _, item := range generated { + if len(item.values) == 0 { + continue + } + result[item.param.Name] = append(result[item.param.Name], item.values...) + } + for _, value := range extra { + key, val, err := splitPair(value) + if err != nil { + return nil, err + } + result[key] = append(result[key], val) + } + return result, nil +} + +func collectHeaders(generated map[string]*parameterValues, extra []string) (map[string]string, error) { + result := map[string]string{} + for _, item := range generated { + if len(item.values) == 0 { + continue + } + result[item.param.Name] = item.values[len(item.values)-1] + } + for _, value := range extra { + key, val, err := splitPair(value) + if err != nil { + return nil, err + } + result[key] = val + } + return result, nil +} + +func buildBody(cmd *cobra.Command, op openapi.Operation, flags *operationFlags) ([]byte, error) { + if op.Body == nil { + if flags.rawBody != "" || len(flags.extraFields) > 0 || flags.input != "" { + return nil, fmt.Errorf("%s does not accept a request body", op.ID) + } + return nil, nil + } + if op.Body.Binary { + if flags.input == "" { + return nil, fmt.Errorf("%s requires --input", op.ID) + } + return readValue(cmd, flags.input) + } + if flags.rawBody != "" { + if len(flags.extraFields) > 0 || anyBodyFlags(flags.body) { + return nil, fmt.Errorf("--body cannot be combined with generated body flags or --field") + } + return readValue(cmd, flags.rawBody) + } + + root := map[string]any{} + target := root + if op.Body.Root != "" { + child := map[string]any{} + root[op.Body.Root] = child + target = child + } + + for _, values := range flags.body { + if len(values.values) == 0 { + continue + } + target[values.field.Name] = parseValues(values.values, values.field.Type, values.field.Array) + } + for _, rawField := range flags.extraFields { + key, value, err := splitPair(rawField) + if err != nil { + return nil, err + } + setNested(target, key, parseLooseValue(value)) + } + if emptyBody(root, op.Body.Root) { + return nil, nil + } + return json.Marshal(root) +} + +func anyBodyFlags(values map[string]*bodyValues) bool { + for _, value := range values { + if len(value.values) > 0 { + return true + } + } + return false +} + +func emptyBody(root map[string]any, rootName string) bool { + if rootName == "" { + return len(root) == 0 + } + child, ok := root[rootName].(map[string]any) + return !ok || len(child) == 0 +} + +func readValue(cmd *cobra.Command, value string) ([]byte, error) { + switch { + case value == "-": + return io.ReadAll(cmd.InOrStdin()) + case strings.HasPrefix(value, "@"): + return os.ReadFile(strings.TrimPrefix(value, "@")) + default: + return []byte(value), nil + } +} + +func parseValues(values []string, typ string, array bool) any { + if array { + result := make([]any, 0, len(values)) + for _, value := range values { + result = append(result, parseTypedValue(value, typ)) + } + return result + } + return parseTypedValue(values[len(values)-1], typ) +} + +func parseTypedValue(value, typ string) any { + if value == "null" { + return nil + } + switch strings.Split(typ, ":")[0] { + case "boolean": + parsed, err := strconv.ParseBool(value) + if err == nil { + return parsed + } + case "integer": + parsed, err := strconv.ParseInt(value, 10, 64) + if err == nil { + return parsed + } + case "number": + parsed, err := strconv.ParseFloat(value, 64) + if err == nil { + return parsed + } + case "object": + var parsed any + if err := json.Unmarshal([]byte(value), &parsed); err == nil { + return parsed + } + } + return parseLooseValue(value) +} + +func parseLooseValue(value string) any { + if value == "null" { + return nil + } + if value == "true" || value == "false" { + parsed, _ := strconv.ParseBool(value) + return parsed + } + if strings.HasPrefix(value, "{") || strings.HasPrefix(value, "[") || strings.HasPrefix(value, `"`) { + var parsed any + if err := json.Unmarshal([]byte(value), &parsed); err == nil { + return parsed + } + } + if parsed, err := strconv.ParseInt(value, 10, 64); err == nil { + return parsed + } + if parsed, err := strconv.ParseFloat(value, 64); err == nil { + return parsed + } + return value +} + +func setNested(root map[string]any, path string, value any) { + parts := strings.Split(path, ".") + current := root + for _, part := range parts[:len(parts)-1] { + next, ok := current[part].(map[string]any) + if !ok { + next = map[string]any{} + current[part] = next + } + current = next + } + current[parts[len(parts)-1]] = value +} + +func splitPair(value string) (string, string, error) { + key, val, ok := strings.Cut(value, "=") + if !ok || key == "" { + return "", "", fmt.Errorf("expected key=value, got %q", value) + } + return key, val, nil +} + +func usageArgs(params []openapi.Parameter) string { + if len(params) == 0 { + return "" + } + var args []string + for _, param := range params { + args = append(args, "<"+strings.ToLower(param.Placeholder)+">") + } + return " " + strings.Join(args, " ") +} + +func operationLong(op openapi.Operation) string { + var b strings.Builder + if op.Summary != "" { + b.WriteString(op.Summary) + } + if op.Description != "" { + if b.Len() > 0 { + b.WriteString("\n\n") + } + b.WriteString(op.Description) + } + if b.Len() > 0 { + b.WriteString("\n\n") + } + fmt.Fprintf(&b, "Operation: %s %s (%s)", op.Method, op.Path, op.ID) + return b.String() +} + +func flagDescription(description, typ string, required bool, enum []string) string { + var parts []string + if description != "" { + description = strings.ReplaceAll(description, "`", "'") + parts = append(parts, strings.Join(strings.Fields(description), " ")) + } + if typ != "" { + parts = append(parts, "type: "+typ) + } + if len(enum) > 0 { + parts = append(parts, "values: "+strings.Join(enum, ", ")) + } + if required { + parts = append(parts, "required") + } + return strings.Join(parts, "; ") +} + +func firstNonEmpty(values ...string) string { + for _, value := range values { + if value != "" { + return value + } + } + return "" +} diff --git a/internal/cli/root_test.go b/internal/cli/root_test.go new file mode 100644 index 0000000..4522d2a --- /dev/null +++ b/internal/cli/root_test.go @@ -0,0 +1,244 @@ +package cli + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "os" + "strings" + "testing" + + "github.com/muxx/redmine-cli/internal/config" +) + +func TestGeneratedCreateCommandSendsBody(t *testing.T) { + t.Setenv("REDMINE_HOST", "") + t.Setenv("REDMINE_API_KEY", "") + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + t.Fatalf("method = %s, want POST", r.Method) + } + if r.URL.Path != "/issues.json" { + t.Fatalf("path = %s", r.URL.Path) + } + if got := r.Header.Get("X-Redmine-API-Key"); got != "secret" { + t.Fatalf("api key header = %q", got) + } + var body map[string]map[string]any + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + t.Fatal(err) + } + issue := body["issue"] + if issue["project_id"] != "demo" { + t.Fatalf("project_id = %#v", issue["project_id"]) + } + if issue["subject"] != "Fix checkout" { + t.Fatalf("subject = %#v", issue["subject"]) + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"issue":{"id":100}}`)) + })) + defer server.Close() + + var out bytes.Buffer + var errOut bytes.Buffer + cmd := NewWithIO("test", bytes.NewReader(nil), &out, &errOut, server.Client()) + cmd.SetArgs([]string{ + "--config", t.TempDir() + "/missing.yml", + "--host", server.URL, + "--api-key", "secret", + "issue", "create", + "--project-id", "demo", + "--subject", "Fix checkout", + }) + + if err := cmd.Execute(); err != nil { + t.Fatalf("execute: %v\nstderr: %s", err, errOut.String()) + } + if !bytes.Contains(out.Bytes(), []byte(`"id": 100`)) { + t.Fatalf("output = %s", out.String()) + } +} + +func TestGeneratedShowCommandUsesPathAndQuery(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + t.Fatalf("method = %s, want GET", r.Method) + } + if r.URL.Path != "/issues/42.json" { + t.Fatalf("path = %s", r.URL.Path) + } + if got := r.URL.Query().Get("include"); got != "journals" { + t.Fatalf("include = %q", got) + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"issue":{"id":42}}`)) + })) + defer server.Close() + + var out bytes.Buffer + var errOut bytes.Buffer + cmd := NewWithIO("test", bytes.NewReader(nil), &out, &errOut, server.Client()) + cmd.SetArgs([]string{ + "--config", t.TempDir() + "/missing.yml", + "--host", server.URL, + "issue", "show", "42", + "--include", "journals", + }) + + if err := cmd.Execute(); err != nil { + t.Fatalf("execute: %v\nstderr: %s", err, errOut.String()) + } + if !bytes.Contains(out.Bytes(), []byte(`"id": 42`)) { + t.Fatalf("output = %s", out.String()) + } +} + +func TestAuthLoginChecksBeforeSaving(t *testing.T) { + clearRedmineEnv(t) + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/users/current.json" { + t.Fatalf("path = %s", r.URL.Path) + } + if got := r.Header.Get("X-Redmine-API-Key"); got != "secret" { + t.Fatalf("api key header = %q", got) + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"user":{"id":7,"login":"jsmith"}}`)) + })) + defer server.Close() + + configPath := t.TempDir() + "/config.yml" + var out bytes.Buffer + var errOut bytes.Buffer + cmd := NewWithIO("test", bytes.NewReader(nil), &out, &errOut, server.Client()) + cmd.SetArgs([]string{ + "--config", configPath, + "auth", "login", + "--host", server.URL, + "--api-key", "secret", + }) + + if err := cmd.Execute(); err != nil { + t.Fatalf("execute: %v\nstderr: %s", err, errOut.String()) + } + if !strings.Contains(out.String(), "Logged in to "+server.URL+" as jsmith") { + t.Fatalf("output = %s", out.String()) + } + cfg, err := config.Load(configPath) + if err != nil { + t.Fatal(err) + } + if cfg.Host != server.URL || cfg.APIKey != "secret" { + t.Fatalf("config = %#v", cfg) + } +} + +func TestAuthLoginDoesNotSaveWhenCheckFails(t *testing.T) { + clearRedmineEnv(t) + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Error(w, "bad key", http.StatusUnauthorized) + })) + defer server.Close() + + configPath := t.TempDir() + "/config.yml" + var out bytes.Buffer + var errOut bytes.Buffer + cmd := NewWithIO("test", bytes.NewReader(nil), &out, &errOut, server.Client()) + cmd.SetArgs([]string{ + "--config", configPath, + "auth", "login", + "--host", server.URL, + "--api-key", "bad", + }) + + err := cmd.Execute() + if err == nil { + t.Fatal("expected auth check error") + } + if !strings.Contains(err.Error(), "authentication check failed") { + t.Fatalf("error = %v", err) + } + if _, statErr := os.Stat(configPath); !os.IsNotExist(statErr) { + t.Fatalf("config file exists after failed login: %v", statErr) + } +} + +func TestAuthStatusChecksSavedConfig(t *testing.T) { + clearRedmineEnv(t) + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/users/current.json" { + t.Fatalf("path = %s", r.URL.Path) + } + if got := r.Header.Get("X-Redmine-API-Key"); got != "secret" { + t.Fatalf("api key header = %q", got) + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"user":{"id":7,"login":"jsmith"}}`)) + })) + defer server.Close() + + configPath := t.TempDir() + "/config.yml" + if err := config.Save(configPath, config.Config{Host: server.URL, APIKey: "secret"}); err != nil { + t.Fatal(err) + } + + var out bytes.Buffer + var errOut bytes.Buffer + cmd := NewWithIO("test", bytes.NewReader(nil), &out, &errOut, server.Client()) + cmd.SetArgs([]string{"--config", configPath, "auth", "status"}) + + if err := cmd.Execute(); err != nil { + t.Fatalf("execute: %v\nstderr: %s", err, errOut.String()) + } + for _, want := range []string{"Auth: api-key", "User: jsmith", "Status: ok"} { + if !strings.Contains(out.String(), want) { + t.Fatalf("output missing %q: %s", want, out.String()) + } + } +} + +func TestAuthStatusChecksEnvConfig(t *testing.T) { + clearRedmineEnv(t) + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/users/current.json" { + t.Fatalf("path = %s", r.URL.Path) + } + if got := r.Header.Get("X-Redmine-API-Key"); got != "env-secret" { + t.Fatalf("api key header = %q", got) + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"user":{"login":"env-user"}}`)) + })) + defer server.Close() + + t.Setenv("REDMINE_HOST", server.URL) + t.Setenv("REDMINE_API_KEY", "env-secret") + + var out bytes.Buffer + var errOut bytes.Buffer + cmd := NewWithIO("test", bytes.NewReader(nil), &out, &errOut, server.Client()) + cmd.SetArgs([]string{"--config", t.TempDir() + "/missing.yml", "auth", "status"}) + + if err := cmd.Execute(); err != nil { + t.Fatalf("execute: %v\nstderr: %s", err, errOut.String()) + } + if !strings.Contains(out.String(), "User: env-user") { + t.Fatalf("output = %s", out.String()) + } +} + +func clearRedmineEnv(t *testing.T) { + t.Helper() + t.Setenv("REDMINE_HOST", "") + t.Setenv("REDMINE_API_KEY", "") + t.Setenv("REDMINE_USERNAME", "") + t.Setenv("REDMINE_PASSWORD", "") + t.Setenv("REDMINE_SWITCH_USER", "") +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..3fb422b --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,89 @@ +package config + +import ( + "errors" + "os" + "path/filepath" + + "gopkg.in/yaml.v3" +) + +const appName = "redmine-cli" + +// Config stores authentication defaults for Redmine. +type Config struct { + Host string `yaml:"host,omitempty"` + APIKey string `yaml:"api_key,omitempty"` + Username string `yaml:"username,omitempty"` + Password string `yaml:"password,omitempty"` +} + +// DefaultPath returns the CLI configuration path. +func DefaultPath() (string, error) { + if path := os.Getenv("REDMINE_CONFIG"); path != "" { + return path, nil + } + dir, err := os.UserConfigDir() + if err != nil { + return "", err + } + return filepath.Join(dir, appName, "config.yml"), nil +} + +// Load reads configuration. A missing file returns an empty config. +func Load(path string) (Config, error) { + var cfg Config + if path == "" { + var err error + path, err = DefaultPath() + if err != nil { + return cfg, err + } + } + data, err := os.ReadFile(path) + if errors.Is(err, os.ErrNotExist) { + return cfg, nil + } + if err != nil { + return cfg, err + } + if err := yaml.Unmarshal(data, &cfg); err != nil { + return cfg, err + } + return cfg, nil +} + +// Save writes configuration with owner-only permissions. +func Save(path string, cfg Config) error { + if path == "" { + var err error + path, err = DefaultPath() + if err != nil { + return err + } + } + if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { + return err + } + data, err := yaml.Marshal(cfg) + if err != nil { + return err + } + return os.WriteFile(path, data, 0o600) +} + +// Remove deletes configuration. A missing file is not an error. +func Remove(path string) error { + if path == "" { + var err error + path, err = DefaultPath() + if err != nil { + return err + } + } + err := os.Remove(path) + if errors.Is(err, os.ErrNotExist) { + return nil + } + return err +} diff --git a/internal/openapi/generated.go b/internal/openapi/generated.go new file mode 100644 index 0000000..fd895b7 --- /dev/null +++ b/internal/openapi/generated.go @@ -0,0 +1,2006 @@ +// Code generated by redmine-openapi-gen; DO NOT EDIT. + +package openapi + +const SpecTitle = "Redmine API" +const SpecVersion = "1.7.1+redmine.6.1" + +var Operations = []Operation{ + { + ID: "getIssues", + Group: "issue", + GroupAlias: []string{"issues"}, + Command: "list", + Method: "GET", + Path: "/issues.{format}", + Summary: "List issues", + Description: "", + PathParams: nil, + QueryParams: []Parameter{ + {Name: "offset", Flag: "offset", Placeholder: "OFFSET", Required: false, Type: "integer", Description: "", Enum: nil}, + {Name: "limit", Flag: "limit", Placeholder: "LIMIT", Required: false, Type: "integer", Description: "", Enum: nil}, + {Name: "nometa", Flag: "nometa", Placeholder: "NOMETA", Required: false, Type: "integer", Description: "", Enum: []string{"1"}}, + {Name: "sort", Flag: "sort", Placeholder: "SORT", Required: false, Type: "string", Description: "Sort order. Comma-separated list of `field` or `field:desc`. Default direction is ascending.\nExamples: `id:desc`, `status:desc,id`", Enum: nil}, + {Name: "include", Flag: "include", Placeholder: "INCLUDE", Required: false, Type: "string", Description: "Comma-separated list of associated data to include for each issue.\nValues: `attachments` (file attachments), `relations` (issue relations)", Enum: nil}, + {Name: "issue_id", Flag: "issue-id", Placeholder: "ISSUE_ID", Required: false, Type: "string", Description: "Filter by issue ID.\nFormat: `[operator]value`\nOperators: `=` (default), `>=`, `<=`, `><` (between, pipe-separated), `*` (any), `!*` (none)\nExamples: `1`, `>=100`, `><1|100`", Enum: nil}, + {Name: "project_id", Flag: "project-id", Placeholder: "PROJECT_ID", Required: false, Type: "string", Description: "Filter by project.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal)\nSpecial values: `mine` (projects where current user is a member), `bookmarks` (bookmarked projects)\nExamples: `1|2`, `mine`, `!mine`", Enum: nil}, + {Name: "subproject_id", Flag: "subproject-id", Placeholder: "SUBPROJECT_ID", Required: false, Type: "string", Description: "Filter by subproject.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal), `*` (any), `!*` (none)\nExamples: `1|2`, `*`, `!*`", Enum: nil}, + {Name: "tracker_id", Flag: "tracker-id", Placeholder: "TRACKER_ID", Required: false, Type: "string", Description: "Filter by tracker.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal), `ev` (has ever been), `!ev` (has never been), `cf` (changed from)\nExamples: `1|2`, `!3`", Enum: nil}, + {Name: "status_id", Flag: "status-id", Placeholder: "STATUS_ID", Required: false, Type: "string", Description: "Filter by issue status.\nFormat: `[operator]value[|value2|...]`\nOperators: `o` (open), `=` (default), `!` (not equal), `c` (closed), `*` (any), `ev` (has ever been), `!ev` (has never been), `cf` (changed from)\nExamples: `o`, `1|2`, `c`, `!3`", Enum: nil}, + {Name: "priority_id", Flag: "priority-id", Placeholder: "PRIORITY_ID", Required: false, Type: "string", Description: "Filter by priority.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal), `ev` (has ever been), `!ev` (has never been), `cf` (changed from)\nExamples: `1|2`, `!3`", Enum: nil}, + {Name: "assigned_to_id", Flag: "assigned-to-id", Placeholder: "ASSIGNED_TO_ID", Required: false, Type: "string", Description: "Filter by assignee.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal), `*` (any), `!*` (none), `ev` (has ever been), `!ev` (has never been), `cf` (changed from)\nSpecial values: `me` (current user)\nExamples: `1`, `me`, `!*`, `!me`", Enum: nil}, + {Name: "author_id", Flag: "author-id", Placeholder: "AUTHOR_ID", Required: false, Type: "string", Description: "Filter by author.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal)\nSpecial values: `me` (current user)\nExamples: `1`, `me`, `!me`", Enum: nil}, + {Name: "author.group", Flag: "author-group", Placeholder: "AUTHOR.GROUP", Required: false, Type: "string", Description: "Filter by author's group.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal)\nExamples: `1|2`", Enum: nil}, + {Name: "author.role", Flag: "author-role", Placeholder: "AUTHOR.ROLE", Required: false, Type: "string", Description: "Filter by author's role.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal)\nExamples: `1|2`", Enum: nil}, + {Name: "member_of_group", Flag: "member-of-group", Placeholder: "MEMBER_OF_GROUP", Required: false, Type: "string", Description: "Filter by assignee's group membership.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal), `*` (any), `!*` (none)\nExamples: `1`, `*`", Enum: nil}, + {Name: "assigned_to_role", Flag: "assigned-to-role", Placeholder: "ASSIGNED_TO_ROLE", Required: false, Type: "string", Description: "Filter by assignee's role.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal), `*` (any), `!*` (none)\nExamples: `1`, `*`", Enum: nil}, + {Name: "fixed_version_id", Flag: "fixed-version-id", Placeholder: "FIXED_VERSION_ID", Required: false, Type: "string", Description: "Filter by target version.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal), `*` (any), `!*` (none), `ev` (has ever been), `!ev` (has never been), `cf` (changed from)\nExamples: `1|2`, `!*`", Enum: nil}, + {Name: "fixed_version.due_date", Flag: "fixed-version-due-date", Placeholder: "FIXED_VERSION.DUE_DATE", Required: false, Type: "string", Description: "Filter by target version's due date.\nFormat: `[value]`\nOperators: `=` (on date), `>=` (on or after), `<=` (on or before), `><` (between, pipe-separated), `t` (today), `ld` (yesterday), `nd` (tomorrow), `w` (this week), `lw` (last week), `l2w` (last 2 weeks), `nw` (next week), `m` (this month), `lm` (last month), `nm` (next month), `y` (this year), `t-` (n days ago), `>t-` (less than n days ago), `t+` (in more than n days), `=2024-01-01`, `t`, `>t-7`", Enum: nil}, + {Name: "fixed_version.status", Flag: "fixed-version-status", Placeholder: "FIXED_VERSION.STATUS", Required: false, Type: "string", Description: "Filter by target version's status.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal)\nValues: `open`, `locked`, `closed`\nExamples: `open`, `open|locked`, `!closed`", Enum: nil}, + {Name: "category_id", Flag: "category-id", Placeholder: "CATEGORY_ID", Required: false, Type: "string", Description: "Filter by category.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal), `*` (any), `!*` (none), `ev` (has ever been), `!ev` (has never been), `cf` (changed from)\nExamples: `1|2`, `!*`", Enum: nil}, + {Name: "parent_id", Flag: "parent-id", Placeholder: "PARENT_ID", Required: false, Type: "string", Description: "Filter by parent issue.\nFormat: `[operator]value`\nOperators: `=` (exact parent ID), `~` (descendant of, subtree search), `*` (any, i.e. is a subtask), `!*` (none, i.e. is a root issue)\nExamples: `123`, `~123`, `*`, `!*`", Enum: nil}, + {Name: "child_id", Flag: "child-id", Placeholder: "CHILD_ID", Required: false, Type: "string", Description: "Filter by child issue.\nFormat: `[operator]value`\nOperators: `=` (exact child ID), `~` (ancestor of, subtree search), `*` (any, i.e. has subtasks), `!*` (none, i.e. is a leaf issue)\nExamples: `123`, `~123`, `*`, `!*`", Enum: nil}, + {Name: "cf_x", Flag: "cf-x", Placeholder: "CF_X", Required: false, Type: "object", Description: "Filter by custom field value. Replace `x` with the custom field ID (e.g., `cf_1`, `cf_12`).\nFormat and operators depend on the custom field type (string, int, date, list, etc.) and follow the same conventions as other filter parameters.\nExamples: `cf_1=~keyword`, `cf_5=1|2`, `cf_10=>=2024-01-01`", Enum: nil}, + {Name: "subject", Flag: "subject", Placeholder: "SUBJECT", Required: false, Type: "string", Description: "Filter by subject.\nFormat: `value`\nOperators: `~` (contains), `!~` (not contains), `*~` (contains any word), `^` (starts with), `$` (ends with), `*` (any), `!*` (none)\nExamples: `~keyword`, `^prefix`", Enum: nil}, + {Name: "description", Flag: "description", Placeholder: "DESCRIPTION", Required: false, Type: "string", Description: "Filter by description.\nFormat: `value`\nOperators: `~` (contains), `!~` (not contains), `*~` (contains any word), `^` (starts with), `$` (ends with), `*` (any), `!*` (none)\nExamples: `~keyword`, `!~excluded`", Enum: nil}, + {Name: "notes", Flag: "notes", Placeholder: "NOTES", Required: false, Type: "string", Description: "Filter by journal notes.\nFormat: `value`\nOperators: `~` (contains), `!~` (not contains), `*~` (contains any word), `^` (starts with), `$` (ends with), `*` (any), `!*` (none)\nExamples: `~keyword`", Enum: nil}, + {Name: "created_on", Flag: "created-on", Placeholder: "CREATED_ON", Required: false, Type: "string", Description: "Filter by creation date.\nFormat: `[value]`\nOperators: `=` (on date), `>=` (on or after), `<=` (on or before), `><` (between, pipe-separated), `t` (today), `ld` (yesterday), `w` (this week), `lw` (last week), `l2w` (last 2 weeks), `m` (this month), `lm` (last month), `y` (this year), `t-` (n days ago), `>t-` (less than n days ago), `=2024-01-01`, `t`, `>t-7`, `lm`", Enum: nil}, + {Name: "updated_on", Flag: "updated-on", Placeholder: "UPDATED_ON", Required: false, Type: "string", Description: "Filter by last updated date.\nFormat: `[value]`\nOperators: `=` (on date), `>=` (on or after), `<=` (on or before), `><` (between, pipe-separated), `t` (today), `ld` (yesterday), `w` (this week), `lw` (last week), `l2w` (last 2 weeks), `m` (this month), `lm` (last month), `y` (this year), `t-` (n days ago), `>t-` (less than n days ago), `=2024-01-01`, `t`, `>t-7`, `lm`", Enum: nil}, + {Name: "closed_on", Flag: "closed-on", Placeholder: "CLOSED_ON", Required: false, Type: "string", Description: "Filter by closed date.\nFormat: `[value]`\nOperators: `=` (on date), `>=` (on or after), `<=` (on or before), `><` (between, pipe-separated), `t` (today), `ld` (yesterday), `w` (this week), `lw` (last week), `l2w` (last 2 weeks), `m` (this month), `lm` (last month), `y` (this year), `t-` (n days ago), `>t-` (less than n days ago), `=2024-01-01`, `t`, `>t-7`, `lm`", Enum: nil}, + {Name: "start_date", Flag: "start-date", Placeholder: "START_DATE", Required: false, Type: "string", Description: "Filter by start date.\nFormat: `[value]`\nOperators: `=` (on date), `>=` (on or after), `<=` (on or before), `><` (between, pipe-separated), `t` (today), `ld` (yesterday), `nd` (tomorrow), `w` (this week), `lw` (last week), `l2w` (last 2 weeks), `nw` (next week), `m` (this month), `lm` (last month), `nm` (next month), `y` (this year), `t-` (n days ago), `>t-` (less than n days ago), `t+` (in more than n days), `=2024-01-01`, `t`, `>t-7`", Enum: nil}, + {Name: "due_date", Flag: "due-date", Placeholder: "DUE_DATE", Required: false, Type: "string", Description: "Filter by due date.\nFormat: `[value]`\nOperators: `=` (on date), `>=` (on or after), `<=` (on or before), `><` (between, pipe-separated), `t` (today), `ld` (yesterday), `nd` (tomorrow), `w` (this week), `lw` (last week), `l2w` (last 2 weeks), `nw` (next week), `m` (this month), `lm` (last month), `nm` (next month), `y` (this year), `t-` (n days ago), `>t-` (less than n days ago), `t+` (in more than n days), `=2024-01-01`, `t`, `>t-7`", Enum: nil}, + {Name: "estimated_hours", Flag: "estimated-hours", Placeholder: "ESTIMATED_HOURS", Required: false, Type: "string", Description: "Filter by estimated hours.\nFormat: `[operator]value`\nOperators: `=` (default), `>=`, `<=`, `><` (between, pipe-separated), `*` (any), `!*` (none)\nExamples: `>=5`, `><1|10`, `!*`", Enum: nil}, + {Name: "spent_time", Flag: "spent-time", Placeholder: "SPENT_TIME", Required: false, Type: "string", Description: "Filter by total spent time.\nFormat: `[operator]value`\nOperators: `=` (default), `>=`, `<=`, `><` (between, pipe-separated), `*` (any), `!*` (none)\nExamples: `>=5`, `><1|10`, `!*`", Enum: nil}, + {Name: "done_ratio", Flag: "done-ratio", Placeholder: "DONE_RATIO", Required: false, Type: "string", Description: "Filter by done ratio (percentage).\nFormat: `[operator]value`\nOperators: `=` (default), `>=`, `<=`, `><` (between, pipe-separated), `*` (any), `!*` (none)\nExamples: `>=50`, `><0|99`, `100`", Enum: nil}, + {Name: "is_private", Flag: "is-private", Placeholder: "IS_PRIVATE", Required: false, Type: "string", Description: "Filter by private flag.\nFormat: `[operator]value`\nOperators: `=` (default), `!` (not equal)\nValues: `1` (private), `0` (not private)\nExamples: `1`, `0`", Enum: nil}, + {Name: "attachment", Flag: "attachment", Placeholder: "ATTACHMENT", Required: false, Type: "string", Description: "Filter by attachment filename.\nFormat: `value`\nOperators: `~` (contains), `!~` (not contains), `*~` (contains any word), `^` (starts with), `$` (ends with), `*` (any), `!*` (none)\nExamples: `~report`, `*.pdf`", Enum: nil}, + {Name: "attachment_description", Flag: "attachment-description", Placeholder: "ATTACHMENT_DESCRIPTION", Required: false, Type: "string", Description: "Filter by attachment description.\nFormat: `value`\nOperators: `~` (contains), `!~` (not contains), `*~` (contains any word), `^` (starts with), `$` (ends with), `*` (any), `!*` (none)\nExamples: `~screenshot`", Enum: nil}, + {Name: "watcher_id", Flag: "watcher-id", Placeholder: "WATCHER_ID", Required: false, Type: "string", Description: "Filter by watcher.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal)\nSpecial values: `me` (current user)\nExamples: `1`, `me`, `!me`", Enum: nil}, + {Name: "updated_by", Flag: "updated-by", Placeholder: "UPDATED_BY", Required: false, Type: "string", Description: "Filter by user who updated the issue.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal)\nSpecial values: `me` (current user)\nExamples: `1`, `me`", Enum: nil}, + {Name: "last_updated_by", Flag: "last-updated-by", Placeholder: "LAST_UPDATED_BY", Required: false, Type: "string", Description: "Filter by user who last updated the issue.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal)\nSpecial values: `me` (current user)\nExamples: `1`, `me`", Enum: nil}, + {Name: "project.status", Flag: "project-status", Placeholder: "PROJECT.STATUS", Required: false, Type: "string", Description: "Filter by project status.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal)\nValues: `1` (active), `5` (closed)\nExamples: `1`, `!5`", Enum: nil}, + {Name: "relates", Flag: "relates", Placeholder: "RELATES", Required: false, Type: "string", Description: "Filter by \"relates to\" relation.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal), `=p` (same project), `=!p` (different project), `!p` (not in project), `*o` (related to open issue), `!o` (related to closed issue), `*` (any), `!*` (none)\nExamples: `123`, `=p`, `*o`, `*`", Enum: nil}, + {Name: "duplicates", Flag: "duplicates", Placeholder: "DUPLICATES", Required: false, Type: "string", Description: "Filter by \"duplicates\" relation.\nFormat: `[operator]value[|value2|...]`\nOperators: `=`, `!`, `=p`, `=!p`, `!p`, `*o`, `!o`, `*`, `!*`\nExamples: `123`, `*`", Enum: nil}, + {Name: "duplicated", Flag: "duplicated", Placeholder: "DUPLICATED", Required: false, Type: "string", Description: "Filter by \"duplicated by\" relation.\nFormat: `[operator]value[|value2|...]`\nOperators: `=`, `!`, `=p`, `=!p`, `!p`, `*o`, `!o`, `*`, `!*`\nExamples: `123`, `*`", Enum: nil}, + {Name: "blocks", Flag: "blocks", Placeholder: "BLOCKS", Required: false, Type: "string", Description: "Filter by \"blocks\" relation.\nFormat: `[operator]value[|value2|...]`\nOperators: `=`, `!`, `=p`, `=!p`, `!p`, `*o`, `!o`, `*`, `!*`\nExamples: `123`, `*o`", Enum: nil}, + {Name: "blocked", Flag: "blocked", Placeholder: "BLOCKED", Required: false, Type: "string", Description: "Filter by \"blocked by\" relation.\nFormat: `[operator]value[|value2|...]`\nOperators: `=`, `!`, `=p`, `=!p`, `!p`, `*o`, `!o`, `*`, `!*`\nExamples: `123`, `*o`", Enum: nil}, + {Name: "precedes", Flag: "precedes", Placeholder: "PRECEDES", Required: false, Type: "string", Description: "Filter by \"precedes\" relation.\nFormat: `[operator]value[|value2|...]`\nOperators: `=`, `!`, `=p`, `=!p`, `!p`, `*o`, `!o`, `*`, `!*`\nExamples: `123`, `*`", Enum: nil}, + {Name: "follows", Flag: "follows", Placeholder: "FOLLOWS", Required: false, Type: "string", Description: "Filter by \"follows\" relation.\nFormat: `[operator]value[|value2|...]`\nOperators: `=`, `!`, `=p`, `=!p`, `!p`, `*o`, `!o`, `*`, `!*`\nExamples: `123`, `*`", Enum: nil}, + {Name: "copied_to", Flag: "copied-to", Placeholder: "COPIED_TO", Required: false, Type: "string", Description: "Filter by \"copied to\" relation.\nFormat: `[operator]value[|value2|...]`\nOperators: `=`, `!`, `=p`, `=!p`, `!p`, `*o`, `!o`, `*`, `!*`\nExamples: `123`, `*`", Enum: nil}, + {Name: "copied_from", Flag: "copied-from", Placeholder: "COPIED_FROM", Required: false, Type: "string", Description: "Filter by \"copied from\" relation.\nFormat: `[operator]value[|value2|...]`\nOperators: `=`, `!`, `=p`, `=!p`, `!p`, `*o`, `!o`, `*`, `!*`\nExamples: `123`, `*`", Enum: nil}, + {Name: "any_searchable", Flag: "any-searchable", Placeholder: "ANY_SEARCHABLE", Required: false, Type: "string", Description: "Full-text search across all searchable fields.\nFormat: `value`\nOperators: `~` (contains all words), `*~` (contains any word), `!~` (not contains)\nExamples: `~keyword`, `*~foo bar`", Enum: nil}, + {Name: "query_id", Flag: "query-id", Placeholder: "QUERY_ID", Required: false, Type: "integer", Description: "ID of a saved query to apply. When specified, the saved query's filter criteria are used.\nOther filter parameters in the same request are ignored.\nUse GET /queries.json to retrieve available saved queries.", Enum: nil}, + }, + HeaderParams: []Parameter{ + {Name: "X-Redmine-Nometa", Flag: "x-redmine-nometa", Placeholder: "X_REDMINE_NOMETA", Required: false, Type: "integer", Description: "", Enum: []string{"1"}}, + }, + Body: nil, + ResponseBinary: false, + }, + { + ID: "createIssue", + Group: "issue", + GroupAlias: []string{"issues"}, + Command: "create", + Method: "POST", + Path: "/issues.{format}", + Summary: "Create issue", + Description: "", + PathParams: nil, + QueryParams: nil, + HeaderParams: nil, + Body: &Body{ContentType: "application/json", Root: "issue", Binary: false, Fields: []BodyField{ + {Name: "assigned_to_id", Flag: "assigned-to-id", Required: false, Type: "integer", Array: false, Nullable: true, Description: "", Enum: nil}, + {Name: "category_id", Flag: "category-id", Required: false, Type: "integer", Array: false, Nullable: true, Description: "", Enum: nil}, + {Name: "custom_field_values", Flag: "custom-field-values", Required: false, Type: "object", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "custom_fields", Flag: "custom-fields", Required: false, Type: "object", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "description", Flag: "description", Required: false, Type: "string", Array: false, Nullable: true, Description: "", Enum: nil}, + {Name: "done_ratio", Flag: "done-ratio", Required: false, Type: "integer", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "due_date", Flag: "due-date", Required: false, Type: "string:date", Array: false, Nullable: true, Description: "", Enum: nil}, + {Name: "estimated_hours", Flag: "estimated-hours", Required: false, Type: "number", Array: false, Nullable: true, Description: "", Enum: nil}, + {Name: "fixed_version_id", Flag: "fixed-version-id", Required: false, Type: "integer", Array: false, Nullable: true, Description: "", Enum: nil}, + {Name: "is_private", Flag: "is-private", Required: false, Type: "boolean", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "parent_issue_id", Flag: "parent-issue-id", Required: false, Type: "integer", Array: false, Nullable: true, Description: "", Enum: nil}, + {Name: "priority_id", Flag: "priority-id", Required: false, Type: "integer", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "project_id", Flag: "project-id", Required: true, Type: "integer|string", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "start_date", Flag: "start-date", Required: false, Type: "string:date", Array: false, Nullable: true, Description: "", Enum: nil}, + {Name: "status_id", Flag: "status-id", Required: false, Type: "integer", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "subject", Flag: "subject", Required: true, Type: "string", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "tracker_id", Flag: "tracker-id", Required: false, Type: "integer", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "uploads", Flag: "uploads", Required: false, Type: "object", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "watcher_user_ids", Flag: "watcher-user-ids", Required: false, Type: "integer", Array: true, Nullable: false, Description: "", Enum: nil}, + }}, + ResponseBinary: false, + }, + { + ID: "getIssue", + Group: "issue", + GroupAlias: []string{"issues"}, + Command: "show", + Method: "GET", + Path: "/issues/{issue_id}.{format}", + Summary: "Show issue", + Description: "", + PathParams: []Parameter{ + {Name: "issue_id", Flag: "issue-id", Placeholder: "ISSUE_ID", Required: true, Type: "integer", Description: "", Enum: nil}, + }, + QueryParams: []Parameter{ + {Name: "include", Flag: "include", Placeholder: "INCLUDE", Required: false, Type: "string", Description: "Comma-separated list of associated data to include in the response.\nValues: `children` (child issues), `attachments` (file attachments), `relations` (issue relations), `changesets` (associated VCS changesets), `journals` (change history and notes), `watchers` (users watching the issue), `allowed_statuses` (status transitions available for the current user)", Enum: nil}, + }, + HeaderParams: nil, + Body: nil, + ResponseBinary: false, + }, + { + ID: "updateIssue", + Group: "issue", + GroupAlias: []string{"issues"}, + Command: "update", + Method: "PUT", + Path: "/issues/{issue_id}.{format}", + Summary: "Update issue", + Description: "", + PathParams: []Parameter{ + {Name: "issue_id", Flag: "issue-id", Placeholder: "ISSUE_ID", Required: true, Type: "integer", Description: "", Enum: nil}, + }, + QueryParams: nil, + HeaderParams: nil, + Body: &Body{ContentType: "application/json", Root: "", Binary: false, Fields: []BodyField{ + {Name: "issue", Flag: "issue", Required: false, Type: "object", Array: false, Nullable: false, Description: "", Enum: nil}, + }}, + ResponseBinary: false, + }, + { + ID: "deleteIssue", + Group: "issue", + GroupAlias: []string{"issues"}, + Command: "delete", + Method: "DELETE", + Path: "/issues/{issue_id}.{format}", + Summary: "Delete issue", + Description: "", + PathParams: []Parameter{ + {Name: "issue_id", Flag: "issue-id", Placeholder: "ISSUE_ID", Required: true, Type: "integer", Description: "", Enum: nil}, + }, + QueryParams: nil, + HeaderParams: nil, + Body: nil, + ResponseBinary: false, + }, + { + ID: "addWatcher", + Group: "issue", + GroupAlias: []string{"issues"}, + Command: "add-watcher", + Method: "POST", + Path: "/issues/{issue_id}/watchers.{format}", + Summary: "Add watcher", + Description: "", + PathParams: []Parameter{ + {Name: "issue_id", Flag: "issue-id", Placeholder: "ISSUE_ID", Required: true, Type: "integer", Description: "", Enum: nil}, + }, + QueryParams: nil, + HeaderParams: nil, + Body: &Body{ContentType: "application/json", Root: "", Binary: false, Fields: []BodyField{ + {Name: "user_id", Flag: "user-id", Required: true, Type: "integer", Array: false, Nullable: false, Description: "", Enum: nil}, + }}, + ResponseBinary: false, + }, + { + ID: "removeWatcher", + Group: "issue", + GroupAlias: []string{"issues"}, + Command: "remove-watcher", + Method: "DELETE", + Path: "/issues/{issue_id}/watchers/{user_id}.{format}", + Summary: "Remove watcher", + Description: "", + PathParams: []Parameter{ + {Name: "issue_id", Flag: "issue-id", Placeholder: "ISSUE_ID", Required: true, Type: "integer", Description: "", Enum: nil}, + {Name: "user_id", Flag: "user-id", Placeholder: "USER_ID", Required: true, Type: "integer", Description: "", Enum: nil}, + }, + QueryParams: nil, + HeaderParams: nil, + Body: nil, + ResponseBinary: false, + }, + { + ID: "getIssuesByProject", + Group: "issue", + GroupAlias: []string{"issues"}, + Command: "list-by-project", + Method: "GET", + Path: "/projects/{project_id}/issues.{format}", + Summary: "List issues by project", + Description: "", + PathParams: []Parameter{ + {Name: "project_id", Flag: "project-id", Placeholder: "PROJECT_ID", Required: true, Type: "integer|string", Description: "", Enum: nil}, + }, + QueryParams: []Parameter{ + {Name: "offset", Flag: "offset", Placeholder: "OFFSET", Required: false, Type: "integer", Description: "", Enum: nil}, + {Name: "limit", Flag: "limit", Placeholder: "LIMIT", Required: false, Type: "integer", Description: "", Enum: nil}, + {Name: "nometa", Flag: "nometa", Placeholder: "NOMETA", Required: false, Type: "integer", Description: "", Enum: []string{"1"}}, + {Name: "sort", Flag: "sort", Placeholder: "SORT", Required: false, Type: "string", Description: "Sort order. Comma-separated list of `field` or `field:desc`. Default direction is ascending.\nExamples: `id:desc`, `status:desc,id`", Enum: nil}, + {Name: "include", Flag: "include", Placeholder: "INCLUDE", Required: false, Type: "string", Description: "Comma-separated list of associated data to include for each issue.\nValues: `attachments` (file attachments), `relations` (issue relations)", Enum: nil}, + {Name: "issue_id", Flag: "issue-id", Placeholder: "ISSUE_ID", Required: false, Type: "string", Description: "Filter by issue ID.\nFormat: `[operator]value`\nOperators: `=` (default), `>=`, `<=`, `><` (between, pipe-separated), `*` (any), `!*` (none)\nExamples: `1`, `>=100`, `><1|100`", Enum: nil}, + {Name: "subproject_id", Flag: "subproject-id", Placeholder: "SUBPROJECT_ID", Required: false, Type: "string", Description: "Filter by subproject.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal), `*` (any), `!*` (none)\nExamples: `1|2`, `*`, `!*`", Enum: nil}, + {Name: "tracker_id", Flag: "tracker-id", Placeholder: "TRACKER_ID", Required: false, Type: "string", Description: "Filter by tracker.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal), `ev` (has ever been), `!ev` (has never been), `cf` (changed from)\nExamples: `1|2`, `!3`", Enum: nil}, + {Name: "status_id", Flag: "status-id", Placeholder: "STATUS_ID", Required: false, Type: "string", Description: "Filter by issue status.\nFormat: `[operator]value[|value2|...]`\nOperators: `o` (open), `=` (default), `!` (not equal), `c` (closed), `*` (any), `ev` (has ever been), `!ev` (has never been), `cf` (changed from)\nExamples: `o`, `1|2`, `c`, `!3`", Enum: nil}, + {Name: "priority_id", Flag: "priority-id", Placeholder: "PRIORITY_ID", Required: false, Type: "string", Description: "Filter by priority.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal), `ev` (has ever been), `!ev` (has never been), `cf` (changed from)\nExamples: `1|2`, `!3`", Enum: nil}, + {Name: "assigned_to_id", Flag: "assigned-to-id", Placeholder: "ASSIGNED_TO_ID", Required: false, Type: "string", Description: "Filter by assignee.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal), `*` (any), `!*` (none), `ev` (has ever been), `!ev` (has never been), `cf` (changed from)\nSpecial values: `me` (current user)\nExamples: `1`, `me`, `!*`, `!me`", Enum: nil}, + {Name: "author_id", Flag: "author-id", Placeholder: "AUTHOR_ID", Required: false, Type: "string", Description: "Filter by author.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal)\nSpecial values: `me` (current user)\nExamples: `1`, `me`, `!me`", Enum: nil}, + {Name: "author.group", Flag: "author-group", Placeholder: "AUTHOR.GROUP", Required: false, Type: "string", Description: "Filter by author's group.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal)\nExamples: `1|2`", Enum: nil}, + {Name: "author.role", Flag: "author-role", Placeholder: "AUTHOR.ROLE", Required: false, Type: "string", Description: "Filter by author's role.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal)\nExamples: `1|2`", Enum: nil}, + {Name: "member_of_group", Flag: "member-of-group", Placeholder: "MEMBER_OF_GROUP", Required: false, Type: "string", Description: "Filter by assignee's group membership.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal), `*` (any), `!*` (none)\nExamples: `1`, `*`", Enum: nil}, + {Name: "assigned_to_role", Flag: "assigned-to-role", Placeholder: "ASSIGNED_TO_ROLE", Required: false, Type: "string", Description: "Filter by assignee's role.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal), `*` (any), `!*` (none)\nExamples: `1`, `*`", Enum: nil}, + {Name: "fixed_version_id", Flag: "fixed-version-id", Placeholder: "FIXED_VERSION_ID", Required: false, Type: "string", Description: "Filter by target version.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal), `*` (any), `!*` (none), `ev` (has ever been), `!ev` (has never been), `cf` (changed from)\nExamples: `1|2`, `!*`", Enum: nil}, + {Name: "fixed_version.due_date", Flag: "fixed-version-due-date", Placeholder: "FIXED_VERSION.DUE_DATE", Required: false, Type: "string", Description: "Filter by target version's due date.\nFormat: `[value]`\nOperators: `=` (on date), `>=` (on or after), `<=` (on or before), `><` (between, pipe-separated), `t` (today), `ld` (yesterday), `nd` (tomorrow), `w` (this week), `lw` (last week), `l2w` (last 2 weeks), `nw` (next week), `m` (this month), `lm` (last month), `nm` (next month), `y` (this year), `t-` (n days ago), `>t-` (less than n days ago), `t+` (in more than n days), `=2024-01-01`, `t`, `>t-7`", Enum: nil}, + {Name: "fixed_version.status", Flag: "fixed-version-status", Placeholder: "FIXED_VERSION.STATUS", Required: false, Type: "string", Description: "Filter by target version's status.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal)\nValues: `open`, `locked`, `closed`\nExamples: `open`, `open|locked`, `!closed`", Enum: nil}, + {Name: "category_id", Flag: "category-id", Placeholder: "CATEGORY_ID", Required: false, Type: "string", Description: "Filter by category.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal), `*` (any), `!*` (none), `ev` (has ever been), `!ev` (has never been), `cf` (changed from)\nExamples: `1|2`, `!*`", Enum: nil}, + {Name: "parent_id", Flag: "parent-id", Placeholder: "PARENT_ID", Required: false, Type: "string", Description: "Filter by parent issue.\nFormat: `[operator]value`\nOperators: `=` (exact parent ID), `~` (descendant of, subtree search), `*` (any, i.e. is a subtask), `!*` (none, i.e. is a root issue)\nExamples: `123`, `~123`, `*`, `!*`", Enum: nil}, + {Name: "child_id", Flag: "child-id", Placeholder: "CHILD_ID", Required: false, Type: "string", Description: "Filter by child issue.\nFormat: `[operator]value`\nOperators: `=` (exact child ID), `~` (ancestor of, subtree search), `*` (any, i.e. has subtasks), `!*` (none, i.e. is a leaf issue)\nExamples: `123`, `~123`, `*`, `!*`", Enum: nil}, + {Name: "cf_x", Flag: "cf-x", Placeholder: "CF_X", Required: false, Type: "object", Description: "Filter by custom field value. Replace `x` with the custom field ID (e.g., `cf_1`, `cf_12`).\nFormat and operators depend on the custom field type (string, int, date, list, etc.) and follow the same conventions as other filter parameters.\nExamples: `cf_1=~keyword`, `cf_5=1|2`, `cf_10=>=2024-01-01`", Enum: nil}, + {Name: "subject", Flag: "subject", Placeholder: "SUBJECT", Required: false, Type: "string", Description: "Filter by subject.\nFormat: `value`\nOperators: `~` (contains), `!~` (not contains), `*~` (contains any word), `^` (starts with), `$` (ends with), `*` (any), `!*` (none)\nExamples: `~keyword`, `^prefix`", Enum: nil}, + {Name: "description", Flag: "description", Placeholder: "DESCRIPTION", Required: false, Type: "string", Description: "Filter by description.\nFormat: `value`\nOperators: `~` (contains), `!~` (not contains), `*~` (contains any word), `^` (starts with), `$` (ends with), `*` (any), `!*` (none)\nExamples: `~keyword`, `!~excluded`", Enum: nil}, + {Name: "notes", Flag: "notes", Placeholder: "NOTES", Required: false, Type: "string", Description: "Filter by journal notes.\nFormat: `value`\nOperators: `~` (contains), `!~` (not contains), `*~` (contains any word), `^` (starts with), `$` (ends with), `*` (any), `!*` (none)\nExamples: `~keyword`", Enum: nil}, + {Name: "created_on", Flag: "created-on", Placeholder: "CREATED_ON", Required: false, Type: "string", Description: "Filter by creation date.\nFormat: `[value]`\nOperators: `=` (on date), `>=` (on or after), `<=` (on or before), `><` (between, pipe-separated), `t` (today), `ld` (yesterday), `w` (this week), `lw` (last week), `l2w` (last 2 weeks), `m` (this month), `lm` (last month), `y` (this year), `t-` (n days ago), `>t-` (less than n days ago), `=2024-01-01`, `t`, `>t-7`, `lm`", Enum: nil}, + {Name: "updated_on", Flag: "updated-on", Placeholder: "UPDATED_ON", Required: false, Type: "string", Description: "Filter by last updated date.\nFormat: `[value]`\nOperators: `=` (on date), `>=` (on or after), `<=` (on or before), `><` (between, pipe-separated), `t` (today), `ld` (yesterday), `w` (this week), `lw` (last week), `l2w` (last 2 weeks), `m` (this month), `lm` (last month), `y` (this year), `t-` (n days ago), `>t-` (less than n days ago), `=2024-01-01`, `t`, `>t-7`, `lm`", Enum: nil}, + {Name: "closed_on", Flag: "closed-on", Placeholder: "CLOSED_ON", Required: false, Type: "string", Description: "Filter by closed date.\nFormat: `[value]`\nOperators: `=` (on date), `>=` (on or after), `<=` (on or before), `><` (between, pipe-separated), `t` (today), `ld` (yesterday), `w` (this week), `lw` (last week), `l2w` (last 2 weeks), `m` (this month), `lm` (last month), `y` (this year), `t-` (n days ago), `>t-` (less than n days ago), `=2024-01-01`, `t`, `>t-7`, `lm`", Enum: nil}, + {Name: "start_date", Flag: "start-date", Placeholder: "START_DATE", Required: false, Type: "string", Description: "Filter by start date.\nFormat: `[value]`\nOperators: `=` (on date), `>=` (on or after), `<=` (on or before), `><` (between, pipe-separated), `t` (today), `ld` (yesterday), `nd` (tomorrow), `w` (this week), `lw` (last week), `l2w` (last 2 weeks), `nw` (next week), `m` (this month), `lm` (last month), `nm` (next month), `y` (this year), `t-` (n days ago), `>t-` (less than n days ago), `t+` (in more than n days), `=2024-01-01`, `t`, `>t-7`", Enum: nil}, + {Name: "due_date", Flag: "due-date", Placeholder: "DUE_DATE", Required: false, Type: "string", Description: "Filter by due date.\nFormat: `[value]`\nOperators: `=` (on date), `>=` (on or after), `<=` (on or before), `><` (between, pipe-separated), `t` (today), `ld` (yesterday), `nd` (tomorrow), `w` (this week), `lw` (last week), `l2w` (last 2 weeks), `nw` (next week), `m` (this month), `lm` (last month), `nm` (next month), `y` (this year), `t-` (n days ago), `>t-` (less than n days ago), `t+` (in more than n days), `=2024-01-01`, `t`, `>t-7`", Enum: nil}, + {Name: "estimated_hours", Flag: "estimated-hours", Placeholder: "ESTIMATED_HOURS", Required: false, Type: "string", Description: "Filter by estimated hours.\nFormat: `[operator]value`\nOperators: `=` (default), `>=`, `<=`, `><` (between, pipe-separated), `*` (any), `!*` (none)\nExamples: `>=5`, `><1|10`, `!*`", Enum: nil}, + {Name: "spent_time", Flag: "spent-time", Placeholder: "SPENT_TIME", Required: false, Type: "string", Description: "Filter by total spent time.\nFormat: `[operator]value`\nOperators: `=` (default), `>=`, `<=`, `><` (between, pipe-separated), `*` (any), `!*` (none)\nExamples: `>=5`, `><1|10`, `!*`", Enum: nil}, + {Name: "done_ratio", Flag: "done-ratio", Placeholder: "DONE_RATIO", Required: false, Type: "string", Description: "Filter by done ratio (percentage).\nFormat: `[operator]value`\nOperators: `=` (default), `>=`, `<=`, `><` (between, pipe-separated), `*` (any), `!*` (none)\nExamples: `>=50`, `><0|99`, `100`", Enum: nil}, + {Name: "is_private", Flag: "is-private", Placeholder: "IS_PRIVATE", Required: false, Type: "string", Description: "Filter by private flag.\nFormat: `[operator]value`\nOperators: `=` (default), `!` (not equal)\nValues: `1` (private), `0` (not private)\nExamples: `1`, `0`", Enum: nil}, + {Name: "attachment", Flag: "attachment", Placeholder: "ATTACHMENT", Required: false, Type: "string", Description: "Filter by attachment filename.\nFormat: `value`\nOperators: `~` (contains), `!~` (not contains), `*~` (contains any word), `^` (starts with), `$` (ends with), `*` (any), `!*` (none)\nExamples: `~report`, `*.pdf`", Enum: nil}, + {Name: "attachment_description", Flag: "attachment-description", Placeholder: "ATTACHMENT_DESCRIPTION", Required: false, Type: "string", Description: "Filter by attachment description.\nFormat: `value`\nOperators: `~` (contains), `!~` (not contains), `*~` (contains any word), `^` (starts with), `$` (ends with), `*` (any), `!*` (none)\nExamples: `~screenshot`", Enum: nil}, + {Name: "watcher_id", Flag: "watcher-id", Placeholder: "WATCHER_ID", Required: false, Type: "string", Description: "Filter by watcher.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal)\nSpecial values: `me` (current user)\nExamples: `1`, `me`, `!me`", Enum: nil}, + {Name: "updated_by", Flag: "updated-by", Placeholder: "UPDATED_BY", Required: false, Type: "string", Description: "Filter by user who updated the issue.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal)\nSpecial values: `me` (current user)\nExamples: `1`, `me`", Enum: nil}, + {Name: "last_updated_by", Flag: "last-updated-by", Placeholder: "LAST_UPDATED_BY", Required: false, Type: "string", Description: "Filter by user who last updated the issue.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal)\nSpecial values: `me` (current user)\nExamples: `1`, `me`", Enum: nil}, + {Name: "project.status", Flag: "project-status", Placeholder: "PROJECT.STATUS", Required: false, Type: "string", Description: "Filter by project status.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal)\nValues: `1` (active), `5` (closed)\nExamples: `1`, `!5`", Enum: nil}, + {Name: "relates", Flag: "relates", Placeholder: "RELATES", Required: false, Type: "string", Description: "Filter by \"relates to\" relation.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal), `=p` (same project), `=!p` (different project), `!p` (not in project), `*o` (related to open issue), `!o` (related to closed issue), `*` (any), `!*` (none)\nExamples: `123`, `=p`, `*o`, `*`", Enum: nil}, + {Name: "duplicates", Flag: "duplicates", Placeholder: "DUPLICATES", Required: false, Type: "string", Description: "Filter by \"duplicates\" relation.\nFormat: `[operator]value[|value2|...]`\nOperators: `=`, `!`, `=p`, `=!p`, `!p`, `*o`, `!o`, `*`, `!*`\nExamples: `123`, `*`", Enum: nil}, + {Name: "duplicated", Flag: "duplicated", Placeholder: "DUPLICATED", Required: false, Type: "string", Description: "Filter by \"duplicated by\" relation.\nFormat: `[operator]value[|value2|...]`\nOperators: `=`, `!`, `=p`, `=!p`, `!p`, `*o`, `!o`, `*`, `!*`\nExamples: `123`, `*`", Enum: nil}, + {Name: "blocks", Flag: "blocks", Placeholder: "BLOCKS", Required: false, Type: "string", Description: "Filter by \"blocks\" relation.\nFormat: `[operator]value[|value2|...]`\nOperators: `=`, `!`, `=p`, `=!p`, `!p`, `*o`, `!o`, `*`, `!*`\nExamples: `123`, `*o`", Enum: nil}, + {Name: "blocked", Flag: "blocked", Placeholder: "BLOCKED", Required: false, Type: "string", Description: "Filter by \"blocked by\" relation.\nFormat: `[operator]value[|value2|...]`\nOperators: `=`, `!`, `=p`, `=!p`, `!p`, `*o`, `!o`, `*`, `!*`\nExamples: `123`, `*o`", Enum: nil}, + {Name: "precedes", Flag: "precedes", Placeholder: "PRECEDES", Required: false, Type: "string", Description: "Filter by \"precedes\" relation.\nFormat: `[operator]value[|value2|...]`\nOperators: `=`, `!`, `=p`, `=!p`, `!p`, `*o`, `!o`, `*`, `!*`\nExamples: `123`, `*`", Enum: nil}, + {Name: "follows", Flag: "follows", Placeholder: "FOLLOWS", Required: false, Type: "string", Description: "Filter by \"follows\" relation.\nFormat: `[operator]value[|value2|...]`\nOperators: `=`, `!`, `=p`, `=!p`, `!p`, `*o`, `!o`, `*`, `!*`\nExamples: `123`, `*`", Enum: nil}, + {Name: "copied_to", Flag: "copied-to", Placeholder: "COPIED_TO", Required: false, Type: "string", Description: "Filter by \"copied to\" relation.\nFormat: `[operator]value[|value2|...]`\nOperators: `=`, `!`, `=p`, `=!p`, `!p`, `*o`, `!o`, `*`, `!*`\nExamples: `123`, `*`", Enum: nil}, + {Name: "copied_from", Flag: "copied-from", Placeholder: "COPIED_FROM", Required: false, Type: "string", Description: "Filter by \"copied from\" relation.\nFormat: `[operator]value[|value2|...]`\nOperators: `=`, `!`, `=p`, `=!p`, `!p`, `*o`, `!o`, `*`, `!*`\nExamples: `123`, `*`", Enum: nil}, + {Name: "any_searchable", Flag: "any-searchable", Placeholder: "ANY_SEARCHABLE", Required: false, Type: "string", Description: "Full-text search across all searchable fields.\nFormat: `value`\nOperators: `~` (contains all words), `*~` (contains any word), `!~` (not contains)\nExamples: `~keyword`, `*~foo bar`", Enum: nil}, + {Name: "query_id", Flag: "query-id", Placeholder: "QUERY_ID", Required: false, Type: "integer", Description: "ID of a saved query to apply. When specified, the saved query's filter criteria are used.\nOther filter parameters in the same request are ignored.\nUse GET /queries.json to retrieve available saved queries.", Enum: nil}, + }, + HeaderParams: []Parameter{ + {Name: "X-Redmine-Nometa", Flag: "x-redmine-nometa", Placeholder: "X_REDMINE_NOMETA", Required: false, Type: "integer", Description: "", Enum: []string{"1"}}, + }, + Body: nil, + ResponseBinary: false, + }, + { + ID: "createIssueUnderProject", + Group: "issue", + GroupAlias: []string{"issues"}, + Command: "create-under-project", + Method: "POST", + Path: "/projects/{project_id}/issues.{format}", + Summary: "Create issue under project", + Description: "", + PathParams: []Parameter{ + {Name: "project_id", Flag: "project-id", Placeholder: "PROJECT_ID", Required: true, Type: "integer|string", Description: "", Enum: nil}, + }, + QueryParams: nil, + HeaderParams: nil, + Body: &Body{ContentType: "application/json", Root: "issue", Binary: false, Fields: []BodyField{ + {Name: "assigned_to_id", Flag: "assigned-to-id", Required: false, Type: "integer", Array: false, Nullable: true, Description: "", Enum: nil}, + {Name: "category_id", Flag: "category-id", Required: false, Type: "integer", Array: false, Nullable: true, Description: "", Enum: nil}, + {Name: "custom_field_values", Flag: "custom-field-values", Required: false, Type: "object", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "custom_fields", Flag: "custom-fields", Required: false, Type: "object", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "description", Flag: "description", Required: false, Type: "string", Array: false, Nullable: true, Description: "", Enum: nil}, + {Name: "done_ratio", Flag: "done-ratio", Required: false, Type: "integer", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "due_date", Flag: "due-date", Required: false, Type: "string:date", Array: false, Nullable: true, Description: "", Enum: nil}, + {Name: "estimated_hours", Flag: "estimated-hours", Required: false, Type: "number", Array: false, Nullable: true, Description: "", Enum: nil}, + {Name: "fixed_version_id", Flag: "fixed-version-id", Required: false, Type: "integer", Array: false, Nullable: true, Description: "", Enum: nil}, + {Name: "is_private", Flag: "is-private", Required: false, Type: "boolean", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "parent_issue_id", Flag: "parent-issue-id", Required: false, Type: "integer", Array: false, Nullable: true, Description: "", Enum: nil}, + {Name: "priority_id", Flag: "priority-id", Required: false, Type: "integer", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "start_date", Flag: "start-date", Required: false, Type: "string:date", Array: false, Nullable: true, Description: "", Enum: nil}, + {Name: "status_id", Flag: "status-id", Required: false, Type: "integer", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "subject", Flag: "subject", Required: true, Type: "string", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "tracker_id", Flag: "tracker-id", Required: false, Type: "integer", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "uploads", Flag: "uploads", Required: false, Type: "object", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "watcher_user_ids", Flag: "watcher-user-ids", Required: false, Type: "integer", Array: true, Nullable: false, Description: "", Enum: nil}, + }}, + ResponseBinary: false, + }, + { + ID: "getProjects", + Group: "project", + GroupAlias: []string{"projects"}, + Command: "list", + Method: "GET", + Path: "/projects.{format}", + Summary: "List projects", + Description: "", + PathParams: nil, + QueryParams: []Parameter{ + {Name: "offset", Flag: "offset", Placeholder: "OFFSET", Required: false, Type: "integer", Description: "", Enum: nil}, + {Name: "limit", Flag: "limit", Placeholder: "LIMIT", Required: false, Type: "integer", Description: "", Enum: nil}, + {Name: "nometa", Flag: "nometa", Placeholder: "NOMETA", Required: false, Type: "integer", Description: "", Enum: []string{"1"}}, + {Name: "include", Flag: "include", Placeholder: "INCLUDE", Required: false, Type: "string", Description: "Comma-separated list of associated data to include for each project.\nValues: `trackers` (available issue trackers), `issue_categories` (issue categories), `time_entry_activities` (time entry activity types), `enabled_modules` (enabled modules/features), `issue_custom_fields` (custom fields for issues)", Enum: nil}, + {Name: "status", Flag: "status", Placeholder: "STATUS", Required: false, Type: "string", Description: "Filter by project status.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal)\nValues: `1` (active), `5` (closed)\nExamples: `1`, `1|5`, `!1`", Enum: nil}, + {Name: "id", Flag: "id", Placeholder: "ID", Required: false, Type: "string", Description: "Filter by project ID.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal)\nSpecial values: `mine` (projects where current user is a member), `bookmarks` (bookmarked projects)\nExamples: `1|2|3`, `mine`, `!mine`, `mine|bookmarks`", Enum: nil}, + {Name: "name", Flag: "name", Placeholder: "NAME", Required: false, Type: "string", Description: "Filter by project name.\nFormat: `value`\nOperators: `~` (contains), `!~` (not contains), `*~` (contains any word), `^` (starts with), `$` (ends with), `*` (any), `!*` (none)\nExamples: `~keyword`, `^prefix`", Enum: nil}, + {Name: "description", Flag: "description", Placeholder: "DESCRIPTION", Required: false, Type: "string", Description: "Filter by project description.\nFormat: `value`\nOperators: `~` (contains), `!~` (not contains), `*~` (contains any word), `^` (starts with), `$` (ends with), `*` (any), `!*` (none)\nExamples: `~keyword`, `!~excluded`", Enum: nil}, + {Name: "parent_id", Flag: "parent-id", Placeholder: "PARENT_ID", Required: false, Type: "string", Description: "Filter by parent project.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal), `*` (any, i.e. is a subproject), `!*` (none, i.e. is a root project)\nSpecial values: `mine` (projects where current user is a member), `bookmarks` (bookmarked projects)\nExamples: `1|2`, `mine`, `*`, `!*`", Enum: nil}, + {Name: "is_public", Flag: "is-public", Placeholder: "IS_PUBLIC", Required: false, Type: "string", Description: "Filter by public/private status.\nFormat: `[operator]value`\nOperators: `=` (default), `!` (not equal)\nValues: `1` (public), `0` (private)\nExamples: `1`, `!1`", Enum: nil}, + {Name: "created_on", Flag: "created-on", Placeholder: "CREATED_ON", Required: false, Type: "string", Description: "Filter by creation date.\nFormat: `[value]`\nOperators: `=` (on date), `>=` (on or after), `<=` (on or before), `><` (between, pipe-separated), `t` (today), `ld` (yesterday), `w` (this week), `lw` (last week), `l2w` (last 2 weeks), `m` (this month), `lm` (last month), `y` (this year), `t-` (n days ago), `>t-` (less than n days ago), `=2024-01-01`, `><2024-01-01|2024-12-31`, `t`, `>t-7`, `lm`", Enum: nil}, + {Name: "updated_on", Flag: "updated-on", Placeholder: "UPDATED_ON", Required: false, Type: "string", Description: "Filter by last updated date.\nFormat: `[value]`\nOperators: `=` (on date), `>=` (on or after), `<=` (on or before), `><` (between, pipe-separated), `t` (today), `ld` (yesterday), `w` (this week), `lw` (last week), `l2w` (last 2 weeks), `m` (this month), `lm` (last month), `y` (this year), `t-` (n days ago), `>t-` (less than n days ago), `=2024-01-01`, `><2024-01-01|2024-12-31`, `t`, `>t-7`, `lm`", Enum: nil}, + }, + HeaderParams: []Parameter{ + {Name: "X-Redmine-Nometa", Flag: "x-redmine-nometa", Placeholder: "X_REDMINE_NOMETA", Required: false, Type: "integer", Description: "", Enum: []string{"1"}}, + }, + Body: nil, + ResponseBinary: false, + }, + { + ID: "createProject", + Group: "project", + GroupAlias: []string{"projects"}, + Command: "create", + Method: "POST", + Path: "/projects.{format}", + Summary: "Create project", + Description: "", + PathParams: nil, + QueryParams: nil, + HeaderParams: nil, + Body: &Body{ContentType: "application/json", Root: "project", Binary: false, Fields: []BodyField{ + {Name: "custom_field_values", Flag: "custom-field-values", Required: false, Type: "object", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "custom_fields", Flag: "custom-fields", Required: false, Type: "object", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "default_assigned_to_id", Flag: "default-assigned-to-id", Required: false, Type: "integer", Array: false, Nullable: true, Description: "Default assignee for new issues in this project. Set to null to clear.", Enum: nil}, + {Name: "default_issue_query_id", Flag: "default-issue-query-id", Required: false, Type: "integer", Array: false, Nullable: true, Description: "Default saved query for the project's issue list page. Set to null to clear.", Enum: nil}, + {Name: "default_version_id", Flag: "default-version-id", Required: false, Type: "integer", Array: false, Nullable: true, Description: "Default target version for new issues in this project. Set to null to clear.", Enum: nil}, + {Name: "description", Flag: "description", Required: false, Type: "string", Array: false, Nullable: true, Description: "", Enum: nil}, + {Name: "enabled_module_names", Flag: "enabled-module-names", Required: false, Type: "string", Array: true, Nullable: false, Description: "Enabled module names for this project.\nValues: `issue_tracking`, `time_tracking`, `news`, `documents`, `files`, `wiki`, `repository`, `boards`, `calendar`, `gantt`", Enum: nil}, + {Name: "homepage", Flag: "homepage", Required: false, Type: "string", Array: false, Nullable: true, Description: "", Enum: nil}, + {Name: "identifier", Flag: "identifier", Required: true, Type: "string", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "inherit_members", Flag: "inherit-members", Required: false, Type: "boolean", Array: false, Nullable: false, Description: "Inherit members from the parent project. Only effective for subprojects (parent_id is set).", Enum: nil}, + {Name: "is_public", Flag: "is-public", Required: false, Type: "boolean", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "issue_custom_field_ids", Flag: "issue-custom-field-ids", Required: false, Type: "integer", Array: true, Nullable: false, Description: "IDs of issue custom fields enabled for this project. Custom fields with `is_for_all=true` are always available regardless of this setting.", Enum: nil}, + {Name: "name", Flag: "name", Required: true, Type: "string", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "parent_id", Flag: "parent-id", Required: false, Type: "integer", Array: false, Nullable: true, Description: "", Enum: nil}, + {Name: "tracker_ids", Flag: "tracker-ids", Required: false, Type: "integer", Array: true, Nullable: false, Description: "IDs of trackers available in this project.", Enum: nil}, + }}, + ResponseBinary: false, + }, + { + ID: "getProject", + Group: "project", + GroupAlias: []string{"projects"}, + Command: "show", + Method: "GET", + Path: "/projects/{project_id}.{format}", + Summary: "Show project", + Description: "", + PathParams: []Parameter{ + {Name: "project_id", Flag: "project-id", Placeholder: "PROJECT_ID", Required: true, Type: "integer|string", Description: "", Enum: nil}, + }, + QueryParams: []Parameter{ + {Name: "include", Flag: "include", Placeholder: "INCLUDE", Required: false, Type: "string", Description: "Comma-separated list of associated data to include in the response.\nValues: `trackers` (available issue trackers), `issue_categories` (issue categories), `time_entry_activities` (time entry activity types), `enabled_modules` (enabled modules/features), `issue_custom_fields` (custom fields for issues)", Enum: nil}, + }, + HeaderParams: nil, + Body: nil, + ResponseBinary: false, + }, + { + ID: "updateProject", + Group: "project", + GroupAlias: []string{"projects"}, + Command: "update", + Method: "PUT", + Path: "/projects/{project_id}.{format}", + Summary: "Update project", + Description: "", + PathParams: []Parameter{ + {Name: "project_id", Flag: "project-id", Placeholder: "PROJECT_ID", Required: true, Type: "integer|string", Description: "", Enum: nil}, + }, + QueryParams: nil, + HeaderParams: nil, + Body: &Body{ContentType: "application/json", Root: "", Binary: false, Fields: []BodyField{ + {Name: "project", Flag: "project", Required: false, Type: "object", Array: false, Nullable: false, Description: "", Enum: nil}, + }}, + ResponseBinary: false, + }, + { + ID: "deleteProject", + Group: "project", + GroupAlias: []string{"projects"}, + Command: "delete", + Method: "DELETE", + Path: "/projects/{project_id}.{format}", + Summary: "Delete project", + Description: "", + PathParams: []Parameter{ + {Name: "project_id", Flag: "project-id", Placeholder: "PROJECT_ID", Required: true, Type: "integer|string", Description: "", Enum: nil}, + }, + QueryParams: nil, + HeaderParams: nil, + Body: nil, + ResponseBinary: false, + }, + { + ID: "archiveProject", + Group: "project", + GroupAlias: []string{"projects"}, + Command: "archive", + Method: "PUT", + Path: "/projects/{project_id}/archive.{format}", + Summary: "Archive project", + Description: "", + PathParams: []Parameter{ + {Name: "project_id", Flag: "project-id", Placeholder: "PROJECT_ID", Required: true, Type: "integer|string", Description: "", Enum: nil}, + }, + QueryParams: nil, + HeaderParams: nil, + Body: nil, + ResponseBinary: false, + }, + { + ID: "closeProject", + Group: "project", + GroupAlias: []string{"projects"}, + Command: "close", + Method: "PUT", + Path: "/projects/{project_id}/close.{format}", + Summary: "Close project", + Description: "Undocumented: This operation is not listed in the official documentation.", + PathParams: []Parameter{ + {Name: "project_id", Flag: "project-id", Placeholder: "PROJECT_ID", Required: true, Type: "integer|string", Description: "", Enum: nil}, + }, + QueryParams: nil, + HeaderParams: nil, + Body: nil, + ResponseBinary: false, + }, + { + ID: "reopenProject", + Group: "project", + GroupAlias: []string{"projects"}, + Command: "reopen", + Method: "PUT", + Path: "/projects/{project_id}/reopen.{format}", + Summary: "Reopen project", + Description: "Undocumented: This operation is not listed in the official documentation.", + PathParams: []Parameter{ + {Name: "project_id", Flag: "project-id", Placeholder: "PROJECT_ID", Required: true, Type: "integer|string", Description: "", Enum: nil}, + }, + QueryParams: nil, + HeaderParams: nil, + Body: nil, + ResponseBinary: false, + }, + { + ID: "unarchiveProject", + Group: "project", + GroupAlias: []string{"projects"}, + Command: "unarchive", + Method: "PUT", + Path: "/projects/{project_id}/unarchive.{format}", + Summary: "Unarchive project", + Description: "", + PathParams: []Parameter{ + {Name: "project_id", Flag: "project-id", Placeholder: "PROJECT_ID", Required: true, Type: "integer|string", Description: "", Enum: nil}, + }, + QueryParams: nil, + HeaderParams: nil, + Body: nil, + ResponseBinary: false, + }, + { + ID: "getMembership", + Group: "project-membership", + GroupAlias: []string{"project-memberships", "membership", "memberships"}, + Command: "show", + Method: "GET", + Path: "/memberships/{membership_id}.{format}", + Summary: "Show membership", + Description: "", + PathParams: []Parameter{ + {Name: "membership_id", Flag: "membership-id", Placeholder: "MEMBERSHIP_ID", Required: true, Type: "integer", Description: "", Enum: nil}, + }, + QueryParams: nil, + HeaderParams: nil, + Body: nil, + ResponseBinary: false, + }, + { + ID: "updateMembership", + Group: "project-membership", + GroupAlias: []string{"project-memberships", "membership", "memberships"}, + Command: "update", + Method: "PUT", + Path: "/memberships/{membership_id}.{format}", + Summary: "Update membership", + Description: "", + PathParams: []Parameter{ + {Name: "membership_id", Flag: "membership-id", Placeholder: "MEMBERSHIP_ID", Required: true, Type: "integer", Description: "", Enum: nil}, + }, + QueryParams: nil, + HeaderParams: nil, + Body: &Body{ContentType: "application/json", Root: "", Binary: false, Fields: []BodyField{ + {Name: "membership", Flag: "membership", Required: false, Type: "object", Array: false, Nullable: false, Description: "", Enum: nil}, + }}, + ResponseBinary: false, + }, + { + ID: "deleteMembership", + Group: "project-membership", + GroupAlias: []string{"project-memberships", "membership", "memberships"}, + Command: "delete", + Method: "DELETE", + Path: "/memberships/{membership_id}.{format}", + Summary: "Delete membership", + Description: "", + PathParams: []Parameter{ + {Name: "membership_id", Flag: "membership-id", Placeholder: "MEMBERSHIP_ID", Required: true, Type: "integer", Description: "", Enum: nil}, + }, + QueryParams: nil, + HeaderParams: nil, + Body: nil, + ResponseBinary: false, + }, + { + ID: "getMemberships", + Group: "project-membership", + GroupAlias: []string{"project-memberships", "membership", "memberships"}, + Command: "list", + Method: "GET", + Path: "/projects/{project_id}/memberships.{format}", + Summary: "List memberships", + Description: "", + PathParams: []Parameter{ + {Name: "project_id", Flag: "project-id", Placeholder: "PROJECT_ID", Required: true, Type: "integer|string", Description: "", Enum: nil}, + }, + QueryParams: []Parameter{ + {Name: "offset", Flag: "offset", Placeholder: "OFFSET", Required: false, Type: "integer", Description: "", Enum: nil}, + {Name: "limit", Flag: "limit", Placeholder: "LIMIT", Required: false, Type: "integer", Description: "", Enum: nil}, + {Name: "nometa", Flag: "nometa", Placeholder: "NOMETA", Required: false, Type: "integer", Description: "", Enum: []string{"1"}}, + }, + HeaderParams: []Parameter{ + {Name: "X-Redmine-Nometa", Flag: "x-redmine-nometa", Placeholder: "X_REDMINE_NOMETA", Required: false, Type: "integer", Description: "", Enum: []string{"1"}}, + }, + Body: nil, + ResponseBinary: false, + }, + { + ID: "createMembership", + Group: "project-membership", + GroupAlias: []string{"project-memberships", "membership", "memberships"}, + Command: "create", + Method: "POST", + Path: "/projects/{project_id}/memberships.{format}", + Summary: "Create membership", + Description: "", + PathParams: []Parameter{ + {Name: "project_id", Flag: "project-id", Placeholder: "PROJECT_ID", Required: true, Type: "integer|string", Description: "", Enum: nil}, + }, + QueryParams: nil, + HeaderParams: nil, + Body: &Body{ContentType: "application/json", Root: "membership", Binary: false, Fields: []BodyField{ + {Name: "role_ids", Flag: "role-ids", Required: true, Type: "integer", Array: true, Nullable: false, Description: "", Enum: nil}, + {Name: "user_id", Flag: "user-id", Required: true, Type: "integer", Array: false, Nullable: false, Description: "", Enum: nil}, + }}, + ResponseBinary: false, + }, + { + ID: "getUsers", + Group: "user", + GroupAlias: []string{"users"}, + Command: "list", + Method: "GET", + Path: "/users.{format}", + Summary: "List users", + Description: "", + PathParams: nil, + QueryParams: []Parameter{ + {Name: "offset", Flag: "offset", Placeholder: "OFFSET", Required: false, Type: "integer", Description: "", Enum: nil}, + {Name: "limit", Flag: "limit", Placeholder: "LIMIT", Required: false, Type: "integer", Description: "", Enum: nil}, + {Name: "nometa", Flag: "nometa", Placeholder: "NOMETA", Required: false, Type: "integer", Description: "", Enum: []string{"1"}}, + {Name: "include", Flag: "include", Placeholder: "INCLUDE", Required: false, Type: "string", Description: "Comma-separated list of associated data to include for each user.\nValues: `auth_source` (authentication source information)", Enum: nil}, + {Name: "status", Flag: "status", Placeholder: "STATUS", Required: false, Type: "string", Description: "Filter by user status.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal), `*` (any), `!*` (none)\nValues: `1` (active), `2` (registered), `3` (locked)\nExamples: `1`, `1|2`, `!3`", Enum: nil}, + {Name: "name", Flag: "name", Placeholder: "NAME", Required: false, Type: "string", Description: "Filter by name, login, or email (contains search).\nThe API always uses the `~` (contains) operator regardless of any operator prefix.\nExamples: `john`, `admin`", Enum: nil}, + {Name: "group_id", Flag: "group-id", Placeholder: "GROUP_ID", Required: false, Type: "string", Description: "Filter by group membership (legacy parameter).\nInternally converted to `is_member_of_group` filter with `=` operator.\nValue is the group ID.", Enum: nil}, + {Name: "auth_source_id", Flag: "auth-source-id", Placeholder: "AUTH_SOURCE_ID", Required: false, Type: "string", Description: "Filter by authentication source. Undocumented\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal), `*` (any), `!*` (none)\nExamples: `1`, `!1`", Enum: nil}, + {Name: "twofa_scheme", Flag: "twofa-scheme", Placeholder: "TWOFA_SCHEME", Required: false, Type: "string", Description: "Filter by two-factor authentication scheme. Undocumented\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal), `*` (any), `!*` (none)\nOnly available when two-factor authentication is enabled in Redmine settings.", Enum: nil}, + {Name: "login", Flag: "login", Placeholder: "LOGIN", Required: false, Type: "string", Description: "Filter by login. Undocumented\nFormat: `[operator]value`\nOperators: `~` (contains, default), `*~` (contains any word), `=` (exact match), `!~` (does not contain), `!` (not equal), `^` (starts with), `$` (ends with), `*` (any), `!*` (none)\nExamples: `~admin`, `^john`", Enum: nil}, + {Name: "firstname", Flag: "firstname", Placeholder: "FIRSTNAME", Required: false, Type: "string", Description: "Filter by first name. Undocumented\nFormat: `[operator]value`\nOperators: `~` (contains, default), `*~` (contains any word), `=` (exact match), `!~` (does not contain), `!` (not equal), `^` (starts with), `$` (ends with), `*` (any), `!*` (none)\nExamples: `~John`, `=John`", Enum: nil}, + {Name: "lastname", Flag: "lastname", Placeholder: "LASTNAME", Required: false, Type: "string", Description: "Filter by last name. Undocumented\nFormat: `[operator]value`\nOperators: `~` (contains, default), `*~` (contains any word), `=` (exact match), `!~` (does not contain), `!` (not equal), `^` (starts with), `$` (ends with), `*` (any), `!*` (none)\nExamples: `~Doe`, `=Doe`", Enum: nil}, + {Name: "mail", Flag: "mail", Placeholder: "MAIL", Required: false, Type: "string", Description: "Filter by email address. Undocumented\nFormat: `[operator]value`\nOperators: `~` (contains, default), `*~` (contains any word), `=` (exact match), `!~` (does not contain), `!` (not equal), `^` (starts with), `$` (ends with), `*` (any), `!*` (none)\nExamples: `~example.com`, `$@example.com`", Enum: nil}, + {Name: "created_on", Flag: "created-on", Placeholder: "CREATED_ON", Required: false, Type: "string", Description: "Filter by creation date. Undocumented\nFormat: `[operator]value`\nOperators: `=` (on date), `>=`, `<=`, `><` (between, pipe-separated), `>t-` (more than N days ago), `=2024-01-01`, `>t-30`, `lm`", Enum: nil}, + {Name: "last_login_on", Flag: "last-login-on", Placeholder: "LAST_LOGIN_ON", Required: false, Type: "string", Description: "Filter by last login date. Undocumented\nFormat: `[operator]value`\nOperators: `=` (on date), `>=`, `<=`, `><` (between, pipe-separated), `>t-` (more than N days ago), `=2024-01-01`, `>t-30`, `!*`", Enum: nil}, + {Name: "admin", Flag: "admin", Placeholder: "ADMIN", Required: false, Type: "string", Description: "Filter by admin privilege. Undocumented\nFormat: `[operator]value`\nOperators: `=` (default), `!` (not equal)\nValues: `1` (admin), `0` (non-admin)\nExamples: `1`, `!1`", Enum: nil}, + }, + HeaderParams: []Parameter{ + {Name: "X-Redmine-Nometa", Flag: "x-redmine-nometa", Placeholder: "X_REDMINE_NOMETA", Required: false, Type: "integer", Description: "", Enum: []string{"1"}}, + }, + Body: nil, + ResponseBinary: false, + }, + { + ID: "createUser", + Group: "user", + GroupAlias: []string{"users"}, + Command: "create", + Method: "POST", + Path: "/users.{format}", + Summary: "Create user", + Description: "", + PathParams: nil, + QueryParams: nil, + HeaderParams: nil, + Body: &Body{ContentType: "application/json", Root: "", Binary: false, Fields: []BodyField{ + {Name: "pref", Flag: "pref", Required: false, Type: "object", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "send_information", Flag: "send-information", Required: false, Type: "boolean", Array: false, Nullable: false, Description: "Set to true to send an account information email to the user. Not stored; only triggers email delivery.", Enum: nil}, + {Name: "user", Flag: "user", Required: true, Type: "object", Array: false, Nullable: false, Description: "", Enum: nil}, + }}, + ResponseBinary: false, + }, + { + ID: "getCurrentUser", + Group: "user", + GroupAlias: []string{"users"}, + Command: "show-current", + Method: "GET", + Path: "/users/current.{format}", + Summary: "Show current user", + Description: "", + PathParams: nil, + QueryParams: []Parameter{ + {Name: "include", Flag: "include", Placeholder: "INCLUDE", Required: false, Type: "string", Description: "Comma-separated list of associated data to include in the response.\nValues: `memberships` (project memberships with roles), `groups` (groups the user belongs to), `auth_source` (authentication source information)", Enum: nil}, + }, + HeaderParams: nil, + Body: nil, + ResponseBinary: false, + }, + { + ID: "getUser", + Group: "user", + GroupAlias: []string{"users"}, + Command: "show", + Method: "GET", + Path: "/users/{user_id}.{format}", + Summary: "Show user", + Description: "", + PathParams: []Parameter{ + {Name: "user_id", Flag: "user-id", Placeholder: "USER_ID", Required: true, Type: "integer", Description: "", Enum: nil}, + }, + QueryParams: []Parameter{ + {Name: "include", Flag: "include", Placeholder: "INCLUDE", Required: false, Type: "string", Description: "Comma-separated list of associated data to include in the response.\nValues: `memberships` (project memberships with roles), `groups` (groups the user belongs to), `auth_source` (authentication source information)", Enum: nil}, + }, + HeaderParams: nil, + Body: nil, + ResponseBinary: false, + }, + { + ID: "updateUser", + Group: "user", + GroupAlias: []string{"users"}, + Command: "update", + Method: "PUT", + Path: "/users/{user_id}.{format}", + Summary: "Update user", + Description: "", + PathParams: []Parameter{ + {Name: "user_id", Flag: "user-id", Placeholder: "USER_ID", Required: true, Type: "integer", Description: "", Enum: nil}, + }, + QueryParams: nil, + HeaderParams: nil, + Body: &Body{ContentType: "application/json", Root: "", Binary: false, Fields: []BodyField{ + {Name: "pref", Flag: "pref", Required: false, Type: "object", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "send_information", Flag: "send-information", Required: false, Type: "boolean", Array: false, Nullable: false, Description: "Set to true to send an account information email to the user. Not stored; only triggers email delivery.", Enum: nil}, + {Name: "user", Flag: "user", Required: false, Type: "object", Array: false, Nullable: false, Description: "", Enum: nil}, + }}, + ResponseBinary: false, + }, + { + ID: "deleteUser", + Group: "user", + GroupAlias: []string{"users"}, + Command: "delete", + Method: "DELETE", + Path: "/users/{user_id}.{format}", + Summary: "Delete user", + Description: "", + PathParams: []Parameter{ + {Name: "user_id", Flag: "user-id", Placeholder: "USER_ID", Required: true, Type: "integer", Description: "", Enum: nil}, + }, + QueryParams: nil, + HeaderParams: nil, + Body: nil, + ResponseBinary: false, + }, + { + ID: "createTimeEntryUnderIssue", + Group: "time-entry", + GroupAlias: []string{"time-entries", "entry", "entries"}, + Command: "create-under-issue", + Method: "POST", + Path: "/issues/{issue_id}/time_entries.{format}", + Summary: "Create time entry under issue", + Description: "", + PathParams: []Parameter{ + {Name: "issue_id", Flag: "issue-id", Placeholder: "ISSUE_ID", Required: true, Type: "integer", Description: "", Enum: nil}, + }, + QueryParams: nil, + HeaderParams: nil, + Body: &Body{ContentType: "application/json", Root: "time_entry", Binary: false, Fields: []BodyField{ + {Name: "activity_id", Flag: "activity-id", Required: false, Type: "integer", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "comments", Flag: "comments", Required: false, Type: "string", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "custom_field_values", Flag: "custom-field-values", Required: false, Type: "object", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "custom_fields", Flag: "custom-fields", Required: false, Type: "object", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "hours", Flag: "hours", Required: true, Type: "number", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "project_id", Flag: "project-id", Required: false, Type: "integer", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "spent_on", Flag: "spent-on", Required: false, Type: "string:date", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "user_id", Flag: "user-id", Required: false, Type: "integer", Array: false, Nullable: false, Description: "", Enum: nil}, + }}, + ResponseBinary: false, + }, + { + ID: "getTimeEntriesByProject", + Group: "time-entry", + GroupAlias: []string{"time-entries", "entry", "entries"}, + Command: "list-by-project", + Method: "GET", + Path: "/projects/{project_id}/time_entries.{format}", + Summary: "List time entries by project", + Description: "", + PathParams: []Parameter{ + {Name: "project_id", Flag: "project-id", Placeholder: "PROJECT_ID", Required: true, Type: "integer|string", Description: "", Enum: nil}, + }, + QueryParams: []Parameter{ + {Name: "offset", Flag: "offset", Placeholder: "OFFSET", Required: false, Type: "integer", Description: "", Enum: nil}, + {Name: "limit", Flag: "limit", Placeholder: "LIMIT", Required: false, Type: "integer", Description: "", Enum: nil}, + {Name: "nometa", Flag: "nometa", Placeholder: "NOMETA", Required: false, Type: "integer", Description: "", Enum: []string{"1"}}, + {Name: "user_id", Flag: "user-id", Placeholder: "USER_ID", Required: false, Type: "string", Description: "Filter by user.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal), `*` (any), `!*` (none)\nSpecial values: `me` (current user)\nExamples: `1`, `me`, `!*`", Enum: nil}, + {Name: "spent_on", Flag: "spent-on", Placeholder: "SPENT_ON", Required: false, Type: "string", Description: "Filter by date spent.\nFormat: `[operator]value`\nOperators: `=` (on date), `>=`, `<=`, `><` (between, pipe-separated), `>t-` (more than N days ago), `=2024-01-01`, `><2024-01-01|2024-12-31`, `>t-7`, `t`, `lw`", Enum: nil}, + {Name: "from", Flag: "from", Placeholder: "FROM", Required: false, Type: "string:date", Description: "Shortcut for `spent_on` filter. When used with `to`, filters as between (`>=from` and `<=to`). When used alone, filters as `>=from`.\nFormat: `YYYY-MM-DD`", Enum: nil}, + {Name: "to", Flag: "to", Placeholder: "TO", Required: false, Type: "string:date", Description: "Shortcut for `spent_on` filter. When used with `from`, filters as between (`>=from` and `<=to`). When used alone, filters as `<=to`.\nFormat: `YYYY-MM-DD`", Enum: nil}, + {Name: "subproject_id", Flag: "subproject-id", Placeholder: "SUBPROJECT_ID", Required: false, Type: "string", Description: "Filter by subproject.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal), `*` (any), `!*` (none)\nExamples: `1|2`, `*`, `!*`", Enum: nil}, + {Name: "issue_id", Flag: "issue-id", Placeholder: "ISSUE_ID", Required: false, Type: "string", Description: "Filter by issue.\nFormat: `[operator]value`\nOperators: `=` (exact match), `~` (issue and its subtasks), `*` (any), `!*` (none)\nExamples: `=1`, `~1`, `*`, `!*`", Enum: nil}, + {Name: "issue.tracker_id", Flag: "issue-tracker-id", Placeholder: "ISSUE.TRACKER_ID", Required: false, Type: "string", Description: "Filter by issue's tracker.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal)\nExamples: `1|2`, `!3`", Enum: nil}, + {Name: "issue.status_id", Flag: "issue-status-id", Placeholder: "ISSUE.STATUS_ID", Required: false, Type: "string", Description: "Filter by issue's status.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal)\nExamples: `1|2`, `!3`", Enum: nil}, + {Name: "issue.fixed_version_id", Flag: "issue-fixed-version-id", Placeholder: "ISSUE.FIXED_VERSION_ID", Required: false, Type: "string", Description: "Filter by issue's target version.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal)\nExamples: `1|2`, `!3`", Enum: nil}, + {Name: "issue.parent_id", Flag: "issue-parent-id", Placeholder: "ISSUE.PARENT_ID", Required: false, Type: "string", Description: "Filter by issue's parent.\nFormat: `[operator]value`\nOperators: `=` (exact match), `~` (issue and its subtasks), `*` (any), `!*` (none)\nExamples: `=1`, `~1`, `*`, `!*`", Enum: nil}, + {Name: "issue.category_id", Flag: "issue-category-id", Placeholder: "ISSUE.CATEGORY_ID", Required: false, Type: "string", Description: "Filter by issue's category.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal), `*` (any), `!*` (none)\nExamples: `1|2`, `!*`", Enum: nil}, + {Name: "issue.subject", Flag: "issue-subject", Placeholder: "ISSUE.SUBJECT", Required: false, Type: "string", Description: "Filter by issue's subject.\nFormat: `[operator]value`\nOperators: `~` (contains, default), `*~` (contains any word), `!~` (does not contain), `^` (starts with), `$` (ends with), `*` (any), `!*` (none)\nExamples: `~keyword`, `^prefix`, `$suffix`", Enum: nil}, + {Name: "user.group", Flag: "user-group", Placeholder: "USER.GROUP", Required: false, Type: "string", Description: "Filter by user's group.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal), `*` (any), `!*` (none)\nExamples: `1|2`, `*`", Enum: nil}, + {Name: "user.role", Flag: "user-role", Placeholder: "USER.ROLE", Required: false, Type: "string", Description: "Filter by user's role.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal), `*` (any), `!*` (none)\nExamples: `1|2`, `*`", Enum: nil}, + {Name: "author_id", Flag: "author-id", Placeholder: "AUTHOR_ID", Required: false, Type: "string", Description: "Filter by author.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal), `*` (any), `!*` (none)\nSpecial values: `me` (current user)\nExamples: `1`, `me`, `!*`", Enum: nil}, + {Name: "activity_id", Flag: "activity-id", Placeholder: "ACTIVITY_ID", Required: false, Type: "string", Description: "Filter by activity.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal)\nExamples: `1|2`, `!3`", Enum: nil}, + {Name: "project.status", Flag: "project-status", Placeholder: "PROJECT.STATUS", Required: false, Type: "string", Description: "Filter by project's status.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal)\nValues: `1` (active), `5` (closed)\nExamples: `1`, `!5`", Enum: nil}, + {Name: "comments", Flag: "comments", Placeholder: "COMMENTS", Required: false, Type: "string", Description: "Filter by comments.\nFormat: `[operator]value`\nOperators: `~` (contains, default), `*~` (contains any word), `!~` (does not contain), `^` (starts with), `$` (ends with), `*` (any), `!*` (none)\nExamples: `~keyword`, `^prefix`", Enum: nil}, + {Name: "hours", Flag: "hours", Placeholder: "HOURS", Required: false, Type: "string", Description: "Filter by hours.\nFormat: `[operator]value`\nOperators: `=` (default), `>=`, `<=`, `><` (between, pipe-separated), `*` (any), `!*` (none)\nExamples: `=8`, `>=4`, `><1|8`", Enum: nil}, + {Name: "sort", Flag: "sort", Placeholder: "SORT", Required: false, Type: "string", Description: "Sort order. Comma-separated list of `field` or `field:desc`. Default direction is ascending.\nExamples: `spent_on:desc`, `hours:desc,spent_on`", Enum: nil}, + }, + HeaderParams: []Parameter{ + {Name: "X-Redmine-Nometa", Flag: "x-redmine-nometa", Placeholder: "X_REDMINE_NOMETA", Required: false, Type: "integer", Description: "", Enum: []string{"1"}}, + }, + Body: nil, + ResponseBinary: false, + }, + { + ID: "createTimeEntryUnderProject", + Group: "time-entry", + GroupAlias: []string{"time-entries", "entry", "entries"}, + Command: "create-under-project", + Method: "POST", + Path: "/projects/{project_id}/time_entries.{format}", + Summary: "Create time entry under project", + Description: "", + PathParams: []Parameter{ + {Name: "project_id", Flag: "project-id", Placeholder: "PROJECT_ID", Required: true, Type: "integer|string", Description: "", Enum: nil}, + }, + QueryParams: nil, + HeaderParams: nil, + Body: &Body{ContentType: "application/json", Root: "time_entry", Binary: false, Fields: []BodyField{ + {Name: "activity_id", Flag: "activity-id", Required: false, Type: "integer", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "comments", Flag: "comments", Required: false, Type: "string", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "custom_field_values", Flag: "custom-field-values", Required: false, Type: "object", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "custom_fields", Flag: "custom-fields", Required: false, Type: "object", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "hours", Flag: "hours", Required: true, Type: "number", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "issue_id", Flag: "issue-id", Required: false, Type: "integer", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "spent_on", Flag: "spent-on", Required: false, Type: "string:date", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "user_id", Flag: "user-id", Required: false, Type: "integer", Array: false, Nullable: false, Description: "", Enum: nil}, + }}, + ResponseBinary: false, + }, + { + ID: "getTimeEntries", + Group: "time-entry", + GroupAlias: []string{"time-entries", "entry", "entries"}, + Command: "list", + Method: "GET", + Path: "/time_entries.{format}", + Summary: "List time entries", + Description: "", + PathParams: nil, + QueryParams: []Parameter{ + {Name: "offset", Flag: "offset", Placeholder: "OFFSET", Required: false, Type: "integer", Description: "", Enum: nil}, + {Name: "limit", Flag: "limit", Placeholder: "LIMIT", Required: false, Type: "integer", Description: "", Enum: nil}, + {Name: "nometa", Flag: "nometa", Placeholder: "NOMETA", Required: false, Type: "integer", Description: "", Enum: []string{"1"}}, + {Name: "user_id", Flag: "user-id", Placeholder: "USER_ID", Required: false, Type: "string", Description: "Filter by user.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal), `*` (any), `!*` (none)\nSpecial values: `me` (current user)\nExamples: `1`, `me`, `!*`", Enum: nil}, + {Name: "project_id", Flag: "project-id", Placeholder: "PROJECT_ID", Required: false, Type: "string", Description: "Filter by project.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal)\nSpecial values: `mine` (projects where current user is a member), `bookmarks` (bookmarked projects)\nExamples: `1|2`, `mine`, `!mine`", Enum: nil}, + {Name: "spent_on", Flag: "spent-on", Placeholder: "SPENT_ON", Required: false, Type: "string", Description: "Filter by date spent.\nFormat: `[operator]value`\nOperators: `=` (on date), `>=`, `<=`, `><` (between, pipe-separated), `>t-` (more than N days ago), `=2024-01-01`, `><2024-01-01|2024-12-31`, `>t-7`, `t`, `lw`", Enum: nil}, + {Name: "from", Flag: "from", Placeholder: "FROM", Required: false, Type: "string:date", Description: "Shortcut for `spent_on` filter. When used with `to`, filters as between (`>=from` and `<=to`). When used alone, filters as `>=from`.\nFormat: `YYYY-MM-DD`", Enum: nil}, + {Name: "to", Flag: "to", Placeholder: "TO", Required: false, Type: "string:date", Description: "Shortcut for `spent_on` filter. When used with `from`, filters as between (`>=from` and `<=to`). When used alone, filters as `<=to`.\nFormat: `YYYY-MM-DD`", Enum: nil}, + {Name: "subproject_id", Flag: "subproject-id", Placeholder: "SUBPROJECT_ID", Required: false, Type: "string", Description: "Filter by subproject.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal), `*` (any), `!*` (none)\nExamples: `1|2`, `*`, `!*`", Enum: nil}, + {Name: "issue_id", Flag: "issue-id", Placeholder: "ISSUE_ID", Required: false, Type: "string", Description: "Filter by issue.\nFormat: `[operator]value`\nOperators: `=` (exact match), `~` (issue and its subtasks), `*` (any), `!*` (none)\nExamples: `=1`, `~1`, `*`, `!*`", Enum: nil}, + {Name: "issue.tracker_id", Flag: "issue-tracker-id", Placeholder: "ISSUE.TRACKER_ID", Required: false, Type: "string", Description: "Filter by issue's tracker.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal)\nExamples: `1|2`, `!3`", Enum: nil}, + {Name: "issue.status_id", Flag: "issue-status-id", Placeholder: "ISSUE.STATUS_ID", Required: false, Type: "string", Description: "Filter by issue's status.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal)\nExamples: `1|2`, `!3`", Enum: nil}, + {Name: "issue.fixed_version_id", Flag: "issue-fixed-version-id", Placeholder: "ISSUE.FIXED_VERSION_ID", Required: false, Type: "string", Description: "Filter by issue's target version.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal)\nExamples: `1|2`, `!3`", Enum: nil}, + {Name: "issue.parent_id", Flag: "issue-parent-id", Placeholder: "ISSUE.PARENT_ID", Required: false, Type: "string", Description: "Filter by issue's parent.\nFormat: `[operator]value`\nOperators: `=` (exact match), `~` (issue and its subtasks), `*` (any), `!*` (none)\nExamples: `=1`, `~1`, `*`, `!*`", Enum: nil}, + {Name: "issue.category_id", Flag: "issue-category-id", Placeholder: "ISSUE.CATEGORY_ID", Required: false, Type: "string", Description: "Filter by issue's category.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal), `*` (any), `!*` (none)\nExamples: `1|2`, `!*`", Enum: nil}, + {Name: "issue.subject", Flag: "issue-subject", Placeholder: "ISSUE.SUBJECT", Required: false, Type: "string", Description: "Filter by issue's subject.\nFormat: `[operator]value`\nOperators: `~` (contains, default), `*~` (contains any word), `!~` (does not contain), `^` (starts with), `$` (ends with), `*` (any), `!*` (none)\nExamples: `~keyword`, `^prefix`, `$suffix`", Enum: nil}, + {Name: "user.group", Flag: "user-group", Placeholder: "USER.GROUP", Required: false, Type: "string", Description: "Filter by user's group.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal), `*` (any), `!*` (none)\nExamples: `1|2`, `*`", Enum: nil}, + {Name: "user.role", Flag: "user-role", Placeholder: "USER.ROLE", Required: false, Type: "string", Description: "Filter by user's role.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal), `*` (any), `!*` (none)\nExamples: `1|2`, `*`", Enum: nil}, + {Name: "author_id", Flag: "author-id", Placeholder: "AUTHOR_ID", Required: false, Type: "string", Description: "Filter by author.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal), `*` (any), `!*` (none)\nSpecial values: `me` (current user)\nExamples: `1`, `me`, `!*`", Enum: nil}, + {Name: "activity_id", Flag: "activity-id", Placeholder: "ACTIVITY_ID", Required: false, Type: "string", Description: "Filter by activity.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal)\nExamples: `1|2`, `!3`", Enum: nil}, + {Name: "project.status", Flag: "project-status", Placeholder: "PROJECT.STATUS", Required: false, Type: "string", Description: "Filter by project's status.\nFormat: `[operator]value[|value2|...]`\nOperators: `=` (default), `!` (not equal)\nValues: `1` (active), `5` (closed)\nExamples: `1`, `!5`", Enum: nil}, + {Name: "comments", Flag: "comments", Placeholder: "COMMENTS", Required: false, Type: "string", Description: "Filter by comments.\nFormat: `[operator]value`\nOperators: `~` (contains, default), `*~` (contains any word), `!~` (does not contain), `^` (starts with), `$` (ends with), `*` (any), `!*` (none)\nExamples: `~keyword`, `^prefix`", Enum: nil}, + {Name: "hours", Flag: "hours", Placeholder: "HOURS", Required: false, Type: "string", Description: "Filter by hours.\nFormat: `[operator]value`\nOperators: `=` (default), `>=`, `<=`, `><` (between, pipe-separated), `*` (any), `!*` (none)\nExamples: `=8`, `>=4`, `><1|8`", Enum: nil}, + {Name: "sort", Flag: "sort", Placeholder: "SORT", Required: false, Type: "string", Description: "Sort order. Comma-separated list of `field` or `field:desc`. Default direction is ascending.\nExamples: `spent_on:desc`, `hours:desc,spent_on`", Enum: nil}, + }, + HeaderParams: []Parameter{ + {Name: "X-Redmine-Nometa", Flag: "x-redmine-nometa", Placeholder: "X_REDMINE_NOMETA", Required: false, Type: "integer", Description: "", Enum: []string{"1"}}, + }, + Body: nil, + ResponseBinary: false, + }, + { + ID: "createTimeEntry", + Group: "time-entry", + GroupAlias: []string{"time-entries", "entry", "entries"}, + Command: "create", + Method: "POST", + Path: "/time_entries.{format}", + Summary: "Create time entry", + Description: "", + PathParams: nil, + QueryParams: nil, + HeaderParams: nil, + Body: &Body{ContentType: "application/json", Root: "time_entry", Binary: false, Fields: []BodyField{ + {Name: "activity_id", Flag: "activity-id", Required: false, Type: "integer", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "comments", Flag: "comments", Required: false, Type: "string", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "custom_field_values", Flag: "custom-field-values", Required: false, Type: "object", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "custom_fields", Flag: "custom-fields", Required: false, Type: "object", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "hours", Flag: "hours", Required: true, Type: "number", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "issue_id", Flag: "issue-id", Required: false, Type: "integer", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "project_id", Flag: "project-id", Required: false, Type: "integer", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "spent_on", Flag: "spent-on", Required: false, Type: "string:date", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "user_id", Flag: "user-id", Required: false, Type: "integer", Array: false, Nullable: false, Description: "", Enum: nil}, + }}, + ResponseBinary: false, + }, + { + ID: "getTimeEntry", + Group: "time-entry", + GroupAlias: []string{"time-entries", "entry", "entries"}, + Command: "show", + Method: "GET", + Path: "/time_entries/{time_entry_id}.{format}", + Summary: "Show time entry", + Description: "", + PathParams: []Parameter{ + {Name: "time_entry_id", Flag: "time-entry-id", Placeholder: "TIME_ENTRY_ID", Required: true, Type: "integer", Description: "", Enum: nil}, + }, + QueryParams: nil, + HeaderParams: nil, + Body: nil, + ResponseBinary: false, + }, + { + ID: "updateTimeEntry", + Group: "time-entry", + GroupAlias: []string{"time-entries", "entry", "entries"}, + Command: "update", + Method: "PUT", + Path: "/time_entries/{time_entry_id}.{format}", + Summary: "Update time entry", + Description: "", + PathParams: []Parameter{ + {Name: "time_entry_id", Flag: "time-entry-id", Placeholder: "TIME_ENTRY_ID", Required: true, Type: "integer", Description: "", Enum: nil}, + }, + QueryParams: nil, + HeaderParams: nil, + Body: &Body{ContentType: "application/json", Root: "", Binary: false, Fields: []BodyField{ + {Name: "time_entry", Flag: "time-entry", Required: false, Type: "object", Array: false, Nullable: false, Description: "", Enum: nil}, + }}, + ResponseBinary: false, + }, + { + ID: "deleteTimeEntry", + Group: "time-entry", + GroupAlias: []string{"time-entries", "entry", "entries"}, + Command: "delete", + Method: "DELETE", + Path: "/time_entries/{time_entry_id}.{format}", + Summary: "Delete time entry", + Description: "", + PathParams: []Parameter{ + {Name: "time_entry_id", Flag: "time-entry-id", Placeholder: "TIME_ENTRY_ID", Required: true, Type: "integer", Description: "", Enum: nil}, + }, + QueryParams: nil, + HeaderParams: nil, + Body: nil, + ResponseBinary: false, + }, + { + ID: "getNewsList", + Group: "news", + GroupAlias: nil, + Command: "list", + Method: "GET", + Path: "/news.{format}", + Summary: "List news", + Description: "", + PathParams: nil, + QueryParams: []Parameter{ + {Name: "offset", Flag: "offset", Placeholder: "OFFSET", Required: false, Type: "integer", Description: "", Enum: nil}, + {Name: "limit", Flag: "limit", Placeholder: "LIMIT", Required: false, Type: "integer", Description: "", Enum: nil}, + {Name: "nometa", Flag: "nometa", Placeholder: "NOMETA", Required: false, Type: "integer", Description: "", Enum: []string{"1"}}, + }, + HeaderParams: []Parameter{ + {Name: "X-Redmine-Nometa", Flag: "x-redmine-nometa", Placeholder: "X_REDMINE_NOMETA", Required: false, Type: "integer", Description: "", Enum: []string{"1"}}, + }, + Body: nil, + ResponseBinary: false, + }, + { + ID: "getNews", + Group: "news", + GroupAlias: nil, + Command: "show", + Method: "GET", + Path: "/news/{news_id}.{format}", + Summary: "Show news", + Description: "Undocumented: This operation is not listed in the official documentation.", + PathParams: []Parameter{ + {Name: "news_id", Flag: "news-id", Placeholder: "NEWS_ID", Required: true, Type: "integer", Description: "", Enum: nil}, + }, + QueryParams: []Parameter{ + {Name: "include", Flag: "include", Placeholder: "INCLUDE", Required: false, Type: "string", Description: "Comma-separated list of associated data to include in the response.\nValues: `attachments` (file attachments), `comments` (comments/replies on the news item)", Enum: nil}, + }, + HeaderParams: nil, + Body: nil, + ResponseBinary: false, + }, + { + ID: "updateNews", + Group: "news", + GroupAlias: nil, + Command: "update", + Method: "PUT", + Path: "/news/{news_id}.{format}", + Summary: "Update news", + Description: "Undocumented: This operation is not listed in the official documentation.", + PathParams: []Parameter{ + {Name: "news_id", Flag: "news-id", Placeholder: "NEWS_ID", Required: true, Type: "integer", Description: "", Enum: nil}, + }, + QueryParams: nil, + HeaderParams: nil, + Body: &Body{ContentType: "application/json", Root: "", Binary: false, Fields: []BodyField{ + {Name: "news", Flag: "news", Required: false, Type: "object", Array: false, Nullable: false, Description: "", Enum: nil}, + }}, + ResponseBinary: false, + }, + { + ID: "deleteNews", + Group: "news", + GroupAlias: nil, + Command: "delete", + Method: "DELETE", + Path: "/news/{news_id}.{format}", + Summary: "Delete news", + Description: "Undocumented: This operation is not listed in the official documentation.", + PathParams: []Parameter{ + {Name: "news_id", Flag: "news-id", Placeholder: "NEWS_ID", Required: true, Type: "integer", Description: "", Enum: nil}, + }, + QueryParams: nil, + HeaderParams: nil, + Body: nil, + ResponseBinary: false, + }, + { + ID: "getNewsListByProject", + Group: "news", + GroupAlias: nil, + Command: "list-by-project", + Method: "GET", + Path: "/projects/{project_id}/news.{format}", + Summary: "List news by project", + Description: "", + PathParams: []Parameter{ + {Name: "project_id", Flag: "project-id", Placeholder: "PROJECT_ID", Required: true, Type: "integer|string", Description: "", Enum: nil}, + }, + QueryParams: []Parameter{ + {Name: "offset", Flag: "offset", Placeholder: "OFFSET", Required: false, Type: "integer", Description: "", Enum: nil}, + {Name: "limit", Flag: "limit", Placeholder: "LIMIT", Required: false, Type: "integer", Description: "", Enum: nil}, + {Name: "nometa", Flag: "nometa", Placeholder: "NOMETA", Required: false, Type: "integer", Description: "", Enum: []string{"1"}}, + }, + HeaderParams: []Parameter{ + {Name: "X-Redmine-Nometa", Flag: "x-redmine-nometa", Placeholder: "X_REDMINE_NOMETA", Required: false, Type: "integer", Description: "", Enum: []string{"1"}}, + }, + Body: nil, + ResponseBinary: false, + }, + { + ID: "createNews", + Group: "news", + GroupAlias: nil, + Command: "create", + Method: "POST", + Path: "/projects/{project_id}/news.{format}", + Summary: "Create news", + Description: "Undocumented: This operation is not listed in the official documentation.", + PathParams: []Parameter{ + {Name: "project_id", Flag: "project-id", Placeholder: "PROJECT_ID", Required: true, Type: "integer|string", Description: "", Enum: nil}, + }, + QueryParams: nil, + HeaderParams: nil, + Body: &Body{ContentType: "application/json", Root: "news", Binary: false, Fields: []BodyField{ + {Name: "description", Flag: "description", Required: true, Type: "string", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "summary", Flag: "summary", Required: false, Type: "string", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "title", Flag: "title", Required: true, Type: "string", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "uploads", Flag: "uploads", Required: false, Type: "object", Array: false, Nullable: false, Description: "", Enum: nil}, + }}, + ResponseBinary: false, + }, + { + ID: "getIssueRelations", + Group: "issue-relation", + GroupAlias: []string{"issue-relations", "relation", "relations"}, + Command: "list", + Method: "GET", + Path: "/issues/{issue_id}/relations.{format}", + Summary: "List issue relations", + Description: "", + PathParams: []Parameter{ + {Name: "issue_id", Flag: "issue-id", Placeholder: "ISSUE_ID", Required: true, Type: "integer", Description: "", Enum: nil}, + }, + QueryParams: nil, + HeaderParams: nil, + Body: nil, + ResponseBinary: false, + }, + { + ID: "createIssueRelation", + Group: "issue-relation", + GroupAlias: []string{"issue-relations", "relation", "relations"}, + Command: "create", + Method: "POST", + Path: "/issues/{issue_id}/relations.{format}", + Summary: "Create issue relation", + Description: "", + PathParams: []Parameter{ + {Name: "issue_id", Flag: "issue-id", Placeholder: "ISSUE_ID", Required: true, Type: "integer", Description: "", Enum: nil}, + }, + QueryParams: nil, + HeaderParams: nil, + Body: &Body{ContentType: "application/json", Root: "relation", Binary: false, Fields: []BodyField{ + {Name: "delay", Flag: "delay", Required: false, Type: "integer", Array: false, Nullable: true, Description: "Number of days of delay for `precedes`/`follows` relations. Ignored for other relation types.", Enum: nil}, + {Name: "issue_to_id", Flag: "issue-to-id", Required: true, Type: "integer", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "relation_type", Flag: "relation-type", Required: true, Type: "string", Array: false, Nullable: false, Description: "Type of relation from this issue to the target issue.\nValues: `relates` (related to), `duplicates` (this duplicates target), `duplicated` (this is duplicated by target), `blocks` (this blocks target), `blocked` (this is blocked by target), `precedes` (this must finish before target starts), `follows` (this must start after target finishes), `copied_to` (this was copied to target), `copied_from` (this was copied from target)", Enum: []string{"relates", "duplicates", "duplicated", "blocks", "blocked", "precedes", "follows", "copied_to", "copied_from"}}, + }}, + ResponseBinary: false, + }, + { + ID: "getIssueRelation", + Group: "issue-relation", + GroupAlias: []string{"issue-relations", "relation", "relations"}, + Command: "show", + Method: "GET", + Path: "/relations/{issue_relation_id}.{format}", + Summary: "Show issue relation", + Description: "", + PathParams: []Parameter{ + {Name: "issue_relation_id", Flag: "issue-relation-id", Placeholder: "ISSUE_RELATION_ID", Required: true, Type: "integer", Description: "", Enum: nil}, + }, + QueryParams: nil, + HeaderParams: nil, + Body: nil, + ResponseBinary: false, + }, + { + ID: "deleteIssueRelation", + Group: "issue-relation", + GroupAlias: []string{"issue-relations", "relation", "relations"}, + Command: "delete", + Method: "DELETE", + Path: "/relations/{issue_relation_id}.{format}", + Summary: "Delete issue relation", + Description: "", + PathParams: []Parameter{ + {Name: "issue_relation_id", Flag: "issue-relation-id", Placeholder: "ISSUE_RELATION_ID", Required: true, Type: "integer", Description: "", Enum: nil}, + }, + QueryParams: nil, + HeaderParams: nil, + Body: nil, + ResponseBinary: false, + }, + { + ID: "getVersionsByProject", + Group: "version", + GroupAlias: []string{"versions"}, + Command: "list-by-project", + Method: "GET", + Path: "/projects/{project_id}/versions.{format}", + Summary: "List versions by project", + Description: "", + PathParams: []Parameter{ + {Name: "project_id", Flag: "project-id", Placeholder: "PROJECT_ID", Required: true, Type: "integer|string", Description: "", Enum: nil}, + }, + QueryParams: []Parameter{ + {Name: "nometa", Flag: "nometa", Placeholder: "NOMETA", Required: false, Type: "integer", Description: "", Enum: []string{"1"}}, + }, + HeaderParams: []Parameter{ + {Name: "X-Redmine-Nometa", Flag: "x-redmine-nometa", Placeholder: "X_REDMINE_NOMETA", Required: false, Type: "integer", Description: "", Enum: []string{"1"}}, + }, + Body: nil, + ResponseBinary: false, + }, + { + ID: "createVersion", + Group: "version", + GroupAlias: []string{"versions"}, + Command: "create", + Method: "POST", + Path: "/projects/{project_id}/versions.{format}", + Summary: "Create version", + Description: "", + PathParams: []Parameter{ + {Name: "project_id", Flag: "project-id", Placeholder: "PROJECT_ID", Required: true, Type: "integer|string", Description: "", Enum: nil}, + }, + QueryParams: nil, + HeaderParams: nil, + Body: &Body{ContentType: "application/json", Root: "version", Binary: false, Fields: []BodyField{ + {Name: "custom_field_values", Flag: "custom-field-values", Required: false, Type: "object", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "custom_fields", Flag: "custom-fields", Required: false, Type: "object", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "default_project_version", Flag: "default-project-version", Required: false, Type: "boolean", Array: false, Nullable: false, Description: "Set to true to make this the default target version for new issues in the project.", Enum: nil}, + {Name: "description", Flag: "description", Required: false, Type: "string", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "due_date", Flag: "due-date", Required: false, Type: "string:date", Array: false, Nullable: true, Description: "", Enum: nil}, + {Name: "name", Flag: "name", Required: true, Type: "string", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "sharing", Flag: "sharing", Required: false, Type: "string", Array: false, Nullable: false, Description: "Version sharing scope.\nValues: `none` (not shared), `descendants` (with subprojects), `hierarchy` (with ancestors and descendants), `tree` (with entire project tree), `system` (with all projects)", Enum: []string{"none", "descendants", "hierarchy", "tree", "system"}}, + {Name: "status", Flag: "status", Required: false, Type: "string", Array: false, Nullable: false, Description: "", Enum: []string{"open", "locked", "closed"}}, + {Name: "wiki_page_title", Flag: "wiki-page-title", Required: false, Type: "string", Array: false, Nullable: true, Description: "Title of a wiki page to associate with this version. Set to null to clear.", Enum: nil}, + }}, + ResponseBinary: false, + }, + { + ID: "getVersion", + Group: "version", + GroupAlias: []string{"versions"}, + Command: "show", + Method: "GET", + Path: "/versions/{version_id}.{format}", + Summary: "Show version", + Description: "", + PathParams: []Parameter{ + {Name: "version_id", Flag: "version-id", Placeholder: "VERSION_ID", Required: true, Type: "integer", Description: "", Enum: nil}, + }, + QueryParams: nil, + HeaderParams: nil, + Body: nil, + ResponseBinary: false, + }, + { + ID: "updateVersion", + Group: "version", + GroupAlias: []string{"versions"}, + Command: "update", + Method: "PUT", + Path: "/versions/{version_id}.{format}", + Summary: "Update version", + Description: "", + PathParams: []Parameter{ + {Name: "version_id", Flag: "version-id", Placeholder: "VERSION_ID", Required: true, Type: "integer", Description: "", Enum: nil}, + }, + QueryParams: nil, + HeaderParams: nil, + Body: &Body{ContentType: "application/json", Root: "", Binary: false, Fields: []BodyField{ + {Name: "version", Flag: "version", Required: false, Type: "object", Array: false, Nullable: false, Description: "", Enum: nil}, + }}, + ResponseBinary: false, + }, + { + ID: "deleteVersion", + Group: "version", + GroupAlias: []string{"versions"}, + Command: "delete", + Method: "DELETE", + Path: "/versions/{version_id}.{format}", + Summary: "Delete version", + Description: "", + PathParams: []Parameter{ + {Name: "version_id", Flag: "version-id", Placeholder: "VERSION_ID", Required: true, Type: "integer", Description: "", Enum: nil}, + }, + QueryParams: nil, + HeaderParams: nil, + Body: nil, + ResponseBinary: false, + }, + { + ID: "getWikiPages", + Group: "wiki-page", + GroupAlias: []string{"wiki-pages", "page", "pages"}, + Command: "list", + Method: "GET", + Path: "/projects/{project_id}/wiki/index.{format}", + Summary: "List wiki pages", + Description: "", + PathParams: []Parameter{ + {Name: "project_id", Flag: "project-id", Placeholder: "PROJECT_ID", Required: true, Type: "integer|string", Description: "", Enum: nil}, + }, + QueryParams: nil, + HeaderParams: nil, + Body: nil, + ResponseBinary: false, + }, + { + ID: "getWikiPage", + Group: "wiki-page", + GroupAlias: []string{"wiki-pages", "page", "pages"}, + Command: "show", + Method: "GET", + Path: "/projects/{project_id}/wiki/{wiki_page_title}.{format}", + Summary: "Show wiki page", + Description: "", + PathParams: []Parameter{ + {Name: "project_id", Flag: "project-id", Placeholder: "PROJECT_ID", Required: true, Type: "integer|string", Description: "", Enum: nil}, + {Name: "wiki_page_title", Flag: "wiki-page-title", Placeholder: "WIKI_PAGE_TITLE", Required: true, Type: "string", Description: "", Enum: nil}, + }, + QueryParams: []Parameter{ + {Name: "include", Flag: "include", Placeholder: "INCLUDE", Required: false, Type: "string", Description: "Comma-separated list of associated data to include in the response.\nValues: `attachments` (file attachments)", Enum: nil}, + }, + HeaderParams: nil, + Body: nil, + ResponseBinary: false, + }, + { + ID: "updateWikiPage", + Group: "wiki-page", + GroupAlias: []string{"wiki-pages", "page", "pages"}, + Command: "create-or-update", + Method: "PUT", + Path: "/projects/{project_id}/wiki/{wiki_page_title}.{format}", + Summary: "Create or update wiki page", + Description: "", + PathParams: []Parameter{ + {Name: "project_id", Flag: "project-id", Placeholder: "PROJECT_ID", Required: true, Type: "integer|string", Description: "", Enum: nil}, + {Name: "wiki_page_title", Flag: "wiki-page-title", Placeholder: "WIKI_PAGE_TITLE", Required: true, Type: "string", Description: "", Enum: nil}, + }, + QueryParams: nil, + HeaderParams: nil, + Body: &Body{ContentType: "application/json", Root: "", Binary: false, Fields: []BodyField{ + {Name: "wiki_page", Flag: "wiki-page", Required: false, Type: "object", Array: false, Nullable: false, Description: "", Enum: nil}, + }}, + ResponseBinary: false, + }, + { + ID: "deleteWikiPage", + Group: "wiki-page", + GroupAlias: []string{"wiki-pages", "page", "pages"}, + Command: "delete", + Method: "DELETE", + Path: "/projects/{project_id}/wiki/{wiki_page_title}.{format}", + Summary: "Delete wiki page", + Description: "", + PathParams: []Parameter{ + {Name: "project_id", Flag: "project-id", Placeholder: "PROJECT_ID", Required: true, Type: "integer|string", Description: "", Enum: nil}, + {Name: "wiki_page_title", Flag: "wiki-page-title", Placeholder: "WIKI_PAGE_TITLE", Required: true, Type: "string", Description: "", Enum: nil}, + }, + QueryParams: nil, + HeaderParams: nil, + Body: nil, + ResponseBinary: false, + }, + { + ID: "getWikiPageByVersion", + Group: "wiki-page", + GroupAlias: []string{"wiki-pages", "page", "pages"}, + Command: "show-by-specific-version", + Method: "GET", + Path: "/projects/{project_id}/wiki/{wiki_page_title}/{version_id}.{format}", + Summary: "Show wiki page by specific version", + Description: "", + PathParams: []Parameter{ + {Name: "project_id", Flag: "project-id", Placeholder: "PROJECT_ID", Required: true, Type: "integer|string", Description: "", Enum: nil}, + {Name: "wiki_page_title", Flag: "wiki-page-title", Placeholder: "WIKI_PAGE_TITLE", Required: true, Type: "string", Description: "", Enum: nil}, + {Name: "version_id", Flag: "version-id", Placeholder: "VERSION_ID", Required: true, Type: "integer", Description: "", Enum: nil}, + }, + QueryParams: []Parameter{ + {Name: "include", Flag: "include", Placeholder: "INCLUDE", Required: false, Type: "string", Description: "Comma-separated list of associated data to include in the response.\nValues: `attachments` (file attachments)", Enum: nil}, + }, + HeaderParams: nil, + Body: nil, + ResponseBinary: false, + }, + { + ID: "getQueries", + Group: "query", + GroupAlias: []string{"queries"}, + Command: "list", + Method: "GET", + Path: "/queries.{format}", + Summary: "List queries", + Description: "", + PathParams: nil, + QueryParams: []Parameter{ + {Name: "offset", Flag: "offset", Placeholder: "OFFSET", Required: false, Type: "integer", Description: "", Enum: nil}, + {Name: "limit", Flag: "limit", Placeholder: "LIMIT", Required: false, Type: "integer", Description: "", Enum: nil}, + {Name: "nometa", Flag: "nometa", Placeholder: "NOMETA", Required: false, Type: "integer", Description: "", Enum: []string{"1"}}, + }, + HeaderParams: []Parameter{ + {Name: "X-Redmine-Nometa", Flag: "x-redmine-nometa", Placeholder: "X_REDMINE_NOMETA", Required: false, Type: "integer", Description: "", Enum: []string{"1"}}, + }, + Body: nil, + ResponseBinary: false, + }, + { + ID: "downloadAttachmentFile", + Group: "attachment", + GroupAlias: []string{"attachments"}, + Command: "download-file", + Method: "GET", + Path: "/attachments/download/{attachment_id}/{filename}", + Summary: "Download attachment file", + Description: "Undocumented: This operation is not listed in the official documentation.", + PathParams: []Parameter{ + {Name: "attachment_id", Flag: "attachment-id", Placeholder: "ATTACHMENT_ID", Required: true, Type: "integer", Description: "", Enum: nil}, + {Name: "filename", Flag: "filename", Placeholder: "FILENAME", Required: true, Type: "string", Description: "", Enum: nil}, + }, + QueryParams: nil, + HeaderParams: nil, + Body: nil, + ResponseBinary: true, + }, + { + ID: "downloadThumbnail", + Group: "attachment", + GroupAlias: []string{"attachments"}, + Command: "download-thumbnail", + Method: "GET", + Path: "/attachments/thumbnail/{attachment_id}", + Summary: "Download thumbnail", + Description: "Undocumented: This operation is not listed in the official documentation.", + PathParams: []Parameter{ + {Name: "attachment_id", Flag: "attachment-id", Placeholder: "ATTACHMENT_ID", Required: true, Type: "integer", Description: "", Enum: nil}, + }, + QueryParams: []Parameter{ + {Name: "size", Flag: "size", Placeholder: "SIZE", Required: false, Type: "integer", Description: "Desired thumbnail size in pixels (max 800). Rounded up to the nearest 50px increment.\nIf not specified, the configured default thumbnail size is used.", Enum: nil}, + }, + HeaderParams: nil, + Body: nil, + ResponseBinary: true, + }, + { + ID: "getAttachment", + Group: "attachment", + GroupAlias: []string{"attachments"}, + Command: "show", + Method: "GET", + Path: "/attachments/{attachment_id}.{format}", + Summary: "Show attachment", + Description: "", + PathParams: []Parameter{ + {Name: "attachment_id", Flag: "attachment-id", Placeholder: "ATTACHMENT_ID", Required: true, Type: "integer", Description: "", Enum: nil}, + }, + QueryParams: nil, + HeaderParams: nil, + Body: nil, + ResponseBinary: false, + }, + { + ID: "updateAttachment", + Group: "attachment", + GroupAlias: []string{"attachments"}, + Command: "update", + Method: "PATCH", + Path: "/attachments/{attachment_id}.{format}", + Summary: "Update attachment", + Description: "", + PathParams: []Parameter{ + {Name: "attachment_id", Flag: "attachment-id", Placeholder: "ATTACHMENT_ID", Required: true, Type: "integer", Description: "", Enum: nil}, + }, + QueryParams: nil, + HeaderParams: nil, + Body: &Body{ContentType: "application/json", Root: "", Binary: false, Fields: []BodyField{ + {Name: "attachment", Flag: "attachment", Required: false, Type: "object", Array: false, Nullable: false, Description: "", Enum: nil}, + }}, + ResponseBinary: false, + }, + { + ID: "deleteAttachment", + Group: "attachment", + GroupAlias: []string{"attachments"}, + Command: "delete", + Method: "DELETE", + Path: "/attachments/{attachment_id}.{format}", + Summary: "Delete attachment", + Description: "", + PathParams: []Parameter{ + {Name: "attachment_id", Flag: "attachment-id", Placeholder: "ATTACHMENT_ID", Required: true, Type: "integer", Description: "", Enum: nil}, + }, + QueryParams: nil, + HeaderParams: nil, + Body: nil, + ResponseBinary: false, + }, + { + ID: "uploadAttachmentFile", + Group: "attachment", + GroupAlias: []string{"attachments"}, + Command: "upload-file", + Method: "POST", + Path: "/uploads.{format}", + Summary: "Upload attachment file", + Description: "", + PathParams: nil, + QueryParams: []Parameter{ + {Name: "filename", Flag: "filename", Placeholder: "FILENAME", Required: false, Type: "string", Description: "Filename for the uploaded file. If not provided, a random hexadecimal name is generated.", Enum: nil}, + {Name: "content_type", Flag: "content-type", Placeholder: "CONTENT_TYPE", Required: false, Type: "string", Description: "MIME type of the file (e.g., `application/pdf`, `image/png`). If not provided, auto-detected from the filename.", Enum: nil}, + }, + HeaderParams: nil, + Body: &Body{ContentType: "application/octet-stream", Root: "", Binary: true, Fields: nil}, + ResponseBinary: false, + }, + { + ID: "getIssueStatuses", + Group: "issue-status", + GroupAlias: []string{"issue-statuses", "status", "statuses"}, + Command: "list", + Method: "GET", + Path: "/issue_statuses.{format}", + Summary: "List issue statuses", + Description: "", + PathParams: nil, + QueryParams: nil, + HeaderParams: nil, + Body: nil, + ResponseBinary: false, + }, + { + ID: "getTrackers", + Group: "tracker", + GroupAlias: []string{"trackers"}, + Command: "list", + Method: "GET", + Path: "/trackers.{format}", + Summary: "List trackers", + Description: "", + PathParams: nil, + QueryParams: nil, + HeaderParams: nil, + Body: nil, + ResponseBinary: false, + }, + { + ID: "getDocumentCategories", + Group: "enumeration", + GroupAlias: []string{"enumerations"}, + Command: "list-document-categories", + Method: "GET", + Path: "/enumerations/document_categories.{format}", + Summary: "List document categories", + Description: "", + PathParams: nil, + QueryParams: nil, + HeaderParams: nil, + Body: nil, + ResponseBinary: false, + }, + { + ID: "getIssuePriorities", + Group: "enumeration", + GroupAlias: []string{"enumerations"}, + Command: "list-issue-priorities", + Method: "GET", + Path: "/enumerations/issue_priorities.{format}", + Summary: "List issue priorities", + Description: "", + PathParams: nil, + QueryParams: nil, + HeaderParams: nil, + Body: nil, + ResponseBinary: false, + }, + { + ID: "getTimeEntryActivities", + Group: "enumeration", + GroupAlias: []string{"enumerations"}, + Command: "list-time-entry-activities", + Method: "GET", + Path: "/enumerations/time_entry_activities.{format}", + Summary: "List time entry activities", + Description: "", + PathParams: nil, + QueryParams: nil, + HeaderParams: nil, + Body: nil, + ResponseBinary: false, + }, + { + ID: "getIssueCategory", + Group: "issue-category", + GroupAlias: []string{"issue-categories", "category", "categories"}, + Command: "show", + Method: "GET", + Path: "/issue_categories/{issue_category_id}.{format}", + Summary: "Show issue category", + Description: "", + PathParams: []Parameter{ + {Name: "issue_category_id", Flag: "issue-category-id", Placeholder: "ISSUE_CATEGORY_ID", Required: true, Type: "integer", Description: "", Enum: nil}, + }, + QueryParams: nil, + HeaderParams: nil, + Body: nil, + ResponseBinary: false, + }, + { + ID: "updateIssueCategory", + Group: "issue-category", + GroupAlias: []string{"issue-categories", "category", "categories"}, + Command: "update", + Method: "PUT", + Path: "/issue_categories/{issue_category_id}.{format}", + Summary: "Update issue category", + Description: "", + PathParams: []Parameter{ + {Name: "issue_category_id", Flag: "issue-category-id", Placeholder: "ISSUE_CATEGORY_ID", Required: true, Type: "integer", Description: "", Enum: nil}, + }, + QueryParams: nil, + HeaderParams: nil, + Body: &Body{ContentType: "application/json", Root: "", Binary: false, Fields: []BodyField{ + {Name: "issue_category", Flag: "issue-category", Required: false, Type: "object", Array: false, Nullable: false, Description: "", Enum: nil}, + }}, + ResponseBinary: false, + }, + { + ID: "deleteIssueCategory", + Group: "issue-category", + GroupAlias: []string{"issue-categories", "category", "categories"}, + Command: "delete", + Method: "DELETE", + Path: "/issue_categories/{issue_category_id}.{format}", + Summary: "Delete issue category", + Description: "", + PathParams: []Parameter{ + {Name: "issue_category_id", Flag: "issue-category-id", Placeholder: "ISSUE_CATEGORY_ID", Required: true, Type: "integer", Description: "", Enum: nil}, + }, + QueryParams: []Parameter{ + {Name: "reassign_to_id", Flag: "reassign-to-id", Placeholder: "REASSIGN_TO_ID", Required: false, Type: "integer", Description: "ID of another issue category to reassign issues to before deleting this category.\nIf the category has associated issues and this parameter is not provided, the issues will have their category unset.", Enum: nil}, + }, + HeaderParams: nil, + Body: nil, + ResponseBinary: false, + }, + { + ID: "getIssueCategories", + Group: "issue-category", + GroupAlias: []string{"issue-categories", "category", "categories"}, + Command: "list", + Method: "GET", + Path: "/projects/{project_id}/issue_categories.{format}", + Summary: "List issue categories", + Description: "", + PathParams: []Parameter{ + {Name: "project_id", Flag: "project-id", Placeholder: "PROJECT_ID", Required: true, Type: "integer|string", Description: "", Enum: nil}, + }, + QueryParams: []Parameter{ + {Name: "nometa", Flag: "nometa", Placeholder: "NOMETA", Required: false, Type: "integer", Description: "", Enum: []string{"1"}}, + }, + HeaderParams: []Parameter{ + {Name: "X-Redmine-Nometa", Flag: "x-redmine-nometa", Placeholder: "X_REDMINE_NOMETA", Required: false, Type: "integer", Description: "", Enum: []string{"1"}}, + }, + Body: nil, + ResponseBinary: false, + }, + { + ID: "createIssueCategory", + Group: "issue-category", + GroupAlias: []string{"issue-categories", "category", "categories"}, + Command: "create", + Method: "POST", + Path: "/projects/{project_id}/issue_categories.{format}", + Summary: "Create issue category", + Description: "", + PathParams: []Parameter{ + {Name: "project_id", Flag: "project-id", Placeholder: "PROJECT_ID", Required: true, Type: "integer|string", Description: "", Enum: nil}, + }, + QueryParams: nil, + HeaderParams: nil, + Body: &Body{ContentType: "application/json", Root: "issue_category", Binary: false, Fields: []BodyField{ + {Name: "assigned_to_id", Flag: "assigned-to-id", Required: false, Type: "integer", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "name", Flag: "name", Required: true, Type: "string", Array: false, Nullable: false, Description: "", Enum: nil}, + }}, + ResponseBinary: false, + }, + { + ID: "getRoles", + Group: "role", + GroupAlias: []string{"roles"}, + Command: "list", + Method: "GET", + Path: "/roles.{format}", + Summary: "List roles", + Description: "", + PathParams: nil, + QueryParams: nil, + HeaderParams: nil, + Body: nil, + ResponseBinary: false, + }, + { + ID: "getRole", + Group: "role", + GroupAlias: []string{"roles"}, + Command: "show", + Method: "GET", + Path: "/roles/{role_id}.{format}", + Summary: "Show role", + Description: "", + PathParams: []Parameter{ + {Name: "role_id", Flag: "role-id", Placeholder: "ROLE_ID", Required: true, Type: "integer", Description: "", Enum: nil}, + }, + QueryParams: nil, + HeaderParams: nil, + Body: nil, + ResponseBinary: false, + }, + { + ID: "getGroups", + Group: "group", + GroupAlias: []string{"groups"}, + Command: "list", + Method: "GET", + Path: "/groups.{format}", + Summary: "List groups", + Description: "", + PathParams: nil, + QueryParams: []Parameter{ + {Name: "builtin", Flag: "builtin", Placeholder: "BUILTIN", Required: false, Type: "string", Description: "Include built-in groups (e.g., \"Anonymous\", \"Non member\") in the response.\nSet to `1` to include built-in groups. Without this parameter, only user-created groups are returned.", Enum: []string{"1"}}, + }, + HeaderParams: nil, + Body: nil, + ResponseBinary: false, + }, + { + ID: "createGroup", + Group: "group", + GroupAlias: []string{"groups"}, + Command: "create", + Method: "POST", + Path: "/groups.{format}", + Summary: "Create group", + Description: "", + PathParams: nil, + QueryParams: nil, + HeaderParams: nil, + Body: &Body{ContentType: "application/json", Root: "group", Binary: false, Fields: []BodyField{ + {Name: "custom_field_values", Flag: "custom-field-values", Required: false, Type: "object", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "custom_fields", Flag: "custom-fields", Required: false, Type: "object", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "name", Flag: "name", Required: false, Type: "string", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "twofa_required", Flag: "twofa-required", Required: false, Type: "boolean", Array: false, Nullable: false, Description: "Require two-factor authentication for all members of this group.", Enum: nil}, + {Name: "user_ids", Flag: "user-ids", Required: false, Type: "integer", Array: true, Nullable: false, Description: "User IDs to add as members of this group.", Enum: nil}, + }}, + ResponseBinary: false, + }, + { + ID: "getGroup", + Group: "group", + GroupAlias: []string{"groups"}, + Command: "show", + Method: "GET", + Path: "/groups/{group_id}.{format}", + Summary: "Show group", + Description: "", + PathParams: []Parameter{ + {Name: "group_id", Flag: "group-id", Placeholder: "GROUP_ID", Required: true, Type: "integer", Description: "", Enum: nil}, + }, + QueryParams: []Parameter{ + {Name: "include", Flag: "include", Placeholder: "INCLUDE", Required: false, Type: "string", Description: "Comma-separated list of associated data to include in the response.\nValues: `users` (list of users in the group), `memberships` (project memberships with roles)", Enum: nil}, + }, + HeaderParams: nil, + Body: nil, + ResponseBinary: false, + }, + { + ID: "updateGroup", + Group: "group", + GroupAlias: []string{"groups"}, + Command: "update", + Method: "PUT", + Path: "/groups/{group_id}.{format}", + Summary: "Update group", + Description: "", + PathParams: []Parameter{ + {Name: "group_id", Flag: "group-id", Placeholder: "GROUP_ID", Required: true, Type: "integer", Description: "", Enum: nil}, + }, + QueryParams: nil, + HeaderParams: nil, + Body: &Body{ContentType: "application/json", Root: "", Binary: false, Fields: []BodyField{ + {Name: "group", Flag: "group", Required: false, Type: "object", Array: false, Nullable: false, Description: "", Enum: nil}, + }}, + ResponseBinary: false, + }, + { + ID: "deleteGroup", + Group: "group", + GroupAlias: []string{"groups"}, + Command: "delete", + Method: "DELETE", + Path: "/groups/{group_id}.{format}", + Summary: "Delete group", + Description: "", + PathParams: []Parameter{ + {Name: "group_id", Flag: "group-id", Placeholder: "GROUP_ID", Required: true, Type: "integer", Description: "", Enum: nil}, + }, + QueryParams: nil, + HeaderParams: nil, + Body: nil, + ResponseBinary: false, + }, + { + ID: "addUserToGroup", + Group: "group", + GroupAlias: []string{"groups"}, + Command: "add-user-to", + Method: "POST", + Path: "/groups/{group_id}/users.{format}", + Summary: "Add user to group", + Description: "", + PathParams: []Parameter{ + {Name: "group_id", Flag: "group-id", Placeholder: "GROUP_ID", Required: true, Type: "integer", Description: "", Enum: nil}, + }, + QueryParams: nil, + HeaderParams: nil, + Body: &Body{ContentType: "application/json", Root: "", Binary: false, Fields: []BodyField{ + {Name: "user_id", Flag: "user-id", Required: true, Type: "integer", Array: false, Nullable: false, Description: "", Enum: nil}, + }}, + ResponseBinary: false, + }, + { + ID: "removeUserFromGroup", + Group: "group", + GroupAlias: []string{"groups"}, + Command: "remove-user-from", + Method: "DELETE", + Path: "/groups/{group_id}/users/{user_id}.{format}", + Summary: "Remove user from group", + Description: "", + PathParams: []Parameter{ + {Name: "group_id", Flag: "group-id", Placeholder: "GROUP_ID", Required: true, Type: "integer", Description: "", Enum: nil}, + {Name: "user_id", Flag: "user-id", Placeholder: "USER_ID", Required: true, Type: "integer", Description: "", Enum: nil}, + }, + QueryParams: nil, + HeaderParams: nil, + Body: nil, + ResponseBinary: false, + }, + { + ID: "getCustomFields", + Group: "custom-field", + GroupAlias: []string{"custom-fields", "field", "fields"}, + Command: "list", + Method: "GET", + Path: "/custom_fields.{format}", + Summary: "List custom fields", + Description: "", + PathParams: nil, + QueryParams: nil, + HeaderParams: nil, + Body: nil, + ResponseBinary: false, + }, + { + ID: "getSearchByProject", + Group: "search", + GroupAlias: nil, + Command: "search-within-project", + Method: "GET", + Path: "/projects/{project_id}/search.{format}", + Summary: "Search within project", + Description: "", + PathParams: []Parameter{ + {Name: "project_id", Flag: "project-id", Placeholder: "PROJECT_ID", Required: true, Type: "integer|string", Description: "", Enum: nil}, + }, + QueryParams: []Parameter{ + {Name: "limit", Flag: "limit", Placeholder: "LIMIT", Required: false, Type: "integer", Description: "", Enum: nil}, + {Name: "offset", Flag: "offset", Placeholder: "OFFSET", Required: false, Type: "integer", Description: "", Enum: nil}, + {Name: "nometa", Flag: "nometa", Placeholder: "NOMETA", Required: false, Type: "integer", Description: "", Enum: []string{"1"}}, + {Name: "q", Flag: "q", Placeholder: "Q", Required: true, Type: "string", Description: "Search query string. Tokenized into words for matching.\nWhen `all_words` is enabled (default), all tokens must match. Otherwise, any token can match.", Enum: nil}, + {Name: "scope", Flag: "scope", Placeholder: "SCOPE", Required: false, Type: "string", Description: "Search scope.\nValues: `subprojects` (include subprojects of the specified project)\nWhen not specified, only the specified project is searched.", Enum: []string{"subprojects"}}, + {Name: "all_words", Flag: "all-words", Placeholder: "ALL_WORDS", Required: false, Type: "string", Description: "Require all words to match. Default is `true` (all words must match).\nSet to empty string to disable (any word matches). Any non-empty value enables this option.", Enum: nil}, + {Name: "titles_only", Flag: "titles-only", Placeholder: "TITLES_ONLY", Required: false, Type: "string", Description: "Search titles only. Default is `false`.\nAny non-empty value enables this option.", Enum: nil}, + {Name: "issues", Flag: "issues", Placeholder: "ISSUES", Required: false, Type: "integer", Description: "Include issues in search results. Any non-empty value enables this search type.\nIf no search type parameters are specified, all types are searched.", Enum: []string{"1"}}, + {Name: "news", Flag: "news", Placeholder: "NEWS", Required: false, Type: "integer", Description: "Include news in search results. Any non-empty value enables this search type.\nIf no search type parameters are specified, all types are searched.", Enum: []string{"1"}}, + {Name: "wiki_pages", Flag: "wiki-pages", Placeholder: "WIKI_PAGES", Required: false, Type: "integer", Description: "Include wiki pages in search results. Any non-empty value enables this search type.\nIf no search type parameters are specified, all types are searched.", Enum: []string{"1"}}, + {Name: "documents", Flag: "documents", Placeholder: "DOCUMENTS", Required: false, Type: "integer", Description: "Include documents in search results. Any non-empty value enables this search type.\nIf no search type parameters are specified, all types are searched.", Enum: []string{"1"}}, + {Name: "changesets", Flag: "changesets", Placeholder: "CHANGESETS", Required: false, Type: "integer", Description: "Include changesets in search results. Any non-empty value enables this search type.\nIf no search type parameters are specified, all types are searched.", Enum: []string{"1"}}, + {Name: "messages", Flag: "messages", Placeholder: "MESSAGES", Required: false, Type: "integer", Description: "Include forum messages in search results. Any non-empty value enables this search type.\nIf no search type parameters are specified, all types are searched.", Enum: []string{"1"}}, + {Name: "open_issues", Flag: "open-issues", Placeholder: "OPEN_ISSUES", Required: false, Type: "string", Description: "Only return open issues. Default is `false`.\nAny non-empty value enables this option.", Enum: nil}, + {Name: "attachments", Flag: "attachments", Placeholder: "ATTACHMENTS", Required: false, Type: "string", Description: "Search attachments. Default is `0` (do not search attachments).\nValues: `0` (disabled), `1` (search attachments and content), `only` (search attachments only)", Enum: []string{"0", "1", "only"}}, + }, + HeaderParams: []Parameter{ + {Name: "X-Redmine-Nometa", Flag: "x-redmine-nometa", Placeholder: "X_REDMINE_NOMETA", Required: false, Type: "integer", Description: "", Enum: []string{"1"}}, + }, + Body: nil, + ResponseBinary: false, + }, + { + ID: "getSearch", + Group: "search", + GroupAlias: nil, + Command: "run", + Method: "GET", + Path: "/search.{format}", + Summary: "Search", + Description: "", + PathParams: nil, + QueryParams: []Parameter{ + {Name: "limit", Flag: "limit", Placeholder: "LIMIT", Required: false, Type: "integer", Description: "", Enum: nil}, + {Name: "offset", Flag: "offset", Placeholder: "OFFSET", Required: false, Type: "integer", Description: "", Enum: nil}, + {Name: "nometa", Flag: "nometa", Placeholder: "NOMETA", Required: false, Type: "integer", Description: "", Enum: []string{"1"}}, + {Name: "q", Flag: "q", Placeholder: "Q", Required: true, Type: "string", Description: "Search query string. Tokenized into words for matching.\nWhen `all_words` is enabled (default), all tokens must match. Otherwise, any token can match.", Enum: nil}, + {Name: "scope", Flag: "scope", Placeholder: "SCOPE", Required: false, Type: "string", Description: "Search scope.\nValues: `all` (all projects), `my_projects` (projects where current user is a member), `bookmarks` (bookmarked projects), `subprojects` (include subprojects, only effective for project-scoped search)", Enum: []string{"all", "my_projects", "bookmarks", "subprojects"}}, + {Name: "all_words", Flag: "all-words", Placeholder: "ALL_WORDS", Required: false, Type: "string", Description: "Require all words to match. Default is `true` (all words must match).\nSet to empty string to disable (any word matches). Any non-empty value enables this option.", Enum: nil}, + {Name: "titles_only", Flag: "titles-only", Placeholder: "TITLES_ONLY", Required: false, Type: "string", Description: "Search titles only. Default is `false`.\nAny non-empty value enables this option.", Enum: nil}, + {Name: "issues", Flag: "issues", Placeholder: "ISSUES", Required: false, Type: "integer", Description: "Include issues in search results. Any non-empty value enables this search type.\nIf no search type parameters are specified, all types are searched.", Enum: []string{"1"}}, + {Name: "news", Flag: "news", Placeholder: "NEWS", Required: false, Type: "integer", Description: "Include news in search results. Any non-empty value enables this search type.\nIf no search type parameters are specified, all types are searched.", Enum: []string{"1"}}, + {Name: "wiki_pages", Flag: "wiki-pages", Placeholder: "WIKI_PAGES", Required: false, Type: "integer", Description: "Include wiki pages in search results. Any non-empty value enables this search type.\nIf no search type parameters are specified, all types are searched.", Enum: []string{"1"}}, + {Name: "projects", Flag: "projects", Placeholder: "PROJECTS", Required: false, Type: "integer", Description: "Include projects in search results. Any non-empty value enables this search type.\nIf no search type parameters are specified, all types are searched.\nNot available when searching within a specific project.", Enum: []string{"1"}}, + {Name: "documents", Flag: "documents", Placeholder: "DOCUMENTS", Required: false, Type: "integer", Description: "Include documents in search results. Any non-empty value enables this search type.\nIf no search type parameters are specified, all types are searched.", Enum: []string{"1"}}, + {Name: "changesets", Flag: "changesets", Placeholder: "CHANGESETS", Required: false, Type: "integer", Description: "Include changesets in search results. Any non-empty value enables this search type.\nIf no search type parameters are specified, all types are searched.", Enum: []string{"1"}}, + {Name: "messages", Flag: "messages", Placeholder: "MESSAGES", Required: false, Type: "integer", Description: "Include forum messages in search results. Any non-empty value enables this search type.\nIf no search type parameters are specified, all types are searched.", Enum: []string{"1"}}, + {Name: "open_issues", Flag: "open-issues", Placeholder: "OPEN_ISSUES", Required: false, Type: "string", Description: "Only return open issues. Default is `false`.\nAny non-empty value enables this option.", Enum: nil}, + {Name: "attachments", Flag: "attachments", Placeholder: "ATTACHMENTS", Required: false, Type: "string", Description: "Search attachments. Default is `0` (do not search attachments).\nValues: `0` (disabled), `1` (search attachments and content), `only` (search attachments only)", Enum: []string{"0", "1", "only"}}, + }, + HeaderParams: []Parameter{ + {Name: "X-Redmine-Nometa", Flag: "x-redmine-nometa", Placeholder: "X_REDMINE_NOMETA", Required: false, Type: "integer", Description: "", Enum: []string{"1"}}, + }, + Body: nil, + ResponseBinary: false, + }, + { + ID: "getFiles", + Group: "file", + GroupAlias: []string{"files"}, + Command: "list", + Method: "GET", + Path: "/projects/{project_id}/files.{format}", + Summary: "List files", + Description: "", + PathParams: []Parameter{ + {Name: "project_id", Flag: "project-id", Placeholder: "PROJECT_ID", Required: true, Type: "integer|string", Description: "", Enum: nil}, + }, + QueryParams: nil, + HeaderParams: nil, + Body: nil, + ResponseBinary: false, + }, + { + ID: "createFile", + Group: "file", + GroupAlias: []string{"files"}, + Command: "create", + Method: "POST", + Path: "/projects/{project_id}/files.{format}", + Summary: "Create file", + Description: "", + PathParams: []Parameter{ + {Name: "project_id", Flag: "project-id", Placeholder: "PROJECT_ID", Required: true, Type: "integer|string", Description: "", Enum: nil}, + }, + QueryParams: nil, + HeaderParams: nil, + Body: &Body{ContentType: "application/json", Root: "file", Binary: false, Fields: []BodyField{ + {Name: "content_type", Flag: "content-type", Required: false, Type: "string", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "description", Flag: "description", Required: false, Type: "string", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "filename", Flag: "filename", Required: false, Type: "string", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "token", Flag: "token", Required: true, Type: "string", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "version_id", Flag: "version-id", Required: false, Type: "integer", Array: false, Nullable: false, Description: "", Enum: nil}, + }}, + ResponseBinary: false, + }, + { + ID: "getMyAccount", + Group: "my-account", + GroupAlias: []string{"account"}, + Command: "show", + Method: "GET", + Path: "/my/account.{format}", + Summary: "Show my account", + Description: "", + PathParams: nil, + QueryParams: nil, + HeaderParams: nil, + Body: nil, + ResponseBinary: false, + }, + { + ID: "updateMyAccount", + Group: "my-account", + GroupAlias: []string{"account"}, + Command: "update", + Method: "PUT", + Path: "/my/account.{format}", + Summary: "Update my account", + Description: "", + PathParams: nil, + QueryParams: nil, + HeaderParams: nil, + Body: &Body{ContentType: "application/json", Root: "", Binary: false, Fields: []BodyField{ + {Name: "pref", Flag: "pref", Required: false, Type: "object", Array: false, Nullable: false, Description: "", Enum: nil}, + {Name: "user", Flag: "user", Required: false, Type: "object", Array: false, Nullable: false, Description: "", Enum: nil}, + }}, + ResponseBinary: false, + }, + { + ID: "updateJournal", + Group: "journal", + GroupAlias: []string{"journals"}, + Command: "update", + Method: "PUT", + Path: "/journals/{journal_id}.{format}", + Summary: "Update journal", + Description: "Undocumented: This operation is not listed in the official documentation.", + PathParams: []Parameter{ + {Name: "journal_id", Flag: "journal-id", Placeholder: "JOURNAL_ID", Required: true, Type: "integer", Description: "", Enum: nil}, + }, + QueryParams: nil, + HeaderParams: nil, + Body: &Body{ContentType: "application/json", Root: "", Binary: false, Fields: []BodyField{ + {Name: "journal", Flag: "journal", Required: false, Type: "object", Array: false, Nullable: false, Description: "", Enum: nil}, + }}, + ResponseBinary: false, + }, + { + ID: "addRelatedIssue", + Group: "repository", + GroupAlias: []string{"repositories"}, + Command: "add-related-issue", + Method: "POST", + Path: "/projects/{project_id}/repository/{repository_id}/revisions/{revision}/issues.{format}", + Summary: "Add related issue", + Description: "Undocumented: This operation is not listed in the official documentation.", + PathParams: []Parameter{ + {Name: "project_id", Flag: "project-id", Placeholder: "PROJECT_ID", Required: true, Type: "integer|string", Description: "", Enum: nil}, + {Name: "repository_id", Flag: "repository-id", Placeholder: "REPOSITORY_ID", Required: true, Type: "integer|string", Description: "", Enum: nil}, + {Name: "revision", Flag: "revision", Placeholder: "REVISION", Required: true, Type: "string", Description: "", Enum: nil}, + }, + QueryParams: nil, + HeaderParams: nil, + Body: &Body{ContentType: "application/json", Root: "", Binary: false, Fields: []BodyField{ + {Name: "issue_id", Flag: "issue-id", Required: true, Type: "integer", Array: false, Nullable: false, Description: "", Enum: nil}, + }}, + ResponseBinary: false, + }, + { + ID: "removeRelatedIssue", + Group: "repository", + GroupAlias: []string{"repositories"}, + Command: "remove-related-issue", + Method: "DELETE", + Path: "/projects/{project_id}/repository/{repository_id}/revisions/{revision}/issues/{issue_id}.{format}", + Summary: "Remove related issue", + Description: "Undocumented: This operation is not listed in the official documentation.", + PathParams: []Parameter{ + {Name: "project_id", Flag: "project-id", Placeholder: "PROJECT_ID", Required: true, Type: "integer|string", Description: "", Enum: nil}, + {Name: "repository_id", Flag: "repository-id", Placeholder: "REPOSITORY_ID", Required: true, Type: "integer|string", Description: "", Enum: nil}, + {Name: "revision", Flag: "revision", Placeholder: "REVISION", Required: true, Type: "string", Description: "", Enum: nil}, + {Name: "issue_id", Flag: "issue-id", Placeholder: "ISSUE_ID", Required: true, Type: "integer", Description: "", Enum: nil}, + }, + QueryParams: nil, + HeaderParams: nil, + Body: nil, + ResponseBinary: false, + }, +} diff --git a/internal/openapi/types.go b/internal/openapi/types.go new file mode 100644 index 0000000..2990360 --- /dev/null +++ b/internal/openapi/types.go @@ -0,0 +1,51 @@ +package openapi + +// Operation describes one Redmine API operation as command metadata. +type Operation struct { + ID string + Group string + GroupAlias []string + Command string + Method string + Path string + Summary string + Description string + + PathParams []Parameter + QueryParams []Parameter + HeaderParams []Parameter + Body *Body + + ResponseBinary bool +} + +// Parameter describes a generated command flag or positional path argument. +type Parameter struct { + Name string + Flag string + Placeholder string + Required bool + Type string + Description string + Enum []string +} + +// Body describes an operation request body. +type Body struct { + ContentType string + Root string + Binary bool + Fields []BodyField +} + +// BodyField describes a generated body flag. +type BodyField struct { + Name string + Flag string + Required bool + Type string + Array bool + Nullable bool + Description string + Enum []string +} diff --git a/internal/redmine/client.go b/internal/redmine/client.go new file mode 100644 index 0000000..9bf70cf --- /dev/null +++ b/internal/redmine/client.go @@ -0,0 +1,168 @@ +package redmine + +import ( + "bytes" + "context" + "crypto/tls" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "time" + + "github.com/muxx/redmine-cli/internal/openapi" +) + +const defaultTimeout = 30 * time.Second + +// Client executes generated Redmine API operations. +type Client struct { + BaseURL string + APIKey string + Username string + Password string + SwitchUser string + HTTPClient *http.Client +} + +// Request contains runtime values for an OpenAPI operation. +type Request struct { + Operation openapi.Operation + Path map[string]string + Query map[string][]string + Headers map[string]string + Body []byte +} + +// Response is an HTTP response snapshot. +type Response struct { + StatusCode int + Header http.Header + Body []byte + ContentType string +} + +// HTTPError describes a non-2xx API response. +type HTTPError struct { + StatusCode int + Body []byte +} + +func (e *HTTPError) Error() string { + body := strings.TrimSpace(string(e.Body)) + if body == "" { + return fmt.Sprintf("redmine API returned HTTP %d", e.StatusCode) + } + return fmt.Sprintf("redmine API returned HTTP %d: %s", e.StatusCode, body) +} + +// NewHTTPClient returns an HTTP client suitable for CLI use. +func NewHTTPClient(timeout time.Duration, insecure bool) *http.Client { + if timeout == 0 { + timeout = defaultTimeout + } + transport := http.DefaultTransport.(*http.Transport).Clone() + if insecure { + transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} //nolint:gosec + } + return &http.Client{Timeout: timeout, Transport: transport} +} + +// Do executes a generated operation. +func (c Client) Do(ctx context.Context, req Request) (Response, error) { + if c.BaseURL == "" { + return Response{}, fmt.Errorf("redmine host is not configured; set REDMINE_HOST or run `redmine auth login`") + } + endpoint, err := c.endpoint(req) + if err != nil { + return Response{}, err + } + + var body io.Reader + if len(req.Body) > 0 { + body = bytes.NewReader(req.Body) + } + httpReq, err := http.NewRequestWithContext(ctx, req.Operation.Method, endpoint, body) + if err != nil { + return Response{}, err + } + if req.Operation.ResponseBinary { + httpReq.Header.Set("Accept", "*/*") + } else { + httpReq.Header.Set("Accept", "application/json") + } + if len(req.Body) > 0 && req.Operation.Body != nil && req.Operation.Body.ContentType != "" { + httpReq.Header.Set("Content-Type", req.Operation.Body.ContentType) + } + for name, value := range req.Headers { + if value == "" { + continue + } + httpReq.Header.Set(name, value) + } + if c.SwitchUser != "" { + httpReq.Header.Set("X-Redmine-Switch-User", c.SwitchUser) + } + if c.APIKey != "" { + httpReq.Header.Set("X-Redmine-API-Key", c.APIKey) + } + if c.Username != "" || c.Password != "" { + httpReq.SetBasicAuth(c.Username, c.Password) + } + + httpClient := c.HTTPClient + if httpClient == nil { + httpClient = NewHTTPClient(defaultTimeout, false) + } + httpResp, err := httpClient.Do(httpReq) + if err != nil { + return Response{}, err + } + defer httpResp.Body.Close() + + data, err := io.ReadAll(httpResp.Body) + if err != nil { + return Response{}, err + } + resp := Response{ + StatusCode: httpResp.StatusCode, + Header: httpResp.Header.Clone(), + Body: data, + ContentType: httpResp.Header.Get("Content-Type"), + } + if httpResp.StatusCode < 200 || httpResp.StatusCode > 299 { + return resp, &HTTPError{StatusCode: httpResp.StatusCode, Body: data} + } + return resp, nil +} + +func (c Client) endpoint(req Request) (string, error) { + base, err := url.Parse(c.BaseURL) + if err != nil { + return "", err + } + if base.Scheme == "" || base.Host == "" { + return "", fmt.Errorf("redmine host must include scheme and host, got %q", c.BaseURL) + } + + path := req.Operation.Path + path = strings.ReplaceAll(path, "{format}", "json") + for _, param := range req.Operation.PathParams { + value, ok := req.Path[param.Name] + if !ok || value == "" { + return "", fmt.Errorf("missing path argument %s", param.Name) + } + path = strings.ReplaceAll(path, "{"+param.Name+"}", url.PathEscape(value)) + } + + base.Path = strings.TrimRight(base.Path, "/") + path + query := base.Query() + for name, values := range req.Query { + for _, value := range values { + query.Add(name, value) + } + } + base.RawQuery = query.Encode() + return base.String(), nil +} diff --git a/internal/redmine/client_test.go b/internal/redmine/client_test.go new file mode 100644 index 0000000..99b4768 --- /dev/null +++ b/internal/redmine/client_test.go @@ -0,0 +1,54 @@ +package redmine + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/muxx/redmine-cli/internal/openapi" +) + +func TestClientDoBuildsRequest(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + t.Fatalf("method = %s, want GET", r.Method) + } + if r.URL.Path != "/redmine/issues/42.json" { + t.Fatalf("path = %s", r.URL.Path) + } + if got := r.URL.Query()["include"]; len(got) != 2 || got[0] != "journals" || got[1] != "watchers" { + t.Fatalf("include query = %#v", got) + } + if got := r.Header.Get("X-Redmine-API-Key"); got != "secret" { + t.Fatalf("api key header = %q", got) + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"issue":{"id":42}}`)) + })) + defer server.Close() + + client := Client{ + BaseURL: server.URL + "/redmine", + APIKey: "secret", + HTTPClient: server.Client(), + } + resp, err := client.Do(context.Background(), Request{ + Operation: openapi.Operation{ + ID: "getIssue", + Method: http.MethodGet, + Path: "/issues/{issue_id}.{format}", + PathParams: []openapi.Parameter{{ + Name: "issue_id", + }}, + }, + Path: map[string]string{"issue_id": "42"}, + Query: map[string][]string{"include": {"journals", "watchers"}}, + }) + if err != nil { + t.Fatal(err) + } + if resp.StatusCode != http.StatusOK { + t.Fatalf("status = %d", resp.StatusCode) + } +} diff --git a/internal/redmine/output.go b/internal/redmine/output.go new file mode 100644 index 0000000..7b1aa2d --- /dev/null +++ b/internal/redmine/output.go @@ -0,0 +1,87 @@ +package redmine + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "strings" + + "gopkg.in/yaml.v3" +) + +const ( + OutputJSON = "json" + OutputYAML = "yaml" + OutputRaw = "raw" +) + +// WriteResponse renders an API response to the requested output format. +func WriteResponse(w io.Writer, resp Response, format string) error { + if len(resp.Body) == 0 { + return nil + } + if format == "" { + format = OutputJSON + } + + switch format { + case OutputRaw: + _, err := w.Write(resp.Body) + return err + case OutputYAML: + return writeYAML(w, resp.Body) + case OutputJSON: + return writeJSON(w, resp.Body) + default: + return fmt.Errorf("unsupported output format %q", format) + } +} + +func writeJSON(w io.Writer, data []byte) error { + var value any + if err := json.Unmarshal(data, &value); err != nil { + _, writeErr := w.Write(data) + return writeErr + } + encoded, err := json.MarshalIndent(value, "", " ") + if err != nil { + return err + } + _, err = w.Write(append(encoded, '\n')) + return err +} + +func writeYAML(w io.Writer, data []byte) error { + var value any + if err := json.Unmarshal(data, &value); err != nil { + _, writeErr := w.Write(data) + return writeErr + } + encoded, err := yaml.Marshal(normalizeYAML(value)) + if err != nil { + return err + } + _, err = io.Copy(w, bytes.NewReader(encoded)) + return err +} + +func normalizeYAML(value any) any { + switch typed := value.(type) { + case map[string]any: + result := map[string]any{} + for key, val := range typed { + result[key] = normalizeYAML(val) + } + return result + case []any: + for i, val := range typed { + typed[i] = normalizeYAML(val) + } + return typed + case string: + return strings.TrimRight(typed, "\n") + default: + return value + } +} diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml new file mode 100644 index 0000000..ff2fffd --- /dev/null +++ b/openapi/openapi.yaml @@ -0,0 +1,7569 @@ +openapi: 3.0.3 +info: + title: Redmine API + description: Unofficial OpenAPI specification for the Redmine API, tested against Redmine 6.1 + contact: + name: d-yoshi/redmine-openapi + url: https://github.com/d-yoshi/redmine-openapi + license: + name: MIT + url: https://github.com/d-yoshi/redmine-openapi/blob/main/LICENSE + version: 1.7.1+redmine.6.1 +servers: + - url: / +security: + - Basic: [] + - ApiKey: [] + - ApiKeyInQuery: [] + - OAuth2: [] +tags: + - name: Issues + description: 'Status: Stable, Availability: 1.0' + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Issues + - name: Projects + description: 'Status: Stable, Availability: 1.0' + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Projects + - name: Project Memberships + description: 'Status: Alpha, Availability: 1.4' + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Memberships + - name: Users + description: 'Status: Stable, Availability: 1.1' + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Users + - name: Time Entries + description: 'Status: Stable, Availability: 1.1' + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_TimeEntries + - name: News + description: 'Status: Prototype, Availability: 1.1' + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_News + - name: Issue Relations + description: 'Status: Alpha, Availability: 1.3' + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_IssueRelations + - name: Versions + description: 'Status: Alpha, Availability: 1.3' + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Versions + - name: Wiki Pages + description: 'Status: Alpha, Availability: 2.2' + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_WikiPages + - name: Queries + description: 'Status: Alpha, Availability: 1.3' + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Queries + - name: Attachments + description: 'Status: Beta, Availability: 1.3' + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Attachments + - name: Issue Statuses + description: 'Status: Alpha, Availability: 1.3' + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_IssueStatuses + - name: Trackers + description: 'Status: Alpha, Availability: 1.3' + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Trackers + - name: Enumerations + description: 'Status: Alpha, Availability: 2.2' + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Enumerations + - name: Issue Categories + description: 'Status: Alpha, Availability: 1.3' + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_IssueCategories + - name: Roles + description: 'Status: Alpha, Availability: 1.4' + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Roles + - name: Groups + description: 'Status: Alpha, Availability: 2.1' + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Groups + - name: Custom Fields + description: 'Status: Alpha, Availability: 2.4' + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_CustomFields + - name: Search + description: 'Status: Alpha, Availability: 3.3' + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Search + - name: Files + description: 'Status: Alpha, Availability: 3.4' + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Files + - name: My Account + description: 'Status: Alpha, Availability: 4.1' + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_MyAccount + - name: Journals + description: 'Status: Alpha, Availability: 5.0' + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_IssueJournals + - name: Repositories + description: Undocumented +externalDocs: + description: Redmine API Official Developer Guide + url: https://www.redmine.org/projects/redmine/wiki/Rest_api +paths: + /issues.{format}: + get: + tags: + - Issues + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Issues#Listing-issues + summary: List issues + operationId: getIssues + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/offset' + - $ref: '#/components/parameters/limit' + - $ref: '#/components/parameters/nometa' + - $ref: '#/components/parameters/x_redmine_switch_user' + - $ref: '#/components/parameters/x_redmine_nometa' + - name: sort + in: query + description: |- + Sort order. Comma-separated list of `field` or `field:desc`. Default direction is ascending. + Examples: `id:desc`, `status:desc,id` + schema: + type: string + example: id:desc + - name: include + in: query + description: |- + Comma-separated list of associated data to include for each issue. + Values: `attachments` (file attachments), `relations` (issue relations) + schema: + type: array + items: + type: string + enum: + - attachments + - relations + explode: false + - name: issue_id + in: query + description: |- + Filter by issue ID. + Format: `[operator]value` + Operators: `=` (default), `>=`, `<=`, `><` (between, pipe-separated), `*` (any), `!*` (none) + Examples: `1`, `>=100`, `><1|100` + schema: + type: string + - name: project_id + in: query + description: |- + Filter by project. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal) + Special values: `mine` (projects where current user is a member), `bookmarks` (bookmarked projects) + Examples: `1|2`, `mine`, `!mine` + schema: + type: string + - name: subproject_id + in: query + description: |- + Filter by subproject. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal), `*` (any), `!*` (none) + Examples: `1|2`, `*`, `!*` + schema: + type: string + - name: tracker_id + in: query + description: |- + Filter by tracker. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal), `ev` (has ever been), `!ev` (has never been), `cf` (changed from) + Examples: `1|2`, `!3` + schema: + type: string + - name: status_id + in: query + description: |- + Filter by issue status. + Format: `[operator]value[|value2|...]` + Operators: `o` (open), `=` (default), `!` (not equal), `c` (closed), `*` (any), `ev` (has ever been), `!ev` (has never been), `cf` (changed from) + Examples: `o`, `1|2`, `c`, `!3` + schema: + type: string + - name: priority_id + in: query + description: |- + Filter by priority. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal), `ev` (has ever been), `!ev` (has never been), `cf` (changed from) + Examples: `1|2`, `!3` + schema: + type: string + - name: assigned_to_id + in: query + description: |- + Filter by assignee. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal), `*` (any), `!*` (none), `ev` (has ever been), `!ev` (has never been), `cf` (changed from) + Special values: `me` (current user) + Examples: `1`, `me`, `!*`, `!me` + schema: + type: string + - name: author_id + in: query + description: |- + Filter by author. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal) + Special values: `me` (current user) + Examples: `1`, `me`, `!me` + schema: + type: string + - name: author.group + in: query + description: |- + Filter by author's group. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal) + Examples: `1|2` + schema: + type: string + - name: author.role + in: query + description: |- + Filter by author's role. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal) + Examples: `1|2` + schema: + type: string + - name: member_of_group + in: query + description: |- + Filter by assignee's group membership. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal), `*` (any), `!*` (none) + Examples: `1`, `*` + schema: + type: string + - name: assigned_to_role + in: query + description: |- + Filter by assignee's role. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal), `*` (any), `!*` (none) + Examples: `1`, `*` + schema: + type: string + - name: fixed_version_id + in: query + description: |- + Filter by target version. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal), `*` (any), `!*` (none), `ev` (has ever been), `!ev` (has never been), `cf` (changed from) + Examples: `1|2`, `!*` + schema: + type: string + - name: fixed_version.due_date + in: query + description: |- + Filter by target version's due date. + Format: `[value]` + Operators: `=` (on date), `>=` (on or after), `<=` (on or before), `><` (between, pipe-separated), `t` (today), `ld` (yesterday), `nd` (tomorrow), `w` (this week), `lw` (last week), `l2w` (last 2 weeks), `nw` (next week), `m` (this month), `lm` (last month), `nm` (next month), `y` (this year), `t-` (n days ago), `>t-` (less than n days ago), `t+` (in more than n days), `=2024-01-01`, `t`, `>t-7` + schema: + type: string + - name: fixed_version.status + in: query + description: |- + Filter by target version's status. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal) + Values: `open`, `locked`, `closed` + Examples: `open`, `open|locked`, `!closed` + schema: + type: string + - name: category_id + in: query + description: |- + Filter by category. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal), `*` (any), `!*` (none), `ev` (has ever been), `!ev` (has never been), `cf` (changed from) + Examples: `1|2`, `!*` + schema: + type: string + - name: parent_id + in: query + description: |- + Filter by parent issue. + Format: `[operator]value` + Operators: `=` (exact parent ID), `~` (descendant of, subtree search), `*` (any, i.e. is a subtask), `!*` (none, i.e. is a root issue) + Examples: `123`, `~123`, `*`, `!*` + schema: + type: string + - name: child_id + in: query + description: |- + Filter by child issue. + Format: `[operator]value` + Operators: `=` (exact child ID), `~` (ancestor of, subtree search), `*` (any, i.e. has subtasks), `!*` (none, i.e. is a leaf issue) + Examples: `123`, `~123`, `*`, `!*` + schema: + type: string + - name: cf_x + in: query + description: |- + Filter by custom field value. Replace `x` with the custom field ID (e.g., `cf_1`, `cf_12`). + Format and operators depend on the custom field type (string, int, date, list, etc.) and follow the same conventions as other filter parameters. + Examples: `cf_1=~keyword`, `cf_5=1|2`, `cf_10=>=2024-01-01` + schema: + type: object + additionalProperties: + type: string + example: + cf_0: string + - name: subject + in: query + description: |- + Filter by subject. + Format: `value` + Operators: `~` (contains), `!~` (not contains), `*~` (contains any word), `^` (starts with), `$` (ends with), `*` (any), `!*` (none) + Examples: `~keyword`, `^prefix` + schema: + type: string + - name: description + in: query + description: |- + Filter by description. + Format: `value` + Operators: `~` (contains), `!~` (not contains), `*~` (contains any word), `^` (starts with), `$` (ends with), `*` (any), `!*` (none) + Examples: `~keyword`, `!~excluded` + schema: + type: string + - name: notes + in: query + description: |- + Filter by journal notes. + Format: `value` + Operators: `~` (contains), `!~` (not contains), `*~` (contains any word), `^` (starts with), `$` (ends with), `*` (any), `!*` (none) + Examples: `~keyword` + schema: + type: string + - name: created_on + in: query + description: |- + Filter by creation date. + Format: `[value]` + Operators: `=` (on date), `>=` (on or after), `<=` (on or before), `><` (between, pipe-separated), `t` (today), `ld` (yesterday), `w` (this week), `lw` (last week), `l2w` (last 2 weeks), `m` (this month), `lm` (last month), `y` (this year), `t-` (n days ago), `>t-` (less than n days ago), `=2024-01-01`, `t`, `>t-7`, `lm` + schema: + type: string + - name: updated_on + in: query + description: |- + Filter by last updated date. + Format: `[value]` + Operators: `=` (on date), `>=` (on or after), `<=` (on or before), `><` (between, pipe-separated), `t` (today), `ld` (yesterday), `w` (this week), `lw` (last week), `l2w` (last 2 weeks), `m` (this month), `lm` (last month), `y` (this year), `t-` (n days ago), `>t-` (less than n days ago), `=2024-01-01`, `t`, `>t-7`, `lm` + schema: + type: string + - name: closed_on + in: query + description: |- + Filter by closed date. + Format: `[value]` + Operators: `=` (on date), `>=` (on or after), `<=` (on or before), `><` (between, pipe-separated), `t` (today), `ld` (yesterday), `w` (this week), `lw` (last week), `l2w` (last 2 weeks), `m` (this month), `lm` (last month), `y` (this year), `t-` (n days ago), `>t-` (less than n days ago), `=2024-01-01`, `t`, `>t-7`, `lm` + schema: + type: string + - name: start_date + in: query + description: |- + Filter by start date. + Format: `[value]` + Operators: `=` (on date), `>=` (on or after), `<=` (on or before), `><` (between, pipe-separated), `t` (today), `ld` (yesterday), `nd` (tomorrow), `w` (this week), `lw` (last week), `l2w` (last 2 weeks), `nw` (next week), `m` (this month), `lm` (last month), `nm` (next month), `y` (this year), `t-` (n days ago), `>t-` (less than n days ago), `t+` (in more than n days), `=2024-01-01`, `t`, `>t-7` + schema: + type: string + - name: due_date + in: query + description: |- + Filter by due date. + Format: `[value]` + Operators: `=` (on date), `>=` (on or after), `<=` (on or before), `><` (between, pipe-separated), `t` (today), `ld` (yesterday), `nd` (tomorrow), `w` (this week), `lw` (last week), `l2w` (last 2 weeks), `nw` (next week), `m` (this month), `lm` (last month), `nm` (next month), `y` (this year), `t-` (n days ago), `>t-` (less than n days ago), `t+` (in more than n days), `=2024-01-01`, `t`, `>t-7` + schema: + type: string + - name: estimated_hours + in: query + description: |- + Filter by estimated hours. + Format: `[operator]value` + Operators: `=` (default), `>=`, `<=`, `><` (between, pipe-separated), `*` (any), `!*` (none) + Examples: `>=5`, `><1|10`, `!*` + schema: + type: string + - name: spent_time + in: query + description: |- + Filter by total spent time. + Format: `[operator]value` + Operators: `=` (default), `>=`, `<=`, `><` (between, pipe-separated), `*` (any), `!*` (none) + Examples: `>=5`, `><1|10`, `!*` + schema: + type: string + - name: done_ratio + in: query + description: |- + Filter by done ratio (percentage). + Format: `[operator]value` + Operators: `=` (default), `>=`, `<=`, `><` (between, pipe-separated), `*` (any), `!*` (none) + Examples: `>=50`, `><0|99`, `100` + schema: + type: string + - name: is_private + in: query + description: |- + Filter by private flag. + Format: `[operator]value` + Operators: `=` (default), `!` (not equal) + Values: `1` (private), `0` (not private) + Examples: `1`, `0` + schema: + type: string + - name: attachment + in: query + description: |- + Filter by attachment filename. + Format: `value` + Operators: `~` (contains), `!~` (not contains), `*~` (contains any word), `^` (starts with), `$` (ends with), `*` (any), `!*` (none) + Examples: `~report`, `*.pdf` + schema: + type: string + - name: attachment_description + in: query + description: |- + Filter by attachment description. + Format: `value` + Operators: `~` (contains), `!~` (not contains), `*~` (contains any word), `^` (starts with), `$` (ends with), `*` (any), `!*` (none) + Examples: `~screenshot` + schema: + type: string + - name: watcher_id + in: query + description: |- + Filter by watcher. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal) + Special values: `me` (current user) + Examples: `1`, `me`, `!me` + schema: + type: string + - name: updated_by + in: query + description: |- + Filter by user who updated the issue. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal) + Special values: `me` (current user) + Examples: `1`, `me` + schema: + type: string + - name: last_updated_by + in: query + description: |- + Filter by user who last updated the issue. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal) + Special values: `me` (current user) + Examples: `1`, `me` + schema: + type: string + - name: project.status + in: query + description: |- + Filter by project status. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal) + Values: `1` (active), `5` (closed) + Examples: `1`, `!5` + schema: + type: string + - name: relates + in: query + description: |- + Filter by "relates to" relation. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal), `=p` (same project), `=!p` (different project), `!p` (not in project), `*o` (related to open issue), `!o` (related to closed issue), `*` (any), `!*` (none) + Examples: `123`, `=p`, `*o`, `*` + schema: + type: string + - name: duplicates + in: query + description: |- + Filter by "duplicates" relation. + Format: `[operator]value[|value2|...]` + Operators: `=`, `!`, `=p`, `=!p`, `!p`, `*o`, `!o`, `*`, `!*` + Examples: `123`, `*` + schema: + type: string + - name: duplicated + in: query + description: |- + Filter by "duplicated by" relation. + Format: `[operator]value[|value2|...]` + Operators: `=`, `!`, `=p`, `=!p`, `!p`, `*o`, `!o`, `*`, `!*` + Examples: `123`, `*` + schema: + type: string + - name: blocks + in: query + description: |- + Filter by "blocks" relation. + Format: `[operator]value[|value2|...]` + Operators: `=`, `!`, `=p`, `=!p`, `!p`, `*o`, `!o`, `*`, `!*` + Examples: `123`, `*o` + schema: + type: string + - name: blocked + in: query + description: |- + Filter by "blocked by" relation. + Format: `[operator]value[|value2|...]` + Operators: `=`, `!`, `=p`, `=!p`, `!p`, `*o`, `!o`, `*`, `!*` + Examples: `123`, `*o` + schema: + type: string + - name: precedes + in: query + description: |- + Filter by "precedes" relation. + Format: `[operator]value[|value2|...]` + Operators: `=`, `!`, `=p`, `=!p`, `!p`, `*o`, `!o`, `*`, `!*` + Examples: `123`, `*` + schema: + type: string + - name: follows + in: query + description: |- + Filter by "follows" relation. + Format: `[operator]value[|value2|...]` + Operators: `=`, `!`, `=p`, `=!p`, `!p`, `*o`, `!o`, `*`, `!*` + Examples: `123`, `*` + schema: + type: string + - name: copied_to + in: query + description: |- + Filter by "copied to" relation. + Format: `[operator]value[|value2|...]` + Operators: `=`, `!`, `=p`, `=!p`, `!p`, `*o`, `!o`, `*`, `!*` + Examples: `123`, `*` + schema: + type: string + - name: copied_from + in: query + description: |- + Filter by "copied from" relation. + Format: `[operator]value[|value2|...]` + Operators: `=`, `!`, `=p`, `=!p`, `!p`, `*o`, `!o`, `*`, `!*` + Examples: `123`, `*` + schema: + type: string + - name: any_searchable + in: query + description: |- + Full-text search across all searchable fields. + Format: `value` + Operators: `~` (contains all words), `*~` (contains any word), `!~` (not contains) + Examples: `~keyword`, `*~foo bar` + schema: + type: string + - name: query_id + in: query + description: |- + ID of a saved query to apply. When specified, the saved query's filter criteria are used. + Other filter parameters in the same request are ignored. + Use GET /queries.json to retrieve available saved queries. + schema: + type: integer + responses: + '200': + description: '' + content: + application/json: + schema: + type: object + required: + - issues + properties: + issues: + type: array + items: + $ref: '#/components/schemas/issue.simple' + total_count: + type: integer + offset: + type: integer + limit: + type: integer + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '412': + $ref: '#/components/responses/PreconditionFailed' + post: + tags: + - Issues + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Issues#Creating-an-issue + summary: Create issue + operationId: createIssue + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/x_redmine_switch_user' + requestBody: + content: + application/json: + schema: + type: object + required: + - issue + properties: + issue: + type: object + required: + - project_id + - subject + properties: + project_id: + oneOf: + - type: integer + - type: string + tracker_id: + type: integer + status_id: + type: integer + priority_id: + type: integer + subject: + type: string + description: + type: string + nullable: true + start_date: + type: string + format: date + nullable: true + due_date: + type: string + format: date + nullable: true + done_ratio: + type: integer + minimum: 0 + maximum: 100 + category_id: + type: integer + nullable: true + fixed_version_id: + type: integer + nullable: true + assigned_to_id: + type: integer + nullable: true + parent_issue_id: + type: integer + nullable: true + custom_fields: + $ref: '#/components/schemas/custom_fields' + custom_field_values: + $ref: '#/components/schemas/custom_field_values' + watcher_user_ids: + type: array + items: + type: integer + is_private: + type: boolean + estimated_hours: + type: number + nullable: true + uploads: + $ref: '#/components/schemas/uploads' + responses: + '201': + description: '' + content: + application/json: + schema: + type: object + required: + - issue + properties: + issue: + $ref: '#/components/schemas/issue.simple' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '412': + $ref: '#/components/responses/PreconditionFailed' + '422': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/errors' + /issues/{issue_id}.{format}: + get: + tags: + - Issues + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Issues#Showing-an-issue + summary: Show issue + operationId: getIssue + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/issue_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + - name: include + in: query + description: |- + Comma-separated list of associated data to include in the response. + Values: `children` (child issues), `attachments` (file attachments), `relations` (issue relations), `changesets` (associated VCS changesets), `journals` (change history and notes), `watchers` (users watching the issue), `allowed_statuses` (status transitions available for the current user) + schema: + type: array + items: + type: string + enum: + - children + - attachments + - relations + - changesets + - journals + - watchers + - allowed_statuses + explode: false + responses: + '200': + description: '' + content: + application/json: + schema: + type: object + required: + - issue + properties: + issue: + $ref: '#/components/schemas/issue' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + put: + tags: + - Issues + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Issues#Updating-an-issue + summary: Update issue + operationId: updateIssue + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/issue_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + requestBody: + content: + application/json: + schema: + type: object + properties: + issue: + type: object + properties: + project_id: + oneOf: + - type: integer + - type: string + tracker_id: + type: integer + status_id: + type: integer + priority_id: + type: integer + subject: + type: string + description: + type: string + nullable: true + start_date: + type: string + format: date + nullable: true + due_date: + type: string + format: date + nullable: true + done_ratio: + type: integer + minimum: 0 + maximum: 100 + category_id: + type: integer + nullable: true + fixed_version_id: + type: integer + nullable: true + assigned_to_id: + type: integer + nullable: true + parent_issue_id: + type: integer + nullable: true + custom_fields: + $ref: '#/components/schemas/custom_fields' + custom_field_values: + $ref: '#/components/schemas/custom_field_values' + is_private: + type: boolean + estimated_hours: + type: number + nullable: true + notes: + type: string + private_notes: + description: Mark the notes as private. Only visible to users with the "View private notes" permission. + type: boolean + deleted_attachment_ids: + description: IDs of existing attachments to remove from this issue. + type: array + items: + type: integer + uploads: + $ref: '#/components/schemas/uploads' + responses: + '204': + description: '' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + '422': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/errors' + delete: + tags: + - Issues + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Issues#Deleting-an-issue + summary: Delete issue + operationId: deleteIssue + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/issue_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + responses: + '204': + description: '' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + /projects/{project_id}/issues.{format}: + get: + tags: + - Issues + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Issues#Listing-issues + summary: List issues by project + operationId: getIssuesByProject + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/project_id' + - $ref: '#/components/parameters/offset' + - $ref: '#/components/parameters/limit' + - $ref: '#/components/parameters/nometa' + - $ref: '#/components/parameters/x_redmine_switch_user' + - $ref: '#/components/parameters/x_redmine_nometa' + - name: sort + in: query + description: |- + Sort order. Comma-separated list of `field` or `field:desc`. Default direction is ascending. + Examples: `id:desc`, `status:desc,id` + schema: + type: string + example: id:desc + - name: include + in: query + description: |- + Comma-separated list of associated data to include for each issue. + Values: `attachments` (file attachments), `relations` (issue relations) + schema: + type: array + items: + type: string + enum: + - attachments + - relations + explode: false + - name: issue_id + in: query + description: |- + Filter by issue ID. + Format: `[operator]value` + Operators: `=` (default), `>=`, `<=`, `><` (between, pipe-separated), `*` (any), `!*` (none) + Examples: `1`, `>=100`, `><1|100` + schema: + type: string + - name: subproject_id + in: query + description: |- + Filter by subproject. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal), `*` (any), `!*` (none) + Examples: `1|2`, `*`, `!*` + schema: + type: string + - name: tracker_id + in: query + description: |- + Filter by tracker. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal), `ev` (has ever been), `!ev` (has never been), `cf` (changed from) + Examples: `1|2`, `!3` + schema: + type: string + - name: status_id + in: query + description: |- + Filter by issue status. + Format: `[operator]value[|value2|...]` + Operators: `o` (open), `=` (default), `!` (not equal), `c` (closed), `*` (any), `ev` (has ever been), `!ev` (has never been), `cf` (changed from) + Examples: `o`, `1|2`, `c`, `!3` + schema: + type: string + - name: priority_id + in: query + description: |- + Filter by priority. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal), `ev` (has ever been), `!ev` (has never been), `cf` (changed from) + Examples: `1|2`, `!3` + schema: + type: string + - name: assigned_to_id + in: query + description: |- + Filter by assignee. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal), `*` (any), `!*` (none), `ev` (has ever been), `!ev` (has never been), `cf` (changed from) + Special values: `me` (current user) + Examples: `1`, `me`, `!*`, `!me` + schema: + type: string + - name: author_id + in: query + description: |- + Filter by author. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal) + Special values: `me` (current user) + Examples: `1`, `me`, `!me` + schema: + type: string + - name: author.group + in: query + description: |- + Filter by author's group. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal) + Examples: `1|2` + schema: + type: string + - name: author.role + in: query + description: |- + Filter by author's role. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal) + Examples: `1|2` + schema: + type: string + - name: member_of_group + in: query + description: |- + Filter by assignee's group membership. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal), `*` (any), `!*` (none) + Examples: `1`, `*` + schema: + type: string + - name: assigned_to_role + in: query + description: |- + Filter by assignee's role. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal), `*` (any), `!*` (none) + Examples: `1`, `*` + schema: + type: string + - name: fixed_version_id + in: query + description: |- + Filter by target version. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal), `*` (any), `!*` (none), `ev` (has ever been), `!ev` (has never been), `cf` (changed from) + Examples: `1|2`, `!*` + schema: + type: string + - name: fixed_version.due_date + in: query + description: |- + Filter by target version's due date. + Format: `[value]` + Operators: `=` (on date), `>=` (on or after), `<=` (on or before), `><` (between, pipe-separated), `t` (today), `ld` (yesterday), `nd` (tomorrow), `w` (this week), `lw` (last week), `l2w` (last 2 weeks), `nw` (next week), `m` (this month), `lm` (last month), `nm` (next month), `y` (this year), `t-` (n days ago), `>t-` (less than n days ago), `t+` (in more than n days), `=2024-01-01`, `t`, `>t-7` + schema: + type: string + - name: fixed_version.status + in: query + description: |- + Filter by target version's status. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal) + Values: `open`, `locked`, `closed` + Examples: `open`, `open|locked`, `!closed` + schema: + type: string + - name: category_id + in: query + description: |- + Filter by category. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal), `*` (any), `!*` (none), `ev` (has ever been), `!ev` (has never been), `cf` (changed from) + Examples: `1|2`, `!*` + schema: + type: string + - name: parent_id + in: query + description: |- + Filter by parent issue. + Format: `[operator]value` + Operators: `=` (exact parent ID), `~` (descendant of, subtree search), `*` (any, i.e. is a subtask), `!*` (none, i.e. is a root issue) + Examples: `123`, `~123`, `*`, `!*` + schema: + type: string + - name: child_id + in: query + description: |- + Filter by child issue. + Format: `[operator]value` + Operators: `=` (exact child ID), `~` (ancestor of, subtree search), `*` (any, i.e. has subtasks), `!*` (none, i.e. is a leaf issue) + Examples: `123`, `~123`, `*`, `!*` + schema: + type: string + - name: cf_x + in: query + description: |- + Filter by custom field value. Replace `x` with the custom field ID (e.g., `cf_1`, `cf_12`). + Format and operators depend on the custom field type (string, int, date, list, etc.) and follow the same conventions as other filter parameters. + Examples: `cf_1=~keyword`, `cf_5=1|2`, `cf_10=>=2024-01-01` + schema: + type: object + additionalProperties: + type: string + example: + cf_0: string + - name: subject + in: query + description: |- + Filter by subject. + Format: `value` + Operators: `~` (contains), `!~` (not contains), `*~` (contains any word), `^` (starts with), `$` (ends with), `*` (any), `!*` (none) + Examples: `~keyword`, `^prefix` + schema: + type: string + - name: description + in: query + description: |- + Filter by description. + Format: `value` + Operators: `~` (contains), `!~` (not contains), `*~` (contains any word), `^` (starts with), `$` (ends with), `*` (any), `!*` (none) + Examples: `~keyword`, `!~excluded` + schema: + type: string + - name: notes + in: query + description: |- + Filter by journal notes. + Format: `value` + Operators: `~` (contains), `!~` (not contains), `*~` (contains any word), `^` (starts with), `$` (ends with), `*` (any), `!*` (none) + Examples: `~keyword` + schema: + type: string + - name: created_on + in: query + description: |- + Filter by creation date. + Format: `[value]` + Operators: `=` (on date), `>=` (on or after), `<=` (on or before), `><` (between, pipe-separated), `t` (today), `ld` (yesterday), `w` (this week), `lw` (last week), `l2w` (last 2 weeks), `m` (this month), `lm` (last month), `y` (this year), `t-` (n days ago), `>t-` (less than n days ago), `=2024-01-01`, `t`, `>t-7`, `lm` + schema: + type: string + - name: updated_on + in: query + description: |- + Filter by last updated date. + Format: `[value]` + Operators: `=` (on date), `>=` (on or after), `<=` (on or before), `><` (between, pipe-separated), `t` (today), `ld` (yesterday), `w` (this week), `lw` (last week), `l2w` (last 2 weeks), `m` (this month), `lm` (last month), `y` (this year), `t-` (n days ago), `>t-` (less than n days ago), `=2024-01-01`, `t`, `>t-7`, `lm` + schema: + type: string + - name: closed_on + in: query + description: |- + Filter by closed date. + Format: `[value]` + Operators: `=` (on date), `>=` (on or after), `<=` (on or before), `><` (between, pipe-separated), `t` (today), `ld` (yesterday), `w` (this week), `lw` (last week), `l2w` (last 2 weeks), `m` (this month), `lm` (last month), `y` (this year), `t-` (n days ago), `>t-` (less than n days ago), `=2024-01-01`, `t`, `>t-7`, `lm` + schema: + type: string + - name: start_date + in: query + description: |- + Filter by start date. + Format: `[value]` + Operators: `=` (on date), `>=` (on or after), `<=` (on or before), `><` (between, pipe-separated), `t` (today), `ld` (yesterday), `nd` (tomorrow), `w` (this week), `lw` (last week), `l2w` (last 2 weeks), `nw` (next week), `m` (this month), `lm` (last month), `nm` (next month), `y` (this year), `t-` (n days ago), `>t-` (less than n days ago), `t+` (in more than n days), `=2024-01-01`, `t`, `>t-7` + schema: + type: string + - name: due_date + in: query + description: |- + Filter by due date. + Format: `[value]` + Operators: `=` (on date), `>=` (on or after), `<=` (on or before), `><` (between, pipe-separated), `t` (today), `ld` (yesterday), `nd` (tomorrow), `w` (this week), `lw` (last week), `l2w` (last 2 weeks), `nw` (next week), `m` (this month), `lm` (last month), `nm` (next month), `y` (this year), `t-` (n days ago), `>t-` (less than n days ago), `t+` (in more than n days), `=2024-01-01`, `t`, `>t-7` + schema: + type: string + - name: estimated_hours + in: query + description: |- + Filter by estimated hours. + Format: `[operator]value` + Operators: `=` (default), `>=`, `<=`, `><` (between, pipe-separated), `*` (any), `!*` (none) + Examples: `>=5`, `><1|10`, `!*` + schema: + type: string + - name: spent_time + in: query + description: |- + Filter by total spent time. + Format: `[operator]value` + Operators: `=` (default), `>=`, `<=`, `><` (between, pipe-separated), `*` (any), `!*` (none) + Examples: `>=5`, `><1|10`, `!*` + schema: + type: string + - name: done_ratio + in: query + description: |- + Filter by done ratio (percentage). + Format: `[operator]value` + Operators: `=` (default), `>=`, `<=`, `><` (between, pipe-separated), `*` (any), `!*` (none) + Examples: `>=50`, `><0|99`, `100` + schema: + type: string + - name: is_private + in: query + description: |- + Filter by private flag. + Format: `[operator]value` + Operators: `=` (default), `!` (not equal) + Values: `1` (private), `0` (not private) + Examples: `1`, `0` + schema: + type: string + - name: attachment + in: query + description: |- + Filter by attachment filename. + Format: `value` + Operators: `~` (contains), `!~` (not contains), `*~` (contains any word), `^` (starts with), `$` (ends with), `*` (any), `!*` (none) + Examples: `~report`, `*.pdf` + schema: + type: string + - name: attachment_description + in: query + description: |- + Filter by attachment description. + Format: `value` + Operators: `~` (contains), `!~` (not contains), `*~` (contains any word), `^` (starts with), `$` (ends with), `*` (any), `!*` (none) + Examples: `~screenshot` + schema: + type: string + - name: watcher_id + in: query + description: |- + Filter by watcher. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal) + Special values: `me` (current user) + Examples: `1`, `me`, `!me` + schema: + type: string + - name: updated_by + in: query + description: |- + Filter by user who updated the issue. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal) + Special values: `me` (current user) + Examples: `1`, `me` + schema: + type: string + - name: last_updated_by + in: query + description: |- + Filter by user who last updated the issue. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal) + Special values: `me` (current user) + Examples: `1`, `me` + schema: + type: string + - name: project.status + in: query + description: |- + Filter by project status. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal) + Values: `1` (active), `5` (closed) + Examples: `1`, `!5` + schema: + type: string + - name: relates + in: query + description: |- + Filter by "relates to" relation. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal), `=p` (same project), `=!p` (different project), `!p` (not in project), `*o` (related to open issue), `!o` (related to closed issue), `*` (any), `!*` (none) + Examples: `123`, `=p`, `*o`, `*` + schema: + type: string + - name: duplicates + in: query + description: |- + Filter by "duplicates" relation. + Format: `[operator]value[|value2|...]` + Operators: `=`, `!`, `=p`, `=!p`, `!p`, `*o`, `!o`, `*`, `!*` + Examples: `123`, `*` + schema: + type: string + - name: duplicated + in: query + description: |- + Filter by "duplicated by" relation. + Format: `[operator]value[|value2|...]` + Operators: `=`, `!`, `=p`, `=!p`, `!p`, `*o`, `!o`, `*`, `!*` + Examples: `123`, `*` + schema: + type: string + - name: blocks + in: query + description: |- + Filter by "blocks" relation. + Format: `[operator]value[|value2|...]` + Operators: `=`, `!`, `=p`, `=!p`, `!p`, `*o`, `!o`, `*`, `!*` + Examples: `123`, `*o` + schema: + type: string + - name: blocked + in: query + description: |- + Filter by "blocked by" relation. + Format: `[operator]value[|value2|...]` + Operators: `=`, `!`, `=p`, `=!p`, `!p`, `*o`, `!o`, `*`, `!*` + Examples: `123`, `*o` + schema: + type: string + - name: precedes + in: query + description: |- + Filter by "precedes" relation. + Format: `[operator]value[|value2|...]` + Operators: `=`, `!`, `=p`, `=!p`, `!p`, `*o`, `!o`, `*`, `!*` + Examples: `123`, `*` + schema: + type: string + - name: follows + in: query + description: |- + Filter by "follows" relation. + Format: `[operator]value[|value2|...]` + Operators: `=`, `!`, `=p`, `=!p`, `!p`, `*o`, `!o`, `*`, `!*` + Examples: `123`, `*` + schema: + type: string + - name: copied_to + in: query + description: |- + Filter by "copied to" relation. + Format: `[operator]value[|value2|...]` + Operators: `=`, `!`, `=p`, `=!p`, `!p`, `*o`, `!o`, `*`, `!*` + Examples: `123`, `*` + schema: + type: string + - name: copied_from + in: query + description: |- + Filter by "copied from" relation. + Format: `[operator]value[|value2|...]` + Operators: `=`, `!`, `=p`, `=!p`, `!p`, `*o`, `!o`, `*`, `!*` + Examples: `123`, `*` + schema: + type: string + - name: any_searchable + in: query + description: |- + Full-text search across all searchable fields. + Format: `value` + Operators: `~` (contains all words), `*~` (contains any word), `!~` (not contains) + Examples: `~keyword`, `*~foo bar` + schema: + type: string + - name: query_id + in: query + description: |- + ID of a saved query to apply. When specified, the saved query's filter criteria are used. + Other filter parameters in the same request are ignored. + Use GET /queries.json to retrieve available saved queries. + schema: + type: integer + responses: + '200': + description: '' + content: + application/json: + schema: + type: object + required: + - issues + properties: + issues: + type: array + items: + $ref: '#/components/schemas/issue.simple' + total_count: + type: integer + offset: + type: integer + limit: + type: integer + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + post: + tags: + - Issues + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Issues#Creating-an-issue + summary: Create issue under project + operationId: createIssueUnderProject + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/project_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + requestBody: + content: + application/json: + schema: + type: object + required: + - issue + properties: + issue: + type: object + required: + - subject + properties: + tracker_id: + type: integer + status_id: + type: integer + priority_id: + type: integer + subject: + type: string + description: + type: string + nullable: true + start_date: + type: string + format: date + nullable: true + due_date: + type: string + format: date + nullable: true + done_ratio: + type: integer + minimum: 0 + maximum: 100 + category_id: + type: integer + nullable: true + fixed_version_id: + type: integer + nullable: true + assigned_to_id: + type: integer + nullable: true + parent_issue_id: + type: integer + nullable: true + custom_fields: + $ref: '#/components/schemas/custom_fields' + custom_field_values: + $ref: '#/components/schemas/custom_field_values' + watcher_user_ids: + type: array + items: + type: integer + is_private: + type: boolean + estimated_hours: + type: number + nullable: true + uploads: + $ref: '#/components/schemas/uploads' + responses: + '201': + description: '' + content: + application/json: + schema: + type: object + required: + - issue + properties: + issue: + $ref: '#/components/schemas/issue.simple' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + '422': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/errors' + /issues/{issue_id}/watchers.{format}: + post: + tags: + - Issues + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Issues#Adding-a-watcher + summary: Add watcher + operationId: addWatcher + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/issue_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + requestBody: + content: + application/json: + schema: + type: object + required: + - user_id + properties: + user_id: + type: integer + responses: + '204': + description: '' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + /issues/{issue_id}/watchers/{user_id}.{format}: + delete: + tags: + - Issues + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Issues#Removing-a-watcher + summary: Remove watcher + operationId: removeWatcher + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/issue_id' + - $ref: '#/components/parameters/user_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + responses: + '204': + description: '' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + /projects.{format}: + get: + tags: + - Projects + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Projects#Listing-projects + summary: List projects + operationId: getProjects + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/offset' + - $ref: '#/components/parameters/limit' + - $ref: '#/components/parameters/nometa' + - $ref: '#/components/parameters/x_redmine_switch_user' + - $ref: '#/components/parameters/x_redmine_nometa' + - name: include + in: query + description: |- + Comma-separated list of associated data to include for each project. + Values: `trackers` (available issue trackers), `issue_categories` (issue categories), `time_entry_activities` (time entry activity types), `enabled_modules` (enabled modules/features), `issue_custom_fields` (custom fields for issues) + schema: + type: array + items: + type: string + enum: + - trackers + - issue_categories + - time_entry_activities + - enabled_modules + - issue_custom_fields + explode: false + - name: status + in: query + description: |- + Filter by project status. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal) + Values: `1` (active), `5` (closed) + Examples: `1`, `1|5`, `!1` + schema: + type: string + - name: id + in: query + description: |- + Filter by project ID. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal) + Special values: `mine` (projects where current user is a member), `bookmarks` (bookmarked projects) + Examples: `1|2|3`, `mine`, `!mine`, `mine|bookmarks` + schema: + type: string + - name: name + in: query + description: |- + Filter by project name. + Format: `value` + Operators: `~` (contains), `!~` (not contains), `*~` (contains any word), `^` (starts with), `$` (ends with), `*` (any), `!*` (none) + Examples: `~keyword`, `^prefix` + schema: + type: string + - name: description + in: query + description: |- + Filter by project description. + Format: `value` + Operators: `~` (contains), `!~` (not contains), `*~` (contains any word), `^` (starts with), `$` (ends with), `*` (any), `!*` (none) + Examples: `~keyword`, `!~excluded` + schema: + type: string + - name: parent_id + in: query + description: |- + Filter by parent project. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal), `*` (any, i.e. is a subproject), `!*` (none, i.e. is a root project) + Special values: `mine` (projects where current user is a member), `bookmarks` (bookmarked projects) + Examples: `1|2`, `mine`, `*`, `!*` + schema: + type: string + - name: is_public + in: query + description: |- + Filter by public/private status. + Format: `[operator]value` + Operators: `=` (default), `!` (not equal) + Values: `1` (public), `0` (private) + Examples: `1`, `!1` + schema: + type: string + - name: created_on + in: query + description: |- + Filter by creation date. + Format: `[value]` + Operators: `=` (on date), `>=` (on or after), `<=` (on or before), `><` (between, pipe-separated), `t` (today), `ld` (yesterday), `w` (this week), `lw` (last week), `l2w` (last 2 weeks), `m` (this month), `lm` (last month), `y` (this year), `t-` (n days ago), `>t-` (less than n days ago), `=2024-01-01`, `><2024-01-01|2024-12-31`, `t`, `>t-7`, `lm` + schema: + type: string + - name: updated_on + in: query + description: |- + Filter by last updated date. + Format: `[value]` + Operators: `=` (on date), `>=` (on or after), `<=` (on or before), `><` (between, pipe-separated), `t` (today), `ld` (yesterday), `w` (this week), `lw` (last week), `l2w` (last 2 weeks), `m` (this month), `lm` (last month), `y` (this year), `t-` (n days ago), `>t-` (less than n days ago), `=2024-01-01`, `><2024-01-01|2024-12-31`, `t`, `>t-7`, `lm` + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + type: object + required: + - projects + properties: + projects: + type: array + items: + $ref: '#/components/schemas/project.simple' + total_count: + type: integer + offset: + type: integer + limit: + type: integer + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '412': + $ref: '#/components/responses/PreconditionFailed' + post: + tags: + - Projects + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Projects#Creating-a-project + summary: Create project + operationId: createProject + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/x_redmine_switch_user' + requestBody: + content: + application/json: + schema: + type: object + required: + - project + properties: + project: + type: object + required: + - name + - identifier + properties: + name: + type: string + identifier: + type: string + description: + type: string + nullable: true + homepage: + type: string + nullable: true + is_public: + type: boolean + parent_id: + type: integer + nullable: true + inherit_members: + description: Inherit members from the parent project. Only effective for subprojects (parent_id is set). + type: boolean + default_assigned_to_id: + description: Default assignee for new issues in this project. Set to null to clear. + type: integer + nullable: true + default_version_id: + description: Default target version for new issues in this project. Set to null to clear. + type: integer + nullable: true + default_issue_query_id: + description: Default saved query for the project's issue list page. Set to null to clear. + type: integer + nullable: true + tracker_ids: + description: IDs of trackers available in this project. + type: array + items: + type: integer + enabled_module_names: + description: |- + Enabled module names for this project. + Values: `issue_tracking`, `time_tracking`, `news`, `documents`, `files`, `wiki`, `repository`, `boards`, `calendar`, `gantt` + type: array + items: + type: string + issue_custom_field_ids: + description: IDs of issue custom fields enabled for this project. Custom fields with `is_for_all=true` are always available regardless of this setting. + type: array + items: + type: integer + custom_fields: + $ref: '#/components/schemas/custom_fields' + custom_field_values: + $ref: '#/components/schemas/custom_field_values' + responses: + '201': + description: '' + content: + application/json: + schema: + type: object + required: + - project + properties: + project: + $ref: '#/components/schemas/project' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '412': + $ref: '#/components/responses/PreconditionFailed' + '422': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/errors' + /projects/{project_id}.{format}: + get: + tags: + - Projects + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Projects#Showing-a-project + summary: Show project + operationId: getProject + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/project_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + - name: include + in: query + description: |- + Comma-separated list of associated data to include in the response. + Values: `trackers` (available issue trackers), `issue_categories` (issue categories), `time_entry_activities` (time entry activity types), `enabled_modules` (enabled modules/features), `issue_custom_fields` (custom fields for issues) + schema: + type: array + items: + type: string + enum: + - trackers + - issue_categories + - time_entry_activities + - enabled_modules + - issue_custom_fields + explode: false + responses: + '200': + description: '' + content: + application/json: + schema: + type: object + required: + - project + properties: + project: + $ref: '#/components/schemas/project' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + put: + tags: + - Projects + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Projects#Updating-a-project + summary: Update project + operationId: updateProject + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/project_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + requestBody: + content: + application/json: + schema: + type: object + properties: + project: + type: object + properties: + identifier: + type: string + name: + type: string + description: + type: string + nullable: true + homepage: + type: string + nullable: true + is_public: + type: boolean + parent_id: + type: integer + nullable: true + inherit_members: + description: Inherit members from the parent project. Only effective for subprojects (parent_id is set). + type: boolean + default_assigned_to_id: + description: Default assignee for new issues in this project. Set to null to clear. + type: integer + nullable: true + default_version_id: + description: Default target version for new issues in this project. Set to null to clear. + type: integer + nullable: true + default_issue_query_id: + description: Default saved query for the project's issue list page. Set to null to clear. + type: integer + nullable: true + tracker_ids: + description: IDs of trackers available in this project. + type: array + items: + type: integer + enabled_module_names: + description: |- + Enabled module names for this project. + Values: `issue_tracking`, `time_tracking`, `news`, `documents`, `files`, `wiki`, `repository`, `boards`, `calendar`, `gantt` + type: array + items: + type: string + issue_custom_field_ids: + description: IDs of issue custom fields enabled for this project. Custom fields with `is_for_all=true` are always available regardless of this setting. + type: array + items: + type: integer + custom_fields: + $ref: '#/components/schemas/custom_fields' + custom_field_values: + $ref: '#/components/schemas/custom_field_values' + responses: + '204': + description: '' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + '422': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/errors' + delete: + tags: + - Projects + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Projects#Deleting-a-project + summary: Delete project + operationId: deleteProject + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/project_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + responses: + '204': + description: '' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + /projects/{project_id}/archive.{format}: + put: + tags: + - Projects + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Projects#Archiving-a-project + summary: Archive project + operationId: archiveProject + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/project_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + responses: + '204': + description: '' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + '422': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/errors' + /projects/{project_id}/unarchive.{format}: + put: + tags: + - Projects + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Projects#Unarchiving-a-project + summary: Unarchive project + operationId: unarchiveProject + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/project_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + responses: + '204': + description: '' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + /projects/{project_id}/memberships.{format}: + get: + tags: + - Project Memberships + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Memberships#GET + summary: List memberships + operationId: getMemberships + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/project_id' + - $ref: '#/components/parameters/offset' + - $ref: '#/components/parameters/limit' + - $ref: '#/components/parameters/nometa' + - $ref: '#/components/parameters/x_redmine_switch_user' + - $ref: '#/components/parameters/x_redmine_nometa' + responses: + '200': + description: '' + content: + application/json: + schema: + type: object + required: + - memberships + properties: + memberships: + type: array + items: + $ref: '#/components/schemas/membership' + total_count: + type: integer + offset: + type: integer + limit: + type: integer + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + post: + tags: + - Project Memberships + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Memberships#POST + summary: Create membership + operationId: createMembership + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/project_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + requestBody: + content: + application/json: + schema: + type: object + required: + - membership + properties: + membership: + type: object + required: + - user_id + - role_ids + properties: + user_id: + type: integer + role_ids: + type: array + items: + type: integer + responses: + '201': + description: '' + content: + application/json: + schema: + type: object + required: + - membership + properties: + membership: + $ref: '#/components/schemas/membership' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + '422': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/errors' + /memberships/{membership_id}.{format}: + get: + tags: + - Project Memberships + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Memberships#GET-2 + summary: Show membership + operationId: getMembership + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/membership_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + responses: + '200': + description: '' + content: + application/json: + schema: + type: object + required: + - membership + properties: + membership: + $ref: '#/components/schemas/membership' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + put: + tags: + - Project Memberships + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Memberships#PUT + summary: Update membership + operationId: updateMembership + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/membership_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + requestBody: + content: + application/json: + schema: + type: object + properties: + membership: + type: object + required: + - role_ids + properties: + role_ids: + type: array + items: + type: integer + responses: + '204': + description: '' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + '422': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/errors' + delete: + tags: + - Project Memberships + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Memberships#DELETE + summary: Delete membership + operationId: deleteMembership + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/membership_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + responses: + '204': + description: '' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + '422': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/errors' + /projects/{project_id}/close.{format}: + put: + tags: + - Projects + summary: Close project + description: 'Undocumented: This operation is not listed in the official documentation.' + operationId: closeProject + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/project_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + responses: + '204': + description: '' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + /projects/{project_id}/reopen.{format}: + put: + tags: + - Projects + summary: Reopen project + description: 'Undocumented: This operation is not listed in the official documentation.' + operationId: reopenProject + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/project_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + responses: + '204': + description: '' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + /users.{format}: + get: + tags: + - Users + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Users#GET + summary: List users + operationId: getUsers + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/offset' + - $ref: '#/components/parameters/limit' + - $ref: '#/components/parameters/nometa' + - $ref: '#/components/parameters/x_redmine_switch_user' + - $ref: '#/components/parameters/x_redmine_nometa' + - name: include + in: query + description: |- + Comma-separated list of associated data to include for each user. + Values: `auth_source` (authentication source information) + schema: + type: array + items: + type: string + enum: + - auth_source + explode: false + - name: status + in: query + description: |- + Filter by user status. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal), `*` (any), `!*` (none) + Values: `1` (active), `2` (registered), `3` (locked) + Examples: `1`, `1|2`, `!3` + schema: + type: string + - name: name + in: query + description: |- + Filter by name, login, or email (contains search). + The API always uses the `~` (contains) operator regardless of any operator prefix. + Examples: `john`, `admin` + schema: + type: string + - name: group_id + in: query + description: |- + Filter by group membership (legacy parameter). + Internally converted to `is_member_of_group` filter with `=` operator. + Value is the group ID. + schema: + type: string + - name: auth_source_id + in: query + description: |- + Filter by authentication source. Undocumented + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal), `*` (any), `!*` (none) + Examples: `1`, `!1` + schema: + type: string + - name: twofa_scheme + in: query + description: |- + Filter by two-factor authentication scheme. Undocumented + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal), `*` (any), `!*` (none) + Only available when two-factor authentication is enabled in Redmine settings. + schema: + type: string + - name: login + in: query + description: |- + Filter by login. Undocumented + Format: `[operator]value` + Operators: `~` (contains, default), `*~` (contains any word), `=` (exact match), `!~` (does not contain), `!` (not equal), `^` (starts with), `$` (ends with), `*` (any), `!*` (none) + Examples: `~admin`, `^john` + schema: + type: string + - name: firstname + in: query + description: |- + Filter by first name. Undocumented + Format: `[operator]value` + Operators: `~` (contains, default), `*~` (contains any word), `=` (exact match), `!~` (does not contain), `!` (not equal), `^` (starts with), `$` (ends with), `*` (any), `!*` (none) + Examples: `~John`, `=John` + schema: + type: string + - name: lastname + in: query + description: |- + Filter by last name. Undocumented + Format: `[operator]value` + Operators: `~` (contains, default), `*~` (contains any word), `=` (exact match), `!~` (does not contain), `!` (not equal), `^` (starts with), `$` (ends with), `*` (any), `!*` (none) + Examples: `~Doe`, `=Doe` + schema: + type: string + - name: mail + in: query + description: |- + Filter by email address. Undocumented + Format: `[operator]value` + Operators: `~` (contains, default), `*~` (contains any word), `=` (exact match), `!~` (does not contain), `!` (not equal), `^` (starts with), `$` (ends with), `*` (any), `!*` (none) + Examples: `~example.com`, `$@example.com` + schema: + type: string + - name: created_on + in: query + description: |- + Filter by creation date. Undocumented + Format: `[operator]value` + Operators: `=` (on date), `>=`, `<=`, `><` (between, pipe-separated), `>t-` (more than N days ago), `=2024-01-01`, `>t-30`, `lm` + schema: + type: string + - name: last_login_on + in: query + description: |- + Filter by last login date. Undocumented + Format: `[operator]value` + Operators: `=` (on date), `>=`, `<=`, `><` (between, pipe-separated), `>t-` (more than N days ago), `=2024-01-01`, `>t-30`, `!*` + schema: + type: string + - name: admin + in: query + description: |- + Filter by admin privilege. Undocumented + Format: `[operator]value` + Operators: `=` (default), `!` (not equal) + Values: `1` (admin), `0` (non-admin) + Examples: `1`, `!1` + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + type: object + required: + - users + properties: + users: + type: array + items: + $ref: '#/components/schemas/user.simple' + total_count: + type: integer + offset: + type: integer + limit: + type: integer + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '412': + $ref: '#/components/responses/PreconditionFailed' + post: + tags: + - Users + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Users#POST + summary: Create user + operationId: createUser + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/x_redmine_switch_user' + requestBody: + content: + application/json: + schema: + type: object + required: + - user + properties: + user: + type: object + required: + - login + - firstname + - lastname + - mail + properties: + login: + type: string + admin: + type: boolean + password: + type: string + firstname: + type: string + lastname: + type: string + mail: + type: string + language: + type: string + auth_source_id: + description: External authentication source ID (e.g., LDAP). When set, the user authenticates via the external source and the password field is ignored. Set to null for local authentication. + type: integer + nullable: true + mail_notification: + description: |- + Email notification preference. + Values: `all` (for any event on all projects), `selected` (for any event on selected projects only), `only_my_events` (only for things I watch or am involved in), `only_assigned` (only for things I am assigned to), `only_owner` (only for things I am the owner of), `none` (no events) + type: string + enum: + - all + - selected + - only_my_events + - only_assigned + - only_owner + - none + notified_project_ids: + description: Project IDs for which to receive email notifications. Only effective when mail_notification is set to `selected`. + type: array + items: + type: integer + must_change_passwd: + description: Force the user to change their password at next login. + type: boolean + generate_password: + description: Generate a random password automatically. When true, the password field is ignored. Only effective for locally authenticated users (auth_source_id is null). + type: boolean + status: + description: 'User account status: `1` (active), `2` (registered), `3` (locked).' + type: integer + custom_fields: + $ref: '#/components/schemas/custom_fields' + custom_field_values: + $ref: '#/components/schemas/custom_field_values' + send_information: + description: Set to true to send an account information email to the user. Not stored; only triggers email delivery. + type: boolean + pref: + $ref: '#/components/schemas/pref' + responses: + '201': + description: '' + content: + application/json: + schema: + type: object + required: + - user + properties: + user: + $ref: '#/components/schemas/user' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '412': + $ref: '#/components/responses/PreconditionFailed' + '422': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/errors' + /users/{user_id}.{format}: + get: + tags: + - Users + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Users#GET-2 + summary: Show user + operationId: getUser + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/user_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + - name: include + in: query + description: |- + Comma-separated list of associated data to include in the response. + Values: `memberships` (project memberships with roles), `groups` (groups the user belongs to), `auth_source` (authentication source information) + schema: + type: array + items: + type: string + enum: + - memberships + - groups + - auth_source + explode: false + responses: + '200': + description: '' + content: + application/json: + schema: + type: object + required: + - user + properties: + user: + $ref: '#/components/schemas/user' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + put: + tags: + - Users + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Users#PUT + summary: Update user + operationId: updateUser + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/user_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + requestBody: + content: + application/json: + schema: + type: object + properties: + user: + type: object + properties: + login: + type: string + admin: + type: boolean + password: + type: string + firstname: + type: string + lastname: + type: string + mail: + type: string + language: + type: string + auth_source_id: + description: External authentication source ID (e.g., LDAP). When set, the user authenticates via the external source and the password field is ignored. Set to null for local authentication. + type: integer + nullable: true + mail_notification: + description: |- + Email notification preference. + Values: `all` (for any event on all projects), `selected` (for any event on selected projects only), `only_my_events` (only for things I watch or am involved in), `only_assigned` (only for things I am assigned to), `only_owner` (only for things I am the owner of), `none` (no events) + type: string + enum: + - all + - selected + - only_my_events + - only_assigned + - only_owner + - none + notified_project_ids: + description: Project IDs for which to receive email notifications. Only effective when mail_notification is set to `selected`. + type: array + items: + type: integer + must_change_passwd: + description: Force the user to change their password at next login. + type: boolean + generate_password: + description: Generate a random password automatically. When true, the password field is ignored. Only effective for locally authenticated users (auth_source_id is null). + type: boolean + status: + description: 'User account status: `1` (active), `2` (registered), `3` (locked).' + type: integer + custom_fields: + $ref: '#/components/schemas/custom_fields' + custom_field_values: + $ref: '#/components/schemas/custom_field_values' + group_ids: + description: Group IDs to assign the user to. Admin-only. Replaces all current group memberships (not append). + type: array + items: + type: integer + send_information: + description: Set to true to send an account information email to the user. Not stored; only triggers email delivery. + type: boolean + pref: + $ref: '#/components/schemas/pref' + responses: + '204': + description: '' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + '422': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/errors' + delete: + tags: + - Users + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Users#DELETE + summary: Delete user + operationId: deleteUser + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/user_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + responses: + '204': + description: '' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + '422': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/errors' + /users/current.{format}: + get: + tags: + - Users + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Users#GET-2 + summary: Show current user + operationId: getCurrentUser + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/x_redmine_switch_user' + - name: include + in: query + description: |- + Comma-separated list of associated data to include in the response. + Values: `memberships` (project memberships with roles), `groups` (groups the user belongs to), `auth_source` (authentication source information) + schema: + type: array + items: + type: string + enum: + - memberships + - groups + - auth_source + explode: false + responses: + '200': + description: '' + content: + application/json: + schema: + type: object + required: + - user + properties: + user: + $ref: '#/components/schemas/user' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '412': + $ref: '#/components/responses/PreconditionFailed' + /time_entries.{format}: + get: + tags: + - Time Entries + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_TimeEntries#Listing-time-entries + summary: List time entries + operationId: getTimeEntries + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/offset' + - $ref: '#/components/parameters/limit' + - $ref: '#/components/parameters/nometa' + - $ref: '#/components/parameters/x_redmine_switch_user' + - $ref: '#/components/parameters/x_redmine_nometa' + - name: user_id + in: query + description: |- + Filter by user. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal), `*` (any), `!*` (none) + Special values: `me` (current user) + Examples: `1`, `me`, `!*` + schema: + type: string + - name: project_id + in: query + description: |- + Filter by project. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal) + Special values: `mine` (projects where current user is a member), `bookmarks` (bookmarked projects) + Examples: `1|2`, `mine`, `!mine` + schema: + type: string + - name: spent_on + in: query + description: |- + Filter by date spent. + Format: `[operator]value` + Operators: `=` (on date), `>=`, `<=`, `><` (between, pipe-separated), `>t-` (more than N days ago), `=2024-01-01`, `><2024-01-01|2024-12-31`, `>t-7`, `t`, `lw` + schema: + type: string + - name: from + in: query + description: |- + Shortcut for `spent_on` filter. When used with `to`, filters as between (`>=from` and `<=to`). When used alone, filters as `>=from`. + Format: `YYYY-MM-DD` + schema: + type: string + format: date + - name: to + in: query + description: |- + Shortcut for `spent_on` filter. When used with `from`, filters as between (`>=from` and `<=to`). When used alone, filters as `<=to`. + Format: `YYYY-MM-DD` + schema: + type: string + format: date + - name: subproject_id + in: query + description: |- + Filter by subproject. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal), `*` (any), `!*` (none) + Examples: `1|2`, `*`, `!*` + schema: + type: string + - name: issue_id + in: query + description: |- + Filter by issue. + Format: `[operator]value` + Operators: `=` (exact match), `~` (issue and its subtasks), `*` (any), `!*` (none) + Examples: `=1`, `~1`, `*`, `!*` + schema: + type: string + - name: issue.tracker_id + in: query + description: |- + Filter by issue's tracker. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal) + Examples: `1|2`, `!3` + schema: + type: string + - name: issue.status_id + in: query + description: |- + Filter by issue's status. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal) + Examples: `1|2`, `!3` + schema: + type: string + - name: issue.fixed_version_id + in: query + description: |- + Filter by issue's target version. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal) + Examples: `1|2`, `!3` + schema: + type: string + - name: issue.parent_id + in: query + description: |- + Filter by issue's parent. + Format: `[operator]value` + Operators: `=` (exact match), `~` (issue and its subtasks), `*` (any), `!*` (none) + Examples: `=1`, `~1`, `*`, `!*` + schema: + type: string + - name: issue.category_id + in: query + description: |- + Filter by issue's category. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal), `*` (any), `!*` (none) + Examples: `1|2`, `!*` + schema: + type: string + - name: issue.subject + in: query + description: |- + Filter by issue's subject. + Format: `[operator]value` + Operators: `~` (contains, default), `*~` (contains any word), `!~` (does not contain), `^` (starts with), `$` (ends with), `*` (any), `!*` (none) + Examples: `~keyword`, `^prefix`, `$suffix` + schema: + type: string + - name: user.group + in: query + description: |- + Filter by user's group. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal), `*` (any), `!*` (none) + Examples: `1|2`, `*` + schema: + type: string + - name: user.role + in: query + description: |- + Filter by user's role. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal), `*` (any), `!*` (none) + Examples: `1|2`, `*` + schema: + type: string + - name: author_id + in: query + description: |- + Filter by author. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal), `*` (any), `!*` (none) + Special values: `me` (current user) + Examples: `1`, `me`, `!*` + schema: + type: string + - name: activity_id + in: query + description: |- + Filter by activity. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal) + Examples: `1|2`, `!3` + schema: + type: string + - name: project.status + in: query + description: |- + Filter by project's status. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal) + Values: `1` (active), `5` (closed) + Examples: `1`, `!5` + schema: + type: string + - name: comments + in: query + description: |- + Filter by comments. + Format: `[operator]value` + Operators: `~` (contains, default), `*~` (contains any word), `!~` (does not contain), `^` (starts with), `$` (ends with), `*` (any), `!*` (none) + Examples: `~keyword`, `^prefix` + schema: + type: string + - name: hours + in: query + description: |- + Filter by hours. + Format: `[operator]value` + Operators: `=` (default), `>=`, `<=`, `><` (between, pipe-separated), `*` (any), `!*` (none) + Examples: `=8`, `>=4`, `><1|8` + schema: + type: string + - name: sort + in: query + description: |- + Sort order. Comma-separated list of `field` or `field:desc`. Default direction is ascending. + Examples: `spent_on:desc`, `hours:desc,spent_on` + schema: + type: string + example: spent_on:desc + responses: + '200': + description: '' + content: + application/json: + schema: + type: object + required: + - time_entries + properties: + time_entries: + type: array + items: + $ref: '#/components/schemas/time_entry' + total_count: + type: integer + offset: + type: integer + limit: + type: integer + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '412': + $ref: '#/components/responses/PreconditionFailed' + post: + tags: + - Time Entries + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_TimeEntries#Creating-a-time-entry + summary: Create time entry + operationId: createTimeEntry + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/x_redmine_switch_user' + requestBody: + content: + application/json: + schema: + type: object + required: + - time_entry + properties: + time_entry: + type: object + required: + - hours + properties: + issue_id: + type: integer + project_id: + type: integer + spent_on: + type: string + format: date + hours: + type: number + activity_id: + type: integer + comments: + type: string + user_id: + type: integer + custom_fields: + $ref: '#/components/schemas/custom_fields' + custom_field_values: + $ref: '#/components/schemas/custom_field_values' + responses: + '201': + description: '' + content: + application/json: + schema: + type: object + required: + - time_entry + properties: + time_entry: + $ref: '#/components/schemas/time_entry' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '412': + $ref: '#/components/responses/PreconditionFailed' + '422': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/errors' + /time_entries/{time_entry_id}.{format}: + get: + tags: + - Time Entries + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_TimeEntries#Showing-a-time-entry + summary: Show time entry + operationId: getTimeEntry + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/time_entry_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + responses: + '200': + description: '' + content: + application/json: + schema: + type: object + required: + - time_entry + properties: + time_entry: + $ref: '#/components/schemas/time_entry' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + put: + tags: + - Time Entries + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_TimeEntries#Updating-a-time-entry + summary: Update time entry + operationId: updateTimeEntry + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/time_entry_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + requestBody: + content: + application/json: + schema: + type: object + properties: + time_entry: + type: object + properties: + issue_id: + type: integer + project_id: + type: integer + spent_on: + type: string + format: date + hours: + type: number + activity_id: + type: integer + comments: + type: string + user_id: + type: integer + custom_fields: + $ref: '#/components/schemas/custom_fields' + custom_field_values: + $ref: '#/components/schemas/custom_field_values' + responses: + '204': + description: '' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + '422': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/errors' + delete: + tags: + - Time Entries + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_TimeEntries#Deleting-a-time-entry + summary: Delete time entry + operationId: deleteTimeEntry + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/time_entry_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + responses: + '204': + description: '' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + '422': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/errors' + /projects/{project_id}/time_entries.{format}: + get: + tags: + - Time Entries + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_TimeEntries#Listing-time-entries + summary: List time entries by project + operationId: getTimeEntriesByProject + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/project_id' + - $ref: '#/components/parameters/offset' + - $ref: '#/components/parameters/limit' + - $ref: '#/components/parameters/nometa' + - $ref: '#/components/parameters/x_redmine_switch_user' + - $ref: '#/components/parameters/x_redmine_nometa' + - name: user_id + in: query + description: |- + Filter by user. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal), `*` (any), `!*` (none) + Special values: `me` (current user) + Examples: `1`, `me`, `!*` + schema: + type: string + - name: spent_on + in: query + description: |- + Filter by date spent. + Format: `[operator]value` + Operators: `=` (on date), `>=`, `<=`, `><` (between, pipe-separated), `>t-` (more than N days ago), `=2024-01-01`, `><2024-01-01|2024-12-31`, `>t-7`, `t`, `lw` + schema: + type: string + - name: from + in: query + description: |- + Shortcut for `spent_on` filter. When used with `to`, filters as between (`>=from` and `<=to`). When used alone, filters as `>=from`. + Format: `YYYY-MM-DD` + schema: + type: string + format: date + - name: to + in: query + description: |- + Shortcut for `spent_on` filter. When used with `from`, filters as between (`>=from` and `<=to`). When used alone, filters as `<=to`. + Format: `YYYY-MM-DD` + schema: + type: string + format: date + - name: subproject_id + in: query + description: |- + Filter by subproject. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal), `*` (any), `!*` (none) + Examples: `1|2`, `*`, `!*` + schema: + type: string + - name: issue_id + in: query + description: |- + Filter by issue. + Format: `[operator]value` + Operators: `=` (exact match), `~` (issue and its subtasks), `*` (any), `!*` (none) + Examples: `=1`, `~1`, `*`, `!*` + schema: + type: string + - name: issue.tracker_id + in: query + description: |- + Filter by issue's tracker. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal) + Examples: `1|2`, `!3` + schema: + type: string + - name: issue.status_id + in: query + description: |- + Filter by issue's status. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal) + Examples: `1|2`, `!3` + schema: + type: string + - name: issue.fixed_version_id + in: query + description: |- + Filter by issue's target version. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal) + Examples: `1|2`, `!3` + schema: + type: string + - name: issue.parent_id + in: query + description: |- + Filter by issue's parent. + Format: `[operator]value` + Operators: `=` (exact match), `~` (issue and its subtasks), `*` (any), `!*` (none) + Examples: `=1`, `~1`, `*`, `!*` + schema: + type: string + - name: issue.category_id + in: query + description: |- + Filter by issue's category. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal), `*` (any), `!*` (none) + Examples: `1|2`, `!*` + schema: + type: string + - name: issue.subject + in: query + description: |- + Filter by issue's subject. + Format: `[operator]value` + Operators: `~` (contains, default), `*~` (contains any word), `!~` (does not contain), `^` (starts with), `$` (ends with), `*` (any), `!*` (none) + Examples: `~keyword`, `^prefix`, `$suffix` + schema: + type: string + - name: user.group + in: query + description: |- + Filter by user's group. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal), `*` (any), `!*` (none) + Examples: `1|2`, `*` + schema: + type: string + - name: user.role + in: query + description: |- + Filter by user's role. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal), `*` (any), `!*` (none) + Examples: `1|2`, `*` + schema: + type: string + - name: author_id + in: query + description: |- + Filter by author. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal), `*` (any), `!*` (none) + Special values: `me` (current user) + Examples: `1`, `me`, `!*` + schema: + type: string + - name: activity_id + in: query + description: |- + Filter by activity. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal) + Examples: `1|2`, `!3` + schema: + type: string + - name: project.status + in: query + description: |- + Filter by project's status. + Format: `[operator]value[|value2|...]` + Operators: `=` (default), `!` (not equal) + Values: `1` (active), `5` (closed) + Examples: `1`, `!5` + schema: + type: string + - name: comments + in: query + description: |- + Filter by comments. + Format: `[operator]value` + Operators: `~` (contains, default), `*~` (contains any word), `!~` (does not contain), `^` (starts with), `$` (ends with), `*` (any), `!*` (none) + Examples: `~keyword`, `^prefix` + schema: + type: string + - name: hours + in: query + description: |- + Filter by hours. + Format: `[operator]value` + Operators: `=` (default), `>=`, `<=`, `><` (between, pipe-separated), `*` (any), `!*` (none) + Examples: `=8`, `>=4`, `><1|8` + schema: + type: string + - name: sort + in: query + description: |- + Sort order. Comma-separated list of `field` or `field:desc`. Default direction is ascending. + Examples: `spent_on:desc`, `hours:desc,spent_on` + schema: + type: string + example: spent_on:desc + responses: + '200': + description: '' + content: + application/json: + schema: + type: object + required: + - time_entries + properties: + time_entries: + type: array + items: + $ref: '#/components/schemas/time_entry' + total_count: + type: integer + offset: + type: integer + limit: + type: integer + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + post: + tags: + - Time Entries + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_TimeEntries#Creating-a-time-entry + summary: Create time entry under project + operationId: createTimeEntryUnderProject + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/project_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + requestBody: + content: + application/json: + schema: + type: object + required: + - time_entry + properties: + time_entry: + type: object + required: + - hours + properties: + issue_id: + type: integer + spent_on: + type: string + format: date + hours: + type: number + activity_id: + type: integer + comments: + type: string + user_id: + type: integer + custom_fields: + $ref: '#/components/schemas/custom_fields' + custom_field_values: + $ref: '#/components/schemas/custom_field_values' + responses: + '201': + description: '' + content: + application/json: + schema: + type: object + required: + - time_entry + properties: + time_entry: + $ref: '#/components/schemas/time_entry' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + '422': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/errors' + /issues/{issue_id}/time_entries.{format}: + post: + tags: + - Time Entries + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_TimeEntries#Creating-a-time-entry + summary: Create time entry under issue + operationId: createTimeEntryUnderIssue + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/issue_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + requestBody: + content: + application/json: + schema: + type: object + required: + - time_entry + properties: + time_entry: + type: object + required: + - hours + properties: + project_id: + type: integer + spent_on: + type: string + format: date + hours: + type: number + activity_id: + type: integer + comments: + type: string + user_id: + type: integer + custom_fields: + $ref: '#/components/schemas/custom_fields' + custom_field_values: + $ref: '#/components/schemas/custom_field_values' + responses: + '201': + description: '' + content: + application/json: + schema: + type: object + required: + - time_entry + properties: + time_entry: + $ref: '#/components/schemas/time_entry' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + '422': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/errors' + /news.{format}: + get: + tags: + - News + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_News#GET + summary: List news + operationId: getNewsList + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/offset' + - $ref: '#/components/parameters/limit' + - $ref: '#/components/parameters/nometa' + - $ref: '#/components/parameters/x_redmine_switch_user' + - $ref: '#/components/parameters/x_redmine_nometa' + responses: + '200': + description: '' + content: + application/json: + schema: + type: object + required: + - news + properties: + news: + type: array + items: + $ref: '#/components/schemas/news' + total_count: + type: integer + offset: + type: integer + limit: + type: integer + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '412': + $ref: '#/components/responses/PreconditionFailed' + /news/{news_id}.{format}: + get: + tags: + - News + summary: Show news + description: 'Undocumented: This operation is not listed in the official documentation.' + operationId: getNews + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/news_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + - name: include + in: query + description: |- + Comma-separated list of associated data to include in the response. + Values: `attachments` (file attachments), `comments` (comments/replies on the news item) + schema: + type: array + items: + type: string + enum: + - attachments + - comments + explode: false + responses: + '200': + description: '' + content: + application/json: + schema: + type: object + required: + - news + properties: + news: + $ref: '#/components/schemas/news' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + put: + tags: + - News + summary: Update news + description: 'Undocumented: This operation is not listed in the official documentation.' + operationId: updateNews + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/news_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + requestBody: + content: + application/json: + schema: + type: object + properties: + news: + type: object + properties: + title: + type: string + summary: + type: string + description: + type: string + uploads: + $ref: '#/components/schemas/uploads' + responses: + '204': + description: '' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + '422': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/errors' + delete: + tags: + - News + summary: Delete news + description: 'Undocumented: This operation is not listed in the official documentation.' + operationId: deleteNews + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/news_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + responses: + '204': + description: '' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + /projects/{project_id}/news.{format}: + get: + tags: + - News + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_News#GET-2 + summary: List news by project + operationId: getNewsListByProject + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/project_id' + - $ref: '#/components/parameters/offset' + - $ref: '#/components/parameters/limit' + - $ref: '#/components/parameters/nometa' + - $ref: '#/components/parameters/x_redmine_switch_user' + - $ref: '#/components/parameters/x_redmine_nometa' + responses: + '200': + description: '' + content: + application/json: + schema: + type: object + required: + - news + properties: + news: + type: array + items: + $ref: '#/components/schemas/news' + total_count: + type: integer + offset: + type: integer + limit: + type: integer + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + post: + tags: + - News + summary: Create news + description: 'Undocumented: This operation is not listed in the official documentation.' + operationId: createNews + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/project_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + requestBody: + content: + application/json: + schema: + type: object + required: + - news + properties: + news: + type: object + required: + - title + - description + properties: + title: + type: string + summary: + type: string + description: + type: string + uploads: + $ref: '#/components/schemas/uploads' + responses: + '204': + description: '' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + '422': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/errors' + /issues/{issue_id}/relations.{format}: + get: + tags: + - Issue Relations + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_IssueRelations#GET + summary: List issue relations + operationId: getIssueRelations + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/issue_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + responses: + '200': + description: '' + content: + application/json: + schema: + type: object + required: + - relations + properties: + relations: + type: array + items: + $ref: '#/components/schemas/issue_relation' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + post: + tags: + - Issue Relations + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_IssueRelations#POST + summary: Create issue relation + operationId: createIssueRelation + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/issue_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + requestBody: + content: + application/json: + schema: + type: object + required: + - relation + properties: + relation: + type: object + required: + - issue_to_id + - relation_type + properties: + issue_to_id: + type: integer + relation_type: + description: |- + Type of relation from this issue to the target issue. + Values: `relates` (related to), `duplicates` (this duplicates target), `duplicated` (this is duplicated by target), `blocks` (this blocks target), `blocked` (this is blocked by target), `precedes` (this must finish before target starts), `follows` (this must start after target finishes), `copied_to` (this was copied to target), `copied_from` (this was copied from target) + type: string + enum: + - relates + - duplicates + - duplicated + - blocks + - blocked + - precedes + - follows + - copied_to + - copied_from + delay: + description: Number of days of delay for `precedes`/`follows` relations. Ignored for other relation types. + type: integer + nullable: true + responses: + '201': + description: '' + content: + application/json: + schema: + type: object + required: + - relation + properties: + relation: + $ref: '#/components/schemas/issue_relation' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + '422': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/errors' + /relations/{issue_relation_id}.{format}: + get: + tags: + - Issue Relations + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_IssueRelations#GET-2 + summary: Show issue relation + operationId: getIssueRelation + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/issue_relation_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + responses: + '200': + description: '' + content: + application/json: + schema: + type: object + required: + - relation + properties: + relation: + $ref: '#/components/schemas/issue_relation' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + delete: + tags: + - Issue Relations + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_IssueRelations#DELETE + summary: Delete issue relation + operationId: deleteIssueRelation + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/issue_relation_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + responses: + '204': + description: '' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + /projects/{project_id}/versions.{format}: + get: + tags: + - Versions + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Versions#GET + summary: List versions by project + operationId: getVersionsByProject + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/project_id' + - $ref: '#/components/parameters/nometa' + - $ref: '#/components/parameters/x_redmine_switch_user' + - $ref: '#/components/parameters/x_redmine_nometa' + responses: + '200': + description: '' + content: + application/json: + schema: + type: object + required: + - versions + properties: + versions: + type: array + items: + $ref: '#/components/schemas/version.simple' + total_count: + type: integer + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + post: + tags: + - Versions + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Versions#POST + summary: Create version + operationId: createVersion + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/project_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + requestBody: + content: + application/json: + schema: + type: object + required: + - version + properties: + version: + type: object + required: + - name + properties: + name: + type: string + status: + type: string + enum: + - open + - locked + - closed + sharing: + description: |- + Version sharing scope. + Values: `none` (not shared), `descendants` (with subprojects), `hierarchy` (with ancestors and descendants), `tree` (with entire project tree), `system` (with all projects) + type: string + enum: + - none + - descendants + - hierarchy + - tree + - system + due_date: + type: string + format: date + nullable: true + description: + type: string + wiki_page_title: + description: Title of a wiki page to associate with this version. Set to null to clear. + type: string + nullable: true + default_project_version: + description: Set to true to make this the default target version for new issues in the project. + type: boolean + custom_fields: + $ref: '#/components/schemas/custom_fields' + custom_field_values: + $ref: '#/components/schemas/custom_field_values' + responses: + '201': + description: '' + content: + application/json: + schema: + type: object + required: + - version + properties: + version: + $ref: '#/components/schemas/version' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + '422': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/errors' + /versions/{version_id}.{format}: + get: + tags: + - Versions + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Versions#GET-2 + summary: Show version + operationId: getVersion + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/version_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + responses: + '200': + description: '' + content: + application/json: + schema: + type: object + required: + - version + properties: + version: + $ref: '#/components/schemas/version' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + put: + tags: + - Versions + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Versions#PUT + summary: Update version + operationId: updateVersion + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/version_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + requestBody: + content: + application/json: + schema: + type: object + properties: + version: + type: object + properties: + name: + type: string + status: + type: string + enum: + - open + - locked + - closed + sharing: + description: |- + Version sharing scope. + Values: `none` (not shared), `descendants` (with subprojects), `hierarchy` (with ancestors and descendants), `tree` (with entire project tree), `system` (with all projects) + type: string + enum: + - none + - descendants + - hierarchy + - tree + - system + due_date: + type: string + format: date + nullable: true + description: + type: string + wiki_page_title: + description: Title of a wiki page to associate with this version. Set to null to clear. + type: string + nullable: true + default_project_version: + description: Set to true to make this the default target version for new issues in the project. + type: boolean + custom_fields: + $ref: '#/components/schemas/custom_fields' + custom_field_values: + $ref: '#/components/schemas/custom_field_values' + responses: + '204': + description: '' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + '422': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/errors' + delete: + tags: + - Versions + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Versions#DELETE + summary: Delete version + operationId: deleteVersion + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/version_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + responses: + '204': + description: '' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + '422': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/errors' + /projects/{project_id}/wiki/index.{format}: + get: + tags: + - Wiki Pages + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_WikiPages#Getting-the-pages-list-of-a-wiki + summary: List wiki pages + operationId: getWikiPages + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/project_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + responses: + '200': + description: '' + content: + application/json: + schema: + type: object + required: + - wiki_pages + properties: + wiki_pages: + type: array + items: + $ref: '#/components/schemas/wiki_page.simple' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + /projects/{project_id}/wiki/{wiki_page_title}.{format}: + get: + tags: + - Wiki Pages + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_WikiPages#Getting-a-wiki-page + summary: Show wiki page + operationId: getWikiPage + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/project_id' + - $ref: '#/components/parameters/wiki_page_title' + - $ref: '#/components/parameters/x_redmine_switch_user' + - name: include + in: query + description: |- + Comma-separated list of associated data to include in the response. + Values: `attachments` (file attachments) + schema: + type: array + items: + type: string + enum: + - attachments + explode: false + responses: + '200': + description: '' + content: + application/json: + schema: + type: object + required: + - wiki_page + properties: + wiki_page: + $ref: '#/components/schemas/wiki_page' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + put: + tags: + - Wiki Pages + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_WikiPages#Creating-or-updating-a-wiki-page + summary: Create or update wiki page + operationId: updateWikiPage + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/project_id' + - $ref: '#/components/parameters/wiki_page_title' + - $ref: '#/components/parameters/x_redmine_switch_user' + requestBody: + content: + application/json: + schema: + type: object + properties: + wiki_page: + type: object + required: + - text + properties: + text: + type: string + comments: + type: string + version: + description: Content version for optimistic locking. If specified and it does not match the current version, the server returns 409 Conflict to prevent overwriting concurrent changes. + type: integer + parent_title: + description: Title of the parent wiki page. Sets this page as a child in the page hierarchy. + type: string + uploads: + $ref: '#/components/schemas/uploads' + responses: + '201': + description: '' + content: + application/json: + schema: + type: object + required: + - wiki_page + properties: + wiki_page: + $ref: '#/components/schemas/wiki_page' + '204': + description: '' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '409': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + '422': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/errors' + delete: + tags: + - Wiki Pages + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_WikiPages#Deleting-a-wiki-page + summary: Delete wiki page + operationId: deleteWikiPage + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/project_id' + - $ref: '#/components/parameters/wiki_page_title' + - $ref: '#/components/parameters/x_redmine_switch_user' + responses: + '204': + description: '' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + /projects/{project_id}/wiki/{wiki_page_title}/{version_id}.{format}: + get: + tags: + - Wiki Pages + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_WikiPages#Getting-an-old-version-of-a-wiki-page + summary: Show wiki page by specific version + operationId: getWikiPageByVersion + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/project_id' + - $ref: '#/components/parameters/wiki_page_title' + - $ref: '#/components/parameters/version_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + - name: include + in: query + description: |- + Comma-separated list of associated data to include in the response. + Values: `attachments` (file attachments) + schema: + type: array + items: + type: string + enum: + - attachments + explode: false + responses: + '200': + description: '' + content: + application/json: + schema: + type: object + required: + - wiki_page + properties: + wiki_page: + $ref: '#/components/schemas/wiki_page' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + /queries.{format}: + get: + tags: + - Queries + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Queries#GET + summary: List queries + operationId: getQueries + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/offset' + - $ref: '#/components/parameters/limit' + - $ref: '#/components/parameters/nometa' + - $ref: '#/components/parameters/x_redmine_switch_user' + - $ref: '#/components/parameters/x_redmine_nometa' + responses: + '200': + description: '' + content: + application/json: + schema: + type: object + required: + - queries + properties: + queries: + type: array + items: + $ref: '#/components/schemas/query' + total_count: + type: integer + offset: + type: integer + limit: + type: integer + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '412': + $ref: '#/components/responses/PreconditionFailed' + /attachments/{attachment_id}.{format}: + get: + tags: + - Attachments + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Attachments#GET + summary: Show attachment + operationId: getAttachment + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/attachment_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + responses: + '200': + description: '' + content: + application/json: + schema: + type: object + required: + - attachment + properties: + attachment: + $ref: '#/components/schemas/attachment' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + patch: + tags: + - Attachments + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Attachments#PATCH + summary: Update attachment + operationId: updateAttachment + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/attachment_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + requestBody: + content: + application/json: + schema: + type: object + properties: + attachment: + type: object + properties: + filename: + type: string + content_type: + type: string + description: + type: string + responses: + '204': + description: '' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + '422': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/errors' + delete: + tags: + - Attachments + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Attachments#DELETE + summary: Delete attachment + operationId: deleteAttachment + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/attachment_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + responses: + '204': + description: '' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + /attachments/download/{attachment_id}/{filename}: + get: + tags: + - Attachments + summary: Download attachment file + description: 'Undocumented: This operation is not listed in the official documentation.' + operationId: downloadAttachmentFile + parameters: + - $ref: '#/components/parameters/attachment_id' + - $ref: '#/components/parameters/filename' + - $ref: '#/components/parameters/x_redmine_switch_user' + responses: + '200': + description: '' + content: + '*/*': + schema: + type: string + format: binary + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + /attachments/thumbnail/{attachment_id}: + get: + tags: + - Attachments + summary: Download thumbnail + description: 'Undocumented: This operation is not listed in the official documentation.' + operationId: downloadThumbnail + parameters: + - $ref: '#/components/parameters/attachment_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + - name: size + in: query + description: |- + Desired thumbnail size in pixels (max 800). Rounded up to the nearest 50px increment. + If not specified, the configured default thumbnail size is used. + schema: + type: integer + responses: + '200': + description: '' + content: + image/png: + schema: + type: string + format: binary + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + /issue_statuses.{format}: + get: + tags: + - Issue Statuses + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_IssueStatuses#GET + summary: List issue statuses + operationId: getIssueStatuses + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/x_redmine_switch_user' + responses: + '200': + description: '' + content: + application/json: + schema: + type: object + required: + - issue_statuses + properties: + issue_statuses: + type: array + items: + $ref: '#/components/schemas/issue_status' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '412': + $ref: '#/components/responses/PreconditionFailed' + /trackers.{format}: + get: + tags: + - Trackers + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Trackers#GET + summary: List trackers + operationId: getTrackers + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/x_redmine_switch_user' + responses: + '200': + description: '' + content: + application/json: + schema: + type: object + required: + - trackers + properties: + trackers: + type: array + items: + $ref: '#/components/schemas/tracker' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '412': + $ref: '#/components/responses/PreconditionFailed' + /projects/{project_id}/issue_categories.{format}: + get: + tags: + - Issue Categories + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_IssueCategories#GET + summary: List issue categories + operationId: getIssueCategories + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/project_id' + - $ref: '#/components/parameters/nometa' + - $ref: '#/components/parameters/x_redmine_switch_user' + - $ref: '#/components/parameters/x_redmine_nometa' + responses: + '200': + description: '' + content: + application/json: + schema: + type: object + required: + - issue_categories + properties: + issue_categories: + type: array + items: + $ref: '#/components/schemas/issue_category' + total_count: + type: integer + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + post: + tags: + - Issue Categories + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_IssueCategories#POST + summary: Create issue category + operationId: createIssueCategory + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/project_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + requestBody: + content: + application/json: + schema: + type: object + required: + - issue_category + properties: + issue_category: + type: object + required: + - name + properties: + name: + type: string + assigned_to_id: + type: integer + responses: + '201': + description: '' + content: + application/json: + schema: + type: object + required: + - issue_category + properties: + issue_category: + $ref: '#/components/schemas/issue_category' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + '422': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/errors' + /enumerations/issue_priorities.{format}: + get: + tags: + - Enumerations + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Enumerations#GET + summary: List issue priorities + operationId: getIssuePriorities + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/x_redmine_switch_user' + responses: + '200': + description: '' + content: + application/json: + schema: + type: object + required: + - issue_priorities + properties: + issue_priorities: + type: array + items: + $ref: '#/components/schemas/issue_priority' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '412': + $ref: '#/components/responses/PreconditionFailed' + /enumerations/time_entry_activities.{format}: + get: + tags: + - Enumerations + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Enumerations#GET-2 + summary: List time entry activities + operationId: getTimeEntryActivities + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/x_redmine_switch_user' + responses: + '200': + description: '' + content: + application/json: + schema: + type: object + required: + - time_entry_activities + properties: + time_entry_activities: + type: array + items: + $ref: '#/components/schemas/time_entry_activity' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '412': + $ref: '#/components/responses/PreconditionFailed' + /enumerations/document_categories.{format}: + get: + tags: + - Enumerations + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Enumerations#GET-3 + summary: List document categories + operationId: getDocumentCategories + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/x_redmine_switch_user' + responses: + '200': + description: '' + content: + application/json: + schema: + type: object + required: + - document_categories + properties: + document_categories: + type: array + items: + $ref: '#/components/schemas/document_category' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '412': + $ref: '#/components/responses/PreconditionFailed' + /issue_categories/{issue_category_id}.{format}: + get: + tags: + - Issue Categories + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_IssueCategories#GET-2 + summary: Show issue category + operationId: getIssueCategory + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/issue_category_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + responses: + '200': + description: '' + content: + application/json: + schema: + type: object + required: + - issue_category + properties: + issue_category: + $ref: '#/components/schemas/issue_category' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + put: + tags: + - Issue Categories + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_IssueCategories#PUT + summary: Update issue category + operationId: updateIssueCategory + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/issue_category_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + requestBody: + content: + application/json: + schema: + type: object + properties: + issue_category: + type: object + properties: + name: + type: string + assigned_to_id: + type: integer + responses: + '204': + description: '' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + '422': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/errors' + delete: + tags: + - Issue Categories + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_IssueCategories#DELETE + summary: Delete issue category + operationId: deleteIssueCategory + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/issue_category_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + - name: reassign_to_id + in: query + description: |- + ID of another issue category to reassign issues to before deleting this category. + If the category has associated issues and this parameter is not provided, the issues will have their category unset. + schema: + type: integer + responses: + '204': + description: '' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + /roles.{format}: + get: + tags: + - Roles + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Roles#GET + summary: List roles + operationId: getRoles + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/x_redmine_switch_user' + responses: + '200': + description: '' + content: + application/json: + schema: + type: object + required: + - roles + properties: + roles: + type: array + items: + $ref: '#/components/schemas/id_name' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '412': + $ref: '#/components/responses/PreconditionFailed' + /roles/{role_id}.{format}: + get: + tags: + - Roles + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Roles#GET-2 + summary: Show role + operationId: getRole + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/role_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + responses: + '200': + description: '' + content: + application/json: + schema: + type: object + required: + - role + properties: + role: + $ref: '#/components/schemas/role' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + /groups.{format}: + get: + tags: + - Groups + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Groups#GET + summary: List groups + operationId: getGroups + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/x_redmine_switch_user' + - name: builtin + in: query + description: |- + Include built-in groups (e.g., "Anonymous", "Non member") in the response. + Set to `1` to include built-in groups. Without this parameter, only user-created groups are returned. + schema: + type: string + enum: + - '1' + responses: + '200': + description: '' + content: + application/json: + schema: + type: object + required: + - groups + properties: + groups: + type: array + items: + $ref: '#/components/schemas/group.simple' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '412': + $ref: '#/components/responses/PreconditionFailed' + post: + tags: + - Groups + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Groups#POST + summary: Create group + operationId: createGroup + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/x_redmine_switch_user' + requestBody: + content: + application/json: + schema: + type: object + required: + - group + properties: + group: + type: object + properties: + name: + type: string + user_ids: + description: User IDs to add as members of this group. + type: array + items: + type: integer + twofa_required: + description: Require two-factor authentication for all members of this group. + type: boolean + custom_fields: + $ref: '#/components/schemas/custom_fields' + custom_field_values: + $ref: '#/components/schemas/custom_field_values' + responses: + '201': + description: '' + content: + application/json: + schema: + type: object + required: + - group + properties: + group: + $ref: '#/components/schemas/group' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '412': + $ref: '#/components/responses/PreconditionFailed' + '422': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/errors' + /groups/{group_id}.{format}: + get: + tags: + - Groups + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Groups#GET-2 + summary: Show group + operationId: getGroup + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/group_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + - name: include + in: query + description: |- + Comma-separated list of associated data to include in the response. + Values: `users` (list of users in the group), `memberships` (project memberships with roles) + schema: + type: array + items: + type: string + enum: + - users + - memberships + explode: false + responses: + '200': + description: '' + content: + application/json: + schema: + type: object + required: + - group + properties: + group: + $ref: '#/components/schemas/group' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + put: + tags: + - Groups + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Groups#PUT + summary: Update group + operationId: updateGroup + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/group_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + requestBody: + content: + application/json: + schema: + type: object + properties: + group: + type: object + properties: + name: + type: string + user_ids: + type: array + items: + type: integer + twofa_required: + type: boolean + custom_fields: + $ref: '#/components/schemas/custom_fields' + custom_field_values: + $ref: '#/components/schemas/custom_field_values' + responses: + '204': + description: '' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + '422': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/errors' + delete: + tags: + - Groups + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Groups#DELETE + summary: Delete group + operationId: deleteGroup + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/group_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + responses: + '204': + description: '' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + /groups/{group_id}/users.{format}: + post: + tags: + - Groups + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Groups#POST-2 + summary: Add user to group + operationId: addUserToGroup + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/group_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + requestBody: + content: + application/json: + schema: + type: object + required: + - user_id + properties: + user_id: + type: integer + responses: + '204': + description: '' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + '422': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/errors' + /groups/{group_id}/users/{user_id}.{format}: + delete: + tags: + - Groups + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Groups#DELETE-2 + summary: Remove user from group + operationId: removeUserFromGroup + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/group_id' + - $ref: '#/components/parameters/user_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + responses: + '204': + description: '' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + /custom_fields.{format}: + get: + tags: + - Custom Fields + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_CustomFields#GET + summary: List custom fields + operationId: getCustomFields + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/x_redmine_switch_user' + responses: + '200': + description: '' + content: + application/json: + schema: + type: object + required: + - custom_fields + properties: + custom_fields: + type: array + items: + $ref: '#/components/schemas/custom_field' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '412': + $ref: '#/components/responses/PreconditionFailed' + /search.{format}: + get: + tags: + - Search + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Search#GET + summary: Search + operationId: getSearch + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/limit' + - $ref: '#/components/parameters/offset' + - $ref: '#/components/parameters/nometa' + - $ref: '#/components/parameters/x_redmine_switch_user' + - $ref: '#/components/parameters/x_redmine_nometa' + - name: q + in: query + description: |- + Search query string. Tokenized into words for matching. + When `all_words` is enabled (default), all tokens must match. Otherwise, any token can match. + schema: + type: string + required: true + - name: scope + in: query + description: |- + Search scope. + Values: `all` (all projects), `my_projects` (projects where current user is a member), `bookmarks` (bookmarked projects), `subprojects` (include subprojects, only effective for project-scoped search) + schema: + type: string + enum: + - all + - my_projects + - bookmarks + - subprojects + - name: all_words + in: query + description: |- + Require all words to match. Default is `true` (all words must match). + Set to empty string to disable (any word matches). Any non-empty value enables this option. + schema: + type: string + - name: titles_only + in: query + description: |- + Search titles only. Default is `false`. + Any non-empty value enables this option. + schema: + type: string + - name: issues + in: query + description: |- + Include issues in search results. Any non-empty value enables this search type. + If no search type parameters are specified, all types are searched. + schema: + type: integer + enum: + - 1 + - name: news + in: query + description: |- + Include news in search results. Any non-empty value enables this search type. + If no search type parameters are specified, all types are searched. + schema: + type: integer + enum: + - 1 + - name: wiki_pages + in: query + description: |- + Include wiki pages in search results. Any non-empty value enables this search type. + If no search type parameters are specified, all types are searched. + schema: + type: integer + enum: + - 1 + - name: projects + in: query + description: |- + Include projects in search results. Any non-empty value enables this search type. + If no search type parameters are specified, all types are searched. + Not available when searching within a specific project. + schema: + type: integer + enum: + - 1 + - name: documents + in: query + description: |- + Include documents in search results. Any non-empty value enables this search type. + If no search type parameters are specified, all types are searched. + schema: + type: integer + enum: + - 1 + - name: changesets + in: query + description: |- + Include changesets in search results. Any non-empty value enables this search type. + If no search type parameters are specified, all types are searched. + schema: + type: integer + enum: + - 1 + - name: messages + in: query + description: |- + Include forum messages in search results. Any non-empty value enables this search type. + If no search type parameters are specified, all types are searched. + schema: + type: integer + enum: + - 1 + - name: open_issues + in: query + description: |- + Only return open issues. Default is `false`. + Any non-empty value enables this option. + schema: + type: string + - name: attachments + in: query + description: |- + Search attachments. Default is `0` (do not search attachments). + Values: `0` (disabled), `1` (search attachments and content), `only` (search attachments only) + schema: + type: string + enum: + - '0' + - '1' + - only + responses: + '200': + description: '' + content: + application/json: + schema: + type: object + required: + - results + properties: + results: + type: array + items: + $ref: '#/components/schemas/search' + total_count: + type: integer + offset: + type: integer + limit: + type: integer + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '412': + $ref: '#/components/responses/PreconditionFailed' + /projects/{project_id}/search.{format}: + get: + tags: + - Search + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Search#GET + summary: Search within project + operationId: getSearchByProject + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/project_id' + - $ref: '#/components/parameters/limit' + - $ref: '#/components/parameters/offset' + - $ref: '#/components/parameters/nometa' + - $ref: '#/components/parameters/x_redmine_switch_user' + - $ref: '#/components/parameters/x_redmine_nometa' + - name: q + in: query + description: |- + Search query string. Tokenized into words for matching. + When `all_words` is enabled (default), all tokens must match. Otherwise, any token can match. + schema: + type: string + required: true + - name: scope + in: query + description: |- + Search scope. + Values: `subprojects` (include subprojects of the specified project) + When not specified, only the specified project is searched. + schema: + type: string + enum: + - subprojects + - name: all_words + in: query + description: |- + Require all words to match. Default is `true` (all words must match). + Set to empty string to disable (any word matches). Any non-empty value enables this option. + schema: + type: string + - name: titles_only + in: query + description: |- + Search titles only. Default is `false`. + Any non-empty value enables this option. + schema: + type: string + - name: issues + in: query + description: |- + Include issues in search results. Any non-empty value enables this search type. + If no search type parameters are specified, all types are searched. + schema: + type: integer + enum: + - 1 + - name: news + in: query + description: |- + Include news in search results. Any non-empty value enables this search type. + If no search type parameters are specified, all types are searched. + schema: + type: integer + enum: + - 1 + - name: wiki_pages + in: query + description: |- + Include wiki pages in search results. Any non-empty value enables this search type. + If no search type parameters are specified, all types are searched. + schema: + type: integer + enum: + - 1 + - name: documents + in: query + description: |- + Include documents in search results. Any non-empty value enables this search type. + If no search type parameters are specified, all types are searched. + schema: + type: integer + enum: + - 1 + - name: changesets + in: query + description: |- + Include changesets in search results. Any non-empty value enables this search type. + If no search type parameters are specified, all types are searched. + schema: + type: integer + enum: + - 1 + - name: messages + in: query + description: |- + Include forum messages in search results. Any non-empty value enables this search type. + If no search type parameters are specified, all types are searched. + schema: + type: integer + enum: + - 1 + - name: open_issues + in: query + description: |- + Only return open issues. Default is `false`. + Any non-empty value enables this option. + schema: + type: string + - name: attachments + in: query + description: |- + Search attachments. Default is `0` (do not search attachments). + Values: `0` (disabled), `1` (search attachments and content), `only` (search attachments only) + schema: + type: string + enum: + - '0' + - '1' + - only + responses: + '200': + description: '' + content: + application/json: + schema: + type: object + required: + - results + properties: + results: + type: array + items: + $ref: '#/components/schemas/search' + total_count: + type: integer + offset: + type: integer + limit: + type: integer + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + /projects/{project_id}/files.{format}: + get: + tags: + - Files + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Files#GET + summary: List files + operationId: getFiles + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/project_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + responses: + '200': + description: '' + content: + application/json: + schema: + type: object + required: + - files + properties: + files: + type: array + items: + $ref: '#/components/schemas/file' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + post: + tags: + - Files + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_Files#POST + summary: Create file + operationId: createFile + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/project_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + requestBody: + content: + application/json: + schema: + type: object + required: + - file + properties: + file: + type: object + required: + - token + properties: + token: + type: string + version_id: + type: integer + filename: + type: string + description: + type: string + content_type: + type: string + responses: + '204': + description: '' + '400': + description: '' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + /my/account.{format}: + get: + tags: + - My Account + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_MyAccount#GET + summary: Show my account + operationId: getMyAccount + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/x_redmine_switch_user' + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/my_account' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '412': + $ref: '#/components/responses/PreconditionFailed' + put: + tags: + - My Account + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_MyAccount#PUT + summary: Update my account + operationId: updateMyAccount + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/x_redmine_switch_user' + requestBody: + content: + application/json: + schema: + type: object + properties: + user: + type: object + properties: + login: + type: string + admin: + type: boolean + firstname: + type: string + lastname: + type: string + mail: + type: string + language: + type: string + auth_source_id: + type: integer + nullable: true + mail_notification: + type: string + enum: + - all + - selected + - only_my_events + - only_assigned + - only_owner + - none + notified_project_ids: + type: array + items: + type: integer + must_change_passwd: + type: boolean + generate_password: + type: boolean + status: + type: integer + custom_fields: + $ref: '#/components/schemas/custom_fields' + custom_field_values: + $ref: '#/components/schemas/custom_field_values' + group_ids: + type: array + items: + type: integer + pref: + $ref: '#/components/schemas/pref' + responses: + '204': + description: '' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '412': + $ref: '#/components/responses/PreconditionFailed' + '422': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/errors' + /journals/{journal_id}.{format}: + put: + tags: + - Journals + summary: Update journal + description: 'Undocumented: This operation is not listed in the official documentation.' + operationId: updateJournal + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/journal_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + requestBody: + content: + application/json: + schema: + type: object + properties: + journal: + type: object + properties: + notes: + type: string + nullable: true + private_notes: + description: Mark the journal notes as private. Only visible to users with the "View private notes" permission. + type: boolean + responses: + '204': + description: '' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + /uploads.{format}: + post: + tags: + - Attachments + externalDocs: + url: https://www.redmine.org/projects/redmine/wiki/Rest_api#Attaching-files + summary: Upload attachment file + operationId: uploadAttachmentFile + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/x_redmine_switch_user' + - name: filename + in: query + description: Filename for the uploaded file. If not provided, a random hexadecimal name is generated. + schema: + type: string + - name: content_type + in: query + description: MIME type of the file (e.g., `application/pdf`, `image/png`). If not provided, auto-detected from the filename. + schema: + type: string + requestBody: + content: + application/octet-stream: + schema: + type: string + format: binary + responses: + '201': + description: '' + content: + application/json: + schema: + type: object + required: + - upload + properties: + upload: + type: object + required: + - id + - token + properties: + id: + type: integer + token: + type: string + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '406': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + '422': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/errors' + /projects/{project_id}/repository/{repository_id}/revisions/{revision}/issues.{format}: + post: + tags: + - Repositories + summary: Add related issue + description: 'Undocumented: This operation is not listed in the official documentation.' + operationId: addRelatedIssue + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/project_id' + - $ref: '#/components/parameters/repository_id' + - $ref: '#/components/parameters/revision' + - $ref: '#/components/parameters/x_redmine_switch_user' + requestBody: + content: + application/json: + schema: + type: object + required: + - issue_id + properties: + issue_id: + type: integer + responses: + '204': + description: '' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' + '422': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/errors' + /projects/{project_id}/repository/{repository_id}/revisions/{revision}/issues/{issue_id}.{format}: + delete: + tags: + - Repositories + summary: Remove related issue + description: 'Undocumented: This operation is not listed in the official documentation.' + operationId: removeRelatedIssue + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/project_id' + - $ref: '#/components/parameters/repository_id' + - $ref: '#/components/parameters/revision' + - $ref: '#/components/parameters/issue_id' + - $ref: '#/components/parameters/x_redmine_switch_user' + responses: + '204': + description: '' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: '' + '412': + $ref: '#/components/responses/PreconditionFailed' +components: + securitySchemes: + Basic: + type: http + scheme: basic + ApiKey: + type: apiKey + in: header + name: X-Redmine-API-Key + ApiKeyInQuery: + type: apiKey + in: query + name: key + OAuth2: + type: oauth2 + flows: + authorizationCode: + authorizationUrl: /oauth/authorize + tokenUrl: /oauth/token + scopes: {} + responses: + Unauthorized: + description: Authentication required. Returns 401 when API authentication is enabled but no valid credentials are provided, or 403 when the REST API is disabled. + Forbidden: + description: Insufficient permissions. Returns 403 when the authenticated user is not authorized for the requested action. + PreconditionFailed: + description: Invalid X-Redmine-Switch-User header. Returns 412 when the specified user does not exist or is not active. Only applicable to admin users using the X-Redmine-Switch-User header. + parameters: + format: + name: format + in: path + required: true + schema: + type: string + enum: + - json + - xml + offset: + name: offset + in: query + schema: + type: integer + limit: + name: limit + in: query + schema: + type: integer + nometa: + name: nometa + in: query + schema: + type: integer + enum: + - 1 + x_redmine_switch_user: + name: X-Redmine-Switch-User + in: header + schema: + type: string + example: jsmith + x_redmine_nometa: + name: X-Redmine-Nometa + in: header + schema: + type: integer + enum: + - 1 + issue_id: + name: issue_id + in: path + required: true + schema: + type: integer + project_id: + name: project_id + in: path + required: true + schema: + oneOf: + - type: integer + - type: string + user_id: + name: user_id + in: path + required: true + schema: + type: integer + membership_id: + name: membership_id + in: path + required: true + schema: + type: integer + time_entry_id: + name: time_entry_id + in: path + required: true + schema: + type: integer + news_id: + name: news_id + in: path + required: true + schema: + type: integer + issue_relation_id: + name: issue_relation_id + in: path + required: true + schema: + type: integer + version_id: + name: version_id + in: path + required: true + schema: + type: integer + wiki_page_title: + name: wiki_page_title + in: path + required: true + schema: + type: string + attachment_id: + name: attachment_id + in: path + required: true + schema: + type: integer + filename: + name: filename + in: path + required: true + schema: + type: string + issue_category_id: + name: issue_category_id + in: path + required: true + schema: + type: integer + role_id: + name: role_id + in: path + required: true + schema: + type: integer + group_id: + name: group_id + in: path + required: true + schema: + type: integer + journal_id: + name: journal_id + in: path + required: true + schema: + type: integer + repository_id: + name: repository_id + in: path + required: true + schema: + oneOf: + - type: integer + - type: string + revision: + name: revision + in: path + required: true + schema: + type: string + schemas: + id_name: + type: object + required: + - id + - name + properties: + id: + type: integer + name: + type: string + issue_status.simple: + type: object + required: + - id + - name + - is_closed + properties: + id: + type: integer + name: + type: string + is_closed: + type: boolean + custom_field_value: + type: object + required: + - id + - name + - value + properties: + id: + type: integer + name: + type: string + multiple: + type: boolean + value: + oneOf: + - type: string + nullable: true + - type: array + items: + type: string + attachment: + type: object + required: + - id + - filename + - filesize + - content_type + - description + - content_url + - created_on + properties: + id: + type: integer + filename: + type: string + filesize: + type: integer + content_type: + type: string + nullable: true + description: + type: string + nullable: true + content_url: + type: string + thumbnail_url: + type: string + author: + $ref: '#/components/schemas/id_name' + created_on: + type: string + format: date-time + issue.simple: + type: object + required: + - id + - subject + - description + - start_date + - due_date + - done_ratio + - is_private + - estimated_hours + - total_estimated_hours + - created_on + - updated_on + - closed_on + properties: + id: + type: integer + project: + $ref: '#/components/schemas/id_name' + tracker: + $ref: '#/components/schemas/id_name' + status: + $ref: '#/components/schemas/issue_status.simple' + priority: + $ref: '#/components/schemas/id_name' + author: + $ref: '#/components/schemas/id_name' + assigned_to: + $ref: '#/components/schemas/id_name' + category: + $ref: '#/components/schemas/id_name' + fixed_version: + $ref: '#/components/schemas/id_name' + parent: + type: object + properties: + id: + type: integer + subject: + type: string + description: + type: string + nullable: true + start_date: + type: string + format: date + nullable: true + due_date: + type: string + format: date + nullable: true + done_ratio: + type: integer + minimum: 0 + maximum: 100 + is_private: + type: boolean + estimated_hours: + type: number + nullable: true + total_estimated_hours: + type: number + nullable: true + spent_hours: + type: number + total_spent_hours: + type: number + custom_fields: + type: array + items: + $ref: '#/components/schemas/custom_field_value' + created_on: + type: string + format: date-time + updated_on: + type: string + format: date-time + closed_on: + type: string + format: date-time + nullable: true + attachments: + type: array + items: + $ref: '#/components/schemas/attachment' + relations: + type: array + items: + type: object + properties: + id: + type: integer + issue_id: + type: integer + issue_to_id: + type: integer + relation_type: + type: string + enum: + - relates + - duplicates + - duplicated + - blocks + - blocked + - precedes + - follows + - copied_to + - copied_from + delay: + type: integer + nullable: true + custom_fields: + type: array + items: + type: object + required: + - id + properties: + id: + type: integer + value: + oneOf: + - type: string + nullable: true + - type: array + items: + type: string + custom_field_values: + type: object + additionalProperties: + oneOf: + - type: string + nullable: true + - type: array + items: + type: string + uploads: + type: array + items: + type: object + properties: + token: + type: string + filename: + type: string + description: + type: string + content_type: + type: string + errors: + type: object + required: + - errors + properties: + errors: + type: array + items: + type: string + issue: + type: object + required: + - id + - subject + - description + - start_date + - due_date + - done_ratio + - is_private + - estimated_hours + - total_estimated_hours + - created_on + - updated_on + - closed_on + properties: + id: + type: integer + project: + $ref: '#/components/schemas/id_name' + tracker: + $ref: '#/components/schemas/id_name' + status: + $ref: '#/components/schemas/issue_status.simple' + priority: + $ref: '#/components/schemas/id_name' + author: + $ref: '#/components/schemas/id_name' + assigned_to: + $ref: '#/components/schemas/id_name' + category: + $ref: '#/components/schemas/id_name' + fixed_version: + $ref: '#/components/schemas/id_name' + parent: + type: object + properties: + id: + type: integer + subject: + type: string + description: + type: string + nullable: true + start_date: + type: string + format: date + nullable: true + due_date: + type: string + format: date + nullable: true + done_ratio: + type: integer + minimum: 0 + maximum: 100 + is_private: + type: boolean + estimated_hours: + type: number + nullable: true + total_estimated_hours: + type: number + nullable: true + spent_hours: + type: number + total_spent_hours: + type: number + custom_fields: + type: array + items: + $ref: '#/components/schemas/custom_field_value' + created_on: + type: string + format: date-time + updated_on: + type: string + format: date-time + closed_on: + type: string + format: date-time + nullable: true + changesets: + type: array + items: + type: object + properties: + revision: + type: string + user: + $ref: '#/components/schemas/id_name' + comments: + type: string + committed_on: + type: string + format: date-time + children: + type: array + items: + type: object + properties: + id: + type: integer + tracker: + $ref: '#/components/schemas/id_name' + subject: + type: string + attachments: + type: array + items: + $ref: '#/components/schemas/attachment' + relations: + type: array + items: + type: object + properties: + id: + type: integer + issue_id: + type: integer + issue_to_id: + type: integer + relation_type: + type: string + enum: + - relates + - duplicates + - duplicated + - blocks + - blocked + - precedes + - follows + - copied_to + - copied_from + delay: + type: integer + nullable: true + journals: + type: array + items: + type: object + properties: + id: + type: integer + user: + $ref: '#/components/schemas/id_name' + notes: + type: string + created_on: + type: string + format: date-time + updated_on: + type: string + format: date-time + updated_by: + $ref: '#/components/schemas/id_name' + private_notes: + type: boolean + details: + type: array + items: + type: object + properties: + property: + type: string + name: + type: string + old_value: + type: string + nullable: true + new_value: + type: string + nullable: true + watchers: + type: array + items: + $ref: '#/components/schemas/id_name' + allowed_statuses: + type: array + items: + $ref: '#/components/schemas/issue_status.simple' + project.simple: + type: object + required: + - id + - name + - identifier + - description + - homepage + - status + - is_public + - inherit_members + - created_on + - updated_on + properties: + id: + type: integer + name: + type: string + identifier: + type: string + description: + type: string + nullable: true + homepage: + type: string + nullable: true + parent: + $ref: '#/components/schemas/id_name' + status: + type: integer + is_public: + type: boolean + inherit_members: + type: boolean + custom_fields: + type: array + items: + $ref: '#/components/schemas/custom_field_value' + trackers: + type: array + items: + $ref: '#/components/schemas/id_name' + issue_categories: + type: array + items: + $ref: '#/components/schemas/id_name' + time_entry_activities: + type: array + items: + $ref: '#/components/schemas/id_name' + enabled_modules: + type: array + items: + $ref: '#/components/schemas/id_name' + issue_custom_fields: + type: array + items: + $ref: '#/components/schemas/id_name' + created_on: + type: string + format: date-time + updated_on: + type: string + format: date-time + project: + type: object + required: + - id + - name + - identifier + - description + - homepage + - status + - is_public + - inherit_members + - created_on + - updated_on + properties: + id: + type: integer + name: + type: string + identifier: + type: string + description: + type: string + nullable: true + homepage: + type: string + nullable: true + parent: + $ref: '#/components/schemas/id_name' + status: + type: integer + is_public: + type: boolean + inherit_members: + type: boolean + default_version: + $ref: '#/components/schemas/id_name' + default_assignee: + $ref: '#/components/schemas/id_name' + custom_fields: + type: array + items: + $ref: '#/components/schemas/custom_field_value' + trackers: + type: array + items: + $ref: '#/components/schemas/id_name' + issue_categories: + type: array + items: + $ref: '#/components/schemas/id_name' + time_entry_activities: + type: array + items: + $ref: '#/components/schemas/id_name' + enabled_modules: + type: array + items: + $ref: '#/components/schemas/id_name' + issue_custom_fields: + type: array + items: + $ref: '#/components/schemas/id_name' + created_on: + type: string + format: date-time + updated_on: + type: string + format: date-time + membership_role: + type: object + required: + - id + - name + properties: + id: + type: integer + name: + type: string + inherited: + type: boolean + membership: + type: object + required: + - id + - project + - roles + properties: + id: + type: integer + project: + $ref: '#/components/schemas/id_name' + user: + $ref: '#/components/schemas/id_name' + group: + $ref: '#/components/schemas/id_name' + roles: + type: array + items: + $ref: '#/components/schemas/membership_role' + user.simple: + type: object + required: + - id + - login + - admin + - firstname + - lastname + - mail + - created_on + - updated_on + - last_login_on + - passwd_changed_on + - twofa_scheme + - status + properties: + id: + type: integer + login: + type: string + admin: + type: boolean + firstname: + type: string + lastname: + type: string + mail: + type: string + created_on: + type: string + format: date-time + updated_on: + type: string + format: date-time + last_login_on: + type: string + format: date-time + nullable: true + passwd_changed_on: + type: string + format: date-time + nullable: true + avatar_url: + type: string + twofa_scheme: + type: string + nullable: true + status: + type: integer + auth_source: + $ref: '#/components/schemas/id_name' + custom_fields: + type: array + items: + $ref: '#/components/schemas/custom_field_value' + pref: + type: object + properties: + hide_mail: + type: boolean + time_zone: + type: string + nullable: true + comments_sorting: + type: string + enum: + - asc + - desc + warn_on_leaving_unsaved: + type: boolean + no_self_notified: + type: boolean + notify_about_high_priority_issues: + type: boolean + textarea_font: + type: string + enum: + - monospace + - proportional + nullable: true + recently_used_projects: + type: integer + history_default_tab: + type: string + enum: + - notes + - history + - properties + - time_entries + - changesets + - last_tab_visited + toolbar_language_options: + type: string + default_issue_query: + type: integer + nullable: true + default_project_query: + type: integer + nullable: true + auto_watch_on: + type: array + items: + type: string + enum: + - issue_created + - issue_contributed_to + - issue_assigned_to_me + user: + type: object + required: + - id + - login + - firstname + - lastname + - created_on + - updated_on + - last_login_on + - passwd_changed_on + properties: + id: + type: integer + login: + type: string + admin: + type: boolean + firstname: + type: string + lastname: + type: string + mail: + type: string + created_on: + type: string + format: date-time + updated_on: + type: string + format: date-time + last_login_on: + type: string + format: date-time + nullable: true + passwd_changed_on: + type: string + format: date-time + nullable: true + avatar_url: + type: string + twofa_scheme: + type: string + nullable: true + api_key: + type: string + status: + type: integer + custom_fields: + type: array + items: + $ref: '#/components/schemas/custom_field_value' + auth_source: + $ref: '#/components/schemas/id_name' + groups: + type: array + items: + $ref: '#/components/schemas/id_name' + memberships: + type: array + items: + type: object + required: + - id + - project + - roles + properties: + id: + type: integer + project: + $ref: '#/components/schemas/id_name' + roles: + type: array + items: + type: object + required: + - id + - name + properties: + id: + type: integer + name: + type: string + inherited: + type: boolean + time_entry: + type: object + required: + - id + - hours + - comments + - spent_on + - created_on + - updated_on + properties: + id: + type: integer + project: + $ref: '#/components/schemas/id_name' + issue: + type: object + required: + - id + properties: + id: + type: integer + user: + $ref: '#/components/schemas/id_name' + activity: + $ref: '#/components/schemas/id_name' + hours: + type: number + comments: + type: string + nullable: true + spent_on: + type: string + format: date + created_on: + type: string + format: date-time + updated_on: + type: string + format: date-time + custom_fields: + type: array + items: + $ref: '#/components/schemas/custom_field_value' + news_comment: + type: object + required: + - id + - content + properties: + id: + type: integer + author: + $ref: '#/components/schemas/id_name' + content: + type: string + news: + type: object + required: + - id + - title + - description + - created_on + properties: + id: + type: integer + project: + $ref: '#/components/schemas/id_name' + author: + $ref: '#/components/schemas/id_name' + title: + type: string + summary: + type: string + description: + type: string + created_on: + type: string + format: date-time + attachments: + type: array + items: + $ref: '#/components/schemas/attachment' + comments: + type: array + items: + $ref: '#/components/schemas/news_comment' + issue_relation: + type: object + required: + - id + - issue_id + - issue_to_id + - relation_type + - delay + properties: + id: + type: integer + issue_id: + type: integer + issue_to_id: + type: integer + relation_type: + type: string + enum: + - relates + - duplicates + - duplicated + - blocks + - blocked + - precedes + - follows + - copied_to + - copied_from + delay: + type: integer + nullable: true + version.simple: + type: object + required: + - id + - name + - description + - status + - due_date + - sharing + - wiki_page_title + - created_on + - updated_on + properties: + id: + type: integer + project: + $ref: '#/components/schemas/id_name' + name: + type: string + description: + type: string + nullable: true + status: + type: string + enum: + - open + - locked + - closed + due_date: + type: string + format: date + nullable: true + sharing: + type: string + enum: + - none + - descendants + - hierarchy + - tree + - system + wiki_page_title: + type: string + nullable: true + custom_fields: + type: array + items: + $ref: '#/components/schemas/custom_field_value' + created_on: + type: string + format: date-time + updated_on: + type: string + format: date-time + version: + type: object + required: + - id + - name + - description + - status + - due_date + - sharing + - wiki_page_title + - created_on + - updated_on + properties: + id: + type: integer + project: + $ref: '#/components/schemas/id_name' + name: + type: string + description: + type: string + nullable: true + status: + type: string + enum: + - open + - locked + - closed + due_date: + type: string + format: date + nullable: true + sharing: + type: string + enum: + - none + - descendants + - hierarchy + - tree + - system + wiki_page_title: + type: string + nullable: true + estimated_hours: + type: number + spent_hours: + type: number + custom_fields: + type: array + items: + $ref: '#/components/schemas/custom_field_value' + created_on: + type: string + format: date-time + updated_on: + type: string + format: date-time + wiki_page.simple: + type: object + required: + - title + - version + - created_on + - updated_on + properties: + title: + type: string + parent: + type: object + required: + - title + properties: + title: + type: string + version: + type: integer + created_on: + type: string + format: date-time + updated_on: + type: string + format: date-time + wiki_page: + type: object + required: + - title + - text + - version + - comments + - created_on + - updated_on + properties: + title: + type: string + parent: + type: object + required: + - title + properties: + title: + type: string + text: + type: string + version: + type: integer + author: + $ref: '#/components/schemas/id_name' + comments: + type: string + nullable: true + project: + $ref: '#/components/schemas/id_name' + created_on: + type: string + format: date-time + updated_on: + type: string + format: date-time + attachments: + type: array + items: + $ref: '#/components/schemas/attachment' + query: + type: object + required: + - id + - name + - is_public + - project_id + properties: + id: + type: integer + name: + type: string + is_public: + type: boolean + project_id: + type: integer + nullable: true + issue_status: + type: object + required: + - id + - name + - is_closed + - description + properties: + id: + type: integer + name: + type: string + is_closed: + type: boolean + description: + type: string + nullable: true + tracker: + type: object + required: + - id + - name + - description + - enabled_standard_fields + properties: + id: + type: integer + name: + type: string + default_status: + $ref: '#/components/schemas/id_name' + description: + type: string + nullable: true + enabled_standard_fields: + type: array + items: + type: string + issue_category: + type: object + required: + - id + - name + properties: + id: + type: integer + project: + $ref: '#/components/schemas/id_name' + name: + type: string + assigned_to: + $ref: '#/components/schemas/id_name' + issue_priority: + type: object + required: + - id + - name + - is_default + - active + properties: + id: + type: integer + name: + type: string + is_default: + type: boolean + active: + type: boolean + custom_fields: + type: array + items: + $ref: '#/components/schemas/custom_field_value' + time_entry_activity: + type: object + required: + - id + - name + - is_default + - active + properties: + id: + type: integer + name: + type: string + is_default: + type: boolean + active: + type: boolean + custom_fields: + type: array + items: + $ref: '#/components/schemas/custom_field_value' + document_category: + type: object + required: + - id + - name + - is_default + - active + properties: + id: + type: integer + name: + type: string + is_default: + type: boolean + active: + type: boolean + custom_fields: + type: array + items: + $ref: '#/components/schemas/custom_field_value' + role: + type: object + required: + - id + - name + - assignable + - issues_visibility + - time_entries_visibility + - users_visibility + - permissions + properties: + id: + type: integer + name: + type: string + assignable: + type: boolean + issues_visibility: + type: string + time_entries_visibility: + type: string + users_visibility: + type: string + permissions: + type: array + items: + type: string + group.simple: + type: object + required: + - id + - name + properties: + id: + type: integer + name: + type: string + builtin: + type: string + custom_fields: + type: array + items: + $ref: '#/components/schemas/custom_field_value' + group: + type: object + required: + - id + - name + properties: + id: + type: integer + name: + type: string + builtin: + type: string + custom_fields: + type: array + items: + $ref: '#/components/schemas/custom_field_value' + users: + type: array + items: + $ref: '#/components/schemas/id_name' + memberships: + type: array + items: + type: object + required: + - id + - project + - roles + properties: + id: + type: integer + project: + $ref: '#/components/schemas/id_name' + roles: + type: array + items: + $ref: '#/components/schemas/membership_role' + custom_field: + type: object + required: + - id + - name + - description + - field_format + - regexp + - min_length + - max_length + - is_required + - is_filter + - searchable + - multiple + - default_value + - visible + - editable + properties: + id: + type: integer + name: + type: string + description: + type: string + nullable: true + customized_type: + type: string + enum: + - issue + - time_entry + - project + - version + - document + - user + - group + - time_entry_activity + - issue_priority + - document_category + field_format: + type: string + enum: + - enumeration + - string + - version + - attachment + - user + - list + - link + - float + - int + - date + - bool + - text + - progressbar + regexp: + type: string + min_length: + type: integer + nullable: true + max_length: + type: integer + nullable: true + is_required: + type: boolean + is_filter: + type: boolean + searchable: + type: boolean + multiple: + type: boolean + default_value: + type: string + nullable: true + visible: + type: boolean + editable: + type: boolean + trackers: + type: array + items: + $ref: '#/components/schemas/id_name' + roles: + type: array + items: + $ref: '#/components/schemas/id_name' + possible_values: + type: array + items: + type: object + properties: + value: + type: string + label: + type: string + search: + type: object + required: + - id + - title + - type + - url + - description + - datetime + properties: + id: + type: integer + title: + type: string + type: + type: string + url: + type: string + description: + type: string + nullable: true + datetime: + type: string + format: date-time + file: + type: object + required: + - id + - filename + - filesize + - content_type + - description + - content_url + - created_on + - digest + - downloads + properties: + id: + type: integer + filename: + type: string + filesize: + type: integer + content_type: + type: string + nullable: true + description: + type: string + nullable: true + content_url: + type: string + thumbnail_url: + type: string + author: + $ref: '#/components/schemas/id_name' + created_on: + type: string + format: date-time + version: + $ref: '#/components/schemas/id_name' + digest: + type: string + downloads: + type: integer + my_account: + type: object + required: + - user + properties: + user: + type: object + required: + - id + - login + - admin + - firstname + - lastname + - mail + - created_on + - last_login_on + - api_key + properties: + id: + type: integer + login: + type: string + admin: + type: boolean + firstname: + type: string + lastname: + type: string + mail: + type: string + created_on: + type: string + format: date-time + last_login_on: + type: string + format: date-time + nullable: true + api_key: + type: string + custom_fields: + type: array + items: + $ref: '#/components/schemas/custom_field_value'