A new user interface for you! Read more...

File golang-l3t-0.0.1.obscpio of Package golang-l3t

07070100000000000081A4000003E800000064000000015CBEFDCB0000006E000000000000000000000000000000000000001D00000000golang-l3t-0.0.1/.travis.ymllanguage: go

go:
  - 1.8.x
  - master

install:
  - go get -v github.com/Masterminds/glide 
  - make get-deps07070100000001000081A4000003E800000064000000015CBEFDCB0000170B000000000000000000000000000000000000001C00000000golang-l3t-0.0.1/Gopkg.lock# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.


[[projects]]
  name = "github.com/PuerkitoBio/goquery"
  packages = ["."]
  revision = "2d2796f41742ece03e8086188fa4db16a3a0b458"
  version = "v1.5.0"

[[projects]]
  name = "github.com/andybalholm/cascadia"
  packages = ["."]
  revision = "901648c87902174f774fac311d7f176f8647bdaa"
  version = "v1.0.0"

[[projects]]
  branch = "master"
  name = "github.com/bhdn/go-supportconfig"
  packages = ["."]
  revision = "da32e518978ba9a1beb2791897fec8f184b74fc4"

[[projects]]
  branch = "master"
  name = "github.com/bhdn/go-suseapi"
  packages = ["bugzilla"]
  revision = "5a5ad5bede15603bec19ab88a77c4e7a7c79c528"

[[projects]]
  branch = "master"
  name = "github.com/dmacvicar/gorgojo"
  packages = ["plugins/novell"]
  revision = "f91261d568bf71f4a64c8f0ffcd459a4ad6d1d1d"

[[projects]]
  branch = "master"
  name = "github.com/eidolon/wordwrap"
  packages = ["."]
  revision = "e0f54129b8bb1d473949f54546a365468435aeaf"

[[projects]]
  name = "github.com/fsnotify/fsnotify"
  packages = ["."]
  revision = "c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9"
  version = "v1.4.7"

[[projects]]
  branch = "master"
  name = "github.com/google/shlex"
  packages = ["."]
  revision = "c34317bd91bf98fab745d77b03933cf8769299fe"

[[projects]]
  name = "github.com/hashicorp/hcl"
  packages = [
    ".",
    "hcl/ast",
    "hcl/parser",
    "hcl/printer",
    "hcl/scanner",
    "hcl/strconv",
    "hcl/token",
    "json/parser",
    "json/scanner",
    "json/token"
  ]
  revision = "8cb6e5b959231cc1119e43259c4a608f9c51a241"
  version = "v1.0.0"

[[projects]]
  branch = "master"
  name = "github.com/headzoo/surf"
  packages = [
    ".",
    "agent",
    "browser",
    "errors",
    "jar",
    "util"
  ]
  revision = "a4a8c16c01dc47ef3a25326d21745806f3e6797a"

[[projects]]
  name = "github.com/inconshreveable/mousetrap"
  packages = ["."]
  revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75"
  version = "v1.0"

[[projects]]
  name = "github.com/kr/pretty"
  packages = ["."]
  revision = "73f6ac0b30a98e433b289500d779f50c1a6f0712"
  version = "v0.1.0"

[[projects]]
  name = "github.com/kr/text"
  packages = ["."]
  revision = "e2ffdb16a802fe2bb95e2e35ff34f0e53aeef34f"
  version = "v0.1.0"

[[projects]]
  name = "github.com/magiconair/properties"
  packages = ["."]
  revision = "c2353362d570a7bfa228149c62842019201cfb71"
  version = "v1.8.0"

[[projects]]
  name = "github.com/mattn/go-runewidth"
  packages = ["."]
  revision = "3ee7d812e62a0804a7d0a324e0249ca2db3476d3"
  version = "v0.0.4"

[[projects]]
  name = "github.com/microcosm-cc/bluemonday"
  packages = ["."]
  revision = "506f3da9b7c86d737e91f16b7431df8635871552"
  version = "v1.0.2"

[[projects]]
  name = "github.com/mitchellh/mapstructure"
  packages = ["."]
  revision = "3536a929edddb9a5b34bd6861dc4a9647cb459fe"
  version = "v1.1.2"

[[projects]]
  name = "github.com/opencontainers/runc"
  packages = ["libcontainer/utils"]
  revision = "baf6536d6259209c3edfa2b22237af82942d3dfa"
  version = "v0.1.1"

[[projects]]
  name = "github.com/pelletier/go-toml"
  packages = ["."]
  revision = "63909f0a90ab0f36909e8e044e46ace10cf13ba2"
  version = "v1.3.0"

[[projects]]
  name = "github.com/spf13/afero"
  packages = [
    ".",
    "mem"
  ]
  revision = "588a75ec4f32903aa5e39a2619ba6a4631e28424"
  version = "v1.2.2"

[[projects]]
  name = "github.com/spf13/cast"
  packages = ["."]
  revision = "8c9545af88b134710ab1cd196795e7f2388358d7"
  version = "v1.3.0"

[[projects]]
  name = "github.com/spf13/cobra"
  packages = ["."]
  revision = "ef82de70bb3f60c65fb8eebacbb2d122ef517385"
  version = "v0.0.3"

[[projects]]
  name = "github.com/spf13/jwalterweatherman"
  packages = ["."]
  revision = "94f6ae3ed3bceceafa716478c5fbf8d29ca601a1"
  version = "v1.1.0"

[[projects]]
  name = "github.com/spf13/pflag"
  packages = ["."]
  revision = "298182f68c66c05229eb03ac171abe6e309ee79a"
  version = "v1.0.3"

[[projects]]
  name = "github.com/spf13/viper"
  packages = ["."]
  revision = "9e56dacc08fbbf8c9ee2dbc717553c758ce42bc9"
  version = "v1.3.2"

[[projects]]
  branch = "master"
  name = "golang.org/x/crypto"
  packages = ["ssh/terminal"]
  revision = "a5d413f7728c81fb97d96a2b722368945f651e78"

[[projects]]
  branch = "master"
  name = "golang.org/x/net"
  packages = [
    "html",
    "html/atom"
  ]
  revision = "74de082e2cca95839e88aa0aeee5aadf6ce7710f"

[[projects]]
  branch = "master"
  name = "golang.org/x/sys"
  packages = [
    "unix",
    "windows"
  ]
  revision = "baf5eb976a8cd65845293cd814ea151018552292"

[[projects]]
  name = "golang.org/x/text"
  packages = [
    "internal/gen",
    "internal/triegen",
    "internal/ucd",
    "transform",
    "unicode/cldr",
    "unicode/norm"
  ]
  revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0"
  version = "v0.3.0"

[[projects]]
  name = "gopkg.in/Sirupsen/logrus.v0"
  packages = ["."]
  revision = "ba1b36c82c5e05c4f912a88eab0dcd91a171688f"
  version = "v0.11.5"

[[projects]]
  branch = "v1"
  name = "gopkg.in/check.v1"
  packages = ["."]
  revision = "788fd78401277ebd861206a03c884797c6ec5541"

[[projects]]
  name = "gopkg.in/cheggaaa/pb.v1"
  packages = ["."]
  revision = "f907f6f5dd81f77c2bbc1cde92e4c5a04720cb11"
  version = "v1.0.28"

[[projects]]
  name = "gopkg.in/ini.v1"
  packages = ["."]
  revision = "c85607071cf08ca1adaf48319cd1aa322e81d8c1"
  version = "v1.42.0"

[[projects]]
  branch = "v1"
  name = "gopkg.in/mattes/go-expand-tilde.v1"
  packages = ["."]
  revision = "cb884138e64c9a8bf5c7d6106d74b0fca082df0c"

[[projects]]
  name = "gopkg.in/yaml.v2"
  packages = ["."]
  revision = "51d6538a90f86fe93ac480b35f37b2be17fef232"
  version = "v2.2.2"

[solve-meta]
  analyzer-name = "dep"
  analyzer-version = 1
  inputs-digest = "81e3b20d6fb14258900152c003788bdea5b4a11da6bfbb27c7ab9133d8d0dd08"
  solver-name = "gps-cdcl"
  solver-version = 1
07070100000002000081A4000003E800000064000000015CBEFDCB000006F2000000000000000000000000000000000000001C00000000golang-l3t-0.0.1/Gopkg.toml# Gopkg.toml example
#
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
#   name = "github.com/user/project"
#   version = "1.0.0"
#
# [[constraint]]
#   name = "github.com/user/project2"
#   branch = "dev"
#   source = "github.com/myfork/project2"
#
# [[override]]
#   name = "github.com/x/y"
#   version = "2.4.0"
#
# [prune]
#   non-go = false
#   go-tests = true
#   unused-packages = true


[[constraint]]
  name = "github.com/PuerkitoBio/goquery"
  version = "1.5.0"

[[constraint]]
  branch = "master"
  name = "github.com/dmacvicar/gorgojo"

[[constraint]]
  branch = "master"
  name = "github.com/eidolon/wordwrap"

[[constraint]]
  branch = "master"
  name = "github.com/google/shlex"

[[constraint]]
  name = "github.com/headzoo/surf"
  branch = "master"
#  version = "1.0.0"

[[constraint]]
  name = "github.com/microcosm-cc/bluemonday"
  version = "1.0.2"


[[constraint]]
  name = "github.com/spf13/cobra"
  version = "0.0.3"

[[constraint]]
  name = "github.com/spf13/viper"
  version = "1.3.2"

[[constraint]]
  branch = "master"
  name = "golang.org/x/crypto"

[[constraint]]
  branch = "v1"
  name = "gopkg.in/check.v1"

[[constraint]]
  branch = "v1"
  name = "gopkg.in/mattes/go-expand-tilde.v1"

[[constraint]]
  name = "gopkg.in/yaml.v2"
  version = "2.2.2"

[prune]
  go-tests = true
  unused-packages = true


[[constraint]]
  name = "gopkg.in/cheggaaa/pb.v1"
  version = "1.0.28"

[[constraint]]
  branch = "master"
  name = "github.com/bhdn/go-suseapi"

[[constraint]]
  branch = "master"
  name = "github.com/bhdn/go-supportconfig"
07070100000003000081A4000003E800000064000000015CBEFDCB000003D4000000000000000000000000000000000000001A00000000golang-l3t-0.0.1/Makefile.PHONY: build build-alpine clean test help default

BIN_NAME=golang-l3t

VERSION := $(shell grep "const Version " version/version.go | sed -E 's/.*"(.+)"$$/\1/')
GIT_COMMIT=$(shell git rev-parse HEAD)
GIT_DIRTY=$(shell test -n "`git status --porcelain`" && echo "+CHANGES" || true)
BUILD_DATE=$(shell date '+%Y-%m-%d-%H:%M:%S')

default: test

help:
	@echo 'Management commands for golang-l3t:'
	@echo
	@echo 'Usage:'
	@echo '    make build           Compile the project.'
	@echo '    make get-deps        runs dep ensure, mostly used for ci.'
	
	@echo '    make clean           Clean the directory tree.'
	@echo

build:
	@echo "building ${BIN_NAME} ${VERSION}"
	@echo "GOPATH=${GOPATH}"
	go build -ldflags "-X github.com/bhdn/golang-l3t/version.GitCommit=${GIT_COMMIT}${GIT_DIRTY} -X github.com/bhdn/golang-l3t/version.BuildDate=${BUILD_DATE}" -o bin/${BIN_NAME} l3t/main.go

get-deps:
	dep ensure

clean:
	@test ! -e bin/${BIN_NAME} || rm bin/${BIN_NAME}

test:
	go test ./...

07070100000004000081A4000003E800000064000000015CBEFDCB00001E95000000000000000000000000000000000000001B00000000golang-l3t-0.0.1/README.md# golang-l3t

A Solid Ground and Bugzilla (web) client.

This is a reimplementation of the [Python l3t
tool](https://gitlab.suse.de/barendartchuk/l3t/) in golang, so some of the
scripts were left available only in the older one.

# l3t

Solid Ground and Bugzilla client and helper tools.

## Getting Started

### Solid Ground

In order to use the SG API, you have to go to the [Solid Ground Preferences
page](https://l3support.suse.de/preferences/) and enable API access,
generating a token that you can use with `l3t`. Run `l3t ls -m`, it will
fail and will create a template configuration file at `~/.l3t.yaml`. Modify
it to use your API keys and the Solid Ground address:

```
l3t:
  auth-token: myusername:mytokenfromsg
  api-url: https://solidgroundaddress/api/1/
  work-dir: ~/issues/
bugzilla:
  url: https://apibugzillaaddress/
log:
  level: info
```

Then try running `l3t ls` again.

### Bugzilla

`l3t get-bug` and `l3t edit-bug` rely on the credentials from `osc`, so ensure `osc ls` works for `api.opensuse.org`: running `osc -A https://api.opensuse.org ls` should request your credentials and ensure they are in the osc configuration. If it doesn't work, check where the osc configuration has been placed. We look for it in `~/.oscrc` and `~/.config/osc/oscrc`.

### On l3slave

It should be installed already. Just try using it.

### On your machine

Install `golang-l3t` from the project `home:barendartchuk:l3t` on `build.suse.de`.

## Commands

To see the list of all commands available, use `l3t --help`. To find out
more about each command use `l3t <command> --help`.

These are some of the commands:

* `l3ls`: the equivalent of the SG overview list of incidents. Use `l3ls -m`
  to see only your incidents.
* `l3g`: shows an incident.
* `l3ac`: accepts an incident.
* `l3as`: assigns an incident.
* `l3p`: pings an incident (requivalent of **ack** in the web interface).
* `l3s` and `l3z`: put incidents to sleep.
* `l3se`: searches for incidents.
* `bzg`: displays a bug from bugzilla (relies in credentials from *~/.oscrc*)
* `bze`: edit bugs (setting needinfo, add comments, change whiteboard, URL, etc)
* `bzag`: downloads attachments from a bug.
* `bzat`: adds attachments to a bug.
* `l3n`: suggests the next incident to work on.
* `l3t-showrc`: dumps the current configuration values. Use `l3t-showrc -d`
  to see defaults the documentation for all configuration options.


## Sample usage

```
$ ./l3n
L3:48671 bsc#1047711 network: Ping from tenant1 to tenant2 instance possible even not shared
$ bzg -n2 1047711
[1047711] Bug description
Assigned-To: bug-assignee@suse.com
Cc-List: ['one@suse.com', 'two@suse.com']
URL: https://trello.com/c/XMEUDWPvz/
Product: SUSE OpenStack Cloud 7
Whiteboard:
Severity: Major NTS-Priority: None

=========== Comment from nts@suse.com (2017-07-11T08:00:56+00:00) (private)

yep, User 0 just foudn the note in docu...:
https://docs.openstack.org/ops-guide/ops-users.html

However when having admin role in Horizon I canonly select the projects user is in, so can't switch to tenant2 but on command line I could.

So in simple words it's know and not fixable atm?

=========== Comment from two@suse.com (2017-07-11T08:14:43+00:00) (private)
(In reply to NTS User from comment #11)
> So in simple words it's know and not fixable atm?
Yes, it's known and expected behavior.

[1047711] assignee: bug-assignee@suse.com
$ l3z -b 1047711
Sleep time for incident L3-Support 48671 network: Ping from tenant1 to tenant2 instance possible even not shared updated.
```

## Currently working bug directories (CWB)

Commands that operate on incidents or bugs, such as `l3ac`, `l3as`, `l3z`,
`bzg`, and `bze` allow running them without any arguments, it figures what
bug it is about by extracting the bug number from the CWD. In order to
enable it, you must set the option *bugs-dir* to the directory where you
keep data for the bugs you work on:

```
l3t:
  work-dir: ~/my-bugs-directory
```

So for example:

```
$ pwd
/home/user/my-bugs-directory/123230/PTF:12345/package.SUSE\_Something
$ l3s
Sleep time for incident 48626 set.
$ bzg
[123230] L3: Test bug
Assigned-To: user@suse.com
Cc-List: user@suse.com
URL: None
Product: SUSE OpenStack Cloud 7
Whiteboard: openL3:48626
Severity: Normal        NTS-Priority: None

=========== Comment from user@suse.com (2017-07-03T13:29:15+00:00)
This is a test incident.

=========== Comment from l3-coord@suse.de (2017-07-03T13:31:23+00:00)
(private)
L3:48626 is now handled by User

[1047068] assignee: user@suse.com
$ bze -r
```

## Examples

### Listing all P1s and P0s

```
$ l3se urgent
[....]
```

### Listing all urgent incidents from a given customer handled by a given agent

```
$ l3se urgent -c customer.com -u theagent
```

(See also the option `--run` in the following section.)

### Putting all of your incidents to sleep

`l3se` has the option --run, which allows to invoke another tool with the
incident ID appended to the command line:

```
$ l3se -c customer.example.com --run l3s
```

### Adding yourself to the Cc list of all Cloud bugs:

```
$ l3se cloud --run bze --ccme -i
```

### Creating symlinks for all ``SR*`` directories pointed in a given bug

```
~/issues/1100105> l3t-make-sr-links
/mounts/ziu/SR101173446873 -> ./SR101173446873
/mounts/ziu/SR101174436342-multi -> ./SR101174436342-multi
```

### Performing SG actions directly from mutt

Assuming you have a message from SG or Bugzilla (including `l3-coord` Ccs):

```
Subject: Re: [l3-coord] [Bug 999999] L3: Something not working
```

You can just type `Z` and then `bzo` in order to take a look at the
complete bug directly from Bugzilla. Or you could type `Z` and `bze --ccme`
to add yourself to the Cc list. Or just `Z` and `bze -c` to make a comment
in the bug.

`l3t-mail-action` extracts the incident or bug number from Bugzilla and
Solid Ground, and then calls another tool using `-i <INCIDENT>` or
`-b <BUG>`. So it can be used to perform actions in mutt directly from the
index or the pager:

```
macro index,pager a "<pipe-message>l3t-mail-action l3t accept<enter>
macro index,pager A "<pipe-message>l3t-mail-action l3t assign<enter>
macro index,pager S "<pipe-message>l3t-mail-action l3t sleep<enter>
```

It can also prompt for an action to be taken:

```
macro pager Z "<pipe-message>dd of=/tmp/bug status=none<enter>\
<shell-escape>l3t-mail-action --file /tmp/bug --ask<enter>"
```

### Using notmuch tags for filtering Bugzilla messages related to running incidents

`l3t-notmuch-tag-running` will look for the currently running incidents and
tag those Bugzilla messages related only to running incidents. So with
neomutt it is possible to view only Bugzilla messages related to your
incidents:

```
set virtual_spoolfile="yes"
virtual-mailboxes "My INBOX" "notmuch://?query=tag:l3t-running"
```

And on `~/.offlineimaprc`:

```
[Account SUSE]
# ....
postsynchook = notmuch new; l3t-notmuch-tag-running
```

When an incident is closed, the related messages will have the
`l3t-running` tag removed.

### Creating a PTF without having to specify bug number or product

As the tools know what is the bug number you are working on (based on the
CWB), you can use this thin wrapper around `ptfsetup` in order to not to
have to type that:

```
user@l3slave:~/issues/1104677> l3t-ptf-create iptables
Running ['ptfsetup', '-D', 'sles12-sp2-x86_64', 'iptables', 'L3:52044']
old PTFS                        (m) MU: 1.4.21-4.1
newer
--------------------------------------------------------------------------------
(none)                                 |  (h) HEAD(==MU)

Running incidents without any published PTF:
Select the package to base new PTF on (h/m):
```

### Getting rid of colors in the output

You can set `color` to false to disable coloring completely, or in
`~/.l3t.yaml`:

```
l3t:
  color: false
  # ... other options ...
```

07070100000005000081A4000003E800000064000000015CBEFDCB000004FF000000000000000000000000000000000000001B00000000golang-l3t-0.0.1/cacher.gopackage l3t

import (
	"bytes"
	"io"
	"io/ioutil"
	"os"
	"path"

	"github.com/bhdn/golang-l3t/config"
	"github.com/bhdn/golang-l3t/log"
	tilde "gopkg.in/mattes/go-expand-tilde.v1"
)

type fsCacher struct {
	Name string
}

type nullFile struct {
	bytes.Buffer
}

func (n *nullFile) Close() error {
	n.Buffer.Reset()
	return nil
}

type tempFile struct {
	os.File

	FinalName string
}

func getTempFile(basedir, finalname string) (*tempFile, error) {
	err := os.MkdirAll(basedir, os.ModePerm)
	if err != nil {
		return nil, err
	}
	file, err := ioutil.TempFile(basedir, "incident-*")
	if err != nil {
		return nil, err
	}
	tmp := tempFile{FinalName: finalname}
	tmp.File = *file
	return &tmp, nil
}

func (t *tempFile) Close() (err error) {
	err = t.File.Close()
	if err == nil {
		err = os.Rename(t.File.Name(), t.FinalName)
	}
	return
}

func (c *fsCacher) GetWriter(id string) io.WriteCloser {
	base, err := tilde.Expand(config.Config().GetString("cache.dir"))
	if err != nil {
		log.Debugf("cache: failed to get the cache dir: %v", err)
		return &nullFile{}
	}
	dir := path.Join(base, c.Name, id[0:2])
	dest := path.Join(dir, id)
	tmp, err := getTempFile(dir, dest)
	if err != nil {
		log.Debugf("cache: failed to get temp file: %v", err)
		return &nullFile{}
	}
	return tmp
}
07070100000006000041ED000003E800000064000000025CBEFDCB00000000000000000000000000000000000000000000001800000000golang-l3t-0.0.1/client07070100000007000081A4000003E800000064000000015CBEFDCB00007582000000000000000000000000000000000000002200000000golang-l3t-0.0.1/client/client.gopackage client

import (
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"net/url"
	"path"
	"sort"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/bhdn/golang-l3t/log"
	"github.com/microcosm-cc/bluemonday"
)

type doer interface {
	Do(*http.Request) (*http.Response, error)
}

// Cacher should be anything that takes the name of the object to be cached
// and returns something that can receive writes with the contents and then
// eventually be closed.
type Cacher interface {
	GetWriter(id string) io.WriteCloser
}

// Config sets the parameters needed to set up the client.
type Config struct {
	// BaseURL is the URL as used by the API calls, so it must in
	// include the version part etc.
	BaseURL string

	// AuthToken must be in the format <username>:<token>, the token
	// being generated in the Solid Ground Preferences page.
	AuthToken string

	// Whether GetIncident fetches all attributes from an Incident
	// DEPRECATED
	Slow bool

	// Sets the number of workers that can fetch attributes in
	// background
	Concurrency int

	Cacher Cacher
}

// Client keeps the state of a Solid Ground client
type Client struct {
	doer        doer
	config      *Config
	concurrency int
	srCache     map[int]*SR
	srCacheLock *sync.Mutex
	Unavailable bool
	cacher      Cacher
}

// New prepares a *Client for the Solid Ground API
func New(config *Config) *Client {
	transport := http.Transport{DisableKeepAlives: false}
	doer := &http.Client{Transport: &transport}
	srCache := make(map[int]*SR, 0)
	client := &Client{config: config, doer: doer, srCache: srCache,
		cacher: config.Cacher, srCacheLock: &sync.Mutex{}}
	client.concurrency = config.Concurrency
	if config.Concurrency == 0 {
		client.concurrency = 1
	}
	if err := client.isAvailable(); err != nil {
		client.Unavailable = true
	}
	return client
}

// RequestError is about something going wrong while preparing the request
// to the API
type RequestError struct{ error }

func (e RequestError) Error() string {
	return fmt.Sprintf("cannot build request: %v", e.error)
}

// ConnectionError means something went wrong while sending the request to
// the API
type ConnectionError struct{ error }

func (e ConnectionError) Error() string {
	return fmt.Sprintf("cannot communicate with server: %v", e.error)
}

// ErrNoCredentials means the user needs to set up the credentils in the
// configuration.
var ErrNoCredentials = errors.New("no credentials set in the configuration. Please generate you token in Solid Ground -> Preferences -> API")

func (client *Client) isAvailable() error {
	token := client.config.AuthToken
	if idx := strings.Index(token, "your-solid-ground-username"); idx > -1 {
		return ErrNoCredentials
	}
	return nil
}

func (client *Client) setAuth(req *http.Request) error {
	if err := client.isAvailable(); err != nil {
		return err
	}
	auth := fmt.Sprintf("ApiKey %s", client.config.AuthToken)
	req.Header.Set("Authorization", auth)
	return nil
}

// Ugly
func joinQueries(queries []url.Values) string {
	encodedValues := make([]string, 0)
	for _, query := range queries {
		if len(query) > 0 {
			encodedValues = append(encodedValues, query.Encode())
		}
	}
	rawQuery := strings.Join(encodedValues, "&")
	return rawQuery
}

func (client *Client) raw(method, urlpath string, queries []url.Values, headers map[string]string, body io.Reader) (*http.Response, error) {
	// fake a url to keep http.Client happy
	u, err := url.Parse(client.config.BaseURL)
	if err != nil {
		return nil, RequestError{err}
	}

	u.Path = path.Join(u.Path, urlpath)
	if strings.HasSuffix(urlpath, "/") {
		u.Path += "/"
	}

	u.RawQuery = joinQueries(queries)
	req, err := http.NewRequest(method, u.String(), body)
	if err != nil {
		return nil, RequestError{err}
	}

	for key, value := range headers {
		req.Header.Set(key, value)
	}

	err = client.setAuth(req)
	if err != nil {
		return nil, err
	}

	log.Debug("sending ", method, " ", u.String())
	rsp, err := client.doer.Do(req)
	if err != nil {
		return nil, ConnectionError{err}
	}

	if rsp.StatusCode != http.StatusOK {
		text := http.StatusText(rsp.StatusCode)
		return nil, ConnectionError{errors.New(text)}
	}

	return rsp, nil
}

// do performs a request and decodes the resulting json into the given
// value. It's low-level, for testing/experimenting only; you should
// usually use a higher level interface that builds on this.
func (client *Client) do(method, path string, queries []url.Values, headers map[string]string, body io.Reader, v interface{}) error {
	var rsp *http.Response
	var err error
	rsp, err = client.raw(method, path, queries, headers, body)
	if err != nil {
		return err
	}
	defer rsp.Body.Close()
	defer io.Copy(ioutil.Discard, rsp.Body)

	if v != nil {
		if err := decodeInto(rsp.Body, v); err != nil {
			return err
		}
	}

	return nil
}

type pingResult struct {
	Meta    *meta
	Objects []interface{}
}

type meta struct {
	Limit      int
	TotalCount int `json:"total_count,omitempty"`
	Offset     int
	Next       string
	Previous   string
}

func decodeInto(reader io.Reader, v interface{}) error {
	dec := json.NewDecoder(reader)
	if err := dec.Decode(v); err != nil {
		r := dec.Buffered()
		buf, err1 := ioutil.ReadAll(r)
		if err1 != nil {
			buf = []byte(fmt.Sprintf("error reading buffered response body: %s", err1))
		}
		return fmt.Errorf("cannot decode %q: %s", buf, err)
	}
	return nil
}

// Ping sends a /ping
// Should not return anything in case no error happened
func (client *Client) PingAPI() error {
	var rsp pingResult
	if err := client.do("GET", "/ping/", nil, nil, nil, &rsp); err != nil {
		return fmt.Errorf("failed to ping: %v", err)
	}
	return nil
}

// Bug represends a bug as cached by Solid Ground. Time stamps in the
// struct have TZ for UTC, but the API doesn't provide any TZ, so these are
// guessed.
type Bug struct {
	Involved     string
	Keywords     string
	Resolution   *string // null,
	CreationTS   time.Time
	State        string // "IN_PROGRESS",
	Product      string
	TTL          time.Time // "2019-03-16T15:20:44",
	Comments     int       // 22,
	HasPatch     bool      `json:"has_patch"`    // true,
	ResourceURI  string    `json:"resource_uri"` // "/api/1/bug/1122053/",
	NovellOnly   bool      // true,
	Severity     string
	Version      string
	Component    string
	Platform     string
	Attachments  int
	InfoProvider string `json:"info_provider"` // "",
	Commentee    string
	Subscribed   string
	Whiteboard   string
	ID           int
	Summary      string
	HasPTFURL    bool `json:"has_ptf_url"`
	Responsible  string
	LastChange   time.Time
	Assignee     string
	LastComment  time.Time
	Updated      time.Time
	Priority     string   // "P1"
	SR           []string `json:"sr"`
	SRs          []int
}

type shadowBug struct {
	Bug
	CreationTS  sgTime `json:"creation_ts"` // "2019-01-15T20:12:00",
	TTL         sgTime
	LastChange  sgTime `json:"last_change"`  // "2019-03-14T14:37:58",
	LastComment sgTime `json:"last_comment"` // "2019-03-14T14:37:58",
	Updated     sgTime // "2019-03-14T15:20:44",
}

func getIDFromURI(path string) (id int, err error) {
	parts := strings.Split(path, "/")
	id, err = strconv.Atoi(parts[len(parts)-2])
	if err != nil {
		err = fmt.Errorf("Bad resource ID from the API: %#v", path)
	}
	return
}

func forEachID(paths []string, handler func(id int) error) error {
	for _, path := range paths {
		id, err := getIDFromURI(path)
		if err != nil {
			return ConnectionError{err}
		}
		err = handler(id)
		if err != nil {
			return err
		}
	}
	return nil
}

// GetBug gets a Bug object, as seen by Solid Gound
// Should not return anything in case no error happened
func (client *Client) GetBug(bugID int) (*Bug, error) {
	var bug Bug
	var shadow shadowBug

	path := fmt.Sprintf("/bug/%d/", bugID)

	if err := client.do("GET", path, nil, nil, nil, &shadow); err != nil {
		return nil, fmt.Errorf("failed to get bug: %v", err)
	}
	bug = shadow.Bug
	bug.CreationTS = shadow.CreationTS.Time
	bug.TTL = shadow.TTL.Time
	bug.LastChange = shadow.LastChange.Time
	bug.LastComment = shadow.LastComment.Time
	bug.Updated = shadow.Updated.Time

	bug.SRs = make([]int, 0)
	err := forEachID(bug.SR, func(id int) error {
		bug.SRs = append(bug.SRs, id)
		return nil
	})
	if err != nil {
		return nil, err
	}

	return &bug, nil
}

// A wrapper that represents the time based on the format emitted by the
// Solid Ground API
type sgTime struct {
	time.Time
}

// UnmarshalJSON
func (m *sgTime) UnmarshalJSON(p []byte) error {
	// based on https://goplay.space/#q-oKnSTtQV
	// and https://github.com/golang/go/issues/21990
	unquoted := strings.Replace(string(p), "\"", "", -1)
	t, err := time.Parse("2006-01-02T15:04:05", unquoted)
	if err != nil {
		return err
	}
	m.Time = t
	return nil
}

// SR represents a Support Request as cached by Solid Ground
type SR struct {
	Hours        string    // "24x7",
	TTL          time.Time // "2019-03-19T21:44:17",
	Status       string    // "Awaiting Engineering",
	IsClosed     bool      `json:"is_closed"` // false,
	ID           string    // "101211396801",
	Created      string    // "2019-01-15T20:14:57",
	Bdesc        string    // "frobnicator crashes",
	ResourceURI  string    `json:"resource_uri"` // "/api/1/sr/101211396801/"
	Lastupdate   time.Time // "2019-03-08T16:22:54",
	Geo          string    // "ZZZZ",
	Errored      bool      // false,
	CusEmail     string    `json:"cus_email"`     // "glorg@zlurg.com",
	CusTitle     string    `json:"cus_title"`     // "",
	CusFirstname string    `json:"cus_firstname"` // "Jokk",
	CusLastname  string    `json:"cus_lastname"`  // "Mokk",
	CusAccount   string    `json:"cus_account"`   // "FOOBARZ AG",
	Ddesc        string    // "Frobnicator crashed today. Help me.",
	Valid        bool      // true,
	Contract     string    // "Foobar free text string"
}

type shadowSR struct {
	SR
	TTL        sgTime
	Lastupdate sgTime
}

// GetSR Gets an SR object, as seen by Solid Gound
// Should not return anything in case no error happened
func (client *Client) GetSR(srID int) (*SR, error) {
	var sr SR
	var shadow shadowSR

	client.srCacheLock.Lock()
	defer client.srCacheLock.Unlock()
	if cached, ok := client.srCache[srID]; ok {
		log.Debug("SR cache hit: ", srID)
		return cached, nil
	}

	path := fmt.Sprintf("/sr/%d/", srID)

	if err := client.do("GET", path, nil, nil, nil, &shadow); err != nil {
		return nil, fmt.Errorf("failed to get sr: %v", err)
	}
	sr = shadow.SR
	sr.TTL = shadow.TTL.Time
	sr.Lastupdate = shadow.Lastupdate.Time

	client.srCache[srID] = &sr

	return &sr, nil
}

// Group representas a group in SG
type Group struct {
	ID          int
	Name        string
	ResourceURI string `json:"resource_uri"`
}

// GetGroup gets an Group object, as seen by Solid Gound
// Should not return anything in case no error happened
func (client *Client) GetGroup(groupID int) (*Group, error) {
	var group Group

	path := fmt.Sprintf("/group/%d/", groupID)

	if err := client.do("GET", path, nil, nil, nil, &group); err != nil {
		return nil, fmt.Errorf("failed to get group: %v", err)
	}
	return &group, nil
}

// User is a User in SG
type User struct {
	ID          int
	ResourceURI string   `json:"resource_uri"`
	GroupsURI   []string `json:"groups"`
	Groups      []*Group
	FirstName   string `json:"first_name"`
	LastName    string `json:"last_name"`
	Email       string
	Username    string
}

// GetUser gets an User object, as seen by Solid Gound
// Should not return anything in case no error happened
func (client *Client) GetUser(userID int) (*User, error) {
	var user User

	path := fmt.Sprintf("/user/%d/", userID)

	if err := client.do("GET", path, nil, nil, nil, &user); err != nil {
		return nil, fmt.Errorf("failed to get user: %v", err)
	}

	user.Groups = make([]*Group, 0)
	err := forEachID(user.GroupsURI, func(id int) error {
		group, err := client.GetGroup(id)
		if err != nil {
			return ConnectionError{errors.New("Failed to fetch a Group from a User")}
		}
		user.Groups = append(user.Groups, group)
		return nil
	})
	if err != nil {
		return nil, err
	}
	return &user, nil
}

// Status represents a status update in SG. Incident has fetched only the
// last status, available in the attribute .LastStatus
type Status struct {
	ID        int
	Status    string
	Change    string
	Next      string
	Timestamp time.Time
}

type shadowStatus struct {
	Status
	Timestamp sgTime
}

// GetStatus gets an Status object, as seen by Solid Gound
// Should not return anything in case no error happened
func (client *Client) GetStatus(statusID int) (*Status, error) {
	var status Status
	var shadow shadowStatus

	path := fmt.Sprintf("/status/%d/", statusID)

	if err := client.do("GET", path, nil, nil, nil, &shadow); err != nil {
		return nil, fmt.Errorf("failed to get status: %v", err)
	}
	status = shadow.Status
	status.Timestamp = shadow.Timestamp.Time

	return &status, nil
}

// Assignment marks whether a user is the primary contact for an incident
type Assignment struct {
	ID      int
	Primary bool
	UserURI string `json:"agent"`
	Agent   *User
}

// GetAssignment Gets an Assignment object, as seen by Solid Gound
// Should not return anything in case no error happened
func (client *Client) GetAssignment(assignmentID int) (*Assignment, error) {
	var assignment Assignment

	path := fmt.Sprintf("/assignment/%d/", assignmentID)

	if err := client.do("GET", path, nil, nil, nil, &assignment); err != nil {
		return nil, fmt.Errorf("failed to get assignment: %v", err)
	}
	userID, err := getIDFromURI(assignment.UserURI)
	if err != nil {
		return nil, err
	}
	user, err := client.GetUser(userID)
	if err != nil {
		return nil, ConnectionError{fmt.Errorf("failed to get the user %v from the assignment %v", userID, assignmentID)}
	}
	assignment.Agent = user

	return &assignment, nil
}

// Incident is an incident, as seen by Solid Ground.
// Timestamps are in UTC, even though the API doesn't specify any TZ.
// Only the last status update is fetched, available in LastStatus.
type Incident struct {
	TsWork     *time.Time `json:"ts_work",omitempty`     //  "2018-08-16T13:30:15"
	TsChange   *time.Time `json:"ts_change",omitempty`   //  "2018-08-16T13:30:15"
	TsFinished *time.Time `json:"ts_finished",omitempty` //  null
	TsBackup   *time.Time `json:"ts_backup",omitempty`   //  null
	LastPing   *time.Time `json:"last_ping",omitempty`   //  "2019-03-22T12:12:10"
	SleepUntil *time.Time `json:"sleep_until",omitempty` //  "2019-03-29T00:00:00"
	TsCanceled *time.Time `json:"ts_canceled",omitempty` //  null
	TsNew      *time.Time `json:"ts_new",omitempty`      //  "2018-08-13T15:58:40"
	TsAnalysis *time.Time `json:"ts_analysis"`

	TimeWork     int `json:"time_work"`     //  0
	TimeAnalysis int `json:"time_analysis"` //  8
	TimeNew      int `json:"time_new"`      //  250287

	Assignments      []*Assignment `json:"assignments"`
	StatusesURIs     []string      `json:"statuses"` // intentionally exposed to users
	LastStatus       *Status
	SR               *SR    `json:"sr"` // only set when fetchAll=true
	SupportID        int    // extracted from "sr"
	Bug              *Bug   `json:"bug"` //  "/api/1/bug/1104677/"
	ComputedPriority string // Highest prio from Bug.Priority or RequestedPriority when state == "new"

	Flags       string `json:"flags"`     //  ""
	Processed   bool   `json:"processed"` //  false
	Score       int    `json:"score"`     //  15
	State       string `json:"state"`     //  "work"
	Todo        bool   `json:"todo"`      //  false
	ID          int    `json:"id"`        //  52044
	Cooperation bool

	InitialComment        string      `json:"initial_comment"`     //  ""
	LastBzResponsible     string      `json:"last_bz_responsible"` //  "user@email.com"
	CustomerDomain        string      `json:"customer_domain"`     //  "customer.com"
	ServiceSeverity       string      `json:"service_severity"`    //  "high"
	ResourceURI           string      `json:"resource_uri"`        //  "/api/1/incident/52044/"
	ScoreFormula          string      `json:"score_formula"`
	CustomerName          string      `json:"customer_name"`           //  "Customer SE"
	AffectedPackages      string      `json:"affected_packages"`       //  ""
	ProductCategory       string      `json:"product_category"`        //  "SLES"
	LastBzComments        int         `json:"last_bz_comments"`        //  35
	LastBzPriority        string      `json:"last_bz_priority"`        //  "P2"
	BugID                 int         `json:"bug_id"`                  //  1104677
	NeedBackup            bool        `json:"need_backup"`             //  false
	StickyNote            string      `json:"sticky_note"`             //  ""
	RequestedPriority     string      `json:"requested_priority"`      //  "P2"
	LastBzState           string      `json:"last_bz_state"`           //  "NEEDINFO"
	FinalState            *FinalState `json:"final_state"`             //  null
	RAGFlag               string      `json:"rag_flag"`                //  "green"
	LastBzAttachments     int         `json:"last_bz_attachments"`     //  0
	RequestType           string      `json:"request_type"`            //  "L3-Support"
	PlannedUpdate         bool        `json:"planned_update"`          //  false
	ProposedAgentDeclined string      `json:"proposed_agent_declined"` //  ""
	PTFPackages           string      `json:"ptf_packages"`            //  ""
	BugSummary            string      `json:"bug_summary"`             //  "frobnicator crashes"
	FinalComment          string      `json:"final_comment"`           //  ""
	AgentsAbsent          bool        `json:"agents_absent"`           //  false
}

type shadowIncident struct {
	Incident

	TsWork         *sgTime  `json:"ts_work",omitempty`     //  "2018-08-16T13:30:15"
	TsChange       *sgTime  `json:"ts_change",omitempty`   //  "2018-08-16T13:30:15"
	TsFinished     *sgTime  `json:"ts_finished",omitempty` //  null
	TsBackup       *sgTime  `json:"ts_backup",omitempty`   //  null
	LastPing       *sgTime  `json:"last_ping",omitempty`   //  "2019-03-22T12:12:10"
	SleepUntil     *sgTime  `json:"sleep_until",omitempty` //  "2019-03-29T00:00:00"
	TsCanceled     *sgTime  `json:"ts_canceled",omitempty` //  null
	TsNew          *sgTime  `json:"ts_new",omitempty`      //  "2018-08-13T15:58:40"
	TsAnalysis     *sgTime  `json:"ts_analysis"`
	AssignmentURIs []string `json:"assignment"`
	SupportURI     string   `json:"support"`
	BugURI         string   `json:"bug"`
}

// FinalState is set after the incident is closed
type FinalState struct {
	Visible     bool
	Description string
	Maintenance bool
	ID          int
	ResourceURI string
}

func (client *Client) fetchBug(shadow *shadowIncident) error {
	bugID, err := getIDFromURI(shadow.BugURI)
	if err != nil {
		return err
	}
	bug, err := client.GetBug(bugID)
	if err != nil {
		return ConnectionError{fmt.Errorf("failed to get the Bug %v from the incident %v: %v", bugID, shadow.ID, err)}
	}
	shadow.Bug = bug

	if shadow.State == "new" {
		prios := []string{bug.Priority, shadow.RequestedPriority}
		sort.Strings(prios)
		shadow.ComputedPriority = prios[0]
	} else {
		shadow.ComputedPriority = shadow.Bug.Priority
	}

	return nil
}

func (client *Client) fetchBasicFields(shadow *shadowIncident) error {
	if shadow.SupportURI != "" {
		srID, err := getIDFromURI(shadow.SupportURI)
		if err != nil {
			return err
		}
		shadow.SupportID = srID
	}

	if err := client.fetchBug(shadow); err != nil {
		return err
	}

	return nil
}

func (client *Client) fetchFields(shadow *shadowIncident) error {
	if shadow.SupportURI != "" {
		srID, err := getIDFromURI(shadow.SupportURI)
		if err != nil {
			return err
		}
		sr, err := client.GetSR(srID)
		if err != nil {
			return ConnectionError{fmt.Errorf("failed to get the SR %v from the incident %v: %v", srID, shadow.ID, err)}
		}
		shadow.SR = sr
	}

	shadow.Assignments = make([]*Assignment, 0)
	err := forEachID(shadow.AssignmentURIs, func(id int) error {
		assignment, err := client.GetAssignment(id)
		if err != nil {
			return ConnectionError{errors.New("Failed to fetch assignment from an incident")}
		}
		shadow.Assignments = append(shadow.Assignments, assignment)
		return nil
	})
	if err != nil {
		return err
	}

	nS := len(shadow.StatusesURIs)
	if nS > 0 {
		lastID, err := getIDFromURI(shadow.StatusesURIs[nS-1])
		if err != nil {
			return ConnectionError{err}
		}
		shadow.LastStatus, err = client.GetStatus(lastID)
		if err != nil {
			return err
		}
	}
	return nil
}

func (client *Client) makeIncident(shadow *shadowIncident, fetchAll bool) (*Incident, error) {
	if err := client.fetchBasicFields(shadow); err != nil {
		return nil, err
	}
	if fetchAll {
		if err := client.fetchFields(shadow); err != nil {
			return nil, err
		}
	}

	var incident Incident
	incident = shadow.Incident

	// oh my eyes
	if shadow.TsWork != nil {
		incident.TsWork = &shadow.TsWork.Time
	}
	if shadow.TsChange != nil {
		incident.TsChange = &shadow.TsChange.Time
	}
	if shadow.TsFinished != nil {
		incident.TsFinished = &shadow.TsFinished.Time
	}
	if shadow.TsBackup != nil {
		incident.TsBackup = &shadow.TsBackup.Time
	}
	if shadow.LastPing != nil {
		incident.LastPing = &shadow.LastPing.Time
	}
	if shadow.SleepUntil != nil {
		incident.SleepUntil = &shadow.SleepUntil.Time
	}
	if shadow.TsCanceled != nil {
		incident.TsCanceled = &shadow.TsCanceled.Time
	}
	if shadow.TsNew != nil {
		incident.TsNew = &shadow.TsNew.Time
	}
	if shadow.TsAnalysis != nil {
		incident.TsAnalysis = &shadow.TsAnalysis.Time
	}

	client.cacheIncident(&incident)

	return &incident, nil
}

func (client *Client) cacheIncident(incident *Incident) {
	cacher, ok := client.cacher.(Cacher)
	if !ok {
		return
	}

	b, err := json.Marshal(incident)
	if err != nil {
		log.Debugf("failed to marshall incident for caching: %v", err)
	} else {
		writer := cacher.GetWriter(fmt.Sprintf("%d", incident.ID))
		writer.Write(b)
		writer.Close()
	}
}

func (client *Client) getIncident(id int, fetchAll bool) (incident *Incident, err error) {
	var shadow shadowIncident

	path := fmt.Sprintf("/incident/%d/", id)

	// Collect the main incident
	if err = client.do("GET", path, nil, nil, nil, &shadow); err != nil {
		return nil, fmt.Errorf("failed to get incident: %v", err)
	}

	incident, err = client.makeIncident(&shadow, fetchAll)
	return
}

// GetIncident Gets an Incident object, as seen by Solid Gound
// Fetches references to all attributes
// Should not return anything in case no error happened
func (client *Client) GetIncident(id int) (incident *Incident, err error) {
	incident, err = client.getIncident(id, true)
	return
}

// GetIncidentLean Gets an Incident object, as seen by Solid Gound
// Do not fetch all references in attributes
func (client *Client) GetIncidentLean(id int) (incident *Incident, err error) {
	incident, err = client.getIncident(id, false)
	return
}

type overviewResult struct {
	Objects []*shadowIncident `json:",omitempty`
	Meta    meta
}

type fetchIncidentResult struct {
	incident *Incident
	error    error
}

// Performs a query for /incident
// Returns a slice of Incident, a boolean marking whether there are more
// incidents (in a paged search) and the error.
func (client *Client) getIncidents(queries []url.Values, fetchAll bool) ([]*Incident, bool, error) {
	var result overviewResult

	const path = "/incident/"

	// Collect the main incident
	if err := client.do("GET", path, queries, nil, nil, &result); err != nil {
		return nil, false, fmt.Errorf("failed to get incident: %v", err)
	}
	log.Debug("found objects: ", len(result.Objects))

	queue := make(chan *shadowIncident, client.concurrency)
	results := make(chan fetchIncidentResult, client.concurrency)
	for i := 0; i < client.concurrency; i++ {
		go func() {
			for shadow := range queue {
				log.Debug("making ", i, shadow.ID, shadow.BugID)
				incident, err := client.makeIncident(shadow, fetchAll)
				log.Debug("make returned ", i, " ", err, incident.ID, incident.Bug.ID)
				results <- fetchIncidentResult{incident: incident, error: err}
			}
			log.Debug("finished making")
		}()
	}
	go func() {
		for _, shadow := range result.Objects {
			log.Debug("about to send shadow ", shadow.ID)
			queue <- shadow
		}
		close(queue)
		log.Debug("finished sending")
	}()

	incidents := make([]*Incident, 0, len(result.Objects))
	for i := 0; i < len(result.Objects); i++ {
		fetchResult := <-results
		log.Debug("collected ", fetchResult.incident.ID)
		if fetchResult.error != nil {
			return nil, false, fetchResult.error
		}

		incidents = append(incidents, fetchResult.incident)
	}
	next := result.Meta.Next != ""
	return incidents, next, nil
}

// Overview gets the "Overview": a list of incidents in a given state
// As it is shown by the status page
// @group can be "new", "processed", "backup", "active", "sleeping", "closed"
// Returns a slice of incidents
func (client *Client) Overview(group string) (incidents []*Incident, err error) {
	query := url.Values{}
	query.Set("overview", group)
	query.Set("limit", "0")
	query.Set("running", "true")

	incidents, _, err = client.getIncidents([]url.Values{query}, false)
	return
}

type modifyIncidentResult struct {
	Reply    string   `json:"reply"`
	Messages []string `json:"messages"`
	Error    string   `json:"error"`
}

func stripHTML(html string) string {
	return bluemonday.StrictPolicy().Sanitize(html)
}

// ModifyIncident is a low-level interface to the operations that can
// modify incidents. Request is a string with the operation to be
// performed. Oddly, no extra parameters are accepted.
func (client *Client) ModifyIncident(id int, request string) ([]string, error) {
	var result modifyIncidentResult
	path := fmt.Sprintf("/incident/%d/", id)
	data := fmt.Sprintf(`{"request": "%s"}`, request)
	header := map[string]string{"Content-Type": "application/json"}

	// Collect the main incident
	if err := client.do("POST", path, nil, header, strings.NewReader(data), &result); err != nil {
		return nil, fmt.Errorf("failed to get incident: %v", err)
	}
	if result.Error != "" {
		return nil, fmt.Errorf(result.Error)
	}
	clean := make([]string, 0, len(result.Messages))
	for _, html := range result.Messages {
		clean = append(clean, stripHTML(html))
	}

	return clean, nil
}

// Ping pings an incident in SG (aka "ack")
func (client *Client) Ping(id int) (messages []string, err error) {
	messages, err = client.ModifyIncident(id, "ping")
	return
}

// Accept accepts an incident in SG
func (client *Client) Accept(id int) (messages []string, err error) {
	messages, err = client.ModifyIncident(id, "accept")
	return
}

// Assign assigns an incident in SG
func (client *Client) Assign(id int) (messages []string, err error) {
	messages, err = client.ModifyIncident(id, "assign")
	return
}

// Sleep puts an incident to sleep
func (client *Client) Sleep(id int) (messages []string, err error) {
	messages, err = client.ModifyIncident(id, "sleep")
	return
}

// SleepShort puts an incident to short sleep
func (client *Client) SleepShort(id int) (messages []string, err error) {
	messages, err = client.ModifyIncident(id, "zzz")
	return
}

// Search keeps the predicates used for searching for incidents. It also
// sets parameters for paging.
type Search struct {
	// Number of incidents in each page returned
	Limit uint
	// Which page to start the search from
	Offset uint
	// Restrict to only running incidents
	Running bool

	CustomFilter string
	CustomCond   string
	CustomMatch  string
	CustomExpr   string

	CustomerDomain string
	Agent          string
	BugID          int
	SRID           int

	// Whether to fetch all the attributes from each incident returned
	// by the search
	FetchAll bool
}

// Search Performs a search in SG and returns one page with the results. To
// move forward with the fetched pages, call it again with Search.Offset +=
// Search.Limit
func (client *Client) Search(search Search) (incidents []*Incident, next bool, err error) {
	queries := make([]url.Values, 0)
	query := url.Values{}
	query.Set("limit", fmt.Sprintf("%d", search.Limit))
	if search.Offset != 0 {
		query.Set("offset", fmt.Sprintf("%d", search.Offset))
	}
	if search.Running {
		query.Set("running", "true")
	}
	queries = append(queries, query)

	makeSearch := func(filter, cond, match string) {
		query := url.Values{}
		query.Set("filter", filter)
		query.Set("cond", cond)
		query.Set("match", match)
		queries = append(queries, query)
	}
	if search.BugID != 0 {
		makeSearch("bug_id", "", fmt.Sprintf("%d", search.BugID))
	}
	if search.CustomerDomain != "" {
		makeSearch("customer_domain", "", search.CustomerDomain)
	}
	if search.Agent != "" {
		makeSearch("agents", "", search.Agent)
	}
	if search.SRID != 0 {
		makeSearch("support__id", "", fmt.Sprintf("%d", search.SRID))
	}
	if search.BugID != 0 {
		makeSearch("bug_id", "", fmt.Sprintf("%d", search.BugID))
	}
	if search.CustomFilter != "" {
		makeSearch(search.CustomFilter, search.CustomCond, search.CustomMatch)
	}
	if search.CustomExpr != "" {
		customQuery, qerr := url.ParseQuery(search.CustomExpr)
		if qerr != nil {
			err = RequestError{fmt.Errorf("failed to parse the custom expression: %v", qerr)}
			return
		}
		queries = append(queries, customQuery)
	}
	incidents, next, err = client.getIncidents(queries, search.FetchAll)
	return
}

// SearchByBug does what it says
func (client *Client) SearchByBug(bugID int, running bool) (incidents []*Incident, err error) {
	incidents, _, err = client.Search(Search{BugID: bugID, Running: running, FetchAll: true})
	return
}

// SearchPaged searches and return results through a channel of Incidents
// and another of errors, abstracting the paged requests
func (client *Client) SearchPaged(search Search) (<-chan *Incident, <-chan error, error) {
	limit := uint(20)
	if search.Limit != 0 {
		limit = search.Limit
	}
	incidents := make(chan *Incident, limit)
	errors := make(chan error, 1)
	go func() {
		defer close(errors)
		defer close(incidents)
		for {
			retrieved, next, err := client.Search(search)
			if err != nil {
				errors <- err
				return
			}
			for _, incident := range retrieved {
				incidents <- incident
			}
			if next {
				search.Offset += search.Limit
			} else {
				return
			}
		}
	}()

	return incidents, errors, nil
}
07070100000008000081A4000003E800000064000000015CBEFDCB000069F1000000000000000000000000000000000000002700000000golang-l3t-0.0.1/client/client_test.gopackage client_test

// Heavily based on how snapd implemented the API client + testing

import (
	"bytes"
	"encoding/json"
	"io"
	"io/ioutil"
	"net/http"
	"regexp"
	"strings"
	"testing"

	. "gopkg.in/check.v1"

	"time"

	"github.com/bhdn/golang-l3t/client"
	"github.com/bhdn/golang-l3t/log"
)

type clientSuite struct {
	cli     *client.Client
	req     *http.Request
	reqs    []*http.Request
	rsp     string
	rsps    []string
	err     error
	doCalls int
	header  http.Header
	status  int
	restore func()
}

var _ = Suite(&clientSuite{})

func TestNew(t *testing.T) {
}

// Hook up check.v1 into the "go test" runner
func Test(t *testing.T) { TestingT(t) }

func (cs *clientSuite) SetUpTest(c *C) {
	url := "https://sg.foobar.com/"
	config := &client.Config{BaseURL: url, AuthToken: "user:token"}
	cs.cli = client.New(config)
	cs.cli.SetDoer(cs)
	cs.err = nil
	cs.req = nil
	cs.reqs = nil
	cs.rsp = ""
	cs.rsps = nil
	cs.req = nil
	cs.header = nil
	cs.status = 200
	cs.doCalls = 0
}

func (cs *clientSuite) Do(req *http.Request) (*http.Response, error) {
	cs.req = req
	cs.reqs = append(cs.reqs, req)
	body := cs.rsp
	if cs.doCalls < len(cs.rsps) {
		body = cs.rsps[cs.doCalls]
		uri := strings.Join(regexp.MustCompile(`"resource_uri" : (.*),`).FindAllString(body, -1), "; ")
		log.Debug("the response now will be ", uri)
	}
	rsp := &http.Response{
		Body:       ioutil.NopCloser(strings.NewReader(body)),
		Header:     cs.header,
		StatusCode: cs.status,
	}
	cs.doCalls++
	return rsp, cs.err
}

func (cs *clientSuite) TestClientWorks(c *C) {
	var v []int
	cs.rsp = `[1,2]`
	reqBody := ioutil.NopCloser(strings.NewReader(""))
	err := cs.cli.Do("GET", "/this", nil, reqBody, &v)
	c.Check(err, IsNil)
	c.Check(v, DeepEquals, []int{1, 2})
	c.Assert(cs.req, NotNil)
	c.Assert(cs.req.URL, NotNil)
	c.Check(cs.req.Method, Equals, "GET")
	c.Check(cs.req.Body, Equals, reqBody)
	c.Check(cs.req.URL.Path, Equals, "/this")
}

func (cs *clientSuite) TestPingAPI(c *C) {
	cs.rsp = `{"meta": {"limit": 20, "next": null,
	              "offset": 0, "previous": null,
		      "total_count": 0}, "objects": []}}`
	err := cs.cli.PingAPI()
	c.Check(err, IsNil)
}

func (cs *clientSuite) TestAuthTokenSet(c *C) {
	cs.rsp = `{"meta": {"limit": 20, "next": null,
	              "offset": 0, "previous": null,
		      "total_count": 0}, "objects": []}}`
	err := cs.cli.PingAPI()
	c.Check(err, IsNil)
	authorization := cs.req.Header.Get("Authorization")
	c.Check(authorization, Equals, "ApiKey user:token")
}

func (cs *clientSuite) TestCannotDecode(c *C) {
	cs.rsp = `<b>Error</b>`
	err := cs.cli.PingAPI()
	c.Check(err, ErrorMatches, ".*cannot decode.*Error.*")
}

const sampleBug = `{
   "involved" : "foobar@email.com,barfoo@email.com",
   "keywords" : "SAUERKRAUT_REQUIRED",
   "resolution" : null,
   "creation_ts" : "2019-01-15T20:12:00",
   "state" : "IN_PROGRESS",
   "product" : "Frob 7",
   "ttl" : "2019-03-16T15:20:44",
   "comments" : 22,
   "has_patch" : true,
   "resource_uri" : "/api/1/bug/1122053/",
   "novellonly" : true,
   "severity" : "Major",
   "version" : "Maintenance Update",
   "component" : "Database",
   "platform" : "x86-64",
   "attachments" : 1,
   "info_provider" : "",
   "commentee" : "comentator@email.com",
   "subscribed" : "foobar@email.com,barfoo@email.com,stalker@email.com",
   "whiteboard" : "openL3:53305",
   "id" : 1122053,
   "summary" : "frobnicator crashes",
   "has_ptf_url" : true,
   "responsible" : "irre@email.com",
   "sr" : [
      "/api/1/sr/101211396801/"
   ],
   "last_change" : "2019-03-14T14:37:58",
   "assignee" : "irre@email.com",
   "last_comment" : "2019-03-14T14:37:58",
   "updated" : "2019-03-14T15:20:44",
   "priority" : "P1"}`

func (cs *clientSuite) TestGetBug(c *C) {
	cs.rsps = []string{sampleBug}

	bug, err := cs.cli.GetBug(1122053)
	c.Check(err, IsNil)
	c.Check(bug.ID, Equals, 1122053)
	c.Check(bug.Summary, Equals, "frobnicator crashes")
	c.Check(bug.Involved, Equals, "foobar@email.com,barfoo@email.com")
	c.Check(bug.Keywords, Equals, "SAUERKRAUT_REQUIRED")
	c.Check(bug.Resolution, Equals, (*string)(nil))
	c.Check(bug.CreationTS, Equals, time.Date(2019, 01, 15, 20, 12, 0, 0, time.UTC))
	c.Check(bug.State, Equals, "IN_PROGRESS")
	c.Check(bug.Product, Equals, "Frob 7")
	c.Check(bug.TTL, Equals, time.Date(2019, 03, 16, 15, 20, 44, 0, time.UTC))
	c.Check(bug.Comments, Equals, 22)
	c.Check(bug.HasPatch, Equals, true)
	c.Check(bug.ResourceURI, Equals, "/api/1/bug/1122053/")
	c.Check(bug.NovellOnly, Equals, true)
	c.Check(bug.Severity, Equals, "Major")
	c.Check(bug.Version, Equals, "Maintenance Update")
	c.Check(bug.Component, Equals, "Database")
	c.Check(bug.Platform, Equals, "x86-64")
	c.Check(bug.Attachments, Equals, 1)
	c.Check(bug.InfoProvider, Equals, "")
	c.Check(bug.Commentee, Equals, "comentator@email.com")
	c.Check(bug.Subscribed, Equals, "foobar@email.com,barfoo@email.com,stalker@email.com")
	c.Check(bug.Whiteboard, Equals, "openL3:53305")
	c.Check(bug.ID, Equals, 1122053)
	c.Check(bug.Summary, Equals, "frobnicator crashes")
	c.Check(bug.HasPTFURL, Equals, true)
	c.Check(bug.Responsible, Equals, "irre@email.com")
	c.Check(bug.LastChange, Equals, time.Date(2019, 03, 14, 14, 37, 58, 0, time.UTC))
	c.Check(bug.Assignee, Equals, "irre@email.com")
	c.Check(bug.LastComment, Equals, time.Date(2019, 03, 14, 14, 37, 58, 0, time.UTC))
	c.Check(bug.Updated, Equals, time.Date(2019, 03, 14, 15, 20, 44, 0, time.UTC))
	c.Check(bug.Priority, Equals, "P1")
	c.Check(len(bug.SRs), Equals, 1)
	sr := bug.SRs[0]
	c.Check(sr, Equals, 101211396801)
}

const sampleSR = `{
   "hours" : "24x7",
   "ttl" : "2019-03-19T21:44:17",
   "status" : "Awaiting Engineering",
   "is_closed" : false,
   "id" : "101211396801",
   "created" : "2019-01-15T20:14:57",
   "bdesc" : "frobnicator crashes",
   "resource_uri" : "/api/1/sr/101211396801/",
   "lastupdate" : "2019-03-08T16:22:54",
   "geo" : "ZZZZ",
   "errored" : false,
   "cus_email" : "glorg@zlurg.com",
   "cus_title" : "",
   "cus_firstname" : "Jokk",
   "cus_lastname" : "Mokk",
   "cus_account" : "FOOBARZ AG",
   "ddesc" : "Frobnicator crashed today. Help me.",
   "valid" : true,
   "contract" : "Foobar free text string"}`

func (cs *clientSuite) TestGetSR(c *C) {
	cs.rsp = sampleSR
	sr, err := cs.cli.GetSR(101211396801)
	c.Check(err, IsNil)
	c.Check(sr.ID, Equals, "101211396801")
	c.Check(sr.Hours, Equals, "24x7")
	c.Check(sr.TTL, Equals, time.Date(2019, 03, 19, 21, 44, 17, 0, time.UTC))
	c.Check(sr.Status, Equals, "Awaiting Engineering")
	c.Check(sr.IsClosed, Equals, false)
	c.Check(sr.ID, Equals, "101211396801")
	c.Check(sr.Created, Equals, "2019-01-15T20:14:57")
	c.Check(sr.Bdesc, Equals, "frobnicator crashes")
	c.Check(sr.ResourceURI, Equals, "/api/1/sr/101211396801/")
	c.Check(sr.Lastupdate, Equals, time.Date(2019, 03, 8, 16, 22, 54, 0, time.UTC))
	c.Check(sr.Geo, Equals, "ZZZZ")
	c.Check(sr.Errored, Equals, false)
	c.Check(sr.CusEmail, Equals, "glorg@zlurg.com")
	c.Check(sr.CusTitle, Equals, "")
	c.Check(sr.CusFirstname, Equals, "Jokk")
	c.Check(sr.CusLastname, Equals, "Mokk")
	c.Check(sr.CusAccount, Equals, "FOOBARZ AG")
	c.Check(sr.Ddesc, Equals, "Frobnicator crashed today. Help me.")
	c.Check(sr.Valid, Equals, true)
	c.Check(sr.Contract, Equals, "Foobar free text string")
}

const sampleGroup = `{"name": "Admin", "resource_uri": "/api/1/group/1/"}`

func (cs *clientSuite) TestGetGroup(c *C) {
	cs.rsp = sampleGroup
	group, err := cs.cli.GetGroup(1)
	c.Check(err, IsNil)
	c.Check(group.Name, Equals, "Admin")
}

const sampleStatus = `{
   "status" : "Some status here",
   "change" : "No change",
   "resource_uri" : "/api/1/status/70018/",
   "id" : 70018,
   "next" : "Resume the investigation.",
   "timestamp" : "2019-03-22T12:12:14"
}`

const sampleStatus2 = `{
   "status" : "Some second status here",
   "change" : "No change",
   "resource_uri" : "/api/1/status/70019/",
   "id" : 70018,
   "next" : "Resume the investigation again.",
   "timestamp" : "2019-03-22T12:12:14"
}`

func (cs *clientSuite) TestGetStatus(c *C) {
	cs.rsp = sampleStatus
	status, err := cs.cli.GetStatus(70018)
	c.Check(err, IsNil)
	c.Check(status.ID, Equals, 70018)
	c.Check(status.Status, Equals, "Some status here")
	c.Check(status.Change, Equals, "No change")
	c.Check(status.Next, Equals, "Resume the investigation.")
}

const sampleUser = `{
   "resource_uri" : "/api/1/user/217/",
   "first_name" : "Firstname",
   "groups" : [
      "/api/1/group/1/"
   ],
   "email" : "foo@email.com",
   "last_name" : "Surname",
   "username" : "fsurname"
}`

func (cs *clientSuite) TestGetUser(c *C) {
	cs.rsps = []string{sampleUser, sampleGroup}
	user, err := cs.cli.GetUser(217)
	c.Check(err, IsNil)
	c.Check(user.FirstName, Equals, "Firstname")
	c.Check(user.LastName, Equals, "Surname")
	c.Check(user.Email, Equals, "foo@email.com")
	c.Check(user.Username, Equals, "fsurname")
	c.Check(len(user.Groups), Equals, 1)
	group := user.Groups[0]
	c.Check(group.Name, Equals, "Admin")
}

const sampleAssignment = `{
   "primary" : true,
   "agent" : "/api/1/user/217/",
   "resource_uri" : "/api/1/assignment/26322/",
   "id" : 26322
}`

func (cs *clientSuite) TestGetAssignment(c *C) {
	cs.rsps = []string{sampleAssignment, sampleUser, sampleGroup}
	assignment, err := cs.cli.GetAssignment(26322)
	c.Check(err, IsNil)
	c.Check(assignment.Primary, Equals, true)
	c.Check(assignment.Agent, NotNil)
	c.Check(assignment.Agent.FirstName, Equals, "Firstname")
	c.Check(len(assignment.Agent.Groups), Equals, 1)
	c.Check(assignment.Agent.Groups[0].Name, Equals, "Admin")
}

const sampleIncident = `{
    "ts_work" : "2018-08-16T13:30:15",
    "projects" : [
       "/api/1/project/15976/",
       "/api/1/project/15977/",
       "/api/1/project/16531/",
       "/api/1/project/16532/",
       "/api/1/project/16620/"
    ],
    "last_bz_priority" : "P2",
    "time_analysis" : 8,
    "initial_comment" : "",
    "assignment" : [
       "/api/1/assignment/26322/"
    ],
    "statuses" : [
       "/api/1/status/70018/",
       "/api/1/status/70019/"
    ],
    "last_bz_responsible" : "user@email.com",
    "time_new" : 250287,
    "customer_domain" : "customer.com",
    "service_severity" : "high",
    "bug" : "/api/1/bug/1104677/",
    "resource_uri" : "/api/1/incident/52044/",
    "score_formula" : "(state_boost = 1) * (age_boost=1) * ((5 - (bug_prio=2)) * 5 + (flag=0) * 2)+ (absence_boost=0)",
    "customer_name" : "Customer SE",
    "affected_packages" : "",
    "product_category" : "SLES",
    "last_bz_comments" : 35,
    "bug_id" : 1104677,
    "need_backup" : false,
    "ts_change" : "2018-08-16T13:30:15",
    "time_work" : 0,
    "sticky_note" : "",
    "score" : 15,
    "requested_priority" : "P2",
    "last_bz_state" : "NEEDINFO",
    "final_state" : null,
    "ts_finished" : null,
    "flags" : "",
    "processed" : false,
    "ts_backup" : null,
    "rag_flag" : "green",
    "last_bz_attachments" : 0,
    "request_type" : "L3-Support",
    "planned_update" : false,
    "last_ping" : "2019-03-22T12:12:10",
    "proposed_agent_declined" : "",
    "support" : "/api/1/sr/101211396801/",
    "ptf_packages" : "",
    "state" : "work",
    "raederwerk" : [],
    "todo" : false,
    "ts_canceled" : null,
    "ts_new" : "2018-08-13T15:58:40",
    "bug_summary" : "frobnicator crashes",
    "sleep_until" : "2019-03-29T00:00:00",
    "id" : 52044,
    "final_comment" : "",
    "agents_absent" : false,
    "cooperation" : false,
    "ts_analysis" : "2018-08-16T13:30:07"
 }`

const sampleIncident2 = `{
    "ts_work" : "2018-08-16T13:30:15",
    "projects" : [
       "/api/1/project/15976/",
       "/api/1/project/15977/",
       "/api/1/project/16531/",
       "/api/1/project/16532/",
       "/api/1/project/16620/"
    ],
    "last_bz_priority" : "P2",
    "time_analysis" : 8,
    "initial_comment" : "",
    "assignment" : [
       "/api/1/assignment/26322/"
    ],
    "statuses" : [ ],
    "last_bz_responsible" : "user@email.com",
    "time_new" : 250287,
    "customer_domain" : "customer.com",
    "service_severity" : "high",
    "bug" : "/api/1/bug/1104677/",
    "resource_uri" : "/api/1/incident/52044/",
    "score_formula" : "(state_boost = 1) * (age_boost=1) * ((5 - (bug_prio=2)) * 5 + (flag=0) * 2)+ (absence_boost=0)",
    "customer_name" : "Customer SE",
    "affected_packages" : "",
    "product_category" : "SLES",
    "last_bz_comments" : 35,
    "bug_id" : 1104677,
    "need_backup" : false,
    "ts_change" : "2018-08-16T13:30:15",
    "time_work" : 0,
    "sticky_note" : "",
    "score" : 15,
    "requested_priority" : "P2",
    "last_bz_state" : "NEEDINFO",
    "final_state" : null,
    "ts_finished" : null,
    "flags" : "",
    "processed" : false,
    "ts_backup" : null,
    "rag_flag" : "green",
    "last_bz_attachments" : 0,
    "request_type" : "L3-Support",
    "planned_update" : false,
    "last_ping" : "2019-03-22T12:12:10",
    "proposed_agent_declined" : "",
    "support" : "",
    "ptf_packages" : "",
    "state" : "work",
    "raederwerk" : [],
    "todo" : false,
    "ts_canceled" : null,
    "ts_new" : "2018-08-13T15:58:40",
    "bug_summary" : "frobnicator crashes",
    "sleep_until" : "2019-03-29T00:00:00",
    "id" : 52045,
    "final_comment" : "",
    "agents_absent" : false,
    "cooperation" : false,
    "ts_analysis" : "2018-08-16T13:30:07"
 }`

func (cs *clientSuite) TestGetIncident(c *C) {
	cs.rsps = []string{sampleIncident, sampleBug, sampleSR,
		sampleAssignment, sampleUser, sampleGroup, sampleStatus2}
	incident, err := cs.cli.GetIncident(70018)
	c.Assert(err, IsNil)

	c.Check(*incident.TsWork, Equals, time.Date(2018, 8, 16, 13, 30, 15, 0, time.UTC))
	c.Check(*incident.TsChange, Equals, time.Date(2018, 8, 16, 13, 30, 15, 0, time.UTC))
	c.Check(incident.TsFinished, IsNil)
	c.Check(incident.TsBackup, IsNil)
	c.Check(incident.TsCanceled, IsNil)
	c.Check(*incident.TsNew, Equals, time.Date(2018, 8, 13, 15, 58, 40, 0, time.UTC))
	c.Check(*incident.TsAnalysis, Equals, time.Date(2018, 8, 16, 13, 30, 07, 0, time.UTC))
	c.Check(*incident.LastPing, Equals, time.Date(2019, 3, 22, 12, 12, 10, 0, time.UTC))
	c.Check(*incident.SleepUntil, Equals, time.Date(2019, 3, 29, 0, 0, 0, 0, time.UTC))
	c.Check(incident.LastBzPriority, Equals, "P2")

	c.Check(incident.TimeAnalysis, Equals, 8)    // 8
	c.Check(incident.InitialComment, Equals, "") // ""
	c.Check(len(incident.Assignments), Equals, 1)
	c.Check(incident.Assignments[0].Primary, Equals, true)
	c.Check(incident.Assignments[0].Agent.FirstName, Equals, "Firstname")
	c.Check(len(incident.Assignments[0].Agent.Groups), Equals, 1)
	c.Check(incident.Assignments[0].Agent.Groups[0].Name, Equals, "Admin")
	c.Check(len(incident.StatusesURIs), Equals, 2)
	c.Check(incident.LastStatus.Status, Equals, "Some second status here")
	c.Check(incident.LastBzResponsible, Equals, "user@email.com")   // "user@email.com"
	c.Check(incident.TimeNew, Equals, 250287)                       // 250287
	c.Check(incident.CustomerDomain, Equals, "customer.com")        // "customer.com"
	c.Check(incident.ServiceSeverity, Equals, "high")               // "high"
	c.Check(incident.Bug.ID, Equals, 1122053)                       // "/api/1/bug/1104677/"
	c.Check(incident.ResourceURI, Equals, "/api/1/incident/52044/") // "/api/1/incident/52044/"
	c.Check(incident.ScoreFormula, Equals,
		"(state_boost = 1) * (age_boost=1) * ((5 - (bug_prio=2)) * 5 + (flag=0) * 2)+ (absence_boost=0)")
	c.Check(incident.CustomerName, Equals, "Customer SE")       //
	c.Check(incident.AffectedPackages, Equals, "")              // ""
	c.Check(incident.ProductCategory, Equals, "SLES")           // "SLES"
	c.Check(incident.LastBzComments, Equals, 35)                // 35
	c.Check(incident.BugID, Equals, 1104677)                    // 1104677
	c.Check(incident.NeedBackup, Equals, false)                 // false
	c.Check(incident.TimeWork, Equals, 0)                       // 0
	c.Check(incident.StickyNote, Equals, "")                    // ""
	c.Check(incident.Score, Equals, 15)                         // 15
	c.Check(incident.RequestedPriority, Equals, "P2")           // "P2"
	c.Check(incident.LastBzState, Equals, "NEEDINFO")           // "NEEDINFO"
	c.Check(incident.FinalState, IsNil)                         // null
	c.Check(incident.Flags, Equals, "")                         // ""
	c.Check(incident.Processed, Equals, false)                  // false
	c.Check(incident.RAGFlag, Equals, "green")                  // "green"
	c.Check(incident.LastBzAttachments, Equals, 0)              // 0
	c.Check(incident.RequestType, Equals, "L3-Support")         //
	c.Check(incident.PlannedUpdate, Equals, false)              // false
	c.Check(incident.ProposedAgentDeclined, Equals, "")         // ""
	c.Check(incident.SR.ID, Equals, "101211396801")             // "/api/1/sr/101181058864/"
	c.Check(incident.PTFPackages, Equals, "")                   // ""
	c.Check(incident.State, Equals, "work")                     // "work"
	c.Check(incident.Todo, Equals, false)                       // false
	c.Check(incident.BugSummary, Equals, "frobnicator crashes") // "frobnicator crashes"
	c.Check(incident.ID, Equals, 52044)                         // 52044
	c.Check(incident.FinalComment, Equals, "")                  // ""
	c.Check(incident.AgentsAbsent, Equals, false)               // false
	c.Check(incident.Cooperation, Equals, false)                // false
	c.Check(incident.ComputedPriority, Equals, "P1")
}

type cacheHelper struct {
	buf FakeBuf
	id  string
}

type FakeBuf struct {
	bytes.Buffer
}

func (f *FakeBuf) Close() error {
	return nil
}

func (c *cacheHelper) GetWriter(id string) io.WriteCloser {
	c.id = id
	return &c.buf
}

func (cs *clientSuite) TestGetIncidentWithCache(c *C) {
	var helper cacheHelper
	url := "https://sg.foobar.com/"
	config := &client.Config{BaseURL: url, AuthToken: "user:token", Cacher: &helper}
	cli := client.New(config)
	cli.SetDoer(cs)
	cs.rsps = []string{sampleIncident, sampleBug, sampleSR,
		sampleAssignment, sampleUser, sampleGroup, sampleStatus2}
	incident, err := cli.GetIncident(52044)
	c.Assert(err, IsNil)
	c.Assert(incident.ID, Equals, 52044)
	var checker client.Incident
	err = json.Unmarshal(helper.buf.Bytes(), &checker)
	c.Assert(err, IsNil)
	c.Check(incident.ID, Equals, 52044)
}

var sampleOverview string = `
{
   "objects" : [
	` + sampleIncident +
	`, ` + sampleIncident2 + `],
   "meta" : {
      "limit" : 20,
      "previous" : null,
      "total_count" : 2,
      "offset" : 0,
      "next" : null
   }
}
`

func (cs *clientSuite) TestOverview(c *C) {
	var incidents []*client.Incident
	var err error

	cs.rsps = []string{sampleOverview,
		// First incident:
		sampleBug,

		// Second incident:
		sampleBug,
	}

	incidents, err = cs.cli.Overview("processed")
	c.Assert(err, IsNil)

	c.Check(len(incidents), Equals, 2)
	c.Check(incidents[0].ID, Equals, 52044)
	c.Check(len(incidents[0].Assignments), Equals, 0,
		Commentf("Assignments should not be fetched in overview"))
	c.Check(len(incidents[0].StatusesURIs), Equals, 2,
		Commentf("Statuses should not be fetched in overview"))
	c.Check(incidents[0].LastStatus, IsNil)
	c.Check(incidents[0].Bug, NotNil)
	c.Check(incidents[0].Bug.ID, Equals, 1122053)
	c.Check(incidents[0].State, Equals, "work")
	c.Check(incidents[0].SR, IsNil)
	c.Check(incidents[0].SupportID, Equals, 101211396801)

	c.Check(incidents[1].ID, Equals, 52045)
	c.Check(len(incidents[1].Assignments), Equals, 0)
	c.Check(incidents[1].Bug.ID, Equals, 1122053)
	c.Check(incidents[1].State, Equals, "work")
}

var sampleEmptyOverview string = `
{
   "objects" : [ ],
   "meta" : {
      "limit" : 20,
      "previous" : null,
      "total_count" : 0,
      "offset" : 0,
      "next" : null
   }
}
`

func (cs *clientSuite) TestOverviewEmptyGroup(c *C) {
	var incidents []*client.Incident
	var err error

	cs.rsps = []string{sampleEmptyOverview}

	incidents, err = cs.cli.Overview("processed")
	c.Assert(err, IsNil)
	c.Assert(len(incidents), Equals, 0)
}

var sampleAccept = `
{"messages": ["Incident <a href=\"/incident/54027/\" title=\"Analysis\" class=\"markincidents\">L3-Support 54027 L4: test cloud bug</a> for bug <a href=\"https://bugzilla.foobar.com/show_bug.cgi?id=1047068\" title=\"L4: test cloud bug\">1047068</a> accepted."],
"reply": "accepted"}
`

func (cs *clientSuite) TestAccept(c *C) {
	cs.rsps = []string{sampleAccept}
	messages, err := cs.cli.Accept(54027)
	c.Assert(err, IsNil)
	c.Assert(len(messages), Equals, 1)
	c.Assert(messages[0], Equals, `Incident L3-Support 54027 L4: test cloud bug for bug 1047068 accepted.`)
}

var samplePing = `
{"messages": ["Acknowledged changes in incident <a href=\"/incident/54027/\" title=\"Work in progress\" class=\"markincidents\">L3-Support 54027 L3: L4: test cloud bug</a>."], "reply": "pinged"}
`

func (cs *clientSuite) TestPing(c *C) {
	cs.rsps = []string{samplePing}
	messages, err := cs.cli.Ping(54027)
	c.Assert(err, IsNil)
	c.Assert(len(messages), Equals, 1)
	c.Assert(messages[0], Equals, `Acknowledged changes in incident L3-Support 54027 L3: L4: test cloud bug.`)
}

var sampleAssign = `
{"messages": ["Incident <a href=\"/incident/54027/\" title=\"Work in progress\" class=\"markincidents\">L3-Support 54027 L4: test cloud bug</a> for bug <a href=\"https://bugzilla.foobar.com/show_bug.cgi?id=1047068\" title=\"L4: test cloud bug\">1047068</a> assigned.", "Updating bug <a href=\"https://bugzilla.foobar.com/show_bug.cgi?id=1047068\" title=\"L3: L4: test cloud bug\">1047068</a>, setting short description to L3: L4: test cloud bug, adding openL3:54027 to whiteboard, setting priority to P3 - Medium"], "reply": "assigned"}
`

func (cs *clientSuite) TestAssign(c *C) {
	cs.rsps = []string{sampleAssign}
	messages, err := cs.cli.Assign(54027)
	c.Assert(err, IsNil)
	c.Assert(len(messages), Equals, 2)
	c.Assert(messages[0], Equals, `Incident L3-Support 54027 L4: test cloud bug for bug 1047068 assigned.`)
	c.Assert(messages[1], Equals, `Updating bug 1047068, setting short description to L3: L4: test cloud bug, adding openL3:54027 to whiteboard, setting priority to P3 - Medium`)
}

var sampleSearchByBug = `
{
   "objects" : [` + sampleIncident + `, ` + sampleIncident2 + `
   ],
   "meta" : {
      "limit" : 20,
      "previous" : null,
      "total_count" : 2,
      "offset" : 0,
      "next" : null
   }
}`

func (cs *clientSuite) TestSearchByBug(c *C) {
	cs.rsps = []string{sampleSearchByBug,
		// First incident:
		sampleBug,
		sampleSR,
		sampleAssignment,
		sampleUser,
		sampleGroup,
		sampleStatus,
		// Second incident:
		//	sampleSR,
		sampleBug,
		sampleAssignment,
		sampleUser,
		sampleGroup,
		sampleStatus,
	}

	incidents, err := cs.cli.SearchByBug(1104677, false)
	c.Assert(err, IsNil)
	c.Assert(len(incidents), Equals, 2)
	c.Check(incidents[0].ID, Equals, 52044)
	c.Check(len(incidents[0].Assignments), Equals, 1)
	c.Check(len(incidents[0].StatusesURIs), Equals, 2)
	c.Check(incidents[0].LastStatus, NotNil)
	c.Check(incidents[0].Bug, NotNil)
	c.Check(incidents[0].Bug.ID, Equals, 1122053)
	c.Check(incidents[0].State, Equals, "work")
	c.Check(incidents[0].SR, NotNil)
	c.Check(incidents[0].SupportID, Equals, 101211396801)

	c.Check(incidents[1].ID, Equals, 52045)
	c.Check(len(incidents[1].Assignments), Equals, 1)
	c.Check(incidents[1].Bug.ID, Equals, 1122053)
	c.Check(incidents[1].State, Equals, "work")
}

var sampleSearchPaged0 = `
{
   "objects" : [` + sampleIncident + `, ` + sampleIncident2 + `
   ],
   "meta" : {
      "limit" : 2,
      "previous" : null,
      "total_count" : 5,
      "offset" : 0,
      "next" : "/api/1/incident/?filter=customer_domain&limit=2&cond=&match=foobar.com&offset="
   }
}`

var sampleSearchPaged1 = `
{
   "objects" : [` + sampleIncident + `, ` + sampleIncident2 + `
   ],
   "meta" : {
      "limit" : 2,
      "previous" : null,
      "total_count" : 5,
      "offset" : 2,
      "next" : "/api/1/incident/?filter=customer_domain&limit=2&cond=&match=foobar.com&offset=2"
   }
}`

var sampleSearchPaged2 = `
{
   "objects" : [` + sampleIncident + `
   ],
   "meta" : {
      "limit" : 2,
      "previous" : null,
      "total_count" : 5,
      "offset" : 4,
      "next" : null
   }
}`

func (cs *clientSuite) TestSearchPaged(c *C) {
	cs.rsps = []string{sampleSearchPaged0,
		// First incident:
		sampleBug,
		// Second incident:
		sampleBug,
		sampleSearchPaged1,
		sampleBug,
		sampleBug,
		sampleSearchPaged2,
		sampleBug,
		sampleBug,
	}

	search := client.Search{CustomerDomain: "foobar.com", Limit: 2}
	incidents, errors, err := cs.cli.SearchPaged(search)
	c.Assert(err, IsNil)
	// First page
	incident, ok := <-incidents
	c.Assert(ok, Equals, true)
	c.Assert(incident.ID, Equals, 52044)
	c.Assert(incident.Bug.ID, Equals, 1122053)
	incident, ok = <-incidents
	c.Assert(ok, Equals, true)
	c.Assert(incident.ID, Equals, 52045)
	c.Assert(incident.Bug.ID, Equals, 1122053)
	// Second page
	incident, ok = <-incidents
	c.Assert(ok, Equals, true)
	c.Assert(incident.ID, Equals, 52044)
	c.Assert(incident.Bug.ID, Equals, 1122053)
	incident, ok = <-incidents
	c.Assert(ok, Equals, true)
	c.Assert(incident.ID, Equals, 52045)
	c.Assert(incident.Bug.ID, Equals, 1122053)
	// Last page
	incident, ok = <-incidents
	c.Assert(ok, Equals, true)
	c.Assert(incident.ID, Equals, 52044)
	c.Assert(incident.Bug.ID, Equals, 1122053)
	// No more incidents
	incident, ok = <-incidents
	c.Assert(ok, Equals, false)
	_, ok = <-errors
	c.Assert(ok, Equals, false)
}

func (cs *clientSuite) TestSearchConstruction(c *C) {
	cs.rsps = []string{sampleSearchPaged0,
		// First incident:
		sampleBug,
		// Second incident:
		sampleBug,
	}
	customer := "foobar.com"
	search := client.Search{CustomerDomain: customer, Limit: 2}
	_, _, err := cs.cli.Search(search)
	c.Assert(err, IsNil)
	c.Assert(len(cs.reqs), Equals, 3)
	searchRequest := cs.reqs[0]
	query := searchRequest.URL.Query()
	c.Assert(query.Get("filter"), Equals, "customer_domain")
	c.Assert(query.Get("cond"), Equals, "")
	c.Assert(query.Get("match"), Equals, customer)
}

func (cs *clientSuite) TestSearchCustomField(c *C) {
	cs.rsps = []string{sampleSearchPaged0,
		// First incident:
		sampleBug,
		// Second incident:
		sampleBug,
	}
	search := client.Search{CustomFilter: "some_field", CustomCond: "something", CustomMatch: "some value"}
	_, _, err := cs.cli.Search(search)
	c.Assert(err, IsNil)
	c.Assert(len(cs.reqs), Equals, 3)
	searchRequest := cs.reqs[0]
	query := searchRequest.URL.Query()
	c.Assert(query.Get("filter"), Equals, "some_field")
	c.Assert(query.Get("cond"), Equals, "something")
	c.Assert(query.Get("match"), Equals, "some value")
}

func (cs *clientSuite) TestSearchCustomExpr(c *C) {
	cs.rsps = []string{sampleSearchPaged0,
		// First incident:
		sampleBug,
		// Second incident:
		sampleBug,
	}
	customer := "foobar.com"
	search := client.Search{CustomerDomain: customer, CustomExpr: "x=y&z=j"}
	_, _, err := cs.cli.Search(search)
	c.Assert(err, IsNil)
	c.Assert(len(cs.reqs), Equals, 3)
	searchRequest := cs.reqs[0]
	query := searchRequest.URL.Query()
	c.Assert(query.Get("filter"), Equals, "customer_domain")
	c.Assert(query.Get("cond"), Equals, "")
	c.Assert(query.Get("match"), Equals, customer)
	c.Assert(query.Get("x"), Equals, "y")
	c.Assert(query.Get("z"), Equals, "j")
}
07070100000009000081A4000003E800000064000000015CBEFDCB0000014F000000000000000000000000000000000000002700000000golang-l3t-0.0.1/client/export_test.gopackage client

import (
	"io"
	"net/url"
)

// SetDoer sets the client's doer to the given one
func (client *Client) SetDoer(d doer) {
	client.doer = d
}

// Do does do.
func (client *Client) Do(method, path string, queries []url.Values, body io.Reader, v interface{}) error {
	return client.do(method, path, queries, nil, body, v)
}
0707010000000A000041ED000003E800000064000000025CBEFDCB00000000000000000000000000000000000000000000001500000000golang-l3t-0.0.1/cmd0707010000000B000081A4000003E800000064000000015CBEFDCB00000226000000000000000000000000000000000000001F00000000golang-l3t-0.0.1/cmd/accept.gopackage cmd

import (
	"fmt"
	"log"

	"github.com/spf13/cobra"
)

var acceptCmd = &cobra.Command{
	Use:     "accept -i <incident> | -b <bug>",
	Aliases: []string{"ac", "l3ac"},
	Short:   "Accepts an incident",
	Run:     acceptMain,
}

func init() {
	prepareIncidentCommand(acceptCmd)
	prepareCommand(acceptCmd)
	RootCmd.AddCommand(acceptCmd)
}

func acceptMain(cmd *cobra.Command, args []string) {
	messages, err := l3tOrCrash().Accept(optIncident)
	if err != nil {
		log.Fatal(err)
	}
	for _, message := range messages {
		fmt.Println(message)
	}
}
0707010000000C000081A4000003E800000064000000015CBEFDCB0000022D000000000000000000000000000000000000001F00000000golang-l3t-0.0.1/cmd/assign.gopackage cmd

import (
	"fmt"
	"log"

	"github.com/spf13/cobra"
)

var assignCmd = &cobra.Command{
	Use:     "assign -i <incident> | -b <bug>",
	Aliases: []string{"as", "l3as"},
	Short:   "Assigns an incident",
	Run:     assignMain,
}

func init() {
	prepareRunningIncidentCommand(assignCmd)
	prepareCommand(assignCmd)
	RootCmd.AddCommand(assignCmd)
}

func assignMain(cmd *cobra.Command, args []string) {
	messages, err := l3tOrCrash().Assign(optIncident)
	if err != nil {
		log.Fatal(err)
	}
	for _, message := range messages {
		fmt.Println(message)
	}
}
0707010000000D000081A4000003E800000064000000015CBEFDCB000009E6000000000000000000000000000000000000002000000000golang-l3t-0.0.1/cmd/command.gopackage cmd

import (
	"bytes"
	"io/ioutil"

	l3tmod "github.com/bhdn/golang-l3t"
	"github.com/bhdn/golang-l3t/config"
	"github.com/bhdn/golang-l3t/log"
	"github.com/spf13/cobra"
)

var (
	optIncident          int
	optBug               int
	optConfigOverrides   map[string]string
	optConfigurationFile string
)

func prepareOptions(cmd *cobra.Command) {
	flags := cmd.Flags()
	flags.IntVarP(&optIncident, "incident", "i", 0, "The incident number")
	flags.IntVarP(&optBug, "bug", "b", 0, "The bug number")
}

func prepareCommand(cmd *cobra.Command) {
	oldRun := cmd.Run
	cmd.Run = func(cmd *cobra.Command, args []string) {
		for k, v := range optConfigOverrides {
			config.Config().Set(k, v)
		}
		if optConfigurationFile != "" {
			b, err := ioutil.ReadFile(optConfigurationFile)
			if err != nil {
				log.Fatalf("Failed to read the configuration file %s: %v", optConfigurationFile, err)
			}
			if err := config.Config().MergeConfig(bytes.NewBuffer(b)); err != nil {
				log.Fatalf("Failed to load the configuration from %s: %v", optConfigurationFile, err)
			}
		}
		log.UpdateLevelFromConfig(config.Config())
		oldRun(cmd, args)
	}
	flags := cmd.Flags()
	flags.StringVar(&optConfigurationFile, "config", "", "Configuration file")
	flags.StringToStringVar(&optConfigOverrides, "set", nil, "Override a configuration option (<group>.<option>=<value>,...)")
}

func prepareBugCommand(cmd *cobra.Command) {
	prepareOptions(cmd)

	oldRun := cmd.Run
	cmd.Run = func(cmd *cobra.Command, args []string) {
		if optBug == 0 {
			bug, err := l3tOrCrash().BugFromSystem(optIncident, optBug)
			if err != nil {
				log.Fatal(err)
			}
			optBug = bug
		}
		oldRun(cmd, args)
	}
}

func prepareIncidentCommand(cmd *cobra.Command) {
	prepareOptions(cmd)

	oldRun := cmd.Run
	cmd.Run = func(cmd *cobra.Command, args []string) {
		if optIncident == 0 {
			incident, err := l3tOrCrash().IncidentFromSystem(optIncident, optBug)
			if err != nil {
				log.Fatal(err)
			}
			optIncident = incident
		}
		oldRun(cmd, args)
	}
}

func prepareRunningIncidentCommand(cmd *cobra.Command) {
	prepareOptions(cmd)

	oldRun := cmd.Run
	cmd.Run = func(cmd *cobra.Command, args []string) {
		if optIncident == 0 {
			incident, err := l3tOrCrash().IncidentRunningFromSystem(optIncident, optBug)
			if err != nil {
				log.Fatal(err)
			}
			optIncident = incident
		}
		oldRun(cmd, args)
	}
}

var l3t *l3tmod.L3t

func l3tOrCrash() *l3tmod.L3t {
	var err error
	if l3t == nil {
		l3t, err = l3tmod.New()
		if err != nil {
			log.Fatal(err)
		}
	}
	return l3t
}
0707010000000E000081A4000003E800000064000000015CBEFDCB00000475000000000000000000000000000000000000001F00000000golang-l3t-0.0.1/cmd/config.gopackage cmd

import (
	"fmt"
	"log"
	"os"

	"github.com/bhdn/golang-l3t/config"
	"github.com/spf13/cobra"
	"gopkg.in/yaml.v2"
)

var optShowDefaults bool

func dumpYaml(obj interface{}) {
	err := yaml.NewEncoder(os.Stdout).Encode(obj)
	if err != nil {
		log.Fatal(err)
	}
}

var configCmd = &cobra.Command{
	Use:     "config",
	Aliases: []string{"showrc", "l3t-showrc"},
	Short:   "Dumps the configuration options",
	Run: func(cmd *cobra.Command, args []string) {
		if optShowDefaults {
			fmt.Print(string(config.Defaults))
		} else {
			config := config.Config()
			all := config.AllSettings()
			dumpYaml(all)
		}
	},
}

var configGetCmd = &cobra.Command{
	Use:     "get",
	Aliases: []string{"g"},
	Short:   "Get one configuration value",
	Run: func(cmd *cobra.Command, args []string) {
		for _, v := range args {
			obj := config.Config().Get(v)
			if obj != nil {
				dumpYaml(obj)
			}
		}
	},
}

func init() {
	flags := configCmd.Flags()
	flags.BoolVarP(&optShowDefaults, "defaults", "d", false, "Shows the default configuration values")
	prepareCommand(configCmd)
	configCmd.AddCommand(configGetCmd)
	RootCmd.AddCommand(configCmd)
}
0707010000000F000081A4000003E800000064000000015CBEFDCB000008D4000000000000000000000000000000000000002300000000golang-l3t-0.0.1/cmd/create-ptf.gopackage cmd

import (
	"bytes"
	"fmt"
	"os"
	"os/exec"

	"github.com/bhdn/golang-l3t/config"
	"github.com/bhdn/golang-l3t/log"
	"github.com/bhdn/golang-l3t/template"
	"github.com/google/shlex"
	"github.com/spf13/cobra"
)

var (
	optArch    string
	optProduct string
)

// versionCmd represents the version command
var createPTFCmd = &cobra.Command{
	Use:     "create-ptf <package-name>",
	Aliases: []string{"ptfc", "ptf-create", "ptfs", "l3t-ptf-create"},
	Short:   "A thin wrapper around ptfsetup to avoid using -D and L3:XXXXX",
	Args:    cobra.ArbitraryArgs,
	Run:     createPTF,
}

func init() {
	prepareRunningIncidentCommand(createPTFCmd)
	prepareCommand(createPTFCmd)
	flags := createPTFCmd.Flags()
	flags.StringVarP(&optArch, "arch", "a", "", "Force the arch to be used")
	flags.StringVarP(&optProduct, "product", "p", "", "Force the product to be used")
	RootCmd.AddCommand(createPTFCmd)
}

func createPTF(cmd *cobra.Command, args []string) {
	incident, err := l3tOrCrash().GetIncident(optIncident)
	if err != nil {
		log.Fatal(err)
	}

	if len(args) == 0 {
		log.Fatal("The package name is missing")
	}

	pkg := args[0]
	extra := []string{}
	if len(args) > 1 {
		extra = args[1:]
	}

	if optProduct == "" {
		optProduct = config.Config().GetString(fmt.Sprintf("ptfsetup.product-map.%s", incident.Bug.Product))
	}

	if optArch == "" {
		optArch = config.Config().GetString(fmt.Sprintf("ptfsetup.arch-map.%s", incident.Bug.Platform))
		if optArch == "" {
			optArch = "x86_64"
			log.Debug("no arch from the platform map, falling back to ", optArch)
		}
	}

	buf := new(bytes.Buffer)
	err = template.ExpandFromConfig("ptfsetup.cmd", buf,
		struct {
			Product  string
			Arch     string
			Package  string
			Incident int
		}{optProduct, optArch, pkg, incident.ID})
	if err != nil {
		log.Fatalf("Failed to expand the command template: %v", err)
	}
	rawArgs := buf.String()
	cmdArgs, err := shlex.Split(rawArgs)
	if err != nil {
		log.Fatalf("Failed to parse %#v: %v", rawArgs, cmdArgs)
	}

	cmdArgs = append(cmdArgs, extra...)

	log.Info("running ", cmdArgs)

	command := exec.Command(cmdArgs[0], cmdArgs[1:]...)
	command.Stdout = os.Stdout
	command.Stderr = os.Stderr
	command.Stdin = os.Stdin
	err = command.Run()
	if err != nil {
		os.Exit(1)
	}
}
07070100000010000081A4000003E800000064000000015CBEFDCB0000264F000000000000000000000000000000000000002100000000golang-l3t-0.0.1/cmd/edit-bug.gopackage cmd

import (
	"bytes"
	"fmt"
	"io/ioutil"
	"os"
	"os/exec"
	"regexp"
	"strings"
	"time"

	"github.com/bhdn/go-suseapi/bugzilla"
	"github.com/bhdn/golang-l3t/config"
	"github.com/bhdn/golang-l3t/formatted"
	"github.com/bhdn/golang-l3t/log"
	"github.com/google/shlex"
	"github.com/spf13/cobra"
)

// versionCmd represents the version command
var editBugCmd = &cobra.Command{
	Use:     "edit-bug -b <bug> | -i <incident> <operation flags>",
	Aliases: []string{"be", "bze", "edit-bug"},
	Short:   "Edits a bug from Bugzilla",
	Long: `Edits a given bug

WARNING: As it fetches the latest form from the bug right before submitting
changes, race conditions might happen.
` + sharedBugHelp + `
Examples:

    # adds a comment to bug 1047068:
    bze -b 1047068 -c

    # adds a comment and set needinfo:
    bze -b 1047068 -c -n someuser@example.com

    # adds a comment and clears needinfo:
    bze -b 1047068 -N -c

    # clears all needinfos set:
    bze -b 1047068 -NN

    # adds a non-private comment to a bug whose Cc list has emails with
    # unknown domains (customers, for example):
    bze -b 1047068 -P -c

    # closes a bug (and a comment is required by bugzilla):
    bze -b 1047068 --status RESOLVED --resolution INVALID -c
`,
	Args: cobra.NoArgs,
	Run:  editBug,
}

var (
	StatusOptions = []string{"NEW", "CONFIRMED", "IN_PROGRESS",
		"RESOLVED", "REOPENED", "VERIFIED"}
	ResolutionOptions = []string{"FIXED", "INVALID", "WONTFIX", "NORESPONSE",
		"UPSTREAM", "FEATURE", "DUPLICATE", "WORKSFORME",
		"MOVED"}
	optIgnoreCollision bool

	optSetNeedinfo    string
	optClearNeedinfo  int
	optRemoveNeedinfo string

	optMessage          string
	optMessageFile      string
	optCommentIsPrivate bool
	optIgnoreAliens     bool
	optAddCc            string
	optRemoveCc         string
	optCcMyself         bool
	optPriority         string
	optAssignee         string
	optURL              string
	optDescription      string
	optWhiteboard       string
	optStatus           string
	optResolution       string
	optDuplicate        int

	optComment bool
)

func init() {
	prepareBugCommand(editBugCmd)
	prepareCommand(editBugCmd)
	joinedStatus := strings.Join(StatusOptions, ", ")
	joinedResolution := strings.Join(ResolutionOptions, ", ")
	flags := editBugCmd.Flags()
	flags.BoolVar(&optIgnoreCollision, "ignore-collision", false, "don't try to prevent mid-air collisions")
	flags.StringVarP(&optSetNeedinfo, "needinfo", "n", "", "set needinto to to an email")
	flags.CountVarP(&optClearNeedinfo, "clear-needinfo", "N", "remove the only needinfo in the bug (repeat to force for all needinfos)")
	flags.StringVarP(&optRemoveNeedinfo, "remove-needinfo", "I", "", "Remove a needinfo flag for an email")
	flags.StringVarP(&optMessage, "message", "m", "", "set the new comment message")
	flags.BoolVarP(&optComment, "comment", "c", false, "comment on the bug using an editor")
	flags.BoolVarP(&optCommentIsPrivate, "private", "p", false, "the new comment is private")
	flags.BoolVarP(&optIgnoreAliens, "non-private-is-ok", "P", false, "make a public comment despite having non-native emails in the Cc list")
	flags.StringVarP(&optAddCc, "cc", "C", "", "add an email to the Cc list")
	flags.StringVarP(&optRemoveCc, "remove-cc", "R", "", "remove an email from the Cc list (case sensitive)")
	flags.BoolVarP(&optCcMyself, "ccme", "M", false, "add yourself to the Cc list")
	flags.StringVar(&optPriority, "priority", "", "sets the priority (P0..P5)")
	flags.StringVarP(&optAssignee, "assignee", "a", "", "sets the assignee email")
	flags.StringVarP(&optURL, "url", "u", "", "sets the bug URL (bug_file_loc)")
	flags.StringVarP(&optDescription, "description", "d", "", "sets the bug title (short_desc)")
	flags.StringVarP(&optWhiteboard, "whiteboard", "w", "", "sets the bug whiteboard")
	flags.StringVarP(&optStatus, "status", "s", "", fmt.Sprintf("sets the bug status (one of %s)", joinedStatus))
	flags.StringVarP(&optResolution, "resolution", "r", "", fmt.Sprintf("sets the bug resolution (one of %s)", joinedResolution))
	flags.IntVarP(&optDuplicate, "duplicate", "", 0, "set the duplicate bug number")
	flags.StringVarP(&optMessageFile, "file", "F", "", "Load the comment from a file")
	RootCmd.AddCommand(editBugCmd)
}

func getEditorArgs() []string {
	editor := os.Getenv("VISUAL")
	if editor == "" {
		editor = os.Getenv("EDITOR")
		if editor == "" {
			editor = "vi"
		}
	}
	args, err := shlex.Split(editor)
	if err != nil {
		log.Fatal("Failed to parse VISUAL or EDITOR: ", err)
	}
	return args
}

func getTempFile() *os.File {
	f, err := ioutil.TempFile("", "l3t-*")
	if err != nil {
		log.Fatal("Failed to get a temporary file: ", err)
	}
	return f
}

func findAliens(bug *bugzilla.Bug) ([]string, error) {
	domains := config.Config().GetString("bugzilla.native-domains")
	re := regexp.MustCompile(domains)
	found := make([]string, 0)
	for _, email := range bug.Cc {
		if !re.MatchString(email) {
			found = append(found, email)
		}
	}
	joinedAliens := strings.Join(found, ", ")
	if len(found) > 0 && !optCommentIsPrivate && !optIgnoreAliens {
		return nil, fmt.Errorf("Refusing to proceed in non-private comment. Not matching %q: %s. Use -P to force.", domains, joinedAliens)
	}
	return found, nil
}

func commentTemplate() ([]byte, time.Time, error) {
	bug, incidents := getBugAndIncidents()

	aliens, err := findAliens(bug)
	if err != nil {
		return nil, time.Now(), err
	}
	buf := new(bytes.Buffer)
	settings := formatted.BugFormatForEditor{PrivateComment: optCommentIsPrivate, Aliens: aliens}
	err = formatted.FormatBugForEditor(bug, incidents, settings, buf)
	if err != nil {
		return nil, time.Now(), fmt.Errorf("Failed to format the bug: %v", err)
	}

	return buf.Bytes(), bug.DeltaTS, nil
}

func callEditor(args []string) {
	cmd := exec.Command(args[0], args[1:]...)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	cmd.Stdin = os.Stdin

	err := cmd.Run()
	if err != nil {
		log.Fatal(fmt.Errorf("failed to run %v: error %v", args, err))
	}
}

var NotModified = fmt.Errorf("The comment file was not modified. Aborting.")

func stripTemplate(contents []byte) (string, error) {
	markerFirst := config.Config().GetString("bugzilla.marker-first")
	markerLast := config.Config().GetString("bugzilla.marker-last")
	comment := new(bytes.Buffer)
	idx := bytes.Index(contents, []byte(markerFirst))
	if len(contents) < len(markerFirst)+1+len(markerLast) {
		return "", fmt.Errorf("The comment file is not large enough to acommodate the marks")
	}
	if idx < 0 {
		return "", fmt.Errorf("Didn't find '%s' in the comment file.", markerFirst)
	}
	comment.Write(contents[:idx])
	between := contents[idx+len(markerFirst)+1:]
	idx = bytes.LastIndex(between, []byte(markerLast))
	if idx >= 0 {
		chomp := 0
		// eat EOL and avoid tripping on EOF
		if len(between) > idx+len(markerLast) {
			chomp = 1
		}
		comment.Write(between[idx+len(markerLast)+chomp:])
	} else {
		return "", fmt.Errorf("Found '%s' but not '%s'. Aborting to prevent garbage "+
			"is sent to the bug.", markerFirst, markerLast)
	}

	return comment.String(), nil
}

func commentModifiedFromEditor(editor []string, file string, template []byte) (string, error) {
	args := append(editor, file)
	callEditor(args)

	b, err := ioutil.ReadFile(file)
	if err != nil {
		return "", fmt.Errorf("failed to read the temporary comment file: %v", err)
	}

	if bytes.Equal(b, template) {
		return "", NotModified
	}

	stripped, err := stripTemplate(b)
	if err != nil {
		return "", err
	}

	return string(stripped), nil
}

var tmpFile *os.File

func openEditor() (string, time.Time, error) {
	template, deltaTS, err := commentTemplate()
	if err != nil {
		return "", time.Now(), err
	}

	editor := getEditorArgs()
	tmpFile = getTempFile()

	_, err = tmpFile.Write(template)
	if err != nil {
		log.Fatal("failed to write to the template tmpFile: ", err)
	}
	if err := tmpFile.Close(); err != nil {
		log.Fatal("failed to close the template tmpFile: ", err)
	}
	message, err := commentModifiedFromEditor(editor, tmpFile.Name(), template)
	if err != nil {
		if err == NotModified {
			os.Remove(tmpFile.Name())
			tmpFile = nil
		}
		return "", time.Now(), err
	}
	return message, deltaTS, err
}

func messageFromFile() (string, error) {
	b, err := ioutil.ReadFile(optMessageFile)
	if err != nil {
		return "", err
	}

	stripped, err := stripTemplate(b)
	if err != nil {
		return "", err
	}

	message := string(stripped)
	return message, err
}

func editBug(cmd *cobra.Command, args []string) {
	var err error
	var deltaTS time.Time

	if optMessageFile != "" {
		optMessage, err = messageFromFile()
		optComment = false // Workaround to keep compatible with the old -c -F usage
	} else if optComment {
		optMessage, deltaTS, err = openEditor()
	}
	checkDeltaTS := optComment && !optIgnoreCollision
	if err == nil {
		changes := bugzilla.Changes{
			CheckDeltaTS:      checkDeltaTS,
			DeltaTS:           deltaTS,
			SetNeedinfo:       optSetNeedinfo,
			AddComment:        optMessage,
			CommentIsPrivate:  optCommentIsPrivate,
			ClearNeedinfo:     optClearNeedinfo > 0,
			ClearAllNeedinfos: optClearNeedinfo > 1,
			RemoveNeedinfo:    optRemoveNeedinfo,
			AddCc:             optAddCc,
			RemoveCc:          optRemoveCc,
			CcMyself:          optCcMyself,
			SetPriority:       optPriority,
			SetAssignee:       optAssignee,
			SetURL:            optURL,
			SetDescription:    optDescription,
			SetWhiteboard:     optWhiteboard,
			SetStatus:         optStatus,
			SetResolution:     optResolution,
			SetDuplicate:      optDuplicate,
		}
		err = l3tOrCrash().UpdateBug(optBug, changes)
	}
	if err != nil {
		fmt.Fprintf(os.Stderr, "error: %s\n", err)
		if tmpFile != nil {
			fmt.Fprintf(os.Stderr, "\nReuse this comment file with -F %s\n", tmpFile.Name())
		}
		os.Exit(1)
	} else {
		if tmpFile != nil {
			os.Remove(tmpFile.Name())
		}
	}
}
07070100000011000081A4000003E800000064000000015CBEFDCB000006F1000000000000000000000000000000000000002600000000golang-l3t-0.0.1/cmd/edit-bug_test.gopackage cmd

import (
	"testing"

	"github.com/bhdn/go-suseapi/bugzilla"
	. "gopkg.in/check.v1"
)

type clientSuite struct {
	bz *bugzilla.Client
}

var _ = Suite(&clientSuite{})

// Hook up check.v1 into the "go test" runner
func Test(t *testing.T) { TestingT(t) }

const (
	markerFirst = `# ---- Any lines below this comment will be ignored ----`
	markerLast  = `# ---- Any lines above this comment will be ignored ----`
)

func (cs *clientSuite) TestStripComment(c *C) {
	text := markerFirst + "\n" + markerLast
	out, err := stripTemplate([]byte(text))
	c.Check(err, IsNil)
	c.Check(out, Equals, ``)

	text = markerFirst + "\n" + markerLast + "\n"
	out, err = stripTemplate([]byte(text))
	c.Check(err, IsNil)
	c.Check(out, Equals, ``)

	text = markerFirst + `
One 
Two
Three lines here
` + markerLast + "\n"
	out, err = stripTemplate([]byte(text))
	c.Check(err, IsNil)
	c.Check(out, Equals, ``)

	text = `Hello world
` + markerFirst + `
One 
Two
Three lines here
` + markerLast + "\n"
	out, err = stripTemplate([]byte(text))
	c.Check(err, IsNil)
	c.Check(out, Equals, "Hello world\n")

	text = `Hello world
` + markerFirst + `
One 
Two
Three lines here`
	out, err = stripTemplate([]byte(text))
	c.Check(err, ErrorMatches, ".*not large enough.*")
	c.Check(out, Equals, "")

	text = `
` + markerFirst + `
One 
Two
Three lines here` + markerLast + `
One line
Two lines
`
	out, err = stripTemplate([]byte(text))
	c.Check(err, IsNil)
	c.Check(err, IsNil)
	c.Check(out, Equals, "\nOne line\nTwo lines\n")

	text = `Hello world
` + markerFirst
	out, err = stripTemplate([]byte(text))
	c.Check(err, ErrorMatches, ".*not large enough.*")
	c.Check(out, Equals, "")

	out, err = stripTemplate([]byte(``))
	c.Check(err, ErrorMatches, ".*not large enough.*")
	c.Check(out, Equals, ``)
}
07070100000012000081A4000003E800000064000000015CBEFDCB0000022E000000000000000000000000000000000000002300000000golang-l3t-0.0.1/cmd/expand-bug.gopackage cmd

import (
	"log"

	"github.com/spf13/cobra"
)

var expandBugCmd = &cobra.Command{
	Use:     "expand-bug <bug template>",
	Aliases: []string{"eb", "l3t-expand-bug"},
	Short:   "Format a bug according to a given (golang) template",
	Run:     expandBugMain,
}

func init() {
	prepareBugCommand(expandBugCmd)
	prepareCommand(expandBugCmd)
	RootCmd.AddCommand(expandBugCmd)
}

func expandBugMain(cmd *cobra.Command, args []string) {
	bug, err := l3tOrCrash().GetBug(optBug)
	if err != nil {
		log.Fatal(err)
	}
	expanderFor("expand.bug", args, bug)
}
07070100000013000081A4000003E800000064000000015CBEFDCB0000042E000000000000000000000000000000000000002800000000golang-l3t-0.0.1/cmd/expand-incident.gopackage cmd

import (
	"log"
	"os"

	"github.com/bhdn/golang-l3t/config"
	"github.com/bhdn/golang-l3t/template"
	"github.com/spf13/cobra"
)

var expandIncidentCmd = &cobra.Command{
	Use:     "expand-incident  -i <incident> | -b <bug> <incident template>",
	Aliases: []string{"ei", "l3t-expand-incident"},
	Short:   "Format an incident according to a given (golang) template",
	Run:     expandIncidentMain,
}

func init() {
	prepareIncidentCommand(expandIncidentCmd)
	prepareCommand(expandIncidentCmd)
	RootCmd.AddCommand(expandIncidentCmd)
}

func expanderFor(confKey string, args []string, thing interface{}) {
	if len(args) == 0 {
		args = append(args, config.Config().GetString(confKey))
	}
	for _, arg := range args {
		config.Config().Set(confKey, arg)
		err := template.ExpandFromConfig(confKey, os.Stdout, thing)
		if err != nil {
			log.Fatal(err)
		}
	}
}

func expandIncidentMain(cmd *cobra.Command, args []string) {
	incident, err := l3tOrCrash().GetIncident(optIncident)
	if err != nil {
		log.Fatal(err)
	}
	expanderFor("expand.incident", args, incident)
}
07070100000014000081A4000003E800000064000000015CBEFDCB000001A4000000000000000000000000000000000000002E00000000golang-l3t-0.0.1/cmd/generate-autocomplete.gopackage cmd

import (
	"os"

	"github.com/spf13/cobra"
)

var generateAutcompleteCmd = &cobra.Command{
	Hidden: true,
	Use:    "generate-autocomplete",
	Short:  "Generate the bash autocompletion",
	Long: `To use them, you can run:

. <(l3t generate-autocomplete)
`,
	Run: func(cmd *cobra.Command, args []string) {
		RootCmd.GenBashCompletion(os.Stdout)
	},
}

func init() {
	RootCmd.AddCommand(generateAutcompleteCmd)
}
07070100000015000081A4000003E800000064000000015CBEFDCB00000D86000000000000000000000000000000000000002700000000golang-l3t-0.0.1/cmd/get-attachment.gopackage cmd

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"os"
	"path"

	"github.com/bhdn/go-suseapi/bugzilla"
	"github.com/bhdn/golang-l3t/template"
	"github.com/spf13/cobra"
	"golang.org/x/crypto/ssh/terminal"
	"gopkg.in/cheggaaa/pb.v1"
)

// versionCmd represents the version command
var getAttachmentCmd = &cobra.Command{
	Use:     "get-attachment -a <attachment id> | -b <bug id> | -i <incident id>",
	Aliases: []string{"att", "bzag"},
	Short:   "Downloads one or more attachments from a bug",
	Args:    cobra.NoArgs,
	Run:     getAttachment,
}

var (
	optAttachID       int
	optDestDir        string
	optDestFile       string
	optIgnoreExisting bool
)

func init() {
	prepareBugCommand(getAttachmentCmd)
	prepareCommand(getAttachmentCmd)
	oldArgs := getAttachmentCmd.Args
	getAttachmentCmd.Args = func(cmd *cobra.Command, args []string) error {
		if optAttachID == 0 {
			return oldArgs(cmd, args)
		}
		return nil
	}
	flags := getAttachmentCmd.Flags()
	flags.IntVarP(&optAttachID, "attachment", "a", 0, "ID of the attachment to be downloaded")
	flags.StringVarP(&optDestDir, "dest", "d", ".", "Destination directory")
	flags.StringVarP(&optDestFile, "output", "o", "", "Destination file")
	flags.BoolVarP(&optIgnoreExisting, "force", "f", false, "Ignore existing files")
	RootCmd.AddCommand(getAttachmentCmd)
}

func getDestinationFile(att *bugzilla.Attachment) string {
	name := optDestFile
	if optDestFile == "" {
		buf := new(bytes.Buffer)
		err := template.ExpandFromConfig("bugzilla.attachment-name", buf, att)
		if err != nil {
			log.Fatal(fmt.Errorf("failed to expand the destination file name: %v", err))
		}
		name = buf.String()
	}
	dest := path.Join(optDestDir, name)
	return dest
}

func shouldSkip(dest string, att *bugzilla.Attachment) bool {
	stat, err := os.Stat(dest)
	if err != nil {
		if os.IsNotExist(err) {
			return false
		}
		log.Fatal(fmt.Errorf("failed while checking the destination file: %v", err))
	}
	return !optIgnoreExisting || stat.Size() == int64(att.Size)
}

func handle(att *bugzilla.Attachment, source io.ReadCloser) {
	dest := getDestinationFile(att)

	fmt.Printf("Downloading to %s: (%d KiB)\n", dest, att.Size/1024)

	term := terminal.IsTerminal(1)
	reader := source
	if term {
		bar := pb.New(att.Size)
		reader = bar.NewProxyReader(source)
		bar.Start()
		defer bar.Finish()
	}

	f, err := os.Create(dest)
	if err != nil {
		log.Fatal(fmt.Errorf("failed to open the destination file: %v", err))
	}

	_, err = io.Copy(f, reader)
	if err != nil {
		log.Fatal(fmt.Errorf("failed to download the file: %v", err))
	}
}

func attachmentsFromBug() []int {
	ids := make([]int, 0)
	bug, err := l3t.GetBug(optBug)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%d attachments found\n", len(bug.Attachments))

	for _, att := range bug.Attachments {
		dest := getDestinationFile(att)
		if shouldSkip(dest, att) {
			fmt.Printf("Skipping existing %s (%d KiB)\n", dest, att.Size/1024)
			continue
		}
		ids = append(ids, att.AttachID)
	}
	return ids
}

func getAttachment(cmd *cobra.Command, args []string) {
	l3t := l3tOrCrash()
	ids := make([]int, 0)

	if optAttachID != 0 {
		ids = append(ids, optAttachID)
	} else {
		if optDestFile != "" {
			log.Fatal("Cannot download multiple files and have -o forcing a single destination filename")
		}
		ids = attachmentsFromBug()
	}

	for _, id := range ids {
		att, f, err := l3t.DownloadAttachment(id)
		if err != nil {
			log.Fatal(err)
		}
		defer f.Close()
		handle(att, f)
	}
}
07070100000016000081A4000003E800000064000000015CBEFDCB000009FA000000000000000000000000000000000000002000000000golang-l3t-0.0.1/cmd/get-bug.gopackage cmd

import (
	"log"

	"github.com/bhdn/go-suseapi/bugzilla"
	"github.com/bhdn/golang-l3t/client"
	"github.com/bhdn/golang-l3t/config"
	"github.com/bhdn/golang-l3t/formatted"
	"github.com/bhdn/golang-l3t/paged"
	"github.com/spf13/cobra"
)

const sharedBugHelp = `
The credentails come from ~/.oscrc, ~/.config/osc/oscrc or the configuration
options bugzilla.username, and bugzilla.password.

-b is not needed when operating inside a bug directory (more on l3t help cwb).

-i can be used too (see l3t help args)
`

// versionCmd represents the version command
var getBZBugCmd = &cobra.Command{
	Use:     "get-bug -b <bug id> | -i <incident id> [flags]",
	Aliases: []string{"b", "bzg", "bug"},
	Short:   "Shows a bug from Bugzilla",
	Long: `Displays a bug from Bugzilla
` + sharedBugHelp,
	Args: cobra.NoArgs,
	Run:  getBZBug,
}

var (
	optLastComments int
	optFromCache    bool
	optNoIncidents  bool
)

func init() {
	prepareBugCommand(getBZBugCmd)
	prepareCommand(getBZBugCmd)
	flags := getBZBugCmd.Flags()
	flags.IntVarP(&optLastComments, "last-comments", "n", 0, "Show only the N last comments")
	flags.BoolVarP(&optFromCache, "cache", "c", false, "Show the bug from the cache (see l3t help cache)")
	flags.BoolVarP(&optNoIncidents, "no-incidents", "I", false, "Don't show incidents")
	RootCmd.AddCommand(getBZBugCmd)
}

func getBugAndIncidents() (*bugzilla.Bug, []*client.Incident) {
	done := make(chan error, 2)
	bugs := make(chan *bugzilla.Bug, 1)
	incidents := make(chan []*client.Incident, 1)
	l3t := l3tOrCrash()

	go func() {
		var bug *bugzilla.Bug
		var err error
		if optFromCache {
			bug, err = l3t.GetBugFromCache(optBug)
		} else {
			bug, err = l3t.GetBug(optBug)
		}
		bugs <- bug
		done <- err
	}()
	go func() {
		var err error
		var found []*client.Incident
		if !optNoIncidents {
			found, err = l3t.SearchIncidentsByBug(optBug)
		}
		incidents <- found
		done <- err
	}()

	for i := 0; i < 2; i++ {
		err := <-done
		if err != nil {
			log.Fatal(err)
		}
	}

	return <-bugs, <-incidents
}

func getBZBug(cmd *cobra.Command, args []string) {
	bug, incidents := getBugAndIncidents()

	stdout, wait, err := paged.PagedOutput()
	if err != nil {
		log.Fatal(err)
	}
	defer func() {
		stdout.Close()
		if err != nil {
			log.Fatal(err)
		}
		err = wait()
		if err != nil {
			log.Fatal(err)
		}

	}()

	format := formatted.BugFormat{
		SortBy:       config.Config().GetString("bugzilla.sort"),
		LastComments: optLastComments,
	}

	err = formatted.FormatBug(bug, incidents, format, stdout)
	if err != nil {
		log.Fatal(err)
	}

}
07070100000017000081A4000003E800000064000000015CBEFDCB000002A9000000000000000000000000000000000000002500000000golang-l3t-0.0.1/cmd/get-incident.gopackage cmd

import (
	"log"
	"os"

	"github.com/bhdn/golang-l3t/formatted"
	"github.com/spf13/cobra"
)

var getIncidentCmd = &cobra.Command{
	Use:     "get-incident -b <bug id> | -i <incident id>",
	Aliases: []string{"i", "l3g"},
	Short:   "Displays information about an incident",
	Run:     getIncidentMain,
}

func init() {
	prepareIncidentCommand(getIncidentCmd)
	prepareCommand(getIncidentCmd)
	RootCmd.AddCommand(getIncidentCmd)
}

func getIncidentMain(cmd *cobra.Command, args []string) {
	incident, err := l3tOrCrash().GetIncident(optIncident)
	if err != nil {
		log.Fatal(err)
	}
	err = formatted.FormatIncident(incident, os.Stdout)
	if err != nil {
		log.Fatal(err)
	}
}
07070100000018000081A4000003E800000064000000015CBEFDCB00000203000000000000000000000000000000000000002300000000golang-l3t-0.0.1/cmd/get-sg-bug.gopackage cmd

import (
	"fmt"
	"log"

	"github.com/spf13/cobra"
)

var getSGBugCmd = &cobra.Command{
	Use:     "get-sg-bug -b <bug id> | -i <incident id>",
	Aliases: []string{"sgb", "l3bz"},
	Short:   "Shows a bug as cached by Solid Ground",
	Run:     getSGBugMain,
}

func init() {
	prepareRunningIncidentCommand(getSGBugCmd)
	RootCmd.AddCommand(getSGBugCmd)
}

func getSGBugMain(cmd *cobra.Command, args []string) {
	bug, err := l3tOrCrash().GetBug(optBug)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(bug)
}
07070100000019000081A4000003E800000064000000015CBEFDCB00000269000000000000000000000000000000000000002200000000golang-l3t-0.0.1/cmd/get-sg-sr.gopackage cmd

import (
	"fmt"
	"log"

	"github.com/spf13/cobra"
)

var getSGSRCmd = &cobra.Command{
	Use:     "sr -b <bug id> | -i <incident id>",
	Aliases: []string{"srg", "l3tsrg"},
	Short:   "Displays information about a given SR as cached by SG",
	Run:     getSGSRMain,
}

var (
	optSRId int
)

func init() {
	flags := getSGSRCmd.Flags()
	flags.IntVarP(&optSRId, "sr", "s", 0, "The SR number")
	RootCmd.AddCommand(getSGSRCmd)
	getSGSRCmd.MarkFlagRequired("sr")
}

func getSGSRMain(cmd *cobra.Command, args []string) {
	sr, err := l3tOrCrash().GetSR(optSRId)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(sr)
}
0707010000001A000081A4000003E800000064000000015CBEFDCB000007ED000000000000000000000000000000000000001D00000000golang-l3t-0.0.1/cmd/help.gopackage cmd

import "github.com/spf13/cobra"

var cwbCmd = &cobra.Command{
	Use:     "cwb",
	Aliases: []string{"CWB", "curitiba", "work-dir"},
	Short:   "What is CWB?",
	Long: `CWB
	
CWB is the short for 'currently working bug' and means that any l3t command
that operates on bugs or incidents doesn't require -i <incident> or
-b <bug> when the current working directory is inside the 'work-dir' and
has a bug number in it, which is set in the 'l3t.work-dir' configuration
option.

So if l3t.work-dir is ~/my-bugs/:

  ~ $ bzg -b 1112345 | head -2
  [1112345] Frobnicator crashes after 5 minutes
  Status: NEW

  ~ $ l3t config get l3t.work-dir
  ~/my-bugs/

  ~ $ cd ~/my-bugs/1112345/
  
  ~/my-bugs/1112345 $ bzg | head -2
  [1112345] Frobnicator crashes after 5 minutes
  Status: NEW

  ~/my-bugs/1112345 $ bze -m "We need supportconfigs" -n customercare@foobar.com

  ~/my-bugs/1112345 $ l3s
  Sleep time for incident Frobnicator crashes after 5 minutes updated.

`,
}

var argsCmd = &cobra.Command{
	Use:   "args",
	Short: "Why -i and -b everywhere?",
	Long: `Why -i and -b everywhere?

Any l3t command that operates on incidents or bugs accepts -i <incident> or
-b <bug>. They can be used interchangeably when there are no ambiguity (ie.
using -b for "sleep" when the bug is bound to more than one running incident).

The mapping happens by querying Solid Ground, so the API access must be setup
for this to work.`,
}

var cacheCmd = &cobra.Command{
	Use:   "cache",
	Short: "Bugs and incident can be saved locally for offline access",
	Long: `Bug and incident caching

In order to allow quick access to data from bugs and incidents, their data
can be cached in ~/.l3t/{bugs,incidents}/ when the configuration option

cache:
  enable: true

is set.

To read the cached version of a bug, you can use 'bzg -c -b 1112345' or
'json_pp < ~/.l3t/bugs/11/1112345'. You can also query the bugs/incidents
with the jq tool.`,
}

func init() {
	RootCmd.AddCommand(argsCmd)
	RootCmd.AddCommand(cacheCmd)
	RootCmd.AddCommand(cwbCmd)
}
0707010000001B000081A4000003E800000064000000015CBEFDCB00000DD1000000000000000000000000000000000000001B00000000golang-l3t-0.0.1/cmd/ls.gopackage cmd

import (
	"bytes"
	"fmt"
	"os"
	"strings"

	l3tmod "github.com/bhdn/golang-l3t"
	"github.com/bhdn/golang-l3t/config"
	"github.com/bhdn/golang-l3t/log"
	"github.com/bhdn/golang-l3t/template"
	"github.com/spf13/cobra"
	"golang.org/x/crypto/ssh/terminal"
)

// versionCmd represents the version command
var lsCmd = &cobra.Command{
	Use:     "ls",
	Aliases: []string{"l", "l3ls"},
	Short:   "Shows the overivew of incidents",
	Args:    checkArgs,
	Run:     lsMain,
}

var (
	optNew, optBackup, optActive, optSleeping, optProcessed, optClosed, optMine bool
	optHideHeaders                                                              bool
)

func addIncidentStateFlags(cmd *cobra.Command) {
	flags := cmd.Flags()
	flags.BoolVarP(&optNew, "new", "n", false, "Show new incidents")
	flags.BoolVarP(&optBackup, "backup", "b", false, "Show incidents that need backup")
	flags.BoolVarP(&optActive, "active", "a", false, "Show active incidents")
	flags.BoolVarP(&optSleeping, "sleeping", "s", false, "Show sleeping incidents")
	flags.BoolVarP(&optProcessed, "processed", "p", false, "Show processed incidents")
	flags.BoolVarP(&optClosed, "closed", "c", false, "Show closed incidents")
}

func init() {
	flags := lsCmd.Flags()
	flags.BoolVarP(&optMine, "mine", "m", false, "Show only my incidents")
	flags.BoolVarP(&optHideHeaders, "no-headers", "H", false, "Show only my incidents")
	addIncidentStateFlags(lsCmd)
	prepareCommand(lsCmd)
	RootCmd.AddCommand(lsCmd)
}

func checkArgs(cmd *cobra.Command, args []string) error {

	if err := cobra.NoArgs(cmd, args); err != nil {
		return err
	}

	anyFilter := optNew || optBackup || optActive || optSleeping || optProcessed || optClosed
	if optMine {
		optActive = true
		optSleeping = true
		optProcessed = true
		optBackup = false
		optNew = false
	} else if !anyFilter {
		optBackup = true
		optSleeping = true
		optProcessed = true
		optActive = true
		optNew = true
	}

	return nil
}

func expandHeader(count int, description string) {
	data := struct {
		Count       int
		Description string
	}{
		count,
		description,
	}
	var buf bytes.Buffer
	if err := template.ExpandFromConfig("l3ls.header", &buf, data); err != nil {
		log.Fatal(err)
	}

	padding := config.Config().GetString("l3ls.padding")
	width := 0
	filling := ""
	if terminal.IsTerminal(1) {
		var err error
		width, _, err = terminal.GetSize(1)
		if err != nil {
			log.Fatal(fmt.Errorf("error while getting the terminal size: %v", err))
		}
		filling = strings.Repeat(padding, width-buf.Len()-1)
	}

	fmt.Println(filling, buf.String())

}

func showGroup(l3t *l3tmod.L3t, group, description string) {
	incidents, err := l3t.Overview(group)
	if err != nil {
		log.Fatal("Could not get the overview: ", err)
		return
	}
	if len(incidents) == 0 {
		log.Debug("no incidents in group", group)
		return
	}

	if !optHideHeaders {
		expandHeader(len(incidents), description)
	}

	templ := template.FromConfig("l3t.incident-oneline")

	for _, incident := range incidents {
		if err := templ.Execute(os.Stdout, incident); err != nil {
			log.Fatal("failed to format: ", err)
		}
	}
}

func lsMain(cmd *cobra.Command, args []string) {
	l3t, err := l3tmod.New()
	if err != nil {
		log.Fatal(err)
	}

	if optSleeping {
		showGroup(l3t, "sleeping", "sleeping incidents")
	}
	if optBackup {
		showGroup(l3t, "backup", "need backup")
	}
	if optProcessed {
		showGroup(l3t, "processed", "processed incidents")
	}
	if optActive {
		showGroup(l3t, "active", "active incidents")
	}
	if optNew {
		showGroup(l3t, "new", "new in queue")
	}
}
0707010000001C000081A4000003E800000064000000015CBEFDCB00000F72000000000000000000000000000000000000002400000000golang-l3t-0.0.1/cmd/mail-action.gopackage cmd

import (
	"bufio"
	"errors"
	"fmt"
	"io"
	"mime"
	"net/mail"
	"os"
	"os/exec"
	"regexp"
	"strconv"
	"strings"

	"github.com/bhdn/golang-l3t/config"
	"github.com/bhdn/golang-l3t/log"
	"github.com/google/shlex"
	"github.com/spf13/cobra"
	"golang.org/x/crypto/ssh/terminal"
)

var (
	optFile  string
	optAsk   bool
	optPause bool
)

func findIncidentOrBug(header *mail.Header) (incident, bug int, err error) {
	rawIncident := header.Get("X-SG-Incident-Id")
	if rawIncident != "" {
		incident, err = strconv.Atoi(rawIncident)
		if err != nil {
			err = fmt.Errorf("failed to parse X-SG-Incident-Id: %v", err)
			return
		}
	}

	dec := new(mime.WordDecoder)
	rawSubject := header.Get("Subject")
	log.Debugf("about to decode %q", rawSubject)
	subject, err := dec.DecodeHeader(rawSubject)
	if err != nil {
		err = fmt.Errorf("failed to decode the subject: %v", err)
		return
	}

	re := regexp.MustCompile(config.Config().GetString("mail-action.regexp"))
	for _, match := range re.FindAllStringSubmatch(subject, -1) {
		if len(match) > 1 {
			bug, err = strconv.Atoi(match[1])
			if err != nil {
				fmt.Errorf("failed to decode the bug number string %q: %v", match[1], err)
				return
			}
			break
		}
	}
	return
}

var ErrNothingToDo = errors.New("Nothing to be done")

func getArgs(args []string) (cmdArgs []string, err error) {
	cmdArgs = make([]string, 0)
	if optAsk {
		fmt.Printf("> ")
		reader := bufio.NewReader(os.Stdin)
		input, err := reader.ReadString('\n')
		if err != nil {
			if err == io.EOF && input == "" {
				return nil, ErrNothingToDo
			}
			log.Fatalf("failed to read the base command from the input: %v (%q)", err, input)
		}
		cmd := strings.TrimSpace(input)
		if cmd == "" {
			return nil, ErrNothingToDo
		}
		if cmd[0] == '!' {
			optPause = true
			cmd = cmd[1:]
		}
		cmdArgs, err = shlex.Split(cmd)
		if err != nil {
			log.Fatalf("failed to split the input: %v", err)
		}
	} else {
		cmdArgs = args
	}
	return
}

func pause() {
	if terminal.IsTerminal(1) {
		fmt.Println("---------- press enter to continue ----------")
		_, err := bufio.NewReader(os.Stdin).ReadString('\n')
		if err != nil {
			log.Fatalf("failed to read stdin: %v", err)
		}
	}
}

func runFor(incident, bug int, args []string) {
	if len(args) < 1 {
		log.Fatal("no command to run!")
	}

	if incident != 0 {
		args = append(args, "-i", fmt.Sprintf("%d", incident))
	}
	if bug != 0 {
		args = append(args, "-b", fmt.Sprintf("%d", bug))
	}

	cmd := exec.Command(args[0], args[1:]...)
	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	if err := cmd.Run(); err != nil {
		os.Exit(1)
	}
	if optPause {
		pause()
	}
}

var mailActionCmd = &cobra.Command{
	Use:     "mail-action [--ask] [--file FILE]",
	Aliases: []string{"l3t-mail-action"},
	Short:   "Runs l3t commands on Bugzilla and SG emails",
	Long: `It Extracts bug/incident numbers from known email formats
and invokes a command with either -i <incident> or -b <incident>`,
	Run: func(cmd *cobra.Command, args []string) {
		var err error
		file := os.Stdin
		if optFile != "" {
			file, err = os.Open(optFile)
			if err != nil {
				log.Fatal(err)
			}
			defer file.Close()
		} else {
			file = os.Stdin
		}
		msg, err := mail.ReadMessage(file)
		if err != nil {
			log.Fatal(fmt.Errorf("failed to parse the email: %v", err))
		}
		incident, bug, err := findIncidentOrBug(&msg.Header)
		if err != nil {
			log.Fatal(err)
		}
		cmdArgs, err := getArgs(args)
		if err != nil {
			if err == ErrNothingToDo {
				fmt.Fprint(os.Stderr, "Nothing to do\n")
				os.Exit(0)
			}
			log.Fatal(err)
		}

		runFor(incident, bug, cmdArgs)
	},
}

func init() {
	flags := mailActionCmd.Flags()
	flags.StringVarP(&optFile, "file", "f", "", "Use a file instead of stdin")
	flags.BoolVarP(&optAsk, "ask", "a", false, "Ask in a prompt the base command to be run")
	flags.BoolVar(&optPause, "pause", false, "Wait for a confirmation (enter) before leaving")
	RootCmd.AddCommand(mailActionCmd)
}
0707010000001D000081A4000003E800000064000000015CBEFDCB00000C62000000000000000000000000000000000000002900000000golang-l3t-0.0.1/cmd/make-sr-symlinks.gopackage cmd

import (
	"fmt"
	"os"
	"path"
	"regexp"

	"github.com/bhdn/go-suseapi/bugzilla"
	"github.com/bhdn/golang-l3t/config"
	"github.com/bhdn/golang-l3t/log"
	"github.com/spf13/cobra"
)

var (
	optDest  string
	optForce bool
)

// versionCmd represents the version command
var makeSRSymlinksCmd = &cobra.Command{
	Use:     "make-sr-symlinks -b <bug id> | -i <incident id>",
	Aliases: []string{"srl", "lsr", "l3t-make-sr-links"},
	Short:   "Looks for SR* references in bug comments and create symlinks locally",
	Args:    cobra.NoArgs,
	Run:     makeSRSymlinks,
}

func init() {
	prepareBugCommand(makeSRSymlinksCmd)
	prepareCommand(makeSRSymlinksCmd)
	flags := makeSRSymlinksCmd.Flags()
	flags.StringVarP(&optDest, "dest", "d", ".", "Destination directory")
	flags.BoolVar(&optForce, "force", false, "Overwrite exiting symlinks")
	RootCmd.AddCommand(makeSRSymlinksCmd)
}

func findReferences(bug *bugzilla.Bug) ([]string, error) {
	expr := config.Config().GetString("ziu.reference-regexp")
	re, err := regexp.Compile(expr)
	if err != nil {
		return nil, fmt.Errorf("Could not compile the regexp for matching ziu references %s: %v", expr, err)
	}

	found := make([]string, 0)
	for _, comment := range bug.Comments {
		for _, match := range re.FindAllStringSubmatch(comment.TheText, -1) {
			if len(match) > 1 {
				for _, sr := range match[1:] {
					found = append(found, sr)
				}
			}
		}
	}
	return found, nil
}

func checkSource(source string) (skip bool, err error) {
	if _, err := os.Stat(source); err != nil {
		if os.IsNotExist(err) {
			if !optForce {
				return true, fmt.Errorf("%q not found. Is it mounted?", source)
			}
		} else {
			return true, fmt.Errorf("Failed while checking %q: %v", source, err)
		}
	}
	return false, nil
}

func checkDest(dest string) (skip bool, err error) {
	if _, err := os.Stat(dest); err != nil {
		if !os.IsNotExist(err) {
			return true, fmt.Errorf("Could not check %q: %v", dest, err)
		}
		// Doesn't exist, go to go
	} else {
		if optForce {
			log.Debug("unlinking ", dest)
			if err := os.Remove(dest); err != nil {
				return true, fmt.Errorf("Failed to remove the existing link %q: %v", dest, err)
			}
		} else {
			log.Info("Already exists: ", dest)
			return true, nil
		}
	}
	return false, nil
}

func ensureDirs(source, dest string) (skip bool, err error) {
	if skip, err = checkSource(source); err != nil || skip {
		return
	}

	skip, err = checkDest(dest)
	return
}

func link(dirname string) error {
	sourceBase := config.Config().GetString("ziu.mount")
	source := path.Join(sourceBase, dirname)
	dest := path.Join(optDest, dirname)

	if skip, err := ensureDirs(source, dest); err != nil || skip {
		return err
	}

	log.Info(source, " -> ", dest)
	if err := os.Symlink(source, dest); err != nil {
		return fmt.Errorf("Failed to create the symlink from %q to %q: %v", source, dest, err)
	}

	return nil
}

func makeSRSymlinks(cmd *cobra.Command, args []string) {
	bug, err := l3tOrCrash().GetBug(optBug)
	if err != nil {
		log.Fatal(err)
	}
	found, err := findReferences(bug)
	if err != nil {
		log.Fatal(err)
	}

	for _, sr := range found {
		err = link(sr)
		if err != nil {
			log.Fatal(err)
		}
	}
}
0707010000001E000081A4000003E800000064000000015CBEFDCB00000A03000000000000000000000000000000000000001D00000000golang-l3t-0.0.1/cmd/next.gopackage cmd

import (
	"os"
	"regexp"
	"sort"
	"strconv"

	"github.com/bhdn/golang-l3t/client"
	"github.com/bhdn/golang-l3t/config"
	"github.com/bhdn/golang-l3t/log"
	"github.com/bhdn/golang-l3t/template"
	"github.com/spf13/cobra"
)

var nextCmd = &cobra.Command{
	Use:     "next [flags]",
	Aliases: []string{"n", "writerblock", "l3n"},
	Short:   "Suggests the next incident to work on",
	Run:     nextMain,
}

var optIgnoreNew bool

// Flags come from ls.go

func init() {
	addIncidentStateFlags(nextCmd)
	flags := nextCmd.Flags()
	flags.BoolVarP(&optIgnoreNew, "skip-new", "N", false, "Ignore new incidents (when l3n.watch-product-regexp is set)")
	prepareCommand(nextCmd)
	RootCmd.AddCommand(nextCmd)
}

func groupsFromStateFlags() []string {
	picked := map[string]bool{
		"sleeping":  optSleeping,
		"backup":    optBackup,
		"processed": optProcessed,
		"active":    optActive,
		"new":       optNew,
	}
	groups := make([]string, 0)
	for k, v := range picked {
		if v {
			groups = append(groups, k)
		}
	}
	return groups
}

const InvalidPriority = 10 // A two-digit priority

func incidentPrio(prio string) int {
	if len(prio) < 2 {
		return InvalidPriority
	}
	rawPrio := prio[1:2]
	iprio, _ := strconv.Atoi(rawPrio)
	return iprio
}

func sortIncidents(incidents []*client.Incident) {
	importanter := func(i, j int) bool {
		iprio := incidentPrio(incidents[i].Bug.Priority)
		jprio := incidentPrio(incidents[j].Bug.Priority)
		if iprio < jprio {
			return true
		} else if iprio > jprio {
			return false
		}
		if incidents[i].Bug.LastComment.Before(incidents[j].Bug.LastComment) {
			return true
		}
		return incidents[i].Bug.LastChange.Before(incidents[j].Bug.LastChange)
	}
	sort.SliceStable(incidents, importanter)
}

func nextMain(cmd *cobra.Command, args []string) {
	l3t := l3tOrCrash()
	watchingRe := config.Config().GetString("l3n.product-category-regexp")
	re := regexp.MustCompile(watchingRe)
	groups := groupsFromStateFlags()
	if len(groups) == 0 {
		groups = []string{"active", "processed", "backup", "sleeping"}
		if watchingRe != "" {
			groups = append([]string{"new"}, groups...)
		}
	}

	for _, group := range groups {
		incidents, err := l3t.Overview(group)
		if err != nil {
			log.Fatal(err)
		}

		sortIncidents(incidents)

		for _, incident := range incidents {
			if group == "new" && !re.MatchString(incident.ProductCategory) {
				continue
			}
			err = template.ExpandFromConfig("l3t.incident-oneline", os.Stdout, incident)
			if err != nil {
				log.Fatal("Failed to expand the incident: ", err)
			}

			break
		}
		break
	}
}
0707010000001F000081A4000003E800000064000000015CBEFDCB0000022B000000000000000000000000000000000000002500000000golang-l3t-0.0.1/cmd/open-bug-web.gopackage cmd

import (
	"log"

	"github.com/spf13/cobra"
)

var openBugWebCmd = &cobra.Command{
	Use:     "open-bug-web -b <bug id> | -i <incident id>",
	Aliases: []string{"ob", "bzo"},
	Short:   "Open the bug in the Bugzilla web interface",
	Run:     openBugWebMain,
}

func init() {
	prepareBugCommand(openBugWebCmd)
	prepareCommand(openBugWebCmd)
	RootCmd.AddCommand(openBugWebCmd)
}

func openBugWebMain(cmd *cobra.Command, args []string) {
	bug, err := l3tOrCrash().GetBugSG(optBug)
	if err != nil {
		log.Fatal(err)
	}
	openWeb(getWeb("bug", bug))
}
07070100000020000081A4000003E800000064000000015CBEFDCB00000549000000000000000000000000000000000000002A00000000golang-l3t-0.0.1/cmd/open-incident-web.gopackage cmd

import (
	"bytes"
	"log"
	"os"
	"os/exec"

	"github.com/bhdn/golang-l3t/config"
	"github.com/bhdn/golang-l3t/template"
	"github.com/google/shlex"
	"github.com/spf13/cobra"
)

var openIncidentWebCmd = &cobra.Command{
	Use:     "open-incident-web -b <bug id> | -i <incident id>",
	Aliases: []string{"oi", "l3o"},
	Short:   "Open the incident in the SG web interface",
	Run:     openIncidentWebMain,
}

func init() {
	prepareIncidentCommand(openIncidentWebCmd)
	prepareCommand(openIncidentWebCmd)
	RootCmd.AddCommand(openIncidentWebCmd)
}

func openWeb(url string) {
	raw := config.Config().GetString("open-web.command")
	args, err := shlex.Split(raw)
	if err != nil {
		log.Fatalf("Failed to parse %q", raw)
	}
	args = append(args, url)

	cmd := exec.Command(args[0], args[1:]...)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	cmd.Stdin = os.Stdin
	err = cmd.Run()
	if err != nil {
		log.Fatal(err)
	}
}

func getWeb(conf string, thing interface{}) string {
	var buf bytes.Buffer
	err := template.ExpandFromConfig("open-web."+conf, &buf, thing)
	if err != nil {
		log.Fatalf("failed to expand the web URL: %v", err)
	}
	return buf.String()
}

func openIncidentWebMain(cmd *cobra.Command, args []string) {
	incident, err := l3tOrCrash().GetIncident(optIncident)
	if err != nil {
		log.Fatal(err)
	}
	openWeb(getWeb("incident", incident))
}
07070100000021000081A4000003E800000064000000015CBEFDCB00000222000000000000000000000000000000000000001D00000000golang-l3t-0.0.1/cmd/ping.gopackage cmd

import (
	"fmt"
	"log"

	"github.com/spf13/cobra"
)

var pingCmd = &cobra.Command{
	Use:     "ping -b <bug id> | -i <incident id>",
	Aliases: []string{"ping", "l3p"},
	Short:   "Pings an incident",
	Run:     pingMain,
}

func init() {
	prepareRunningIncidentCommand(pingCmd)
	prepareCommand(pingCmd)
	RootCmd.AddCommand(pingCmd)
}

func pingMain(cmd *cobra.Command, args []string) {
	messages, err := l3tOrCrash().Ping(optIncident)
	if err != nil {
		log.Fatal(err)
	}
	for _, message := range messages {
		fmt.Println(message)
	}
}
07070100000022000081A4000003E800000064000000015CBEFDCB00000272000000000000000000000000000000000000001D00000000golang-l3t-0.0.1/cmd/root.gopackage cmd

import (
	"fmt"
	"os"

	"github.com/spf13/cobra"
)

var RootCmd = &cobra.Command{
	Use:   "l3t <subcommand or alias> <options>",
	Short: "An SG and Bugzilla client",
	Long: `A Solid Ground and Bugzilla client (among other utilities)

Before proceeding, please ensure you have added your SG API key to
~/.l3t.yaml (see Solid Ground -> Preferences -> API)

For Solid Ground and Bugzilla commands, you can use either -i <incident> or
-b <bug> (when there is no ambiguity).
`,
}

func Execute() {

	if err := RootCmd.Execute(); err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}

func init() {
	cobra.OnInitialize()
}
07070100000023000081A4000003E800000064000000015CBEFDCB00001412000000000000000000000000000000000000002900000000golang-l3t-0.0.1/cmd/search-incidents.gopackage cmd

import (
	"bytes"
	"fmt"
	"log"
	"os"
	"os/exec"
	"strings"

	"github.com/bhdn/golang-l3t/client"
	"github.com/bhdn/golang-l3t/config"
	_ "github.com/bhdn/golang-l3t/formatted"
	"github.com/bhdn/golang-l3t/template"
	"github.com/google/shlex"
	"github.com/spf13/cobra"
)

var searchIncidentsCmd = &cobra.Command{
	Use:     "search-incidents <filters>",
	Aliases: []string{"se", "search", "l3se"},
	Short:   "Searches for incidents",
	Run:     searchIncidentsMain,
	Args:    cobra.MinimumNArgs(0),
	Long: `Searches for incidents

By default l3se runs the search with 'running=true', so all currently
running incidents will be displayed. Any new option added to the command
will get merged to this basic search (unless -R is used).

For example, 'l3se -a flastname' will result render the search string
'running=true&cond=&filter=agents&match=flastname'. While
'l3se -a flastname --expr 'filter=product_category&cond=&match=Cloud'
will render running=true&cond=&filter=agents&match=flastname&filter=product_category&cond=&match=Cloud
and is the list of all running incidents handled by flastname with of the
Cloud category.

Searches can be saved in the configuration file and used as arguments for
l3se. Having in the configuration

l3se:
  searches:
    p0: running=true&filter=bug__priority&cond=in&match=P0
    cloud: running=true&filter=product_category&cond=&match=Cloud

allows one to search with 'l3se p0', or augment it with more options:

  l3se p0 --customer frobstore.com

or merge searches:

  l3se p0 cloud
`,
}

var (
	optListNamedSearches bool
	optNotRunning        bool
	optCustomerDomain    string
	optExpr              string
	optSearchLimit       uint
	optAgent             string
	optSupportRequest    int
	optBugID             int
	optRun               string
)

func init() {
	flags := searchIncidentsCmd.Flags()
	flags.BoolVarP(&optListNamedSearches, "list", "l", false, "List the available named searches")
	flags.BoolVarP(&optNotRunning, "not-running", "R", false, "Display non-running incidents")
	flags.UintVarP(&optSearchLimit, "limit", "L", 5, "Number of incidents to be retrieved in each request")
	flags.StringVarP(&optExpr, "expr", "E", "", "Raw URL query search expression")
	flags.StringVarP(&optCustomerDomain, "customer", "c", "", "Filter by customer domain")
	flags.StringVarP(&optAgent, "agent", "a", "", "Filter by agent username")
	flags.IntVarP(&optSupportRequest, "sr", "", 0, "Filter by SR number")
	flags.IntVarP(&optBugID, "bug", "b", 0, "Filter by bug number")
	flags.StringVarP(&optRun, "run", "", "", "Run a command for each search result (with -i <incident> as argv)")
	prepareCommand(searchIncidentsCmd)
	RootCmd.AddCommand(searchIncidentsCmd)
}

func runForIncident(command string, incident *client.Incident) error {
	fromConf := new(bytes.Buffer)
	err := template.ExpandFromConfig("l3se.run-args", fromConf, incident)
	if err != nil {
		return fmt.Errorf("failed to expand the --run command line: %v", err)
		if err != nil {
			return err
		}
	}

	confArgs, err := shlex.Split(fromConf.String())
	if err != nil {
		return fmt.Errorf("invalid --run shell syntax: %v", err)
	}
	commandArgs, err := shlex.Split(command)
	if err != nil {
		return fmt.Errorf("invalid --run argument from configuration: %v", err)
	}
	args := make([]string, 0)
	args = append(args, commandArgs...)
	args = append(args, confArgs...)

	cmd := exec.Command(args[0], args[1:]...)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	err = cmd.Run()
	if err != nil {
		return fmt.Errorf("failed to run the command: %v", err)
	}
	return nil
}

func getNamedSearches() map[string]string {
	return config.Config().GetStringMapString("l3se.searches")
}

// put any named search from args in optExpr
func expandNamedSearches(args []string) error {
	searches := getNamedSearches()

	found := make([]string, 0)
	for _, arg := range args {
		expr, ok := searches[arg]
		if !ok {
			return fmt.Errorf("No such named search %q", arg)
		}
		found = append(found, expr)
	}

	optExpr += strings.Join(found, "&")

	return nil
}

func listNamedSearchs() {
	for k, v := range getNamedSearches() {
		fmt.Printf("%s: %s\n", k, v)
	}
}

func checkNamedSearchOptions(args []string) {
	if optListNamedSearches {
		listNamedSearchs()
		os.Exit(0)
	}

	if err := expandNamedSearches(args); err != nil {
		log.Fatal(err)
	}
}

func searchIncidentsMain(cmd *cobra.Command, args []string) {
	checkNamedSearchOptions(args)
	search := client.Search{
		Running:        !optNotRunning,
		Limit:          optSearchLimit,
		CustomExpr:     optExpr,
		CustomerDomain: optCustomerDomain,
		Agent:          optAgent,
		SRID:           optSupportRequest,
		BugID:          optBugID,
	}
	incidents, errors, err := l3tOrCrash().SearchPaged(search)
	if err != nil {
		log.Fatal(err)
	}
	templ := template.FromConfig("l3t.incident-oneline")

	for incident := range incidents {
		if err := templ.Execute(os.Stdout, incident); err != nil {
			log.Fatal("failed to format: ", err)
		}
		if optRun != "" {
			err = runForIncident(optRun, incident)
			if err != nil {
				log.Fatal(err)
			}
		}
	}
	err, ok := <-errors
	if ok {
		log.Fatal(err)
	}
}
07070100000024000081A4000003E800000064000000015CBEFDCB00000215000000000000000000000000000000000000001E00000000golang-l3t-0.0.1/cmd/sleep.gopackage cmd

import (
	"fmt"
	"log"

	"github.com/spf13/cobra"
)

var sleepCmd = &cobra.Command{
	Use:     "sleep -b <bug id> | -i <incident id>",
	Aliases: []string{"s", "l3s"},
	Short:   "Puts an incident to sleep",
	Run:     sleepMain,
}

func init() {
	prepareRunningIncidentCommand(sleepCmd)
	RootCmd.AddCommand(sleepCmd)
}

func sleepMain(cmd *cobra.Command, args []string) {
	messages, err := l3tOrCrash().Sleep(optIncident)
	if err != nil {
		log.Fatal(err)
	}
	for _, message := range messages {
		fmt.Println(message)
	}
}
07070100000025000081A4000003E800000064000000015CBEFDCB00000655000000000000000000000000000000000000002C00000000golang-l3t-0.0.1/cmd/split-supportconfig.gopackage cmd

import (
	"fmt"
	"os"
	"strings"
	"unicode"

	"github.com/bhdn/go-supportconfig"
	"github.com/bhdn/golang-l3t/log"
	"github.com/spf13/cobra"
)

var (
	optFlat     bool
	optDestTree string
)

func isAscii(s string) bool {
	for _, ch := range s {
		if ch > unicode.MaxASCII {
			return false
		}
	}
	return true
}

func pathHandler(path string) (string, error) {
	if optFlat {
		path = strings.Replace(path, "/", "_", -1)
	}
	if idx := strings.IndexByte(path, ' '); !isAscii(path) || idx > -1 {
		log.Warnf("unusual name: %q", path)
	}
	fmt.Printf("- %s\n", path)
	return path, nil
}

func split(splitter *supportconfig.Splitter, path string) error {
	f, err := os.Open(path)
	if err != nil {
		return err
	}
	defer f.Close()

	fmt.Printf("%s:\n", path)
	return splitter.Split(f)
}

var splitSupportconfigCmd = &cobra.Command{
	Use:     "split-supportconfig [flags] *.txt",
	Aliases: []string{"l3t-split-supportconfig", "ss"},
	Short:   "Splits a supportconfig .txt file into a directory tree",
	Run: func(cmd *cobra.Command, args []string) {
		config := supportconfig.Config{Base: optDestTree, PathHandler: pathHandler}
		splitter := &supportconfig.Splitter{Config: config}

		for _, name := range args {
			if err := split(splitter, name); err != nil {
				log.Fatal(err)
			}
		}
	},
}

const DefaultDest = "./rootdir"

func init() {
	flags := splitSupportconfigCmd.Flags()
	flags.StringVarP(&optDestTree, "dest", "d", DefaultDest, "Select the destination directory")
	flags.BoolVar(&optFlat, "flat", false, "Create files in a single directory (mangles paths)")
	RootCmd.AddCommand(splitSupportconfigCmd)
}
07070100000026000081A4000003E800000064000000015CBEFDCB0000027A000000000000000000000000000000000000002000000000golang-l3t-0.0.1/cmd/version.gopackage cmd

import (
	"fmt"
	"github.com/bhdn/golang-l3t/version"
	"github.com/spf13/cobra"
)

// versionCmd represents the version command
var versionCmd = &cobra.Command{
	Use:   "version",
	Short: "Print the version number of golang-l3t",
	Long:  `All software has versions. This is golang-l3t`,
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Println("Build Date:", version.BuildDate)
		fmt.Println("Git Commit:", version.GitCommit)
		fmt.Println("Version:", version.Version)
		fmt.Println("Go Version:", version.GoVersion)
		fmt.Println("OS / Arch:", version.OsArch)
	},
}

func init() {
	RootCmd.AddCommand(versionCmd)
}
07070100000027000081A4000003E800000064000000015CBEFDCB00000242000000000000000000000000000000000000001C00000000golang-l3t-0.0.1/cmd/zzz.gopackage cmd

import (
	"fmt"
	"log"

	"github.com/spf13/cobra"
)

var sleepShortCmd = &cobra.Command{
	Use:     "zzz -b <bug id> | -i <incident id>",
	Aliases: []string{"short-sleep", "l3z"},
	Short:   "Puts an incident for short sleep",
	Run:     sleepShortMain,
}

func init() {
	prepareRunningIncidentCommand(sleepShortCmd)
	RootCmd.AddCommand(sleepShortCmd)
}

func sleepShortMain(cmd *cobra.Command, args []string) {
	messages, err := l3tOrCrash().SleepShort(optIncident)
	if err != nil {
		log.Fatal(err)
	}
	for _, message := range messages {
		fmt.Println(message)
	}
}
07070100000028000041ED000003E800000064000000025CBEFDCB00000000000000000000000000000000000000000000001800000000golang-l3t-0.0.1/config07070100000029000081A4000003E800000064000000015CBEFDCB00000A1E000000000000000000000000000000000000002200000000golang-l3t-0.0.1/config/config.gopackage config

import (
	"bytes"
	"fmt"
	"io/ioutil"
	"log"
	"os"
	"time"

	"github.com/spf13/viper"
	tilde "gopkg.in/mattes/go-expand-tilde.v1"
)

// Provider defines a set of read-only methods for accessing the application
// configuration params as defined in one of the config files.
type Provider interface {
	ConfigFileUsed() string
	Get(key string) interface{}
	GetBool(key string) bool
	GetDuration(key string) time.Duration
	GetFloat64(key string) float64
	GetInt(key string) int
	GetInt64(key string) int64
	GetSizeInBytes(key string) uint
	GetString(key string) string
	GetStringMap(key string) map[string]interface{}
	GetStringMapString(key string) map[string]string
	GetStringMapStringSlice(key string) map[string][]string
	GetStringSlice(key string) []string
	GetTime(key string) time.Time
	InConfig(key string) bool
	IsSet(key string) bool
}

var defaultConfig *viper.Viper

func Config() *viper.Viper {
	return defaultConfig
}

func GetPath(key string) string {
	value := defaultConfig.GetString(key)
	path, err := tilde.Expand(value)
	if err != nil {
		log.Fatal("Failed to expand a path: ", err)
	}

	return path
}

func LoadConfigProvider(appName string) Provider {
	return readViperConfig(appName)
}

func init() {
	defaultConfig = readViperConfig("L3T")
}

func getDefaultConfigFile() string {
	path, err := tilde.Expand("~/.l3t.yaml")
	if err != nil {
		log.Fatalf("failed to expand %s: %v", path, err)
	}
	return path
}

func createTemplateConfiguration(template string) {
	path := getDefaultConfigFile()
	err := ioutil.WriteFile(path, []byte(template), 0600)
	if err != nil {
		log.Fatalf("failed to write the template configuration filed %s: %s", path, err)
	}

}

func readViperConfig(appName string) *viper.Viper {
	v := viper.New()
	v.SetConfigType("yaml")
	if err := v.ReadConfig(bytes.NewBuffer(Defaults)); err != nil {
		log.Fatal("failed to parse the config defaults: ", err)
	}
	v.SetEnvPrefix(v.GetString("conf.env-prefix"))
	v.SetConfigName(v.GetString("conf.config-name"))
	template := v.GetString("conf.template")
	for _, path := range v.GetStringSlice("conf.config-paths") {
		v.AddConfigPath(path)
	}
	if err := v.MergeInConfig(); err != nil {
		if _, ok := err.(viper.ConfigFileNotFoundError); ok {
			fmt.Fprintf(os.Stderr, "WARNING: No configuration file found: %s\n", getDefaultConfigFile())
			createTemplateConfiguration(template)
			fmt.Fprintf(os.Stderr, "WARNING: Created a template. Please set your credentials there.\n")
			//os.Exit(1)
		} else {
			log.Fatalf("error: Failed to parse the configuration file: %v", err)
		}
	}
	return v
}
0707010000002A000081A4000003E800000064000000015CBEFDCB000022CC000000000000000000000000000000000000002400000000golang-l3t-0.0.1/config/defaults.gopackage config

var Defaults = []byte(`
l3t:
  api-url: https://solid-ground/api/1/
  auth-token: <your-solid-ground-username>:<the-api-token>
  slow: false
  bugs-dir: ~/issues/
  color: true
  concurrency: 1
  incident-oneline: |4
      {{ if (or (eq .ComputedPriority "P0") (eq .ComputedPriority "P1")) -}}
        {{S 1 -}}
      {{ end -}}
      L3:{{ .ID }}/{{ .Bug.ID}} {{ .BugSummary}} ({{ .ComputedPriority }}, {{ .ProductCategory}}, {{ .Bug.State}}, {{ .CustomerDomain}}){{R}}
  incident-template: |4
      ID: {{ .ID }}
      Summary: {{ .BugSummary }}
      Request Type: {{ .RequestType }}
      Bug: {{ .BugID }}
      {{ with .SR -}}
      SR: {{ .ID }}
      {{ end -}}
      Customer name: {{ .CustomerName }}
      Customer domain: {{ .CustomerDomain | C 42 }}
      Agents: {{ join .Agents ", " }}
      State: {{ .State | C 42 }}
      RAG flag: {{ .RAGFlag }}
      Created: {{ .TsNew }}
      Last ping: {{ .LastPing }}
      Processed: {{ .Processed }}
      Needs backup: {{ .NeedBackup }}
      Product category: {{ .ProductCategory }}
      Product: {{ .Bug.Product }}
      {{ if ne .Bug.Priority .RequestedPriority -}}
      Requested priority: {{ .RequestedPriority }}
      Bug priority: {{ .Bug.Priority }}
      {{ else -}}
      Priority: {{ .ComputedPriority }}
      {{ end -}}
      {{ with .AffectedPackages -}}
      Affected packages: {{ . }}
      {{ end -}}
      {{ with .InitialComment -}}
      Initial comment: {{ wrap 70 .  | indent "    " }}
      {{ end -}}
      {{ if .AgentsAbsent -}}
      Agents absent: {{ "true" | C 1 }}
      {{ end -}}
open-web:
  incident: http://solid-ground/incident/{{ .ID }}
  bug: http://bugzilla.opensuse.org/show_bug.cgi?id={{ .ID }}
  command: xdg-open
expand:
  incident: '{{ .RequestType }}:{{ .ID }}: {{ .BugSummary }}'
  bug: '{{ .BugID }}: {{ .ShortDesc }}'
l3ls:
  padding: '-'
  header: |4
      {{ SB }}[{{ .Count }} {{ .Description }}]{{ R -}}
l3n:
  product-category-regexp: ''
l3se:
  run-args: -i {{ .ID }}
  searches:
    p0: filter=bug__priority&cond=in&match=P0
    cloud: filter=product_category&cond=&match=Cloud
    urgent: filter=bug__priority&cond=in&match=P1%2CP0
bugzilla:
  url: https://bugzilla/
  oscrc:
    - ~/.oscrc
    - ~/.config/osc/oscrc
  username: ''
  password: ''
  # one of oldest_to_newest, newest_to_oldest, or newest_to_oldest_desc_first
  # leave empty to use the web defaults
  sort: ''
  attachment-name: '{{ .AttachID }}-{{ .Filename }}'
  template: |4
      [{{ .BugID | C 78}}] {{ .ShortDesc | C 41}}
      Status: {{ .BugStatus}}
      {{ if .Resolution -}}
      Resolution: {{ .Resolution }}
      {{ if eq .Resolution "DUPLICATE" -}}
      Duplicate: {{ .DupID }}
      {{ end -}}
      {{ end -}}
      {{ $agents := .Agents -}}
      {{ range $incident := .Incidents -}}
      {{ set "state" .State -}}
      {{ if (after .SleepUntil now) -}}
      {{ set "state" "ZZz" -}}
      {{ end -}}
      {{ $incident.RequestType | C 2}}:{{ $incident.ID }} {{ $incident.CustomerDomain }} {{ join (index $agents .ID) ", " | spaced }}{{get "state" | C 42}}
      {{ with .SR -}}
      SR: {{ .ID }}
      {{ end -}}
      {{ end -}}
      {{ with .Needinfo -}}
      Needinfo: {{ . }}
      {{ end -}}
      Assigned-To: {{ .AssignedTo.Name }} <{{ .AssignedTo.Email }}>
      Cc-List: {{ join .Cc ", " }}
      {{ with .BugFileLoc -}}
      URL: {{ . }}
      {{ end -}}
      Product: {{ .Product }}
      {{ with .Component -}}
      Component: {{ . }}
      {{ end -}}
      {{ with .StatusWhiteboard -}}
      Whiteboard: {{ . }}
      {{ end -}}
      {{ if or (eq .Priority "P0 - Crit Sit") (eq .Priority "P1 - Urgent") -}}
      {{ S 1 -}}
      {{ end -}}
      Priority: {{ .Priority }}{{ R }}
      {{ if .Attachments -}}
      Attachments:
      {{ range $_, $att := .Attachments -}}
      - {{ $att.AttachID }}: {{ $att.Filename }} ({{ $att.Desc }}) from {{ $att.Attacher.Email }}
      {{ end -}}
      {{ end }}
      {{ range $comment := .Comments -}}
      {{ set "private" "" -}}
      {{ if (eq .IsPrivate 1) -}}
      {{ set "private" " (private)" -}}
      {{ end -}}
      {{ S 42 }}==== Comment {{ $comment.Count }} from {{ $comment.Who.Name | once | spaced}}<{{ $comment.Who.Email }}> {{ $comment.BugWhen.Format "2006-01-02 15:04" }}{{ get "private" | C 35 }}{{ R }}
      {{ $comment.TheText }}
      
      {{ end -}}
      [{{ .BugID }}] assignee: {{ .AssignedTo.Email }} needinfo: {{ .Needinfo }}
  native-domains: '@(forge.provo.)?(opensuse.org|suse.com|suse.de|suse.cz|novell.com|microfocus.com)$'
  marker-first: '# ---- Any lines below this comment will be ignored ----'
  marker-last: '# ---- Any lines above this comment will be ignored ----'
  editor-template: |4
      {{/* to be used by bze */}}
      {{/* another empty line*/}}
      {{ .MarkerFirst }}
      {{ if .IsPrivate -}}
      #
      # The bug will be marked as private
      #
      {{ end -}}
      {{ if .HasAliens -}}
      # 
      # Beware the following external emails in Cc: {{ join .Aliens ", " }}
      # 
      {{ end -}}
      {{ range $line := .BugLines -}}
      # {{ $line }}
      {{ end -}}
      {{ .MarkerLast }}

conf:
  env-prefix: L3T
  config-name: .l3t
  config-type: yaml
  config-paths:
    - $HOME
    - /etc/
  template: |4
      l3t:
        # Generate your API token in Solid Ground -> Preferences -> API
        auth-token: <your-solid-ground-username>:<the-api-token>
        api-url: https://solid-ground.example.com/api/1/
        
        # "Currently working bug" base directory
        # When the CWD is under $work-dir/<bug-id>/, all l3t commands that operate
        # on bugs or incidents can implicitly use <bug-id>, so "l3t ac"  can
        # be used instead of "l3t ac -b <bug-id>"
        work-dir: ~/issues/
      
      bugzilla:
        url: https://apibugzilla.novell.com/
        # leave them empty to use ~/.oscrc or ~/.config/osc/oscrc:
        username: ''
        password: ''
      
      log:
        level: info

      # In order to see all configuration options available, run "l3t config".
ptfsetup:
  cmd: ptfsetup -D {{ .Product }}-{{ .Arch }} {{ .Package }} L3:{{ .Incident }}
  arch-map:
    x86-64: x86_64
  product-map:
    SUSE CaaS Platform 1: caasp1.0
    SUSE CaaS Platform 3: caasp2.0
    SUSE CaaS Platform 3: caasp3.0
    SUSE OpenStack Cloud 6: cloud6.0
    SUSE OpenStack Cloud 7: cloud7.0
    SUSE OpenStack Cloud 8: cloud8
    Helion OpenStack 8: cloud8
    SUSE Enterprise Storage 4: ses4
    SUSE Enterprise Storage 5: ses5
    SUSE Linux Enterprise Server 11 SP1 (SLES 11 SP1): sles11-sp1
    SUSE Linux Enterprise Server 11 SP2 (SLES 11 SP2): sles11-sp2
    SUSE Linux Enterprise Server 11 SP3 (SLES 11 SP3): sles11-sp3
    SUSE Linux Enterprise Server 11 SP4 (SLES 11 SP4): sles11-sp4
    SUSE Linux Enterprise for SAP Applications 11 SP4: sles11-sp4-sap
    SUSE Linux Enterprise Server 12 in Public Clouds: sles12-sp0-pubcloud
    SUSE Linux Enterprise for SAP Applications 12 SP2: sles12-sp1-sap
    SUSE Linux Enterprise Server 12 SP1: sles12-sp1
    SUSE Linux Enterprise Server 12 SP2: sles12-sp2
    SUSE Linux Enterprise Server 12 SP3: sles12-sp3
    SUSE Linux Enterprise Server 12 SP4: sles12-sp4
    SUSE Linux Enterprise Desktop 12 SP3: sled12-sp3
    SUSE Linux Enterprise Desktop 15: sled15-sp0
    SUSE Linux Enterprise for SAP Applications 11 SP3: sles11-sp3-sap
    SUSE Linux Enterprise for SAP Applications 11 SP4: sles11-sp4-sap
    SUSE Linux Enterprise for SAP Applications 12 SP2: sles12-sp2-sap
    SUSE Linux Enterprise for SAP Applications 12 SP3: sles12-sp3-sap
    SUSE Linux Enterprise High Availability Extension 12 SP1: sles12-sp1-hae
    SUSE Linux Enterprise High Availability Extension 12 SP2: sles12-sp2-hae
    SUSE Linux Enterprise High Availability Extension 12 SP3: sles12-sp2-hae
    SUSE Linux Enterprise High Availability Extension 11 SP4: sles11-sp4-hae
    SUSE Linux Enterprise High Availability Extension 15: sles15-sp0-hae
    SUSE Linux Enterprise Server 10 SP3: sles10-sp3
    SUSE Linux Enterprise Server 10 SP4 (SLES 10 SP4): sles10-sp4
    SUSE Linux Enterprise Server 12 (SLES 12): sles12-sp0
    SUSE Linux Enterprise Server 12 SP3 in Public Clouds: sles12-sp0-pubcloud
    SUSE Linux Enterprise Server 15: sles15-sp0
    SUSE Linux Enterprise Server for SAP Applications 15: sles15-sp0-sap
    SUSE Linux Enterprise Server for Teradata 11 SP3-TD: sles11-sp3-td
    # Viper is broken and doesn't handle dots in keys:
    #SUSE Manager 3.1: susemanager-3.1
    #SUSE Manager 3.2: susemanager-3.2
    SUSE Studio Onsite: studio1.3
mail-action:
  regexp: '\[Bug ([^\]]+)\]'
cache:
  enable: false
  dir: ~/.l3t/
paging:
  doc: Note that env has priority over paging-command
  env: PAGER
  fallback-command: less -M -I -R -F
  less-options: MIRF
colormap:
  42: 42
ziu:
  mount: /mounts/ziu
  reference-regexp: ziu.*?(SR[^\s/:<>|]+)
log:
  level: info
`)
0707010000002B000081A4000003E800000064000000015CBEFDCB0000065C000000000000000000000000000000000000001800000000golang-l3t-0.0.1/cwb.gopackage l3t

import (
	"fmt"
	"os"
	"path/filepath"
	"strings"

	"github.com/bhdn/golang-l3t/config"
	tilde "gopkg.in/mattes/go-expand-tilde.v1"
)

func (l3t *L3t) cwbFromDirs(curdir, cwb string) (bugID int, found bool) {
	if !strings.HasSuffix(curdir, "/") {
		curdir += "/"
	}
	if !strings.HasSuffix(cwb, "/") {
		cwb += "/"
	}
	if strings.HasPrefix(curdir, cwb) {
		dirname := curdir[len(cwb):]
		dirname = strings.Replace(dirname, "_", "-", 1)
		parts := strings.SplitN(dirname, "-", 2)
		count, _ := fmt.Sscanf(parts[0], "%d/", &bugID)
		if count == 1 {
			found = true
			return
		}
	}
	return 0, false
}

// GetCWB returns a bug number based on the currently working directory.
// The bug id is taken from a CWD base name, in case  is is located
// immediately under the path from l3t.bugs-dir. It can be suffixed with
// any string after a dash (-) or an underscore (_). None of this
// conditions matching, it returns an error.
func (l3t *L3t) GetCWB() (int, error) {
	rawWorkdir := config.Config().GetString("l3t.bugs-dir")
	if rawWorkdir == "" {
		return 0, BaseError{fmt.Errorf("No l3t.work-dir in configuration")}
	}
	workdir, err := tilde.Expand(rawWorkdir)
	if err != nil {
		return 0, BaseError{fmt.Errorf("Failed to expand tilde: %v", err)}
	}
	workdir, err = filepath.Abs(workdir)
	if err != nil {
		return 0, BaseError{fmt.Errorf("abspath: %v", err)}
	}
	cwd, err := os.Getwd()
	if err != nil {
		return 0, BaseError{fmt.Errorf("Couldn't get cwd: %v", err)}
	}
	bugID, found := l3t.cwbFromDirs(cwd, workdir)
	if !found {
		return 0, BaseError{fmt.Errorf("Couldn't get cwd: %v", err)}
	}

	return bugID, nil
}
0707010000002C000081A4000003E800000064000000015CBEFDCB000000AB000000000000000000000000000000000000001D00000000golang-l3t-0.0.1/cwb_test.gopackage l3t

// To be used by facade_test
func (l3t *L3t) CwbFromDirs(curdir, cwb string) (bugID int, found bool) {
	bugID, found = l3t.cwbFromDirs(curdir, cwb)
	return
}
0707010000002D000081A4000003E800000064000000015CBEFDCB00002AB7000000000000000000000000000000000000001B00000000golang-l3t-0.0.1/facade.go// Package l3t abstracts access to the main l3t command line facilities,
// already handling configuration files.
//
// It leaks structs from different submodules.
package l3t

import (
	"bufio"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"regexp"
	"strings"

	"github.com/bhdn/go-suseapi/bugzilla"
	"github.com/bhdn/golang-l3t/client"
	"github.com/bhdn/golang-l3t/config"
	"github.com/bhdn/golang-l3t/osc"
)

// L3t keeps the state of the l3t client
type L3t struct {
	client   *client.Client
	bugzilla *bugzilla.Client
}

// BaseError should be the base for all handled errors returned by the API
type BaseError struct{ error }

func (e BaseError) Error() string {
	return fmt.Sprintf("Error: %v", e.error)
}

func getSGClient() *client.Client {
	baseURL := config.Config().GetString("l3t.api-url")
	authToken := config.Config().GetString("l3t.auth-token")
	slow := config.Config().GetBool("l3t.slow")
	concurrency := config.Config().GetInt("l3t.concurrency")
	conf := &client.Config{BaseURL: baseURL, AuthToken: authToken, Slow: slow, Concurrency: concurrency}
	if config.Config().GetBool("cache.enable") {
		conf.Cacher = &fsCacher{Name: "incidents"}
	}
	return client.New(conf)
}

func getBZClient() (*bugzilla.Client, error) {
	username, password, err := osc.GetBZCredentials()
	if err != nil {
		return nil, BaseError{fmt.Errorf("failed to parse credentials from osc: %v. Perhaps run 'osc -A https://api.opensuse.org/ ls' and try again?", err)}
	}
	baseURL := config.Config().GetString("bugzilla.url")
	conf := bugzilla.Config{BaseURL: baseURL, User: username, Password: password}
	if config.Config().GetBool("cache.enable") {
		conf.Cacher = &fsCacher{Name: "bugs"}
	}
	client, err := bugzilla.New(conf)
	if err != nil {
		return nil, BaseError{fmt.Errorf("failed to create the bugzilla client: %v", err)}
	}

	return client, nil
}

// New creates a new Client based on the configuration parsed from
// the config package (likely provided by Viper).
func New() (*L3t, error) {
	sg := getSGClient()
	bz, err := getBZClient()
	if err != nil {
		return nil, err
	}
	return &L3t{client: sg, bugzilla: bz}, nil
}

// GetBugSG gets a Bug from SG API (not to be confused with the Bugzilla
// one.)
func (l3t *L3t) GetBugSG(bugID int) (*client.Bug, error) {
	bug, err := l3t.client.GetBug(bugID)
	return bug, err
}

// GetSR gets a *SR from the SG API
func (l3t *L3t) GetSR(srID int) (*client.SR, error) {
	sr, err := l3t.client.GetSR(srID)
	return sr, err
}

// Overview returns the incidents as done by the main index page of
// SolidGround. Group can be one of "new", "active", "processed",
// "sleeping", "backup", or any other supported by the SG API.
//
// The order of the incidents is affected by the `l3t.concurrency`
// configuration option.
func (l3t *L3t) Overview(group string) (incidents []*client.Incident, err error) {
	incidents, err = l3t.client.Overview(group)
	return
}

// GetBug gets a *bugzilla.Bug from the Bugzilla API (apibuzilla)
func (l3t *L3t) GetBug(id int) (bug *bugzilla.Bug, err error) {
	bug, err = l3t.bugzilla.GetBug(id)
	return
}

// GetIncident returns a *Incident from the SG API. It will fetch all
// attributes (such as "sr", "bug" and the sintectic "LastStatus") if the
// configuration option l3t.slow is set to true.
//
// Incident attributes can be fetched in parallel and this behavior is
// controlled by the l3t.concurrency configuration entry, which is an
// integer setting the number of worker threads.
func (l3t *L3t) GetIncident(id int) (incident *client.Incident, err error) {
	incident, err = l3t.client.GetIncident(id)
	return
}

// Accept accepts an incident in SG, returning the confirmation messages
func (l3t *L3t) Accept(incidentID int) (messages []string, err error) {
	messages, err = l3t.client.Accept(incidentID)
	return
}

// Assign assigns an incident in SG, returning the confirmation messages
func (l3t *L3t) Assign(incidentID int) (messages []string, err error) {
	messages, err = l3t.client.Assign(incidentID)
	return
}

// Ping sends a ping to an incident (such as in ack)
func (l3t *L3t) Ping(incidentID int) (messages []string, err error) {
	messages, err = l3t.client.Ping(incidentID)
	return
}

// Sleep puts an incident to sleep in SG, returning the confirmation messages
func (l3t *L3t) Sleep(incidentID int) (messages []string, err error) {
	messages, err = l3t.client.Sleep(incidentID)
	return
}

// SleepShort puts an incident to short sleep in SG, returning the confirmation messages
func (l3t *L3t) SleepShort(incidentID int) (messages []string, err error) {
	messages, err = l3t.client.SleepShort(incidentID)
	return
}

// SearchIncidentsByBug returns a list of SG Incidents that are bound to a
// specific bug
func (l3t *L3t) SearchIncidentsByBug(bugID int) (incidents []*client.Incident, err error) {
	if l3t.client.Unavailable {
		incidents = []*client.Incident{}
	} else {
		incidents, err = l3t.client.SearchByBug(bugID, false)
	}
	return
}

// SearchPaged performs a paged search, using the terms from @search and
// returning a channel with the incidents and another with the errors.
func (l3t *L3t) SearchPaged(search client.Search) (incidents <-chan *client.Incident, errors <-chan error, err error) {
	incidents, errors, err = l3t.client.SearchPaged(search)
	return
}

// NoIncidentsForBug means no incidents are bound to a given bug
type NoIncidentsForBug struct {
	BaseError
	BugID int
}

func (e NoIncidentsForBug) Error() string {
	return fmt.Sprintf("No incidents found for bug %d", e.BugID)
}

type NoRunningIncidentsForBug NoIncidentsForBug

func (e NoRunningIncidentsForBug) Error() string {
	return fmt.Sprintf("No running incidents found for bug %d", e.BugID)
}

// NoBugOrIncident means no bug or incident has been found
type NoBugOrIncident struct {
	BaseError
}

func (e NoBugOrIncident) Error() string {
	return "no information about bug or incident"
}

// MultipleIncidentsFound means multiple incidents are bound to a given
// bug, in a context that requires only one bug
type MultipleIncidentsFound struct {
	BaseError
	Incidents []*client.Incident
}

func (e MultipleIncidentsFound) Error() string {
	pairs := make([]string, len(e.Incidents))
	for i, incident := range e.Incidents {
		pairs[i] = fmt.Sprintf("%d(%s)", incident.ID, incident.CustomerDomain)
	}
	joined := strings.Join(pairs, ", ")
	return fmt.Sprint("Multiple incidents found: ", joined)
}

func (l3t *L3t) checkReturnedIncidents(incidents []*client.Incident, bugID int, err error, running bool) (int, error) {
	if err != nil {
		return 0, err
	}
	if len(incidents) == 0 {
		if running {
			return 0, NoRunningIncidentsForBug{BugID: bugID}
		} else {
			return 0, NoIncidentsForBug{BugID: bugID}
		}
	}
	if len(incidents) > 1 {
		return 0, MultipleIncidentsFound{Incidents: incidents}
	}

	return incidents[0].ID, nil
}

// singleRunningIncidentByBug looks for an incident (only accepted and
// running ones) based on the bug it's bound to.j
func (l3t *L3t) singleRunningIncidentByBug(bugID int) (incidentID int, err error) {
	incidents, err := l3t.client.SearchByBug(bugID, true)
	return l3t.checkReturnedIncidents(incidents, bugID, err, true)
}

// anyIncidentByBug looks for an incident (running or not) based on the
// bug it's bound to.
func (l3t *L3t) anyIncidentByBug(bugID int) (incidentID int, err error) {
	incidents, err := l3t.client.SearchByBug(bugID, false)
	return l3t.checkReturnedIncidents(incidents, bugID, err, false)
}

// IncidentRunningFromSystem a single incident ID bound to a given incident
// ID or bug ID. In case none are provided, it tries to get the bug ID from
// GetCWB (ie. the bug related to the currently working directly.) and
// infers the incident number from that.
// It only returns running incidents, which doesn't include those in the
// "new" state.
func (l3t *L3t) IncidentRunningFromSystem(incidentID, bugID int) (found int, err error) {
	if incidentID != 0 {
		found = incidentID
		return
	}
	if bugID != 0 {
		found, err = l3t.singleRunningIncidentByBug(bugID)
		return
	}
	bugID, err = l3t.GetCWB()
	if err != nil {
		return 0, NoBugOrIncident{}
	}
	found, err = l3t.singleRunningIncidentByBug(bugID)
	return
}

// IncidentFromSystem returns an incident ID bound to a given incident ID
// or bug ID. In case none are provided, it tries to get the bug ID from
// GetCWB (ie. the bug related to the currently working directly) and
// infers the incident number from that.
func (l3t *L3t) IncidentFromSystem(incidentID, bugID int) (found int, err error) {
	if incidentID != 0 {
		found = incidentID
		return
	}
	if bugID != 0 {
		found, err = l3t.anyIncidentByBug(bugID)
		return
	}
	bugID, err = l3t.GetCWB()
	if err != nil {
		return 0, NoBugOrIncident{}
	}
	found, err = l3t.anyIncidentByBug(bugID)
	return
}

// BugFromSystem returns the bug ID bound to an incident or bug ID. In case
// none are provided, GetCWB is used to get the bug ID from the
// environment.
func (l3t *L3t) BugFromSystem(incidentID, bugID int) (found int, err error) {
	if bugID != 0 {
		found = bugID
		return
	}
	if incidentID != 0 {
		incident, err := l3t.client.GetIncidentLean(incidentID)
		if err != nil {
			return 0, err
		}
		return incident.BugID, nil
	}
	found, err = l3t.GetCWB()
	if err != nil {
		return 0, NoBugOrIncident{}
	}
	return
}

// UpdateBug changes a given bug according to the attributes in changes
func (l3t *L3t) UpdateBug(bugID int, changes bugzilla.Changes) (err error) {
	return l3t.bugzilla.Update(bugID, changes)
}

// DownloadAttachment starts the download of a given attachment ID,
// returning the metadata from the attachment (which is only filled with
// file name, size and ID)
func (l3t *L3t) DownloadAttachment(id int) (att *bugzilla.Attachment, body io.ReadCloser, err error) {
	att, body, err = l3t.bugzilla.DownloadAttachment(id)
	return
}

func cachePath(objType, id string) (path string, err error) {
	enabled := config.Config().GetBool("cache.enable")
	if !enabled {
		return "", BaseError{fmt.Errorf("caching is disabled; it can be eanble by setting cache.enable: true")}
	}
	re := regexp.MustCompile("^[0-9]{3,}$")
	if !re.MatchString(id) {
		return "", BaseError{fmt.Errorf("invalid cache id: %q", id)}
	}
	prefix := id[:2]
	cache := config.GetPath("cache.dir")
	path = filepath.Join(cache, objType, prefix, id)
	return
}

func (l3t *L3t) getBugFromCache(objType, id string) (bug *bugzilla.Bug, err error) {
	path, err := cachePath(objType, id)
	if err != nil {
		return nil, err
	}

	f, err := os.Open(path)
	if err != nil {
		return nil, BaseError{fmt.Errorf("failed to open the cached bug: %v", err)}
	}

	bug, err = l3t.bugzilla.GetBugFromJSON(bufio.NewReader(f))
	if err != nil {
		return nil, BaseError{fmt.Errorf("failed to decode the cached bug: %v", err)}
	}

	return
}

// GetBugFromCache returns a bug as saved in the local json cache
func (l3t *L3t) GetBugFromCache(id int) (bug *bugzilla.Bug, err error) {
	bug, err = l3t.getBugFromCache("bugs", fmt.Sprintf("%d", id))
	return
}
0707010000002E000081A4000003E800000064000000015CBEFDCB000039A1000000000000000000000000000000000000002000000000golang-l3t-0.0.1/facade_test.gopackage l3t_test

import (
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"net/http/httptest"
	"os"
	"path"
	"path/filepath"
	"testing"
	"time"

	l3t "github.com/bhdn/golang-l3t"
	"github.com/bhdn/golang-l3t/config"
	. "gopkg.in/check.v1"
)

type clientSuite struct {
	l3t            *l3t.L3t
	err            error
	serverTeardown func()
	currentReply   int
	replies        map[string]string
}

var _ = Suite(&clientSuite{})

// Hook up check.v1 into the "go test" runner
func Test(t *testing.T) { TestingT(t) }

func (cs *clientSuite) SetUpTest(c *C) {
	ts0 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		found, ok := cs.replies[r.URL.Path]
		if !ok {
			log.Println("got ", r.URL.Path)
			http.Error(w, "Something wrong", 500)
			return
		}
		io.WriteString(w, found)
	}))
	cs.serverTeardown = func() { ts0.Close() }
	config.Config().Set("l3t.api-url", ts0.URL)
	config.Config().Set("l3t.auth-token", "someuser:sometoken")
	l3t, err := l3t.New()
	cs.l3t = l3t
	cs.err = err
	cs.currentReply = 0
	cs.replies = make(map[string]string)

}
func (cs *clientSuite) TearDownTest(c *C) {
	cs.serverTeardown()
}

func (cs *clientSuite) TestNew(c *C) {
	// setup checking
	c.Assert(cs.err, IsNil)
	c.Assert(cs.l3t, NotNil)
}

func (cs *clientSuite) TestIncidentFromSystemWithIncident(c *C) {
	incident, err := cs.l3t.IncidentFromSystem(1234, 0)
	c.Assert(err, IsNil)
	c.Assert(incident, Equals, 1234)
}

const sampleIncident = `{
    "ts_work" : "2018-08-16T13:30:15",
    "projects" : [
       "/api/1/project/15976/",
       "/api/1/project/15977/",
       "/api/1/project/16531/",
       "/api/1/project/16532/",
       "/api/1/project/16620/"
    ],
    "last_bz_priority" : "P2",
    "time_analysis" : 8,
    "initial_comment" : "",
    "assignment" : [
       "/api/1/assignment/26322/"
    ],
    "statuses" : [
       "/api/1/status/70018/",
       "/api/1/status/70019/"
    ],
    "last_bz_responsible" : "user@email.com",
    "time_new" : 250287,
    "customer_domain" : "customer.com",
    "service_severity" : "high",
    "bug" : "/api/1/bug/1104677/",
    "resource_uri" : "/api/1/incident/1234/",
    "score_formula" : "(state_boost = 1) * (age_boost=1) * ((5 - (bug_prio=2)) * 5 + (flag=0) * 2)+ (absence_boost=0)",
    "customer_name" : "Customer SE",
    "affected_packages" : "",
    "product_category" : "SLES",
    "last_bz_comments" : 35,
    "bug_id" : 1104677,
    "need_backup" : false,
    "ts_change" : "2018-08-16T13:30:15",
    "time_work" : 0,
    "sticky_note" : "",
    "score" : 15,
    "requested_priority" : "P2",
    "last_bz_state" : "NEEDINFO",
    "final_state" : null,
    "ts_finished" : null,
    "flags" : "",
    "processed" : false,
    "ts_backup" : null,
    "rag_flag" : "green",
    "last_bz_attachments" : 0,
    "request_type" : "L3-Support",
    "planned_update" : false,
    "last_ping" : "2019-03-22T12:12:10",
    "proposed_agent_declined" : "",
    "support" : "/api/1/sr/101211396801/",
    "ptf_packages" : "",
    "state" : "work",
    "raederwerk" : [],
    "todo" : false,
    "ts_canceled" : null,
    "ts_new" : "2018-08-13T15:58:40",
    "bug_summary" : "frobnicator crashes",
    "sleep_until" : "2019-03-29T00:00:00",
    "id" : 1234,
    "final_comment" : "",
    "agents_absent" : false,
    "cooperation" : false,
    "ts_analysis" : "2018-08-16T13:30:07"
 }`

const sampleBug = `{
   "involved" : "foobar@email.com,barfoo@email.com",
   "keywords" : "SAUERKRAUT_REQUIRED",
   "resolution" : null,
   "creation_ts" : "2019-01-15T20:12:00",
   "state" : "IN_PROGRESS",
   "product" : "Frob 7",
   "ttl" : "2019-03-16T15:20:44",
   "comments" : 22,
   "has_patch" : true,
   "resource_uri" : "/api/1/bug/1122053/",
   "novellonly" : true,
   "severity" : "Major",
   "version" : "Maintenance Update",
   "component" : "Database",
   "platform" : "x86-64",
   "attachments" : 1,
   "info_provider" : "",
   "commentee" : "comentator@email.com",
   "subscribed" : "foobar@email.com,barfoo@email.com,stalker@email.com",
   "whiteboard" : "openL3:53305",
   "id" : 1122053,
   "summary" : "frobnicator crashes",
   "has_ptf_url" : true,
   "responsible" : "irre@email.com",
   "sr" : [
      "/api/1/sr/101211396801/"
   ],
   "last_change" : "2019-03-14T14:37:58",
   "assignee" : "irre@email.com",
   "last_comment" : "2019-03-14T14:37:58",
   "updated" : "2019-03-14T15:20:44",
   "priority" : "P1"}`

var sampleSearchByBug = `
{
   "objects" : [` + sampleIncident + `
   ],
   "meta" : {
      "limit" : 20,
      "previous" : null,
      "total_count" : 1,
      "offset" : 0,
      "next" : null
   }
}`

const sampleUser = `{
   "resource_uri" : "/api/1/user/217/",
   "first_name" : "Firstname",
   "groups" : [
      "/api/1/group/1/"
   ],
   "email" : "foo@email.com",
   "last_name" : "Surname",
   "username" : "fsurname"
}`

const sampleGroup = `{"name": "Admin", "resource_uri": "/api/1/group/1/"}`

const sampleAssignment = `{
   "primary" : true,
   "agent" : "/api/1/user/217/",
   "resource_uri" : "/api/1/assignment/26322/",
   "id" : 26322
}`

const sampleStatus = `{
   "status" : "Some status here",
   "change" : "No change",
   "resource_uri" : "/api/1/status/70018/",
   "id" : 70018,
   "next" : "Resume the investigation.",
   "timestamp" : "2019-03-22T12:12:14"
}`

func makeSampleResults(extra map[string]string) map[string]string {
	base := map[string]string{
		"/bug/1104677/":      sampleBug,
		"/sr/101211396801/":  sampleSR,
		"/assignment/26322/": sampleAssignment,
		"/user/217/":         sampleUser,
		"/group/1/":          sampleGroup,
		"/status/70019/":     sampleStatus,
	}
	for k, v := range extra {
		base[k] = v
	}
	return base
}

func (cs *clientSuite) TestIncidentFromSystemWithBug(c *C) {
	cs.replies = makeSampleResults(map[string]string{
		"/incident/": sampleSearchByBug,
	})
	incident, err := cs.l3t.IncidentFromSystem(0, 101234)
	c.Assert(err, IsNil)
	c.Assert(incident, Equals, 1234)
}

func (cs *clientSuite) enterCWB(c *C, bugId int) func() {
	oldwd, err := os.Getwd()
	c.Assert(err, IsNil)

	workdir := c.MkDir()

	cwbdir := path.Join(workdir, fmt.Sprintf("%d", bugId))
	err = os.Mkdir(cwbdir, 0700)
	c.Assert(err, IsNil)

	os.Chdir(cwbdir)

	config.Config().Set("l3t.bugs-dir", workdir)

	return func() {
		os.Chdir(oldwd)
		os.Remove(cwbdir)
		config.Config().Set("l3t.work-dir", "")
	}
}

const noInformationAboutBug = "no information about bug or incident"

func (cs *clientSuite) TestIncidentFromSystemWithoutCWB(c *C) {
	cs.replies = makeSampleResults(map[string]string{
		"/incident/": sampleSearchByBug,
	})
	incident, err := cs.l3t.IncidentFromSystem(0, 0)
	c.Assert(err, ErrorMatches, noInformationAboutBug)
	c.Assert(incident, Equals, 0)
}

func (cs *clientSuite) TestCWBMatching(c *C) {
	bug, found := cs.l3t.CwbFromDirs("/foo/bar/12345", "/foo/bar")
	c.Assert(found, Equals, true)
	c.Assert(bug, Equals, 12345)

	bug, found = cs.l3t.CwbFromDirs("/foo/bar/12345_foobar", "/foo/bar")
	c.Assert(found, Equals, true)
	c.Assert(bug, Equals, 12345)

	bug, found = cs.l3t.CwbFromDirs("/foo/bar/12345_foobar/subdir/bla", "/foo/bar")
	c.Assert(found, Equals, true)
	c.Assert(bug, Equals, 12345)

	bug, found = cs.l3t.CwbFromDirs("/foo/bar/12345-fooabr", "/foo/bar/")
	c.Assert(found, Equals, true)
	c.Assert(bug, Equals, 12345)

	bug, found = cs.l3t.CwbFromDirs("/foo/nope/12345", "/foo/bar/")
	c.Assert(found, Equals, false)
	c.Assert(bug, Equals, 0)

	bug, found = cs.l3t.CwbFromDirs("/12345", "/foo/bar/")
	c.Assert(found, Equals, false)
	c.Assert(bug, Equals, 0)

	// When the configuration option is unset
	bug, found = cs.l3t.CwbFromDirs("/foo/bar/12345_foobar/subdir/bla", "")
	c.Assert(found, Equals, false)
	c.Assert(bug, Equals, 0)

}

func (cs *clientSuite) TestIncidentFromSystemWithCWB(c *C) {
	cs.replies = makeSampleResults(map[string]string{
		"/incident/": sampleSearchByBug,
	})
	closer := cs.enterCWB(c, 1104677)
	defer closer()
	incident, err := cs.l3t.IncidentFromSystem(0, 0)
	c.Assert(err, IsNil)
	c.Assert(incident, Equals, 1234)
}

func (cs *clientSuite) TestBugFromSystemWithBug(c *C) {
	incident, err := cs.l3t.BugFromSystem(0, 101234)
	c.Assert(err, IsNil)
	c.Assert(incident, Equals, 101234)
}

const sampleSR = `{
   "hours" : "24x7",
   "ttl" : "2019-03-19T21:44:17",
   "status" : "Awaiting Engineering",
   "is_closed" : false,
   "id" : "101211396801",
   "created" : "2019-01-15T20:14:57",
   "bdesc" : "frobnicator crashes",
   "resource_uri" : "/api/1/sr/101211396801/",
   "lastupdate" : "2019-03-08T16:22:54",
   "geo" : "ZZZZ",
   "errored" : false,
   "cus_email" : "glorg@zlurg.com",
   "cus_title" : "",
   "cus_firstname" : "Jokk",
   "cus_lastname" : "Mokk",
   "cus_account" : "FOOBARZ AG",
   "ddesc" : "Frobnicator crashed today. Help me.",
   "valid" : true,
   "contract" : "Foobar free text string"}`

func (cs *clientSuite) TestBugFromSystemWithIncident(c *C) {
	cs.replies = makeSampleResults(map[string]string{
		"/incident/1234/": sampleIncident,
	})
	bug, err := cs.l3t.BugFromSystem(1234, 0)
	c.Assert(err, IsNil)
	c.Assert(bug, Equals, 1104677)
}

func (cs *clientSuite) TestBugFromSystemWithoutCWB(c *C) {
	bug, err := cs.l3t.BugFromSystem(0, 0)
	c.Assert(err, ErrorMatches, noInformationAboutBug)
	c.Assert(bug, Equals, 0)
}

func (cs *clientSuite) TestBugFromSystemWithCWB(c *C) {
	closer := cs.enterCWB(c, 1104677)
	defer closer()
	bug, err := cs.l3t.BugFromSystem(0, 0)
	c.Assert(err, IsNil)
	c.Assert(bug, Equals, 1104677)
}

func getCacheDir(c *C) (path string, cleanup func()) {
	path = c.MkDir()
	origCache := config.Config().GetBool("cache.enable")
	origDir := config.Config().GetString("cache.dir")
	config.Config().Set("cache.enable", true)
	config.Config().Set("cache.dir", path)
	cleanup = func() {
		config.Config().Set("cache.enable", origCache)
		config.Config().Set("cache.dir", origDir)
	}
	return
}

func (cs *clientSuite) TestCacheIncident(c *C) {
	cs.replies = makeSampleResults(map[string]string{
		"/incident/1234/": sampleIncident,
	})
	cacheBase, cleanup := getCacheDir(c)
	defer cleanup()
	l3t, err := l3t.New()
	c.Assert(err, IsNil)
	_, err = l3t.GetIncident(1234)
	c.Assert(err, IsNil)
	cachedFile := path.Join(cacheBase, "incidents", "12", "1234")
	b, err := ioutil.ReadFile(cachedFile)
	c.Assert(err, IsNil)
	c.Assert(string(b), Matches, `.*"id":1234,.*`)
}

var sampleJSON = `

{
   "version" : "GMC",
   "short_desc" : "Bug short_desc here",
   "cf_nts_priority" : [
      "",
      ""
   ],
   "bug_severity" : "Major",
   "status_whiteboard" : "openTreta:1234",
   "cclist_accessible" : 1,
   "remaining_time" : "0.00",
   "Comments" : [
      {
         "thetext" : "Some comment",
         "comment_count" : 0,
         "who" : {
            "email" : "username@foobar.com",
            "name" : "Firstname Lastname"
         },
         "commentid" : 8082652,
         "BugWhen" : "2019-03-20T19:48:42Z",
         "isprivate" : 0
      },
      {
         "comment_count" : 1,
         "thetext" : "Second comment",
         "who" : {
            "email" : "user@foobar.com",
            "name" : "Firstname Lastname"
         },
         "isprivate" : 1,
         "BugWhen" : "2019-03-20T20:04:54Z",
         "commentid" : 8082662
      },
      {
         "comment_count" : 2,
         "thetext" : "Third comment.",
         "who" : {
            "name" : "Firstname Lastname",
            "email" : "user@foobar.com"
         },
         "isprivate" : 0,
         "BugWhen" : "2019-03-20T20:05:48Z",
         "commentid" : 8082664
      }
   ],
   "actual_time" : "0.00",
   "creation_ts" : "2019-03-20T19:48:00Z",
   "estimated_time" : "0.00",
   "cc" : [
      "lfirstname@foobar.com",
      "user@foobar.com"
   ],
   "component" : "Core",
   "op_sys" : "FROB 12",
   "rep_platform" : "x86-64",
   "cf_it_deployment" : [
      "---"
   ],
   "delta_ts" : "2019-04-13T13:08:58Z",
   "bug_id" : 1129974,
   "assigned_to" : {
      "email" : "user@foobar.com",
      "name" : "Firstname Lastname"
   },
   "resolution" : "",
   "classification_id" : 27,
   "everconfirmed" : 1,
   "reporter_accessible" : 1,
   "target_milestone" : "---",
   "comment_sort_order" : "oldest_to_newest",
   "cf_blocker" : [
      "---"
   ],
   "cf_foundby" : [
      "---",
      "---"
   ],
   "bug_status" : "IN_PROGRESS",
   "group" : [
      {
         "id" : 10,
         "email" : "foobaronly"
      },
      {
         "id" : 17,
         "email" : "Foo Bar Enterprise"
      }
   ],
   "reporter" : {
      "email" : "user@foobar.com",
      "name" : "Firstname Lastname"
   },
   "classification" : "Frobnicator Plus",
   "cf_biz_priority" : [
      "",
      ""
   ],
   "dup_id" : 0,
   "priority" : "P2 - High",
   "product" : "Frobnicator Plus",
   "token" : [
      "1234555-i2XdlR-90bqTkLrFRr8nC6YbAG1xDNlSktCEp9r3Ux4"
   ],
   "votes" : 0,
   "Attachments" : [
      {
         "filename" : "dump.tbz",
         "attacher" : {
            "name" : "Firstname Lastname",
            "email" : "user@foobar.com"
         },
         "isobsolete" : 0,
         "delta_ts" : "2019-03-20T20:07:16Z",
         "isprivate" : 0,
         "desc" : "the dump",
         "ispatch" : 0,
         "Date" : "2019-03-20T20:07:00Z",
         "type" : "application/x-bzip",
         "token" : "123455555-Y0xsvTLjEFWr3pKrKjnPCsjNupLWQ1SGK7utZkCFqeU",
         "size" : 6014076,
         "attachid" : 800750
      }
   ],
   "qa_contact" : {
      "name" : "E-mail List",
      "email" : "qa-contact@foobar.com"
   },
   "keywords" : "DSLA_REQUIRED",
   "bug_file_loc" : "",
   "flag" : [
      {
         "id" : 201207,
         "status" : "?",
         "type_id" : 4,
         "name" : "needinfo",
         "requestee" : "user@foobar.com",
         "setter" : "supporter@foobar.com"
      }
   ]
}
`

func (cs *clientSuite) TestGetBugFromCache(c *C) {
	cache, cleanup := getCacheDir(c)
	defer cleanup()

	jsonPath := filepath.Join(cache, "bugs/11/1129974")
	err := os.MkdirAll(filepath.Dir(jsonPath), os.ModePerm)
	c.Assert(err, IsNil)
	err = ioutil.WriteFile(jsonPath, []byte(sampleJSON), 0600)
	c.Assert(err, IsNil)

	bug, err := cs.l3t.GetBugFromCache(1129974)
	c.Assert(err, IsNil)
	c.Assert(bug, NotNil)
	c.Check(bug.BugID, Equals, 1129974)
	c.Check(bug.ShortDesc, Equals, "Bug short_desc here")
	c.Check(bug.AssignedTo.Name, Equals, "Firstname Lastname")
	c.Check(bug.BugSeverity, Equals, "Major")
	c.Check(bug.StatusWhiteboard, Equals, "openTreta:1234")
	c.Check(len(bug.Comments), Equals, 3)
	c.Check(bug.Comments[0].TheText, Equals, "Some comment")
	c.Check(bug.Comments[0].BugWhen, Equals, time.Date(2019, 03, 20, 19, 48, 42, 0, time.UTC))
	c.Check(bug.CreationTS, Equals, time.Date(2019, 03, 20, 19, 48, 0, 0, time.UTC))
	c.Check(bug.Cc, DeepEquals, []string{"lfirstname@foobar.com", "user@foobar.com"})
}
0707010000002F000041ED000003E800000064000000025CBEFDCB00000000000000000000000000000000000000000000001B00000000golang-l3t-0.0.1/formatted07070100000030000081A4000003E800000064000000015CBEFDCB00000DC2000000000000000000000000000000000000002200000000golang-l3t-0.0.1/formatted/bug.gopackage formatted

import (
	"bytes"
	"io"
	"sort"
	"strings"

	"github.com/bhdn/go-suseapi/bugzilla"
	"github.com/bhdn/golang-l3t/client"
	"github.com/bhdn/golang-l3t/config"
	"github.com/bhdn/golang-l3t/template"
)

type sadBugInfo struct {
	bugzilla.Bug
	Needinfo  string
	Incidents []*client.Incident
	Agents    map[int][]string
	Comments  []*bugzilla.Comment
}

func getNeedinfos(bug *bugzilla.Bug) string {
	emails := make([]string, 0)
	for _, flag := range bug.Flags {
		if flag.Name == "needinfo" {
			emails = append(emails, flag.Requestee)
		}
	}
	joined := strings.Join(emails, ", ")
	return joined
}

func getAgents(incidents []*client.Incident) map[int][]string {
	result := make(map[int][]string)
	for _, incident := range incidents {
		agents := make([]string, 0)
		for _, assignment := range incident.Assignments {
			agents = append(agents, assignment.Agent.Username)
		}
		result[incident.ID] = agents
	}
	return result
}

func getComments(bug *bugzilla.Bug, settings BugFormat) []*bugzilla.Comment {
	comments := make([]*bugzilla.Comment, len(bug.Comments))
	copy(comments, bug.Comments)

	if len(comments) < 2 {
		return comments
	}
	sorter := func(i, j int) bool {
		return comments[i].BugWhen.Before(comments[j].BugWhen)
	}

	if settings.LastComments != 0 && settings.LastComments < len(comments) {
		sort.Slice(comments, sorter)
		comments = comments[len(comments)-settings.LastComments : len(comments)]
	}

	if settings.SortBy == "oldest_to_newest" {
	} else if settings.SortBy == "newest_to_oldest" || settings.SortBy == "newest_to_oldest_desc_first" {
		sorter = func(i, j int) bool {
			return comments[j].BugWhen.Before(comments[i].BugWhen)
		}
	} else {
		// Don't sort if any valid value has been used
		return comments
	}

	sort.Slice(comments, sorter)

	if settings.SortBy == "newest_to_oldest_desc_first" {
		last := len(comments) - 1
		comments = append(comments[last:last+1], comments[0:last]...)
	}

	return comments
}

// BugFormat describes how to format the bug
type BugFormat struct {
	SortBy       string
	LastComments int
}

func FormatBug(bug *bugzilla.Bug, incidents []*client.Incident, settings BugFormat, dest io.Writer) error {
	sad := sadBugInfo{}
	sad.Bug = *bug
	sad.Needinfo = getNeedinfos(bug)
	sad.Incidents = incidents
	sad.Agents = getAgents(incidents)
	sad.Comments = getComments(bug, settings)

	err := template.ExpandFromConfig("bugzilla.template", dest, sad)
	return err
}

type bugForEditorInfo struct {
	Bug         *bugzilla.Bug
	Incidents   []*client.Incident
	HasAliens   bool
	Aliens      []string
	IsPrivate   bool
	MarkerFirst string
	MarkerLast  string
	BugLines    []string
}

type BugFormatForEditor struct {
	BugFormat

	PrivateComment bool
	Aliens         []string
}

func FormatBugForEditor(bug *bugzilla.Bug, incidents []*client.Incident, settings BugFormatForEditor, dest io.Writer) error {
	var buf bytes.Buffer
	config.Config().Set("l3t.color", false)
	defer config.Config().Set("l3t.color", true)
	err := FormatBug(bug, incidents, settings.BugFormat, &buf)
	if err != nil {
		return err
	}

	lines := strings.Split(buf.String(), "\n")
	info := bugForEditorInfo{Bug: bug,
		Incidents:   incidents,
		HasAliens:   len(settings.Aliens) > 0,
		Aliens:      settings.Aliens,
		IsPrivate:   settings.PrivateComment,
		MarkerFirst: config.Config().GetString("bugzilla.marker-first"),
		MarkerLast:  config.Config().GetString("bugzilla.marker-last"),
		BugLines:    lines,
	}
	return template.ExpandFromConfig("bugzilla.editor-template", dest, info)
}
07070100000031000081A4000003E800000064000000015CBEFDCB000001E6000000000000000000000000000000000000002700000000golang-l3t-0.0.1/formatted/incident.gopackage formatted

import (
	"io"

	"github.com/bhdn/golang-l3t/client"
	"github.com/bhdn/golang-l3t/template"
)

type augmentedIncident struct {
	client.Incident
	Agents []string
}

func FormatIncident(incident *client.Incident, dest io.Writer) error {
	aug := augmentedIncident{Incident: *incident}
	for _, ass := range incident.Assignments {
		aug.Agents = append(aug.Agents, ass.Agent.Username)
	}
	err := template.ExpandFromConfig("l3t.incident-template", dest, aug)
	return err
}
07070100000032000081A4000003E800000064000000015CBEFDCB0000030D000000000000000000000000000000000000001800000000golang-l3t-0.0.1/go.modmodule github.com/bhdn/golang-l3t

require (
	github.com/PuerkitoBio/goquery v1.5.0
	github.com/Sirupsen/logrus v1.0.6
	github.com/dmacvicar/gorgojo v0.0.0-20181109122632-f91261d568bf
	github.com/eidolon/wordwrap v0.0.0-20161011182207-e0f54129b8bb
	github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf
	github.com/headzoo/surf v0.0.0-20181106134648-a4a8c16c01dc47ef3a25326d21745806f3e6797a
	github.com/microcosm-cc/bluemonday v1.0.2
	github.com/sirupsen/logrus v1.4.0
	github.com/spf13/cobra v0.0.3
	github.com/spf13/viper v1.2.0
	golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2
	gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405
	gopkg.in/ini.v1 v1.42.0 // indirect
	gopkg.in/mattes/go-expand-tilde.v1 v1.0.0-20150330173918-cb884138e64c
	gopkg.in/yaml.v2 v2.2.1
)
07070100000033000081A4000003E800000064000000015CBEFDCB000019F0000000000000000000000000000000000000001800000000golang-l3t-0.0.1/go.sumgithub.com/PuerkitoBio/goquery v1.5.0 h1:uGvmFXOA73IKluu/F84Xd1tt/z07GYm8X49XKHP7EJk=
github.com/PuerkitoBio/goquery v1.5.0/go.mod h1:qD2PgZ9lccMbQlc7eEOjaeRlFQON7xY8kdmcsrnKqMg=
github.com/Sirupsen/logrus v1.0.6 h1:HCAGQRk48dRVPA5Y+Yh0qdCSTzPOyU1tBJ7Q9YzotII=
github.com/Sirupsen/logrus v1.0.6/go.mod h1:rmk17hk6i8ZSAJkSDa7nOxamrG+SP4P0mm+DAvExv4U=
github.com/andybalholm/cascadia v1.0.0 h1:hOCXnnZ5A+3eVDX8pvgl4kofXv2ELss0bKcqRySc45o=
github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dmacvicar/gorgojo v0.0.0-20181109122632-f91261d568bf h1:jULUUDgW3dFmfo/amO/8POlJZZbrRy1ibfNyPxsJVNc=
github.com/dmacvicar/gorgojo v0.0.0-20181109122632-f91261d568bf/go.mod h1:OMh3MoL/dBXKDNVN5AlcEKKVQmwaM/yExco8/VrvNIM=
github.com/eidolon/wordwrap v0.0.0-20161011182207-e0f54129b8bb h1:ioQwBmKdOCpMVS/bDaESqNWXIE/aw4+gsVtysCGMWZ4=
github.com/eidolon/wordwrap v0.0.0-20161011182207-e0f54129b8bb/go.mod h1:ZAPs+OyRzeVJFGvXVDVffgCzQfjg3qU9Ig8G/MU3zZ4=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf h1:7+FW5aGwISbqUtkfmIpZJGRgNFg2ioYPvFaUxdqpDsg=
github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf/go.mod h1:RpwtwJQFrIEPstU94h88MWPXP2ektJZ8cZ0YntAmXiE=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/headzoo/surf v0.0.0-20181106134648-a4a8c16c01dc47ef3a25326d21745806f3e6797a h1:W3Bkf01IyyYlkJt6OaFjR1RGBMUzrIfYxHlwZC8QdyU=
github.com/headzoo/surf v0.0.0-20181106134648-a4a8c16c01dc47ef3a25326d21745806f3e6797a/go.mod h1:/bct0m/iMNEqpn520y01yoaWxsAEigGFPnvyR1ewR5M=
github.com/headzoo/surf v1.0.0 h1:d2h9ftKeQYj7tKqAjQtAA0lJVkO8cTxvzdXLynmNnHM=
github.com/headzoo/surf v1.0.0/go.mod h1:/bct0m/iMNEqpn520y01yoaWxsAEigGFPnvyR1ewR5M=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/microcosm-cc/bluemonday v1.0.2 h1:5lPfLTTAvAbtS0VqT+94yOtFnGfUWYyx0+iToC3Os3s=
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
github.com/mitchellh/mapstructure v1.0.0 h1:vVpGvMXJPqSDh2VYHF7gsfQj8Ncx+Xw5Y1KHeTRY+7I=
github.com/mitchellh/mapstructure v1.0.0/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.4.0 h1:yKenngtzGh+cUSSh6GWbxW2abRqhYUSR/t/6+2QqNvE=
github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.2.0 h1:HHl1DSRbEQN2i8tJmtS6ViPyHx35+p51amrdsiTCrkg=
github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.2 h1:Fy0orTDgHdbnzHcsOgfCN4LtHf0ec3wwtiwJqwvf3Gc=
github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.2.0 h1:M4Rzxlu+RgU4pyBRKhKaVN1VeYOm8h2jgyXnAseDgCc=
github.com/spf13/viper v1.2.0/go.mod h1:P4AexN0a+C9tGAnUFNwDMYYZv3pjFuvmeiMyKRaNVlI=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a h1:gOpx8G595UYyvj8UK4+OFyY4rx037g3fmfhe5SasG3U=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3 h1:eH6Eip3UpmR+yM/qI9Ijluzb1bNv/cAU/n+6l8tRSis=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180906133057-8cf3aee42992 h1:BH3eQWeGbwRU2+wxxuuPOdFBmaiBH81O8BugSjHeTFg=
golang.org/x/sys v0.0.0-20180906133057-8cf3aee42992/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
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/ini.v1 v1.42.0 h1:7N3gPTt50s8GuLortA00n8AqRTk75qOP98+mTPpgzRk=
gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/mattes/go-expand-tilde.v1 v1.0.0-20150330173918-cb884138e64c h1:/Onz8dZtKBCmB8P0JU7+WSCfMekXry7BflVO0SQQrCU=
gopkg.in/mattes/go-expand-tilde.v1 v1.0.0-20150330173918-cb884138e64c/go.mod h1:j6QavCO5cYWud1+2/PFTXL1y6tjjkhSs+qcWgibOIc0=
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
07070100000034000041ED000003E800000064000000025CBEFDCB00000000000000000000000000000000000000000000001500000000golang-l3t-0.0.1/l3t07070100000035000081A4000003E800000064000000015CBEFDCB000001C0000000000000000000000000000000000000001D00000000golang-l3t-0.0.1/l3t/main.gopackage main

import (
	"os"
	"path"

	"github.com/bhdn/golang-l3t/cmd"
)

func main() {
	// Check if os.Args[0] is a command defined in cobra; if yes re
	// start using the main binary
	base := path.Base(os.Args[0])
	cleanBaseArgs := []string{base}
	found, _, err := cmd.RootCmd.Find(cleanBaseArgs)
	if err != nil {
		cmd.Execute()
	} else {
		cmd.RootCmd.RemoveCommand(found)
		_, err := found.ExecuteC()
		if err != nil {
			os.Exit(1)
		}
	}
}
07070100000036000041ED000003E800000064000000025CBEFDCB00000000000000000000000000000000000000000000001500000000golang-l3t-0.0.1/log07070100000037000081A4000003E800000064000000015CBEFDCB000013B7000000000000000000000000000000000000001C00000000golang-l3t-0.0.1/log/log.gopackage log

import (
	"os"

	"github.com/bhdn/golang-l3t/config"
	"gopkg.in/Sirupsen/logrus.v0"
)

// Logger defines a set of methods for writing application logs. Derived from and
// inspired by logrus.Entry.
type Logger interface {
	Debug(args ...interface{})
	Debugf(format string, args ...interface{})
	Debugln(args ...interface{})
	Error(args ...interface{})
	Errorf(format string, args ...interface{})
	Errorln(args ...interface{})
	Fatal(args ...interface{})
	Fatalf(format string, args ...interface{})
	Fatalln(args ...interface{})
	Info(args ...interface{})
	Infof(format string, args ...interface{})
	Infoln(args ...interface{})
	Panic(args ...interface{})
	Panicf(format string, args ...interface{})
	Panicln(args ...interface{})
	Print(args ...interface{})
	Printf(format string, args ...interface{})
	Println(args ...interface{})
	Warn(args ...interface{})
	Warnf(format string, args ...interface{})
	Warning(args ...interface{})
	Warningf(format string, args ...interface{})
	Warningln(args ...interface{})
	Warnln(args ...interface{})
}

var defaultLogger *logrus.Logger

func init() {
	defaultLogger = newLogrusLogger(config.Config())
}

func NewLogger(cfg config.Provider) *logrus.Logger {
	return newLogrusLogger(cfg)
}

func updateLevelFromConfig(l *logrus.Logger, cfg config.Provider) {
	switch cfg.GetString("log.level") {
	case "debug":
		l.Level = logrus.DebugLevel
	case "warning":
		l.Level = logrus.WarnLevel
	case "info":
		l.Level = logrus.InfoLevel
	default:
		l.Level = logrus.DebugLevel
	}
}

func newLogrusLogger(cfg config.Provider) *logrus.Logger {

	l := logrus.New()

	if cfg.GetBool("json_logs") {
		l.Formatter = new(logrus.JSONFormatter)
	}
	l.Out = os.Stderr
	updateLevelFromConfig(l, cfg)

	return l
}

func UpdateLevelFromConfig(cfg config.Provider) {
	updateLevelFromConfig(defaultLogger, cfg)
}

type Fields map[string]interface{}

func (f Fields) With(k string, v interface{}) Fields {
	f[k] = v
	return f
}

func (f Fields) WithFields(f2 Fields) Fields {
	for k, v := range f2 {
		f[k] = v
	}
	return f
}

func WithFields(fields Fields) Logger {
	return defaultLogger.WithFields(logrus.Fields(fields))
}

// Debug package-level convenience method.
func Debug(args ...interface{}) {
	defaultLogger.Debug(args...)
}

// Debugf package-level convenience method.
func Debugf(format string, args ...interface{}) {
	defaultLogger.Debugf(format, args...)
}

// Debugln package-level convenience method.
func Debugln(args ...interface{}) {
	defaultLogger.Debugln(args...)
}

// Error package-level convenience method.
func Error(args ...interface{}) {
	defaultLogger.Error(args...)
}

// Errorf package-level convenience method.
func Errorf(format string, args ...interface{}) {
	defaultLogger.Errorf(format, args...)
}

// Errorln package-level convenience method.
func Errorln(args ...interface{}) {
	defaultLogger.Errorln(args...)
}

// Fatal package-level convenience method.
func Fatal(args ...interface{}) {
	defaultLogger.Fatal(args...)
}

// Fatalf package-level convenience method.
func Fatalf(format string, args ...interface{}) {
	defaultLogger.Fatalf(format, args...)
}

// Fatalln package-level convenience method.
func Fatalln(args ...interface{}) {
	defaultLogger.Fatalln(args...)
}

// Info package-level convenience method.
func Info(args ...interface{}) {
	defaultLogger.Info(args...)
}

// Infof package-level convenience method.
func Infof(format string, args ...interface{}) {
	defaultLogger.Infof(format, args...)
}

// Infoln package-level convenience method.
func Infoln(args ...interface{}) {
	defaultLogger.Infoln(args...)
}

// Panic package-level convenience method.
func Panic(args ...interface{}) {
	defaultLogger.Panic(args...)
}

// Panicf package-level convenience method.
func Panicf(format string, args ...interface{}) {
	defaultLogger.Panicf(format, args...)
}

// Panicln package-level convenience method.
func Panicln(args ...interface{}) {
	defaultLogger.Panicln(args...)
}

// Print package-level convenience method.
func Print(args ...interface{}) {
	defaultLogger.Print(args...)
}

// Printf package-level convenience method.
func Printf(format string, args ...interface{}) {
	defaultLogger.Printf(format, args...)
}

// Println package-level convenience method.
func Println(args ...interface{}) {
	defaultLogger.Println(args...)
}

// Warn package-level convenience method.
func Warn(args ...interface{}) {
	defaultLogger.Warn(args...)
}

// Warnf package-level convenience method.
func Warnf(format string, args ...interface{}) {
	defaultLogger.Warnf(format, args...)
}

// Warning package-level convenience method.
func Warning(args ...interface{}) {
	defaultLogger.Warning(args...)
}

// Warningf package-level convenience method.
func Warningf(format string, args ...interface{}) {
	defaultLogger.Warningf(format, args...)
}

// Warningln package-level convenience method.
func Warningln(args ...interface{}) {
	defaultLogger.Warningln(args...)
}

// Warnln package-level convenience method.
func Warnln(args ...interface{}) {
	defaultLogger.Warnln(args...)
}
07070100000038000041ED000003E800000064000000025CBEFDCB00000000000000000000000000000000000000000000001500000000golang-l3t-0.0.1/osc07070100000039000081A4000003E800000064000000015CBEFDCB00000359000000000000000000000000000000000000001C00000000golang-l3t-0.0.1/osc/osc.gopackage osc

import (
	"fmt"
	"os"

	"github.com/bhdn/golang-l3t/config"
	"github.com/bhdn/golang-l3t/log"
	"github.com/dmacvicar/gorgojo/plugins/novell"
	tilde "gopkg.in/mattes/go-expand-tilde.v1"
)

func GetBZCredentials() (username, password string, err error) {
	username = config.Config().GetString("bugzilla.username")
	password = config.Config().GetString("bugzilla.password")
	if username != "" {
		return username, password, nil
	}

	for _, rawPath := range config.Config().GetStringSlice("bugzilla.oscrc") {
		path, perr := tilde.Expand(rawPath)
		if perr != nil {
			err = fmt.Errorf("failed to expand the oscrc path %q: %v", rawPath, perr)
			return
		}
		log.Debug("osc: trying ", path)

		if _, err = os.Stat(path); err == nil {
			username, password, err = novell.ReadOscCredentials(path)
			if err == nil {
				return
			}
		}
	}

	return
}
0707010000003A000041ED000003E800000064000000025CBEFDCB00000000000000000000000000000000000000000000001700000000golang-l3t-0.0.1/paged0707010000003B000081A4000003E800000064000000015CBEFDCB00000575000000000000000000000000000000000000002000000000golang-l3t-0.0.1/paged/paged.go// Handles paged output
// Takes care of PAGER and LESS
package paged

import (
	"github.com/bhdn/golang-l3t/config"
	"github.com/google/shlex"
	"io"
	"os"
	"os/exec"
	"strings"
)

func fixLessEnv() []string {
	lessOptions := config.Config().GetString("paging.less-options")
	env := os.Environ()
	found := false
	for _, kp := range env {
		if strings.HasPrefix(kp, "LESS=") {
			if idx := strings.Index(kp, "R"); idx < 0 {
				kv := strings.SplitN(kp, "=", 2)
				value := "LESS=" + kv[1] + lessOptions
				env = append(env, value)
				return env
			}
			found = true
			break
		}
	}
	if !found {
		value := "LESS=" + lessOptions
		env = append(env, value)
		return env
	}

	// No change to be sent to the new process
	return nil
}

// Returns a writer, a func for waiting for the pager and and error
func PagedOutput() (io.WriteCloser, func() error, error) {
	pagerEnv := config.Config().GetString("paging.env")
	fullCmd := os.Getenv(pagerEnv)
	if fullCmd == "" {
		fullCmd = config.Config().GetString("paging.fallback-command")
	}
	args, err := shlex.Split(fullCmd)
	if err != nil {
		return nil, nil, err
	}

	cmd := exec.Command(args[0], args[1:]...)
	cmd.Stdout = os.Stdout
	stdin, err := cmd.StdinPipe()
	if err != nil {
		return nil, nil, err
	}

	cmd.Env = fixLessEnv() // nil -> env inherited

	err = cmd.Start()
	if err != nil {
		return nil, nil, err
	}

	return stdin, cmd.Wait, nil
}
0707010000003C000041ED000003E800000064000000025CBEFDCB00000000000000000000000000000000000000000000001A00000000golang-l3t-0.0.1/template0707010000003D000081A4000003E800000064000000015CBEFDCB0000100C000000000000000000000000000000000000002600000000golang-l3t-0.0.1/template/template.gopackage template

import (
	"bytes"
	"fmt"
	"io"
	"strconv"
	"strings"
	"text/template"
	"time"

	"github.com/bhdn/golang-l3t/config"
	"github.com/bhdn/golang-l3t/log"
	"github.com/eidolon/wordwrap"
	"golang.org/x/crypto/ssh/terminal"
)

type HelperFuncs struct {
	colorMap map[int]int
	usedOnce map[string]bool
	scratch  map[string]string
}

func (h *HelperFuncs) uncoloredOutput(color int, rest ...interface{}) string {
	return fmt.Sprint(rest...)
}

func (h *HelperFuncs) coloredOutput(color int, rest ...interface{}) string {
	return fmt.Sprintf("\033[38;5;%dm%s%s", h.mapColor(color), fmt.Sprint(rest...), h.resetColor())
}

func (h *HelperFuncs) setColor(color int, rest ...interface{}) string {
	return fmt.Sprintf("\033[38;5;%dm", h.mapColor(color))
}

func (h *HelperFuncs) setUncolored(color int, rest ...interface{}) string {
	return ""
}

func (h *HelperFuncs) boldOutput(rest ...interface{}) string {
	return fmt.Sprintf("\033[1m%s%s", fmt.Sprint(rest...), h.resetColor())
}

func (h *HelperFuncs) setBold() string {
	return fmt.Sprint("\033[1m")
}

func (h *HelperFuncs) resetColor() string {
	return "\033[0m"
}

func (h *HelperFuncs) indent(prefix, text string) string {
	return wordwrap.Indent(text, prefix, true)
}

func (h *HelperFuncs) wrap(cols int, text string) string {
	wrapper := wordwrap.Wrapper(cols, false)
	lines := strings.Split(text, "\n")
	var buffer bytes.Buffer
	for _, line := range lines {
		buffer.WriteString(wrapper(line))
		buffer.WriteString("\n")
	}
	return buffer.String()
}

func (h *HelperFuncs) once(something string) string {
	_, ok := h.usedOnce[something]
	if !ok {
		h.usedOnce[something] = true
		return something
	}
	return ""
}

// set is a workaround based on this limitation for golang before 1.10rc2:
// https://github.com/golang/go/issues/10608
func (h *HelperFuncs) set(name, value string) string {
	h.scratch[name] = value
	return ""
}

func (h *HelperFuncs) get(name string) string {
	value, ok := h.scratch[name]
	if !ok {
		return ""
	}
	return value
}

func (h *HelperFuncs) spaced(something string) string {
	if something != "" {
		return fmt.Sprintf("%s ", something)
	}
	return something
}

func (h *HelperFuncs) now() time.Time {
	return time.Now()
}

func (h *HelperFuncs) after(a, b time.Time) bool {
	return a.After(b)
}

func (h *HelperFuncs) mapColor(color int) (newColor int) {
	newColor, ok := h.colorMap[color]
	if !ok {
		newColor = color
	}
	return
}

func (h *HelperFuncs) updateColorMap() {
	for k, v := range config.Config().GetStringMapString("colormap") {
		ik, err := strconv.Atoi(k)
		if err != nil {
			log.Debugf("ignoring bad colormap key: %v", k)
			continue
		}

		iv, err := strconv.Atoi(v)
		if err != nil {
			log.Debugf("ignoring bad colormap value: %v", v)
			continue
		}

		h.colorMap[ik] = iv
	}
}

func Helpers() *HelperFuncs {
	h := &HelperFuncs{}
	h.usedOnce = make(map[string]bool)
	h.colorMap = make(map[int]int)
	h.scratch = make(map[string]string)
	h.updateColorMap()
	return h
}

func getFuncMap() template.FuncMap {
	var funcs template.FuncMap
	h := Helpers()

	var baseFuncs = map[string]interface{}{
		"join":   strings.Join,
		"wrap":   h.wrap,
		"indent": h.indent,
		"once":   h.once,
		"spaced": h.spaced,
		"now":    h.now,
		"after":  h.after,
		"set":    h.set,
		"get":    h.get,
	}

	if terminal.IsTerminal(1) && config.Config().GetBool("l3t.color") {
		funcs = template.FuncMap{
			"C":  h.coloredOutput,
			"S":  h.setColor,
			"B":  h.boldOutput,
			"SB": h.setBold,
			"R":  h.resetColor,
		}
	} else {
		funcs = template.FuncMap{
			"C":  h.uncoloredOutput,
			"S":  h.setUncolored,
			"B":  fmt.Sprint,
			"SB": fmt.Sprint,
			"R":  fmt.Sprint,
		}
	}

	for k, v := range baseFuncs {
		funcs[k] = v
	}

	return funcs
}

func ExpandFromConfig(configOption string, dest io.Writer, object interface{}) error {
	templ := FromConfig(configOption)
	return templ.Execute(dest, object)
}

func FromConfig(configOption string) *template.Template {
	raw := config.Config().GetString(configOption)
	templ := template.New(configOption)
	templ.Funcs(getFuncMap())
	templ.Parse(raw)
	return templ
}
0707010000003E000041ED000003E800000064000000025CBEFDCB00000000000000000000000000000000000000000000001900000000golang-l3t-0.0.1/version0707010000003F000081A4000003E800000064000000015CBEFDCB0000016B000000000000000000000000000000000000002400000000golang-l3t-0.0.1/version/version.gopackage version

import (
        "runtime"
        "fmt"
)

// The git commit that was compiled. This will be filled in by the compiler.
var GitCommit string

// The main version number that is being run at the moment.
const Version = "0.1.0"

var BuildDate = ""

var GoVersion = runtime.Version()

var OsArch = fmt.Sprintf("%s %s", runtime.GOOS, runtime.GOARCH)07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!397 blocks