aboutsummaryrefslogtreecommitdiffstats
path: root/bridge/launchpad
diff options
context:
space:
mode:
authorMichael Muré <batolettre@gmail.com>2018-12-17 12:51:52 +0100
committerGitHub <noreply@github.com>2018-12-17 12:51:52 +0100
commit24f568a5c61496c516ba103505d7b38eab3d3040 (patch)
tree0d73ec1d85a420cf790c9ec6557b658d53912c85 /bridge/launchpad
parentc92da552d8caeb6129f6f97146459498ac316387 (diff)
parentd6ddf0ef5c64cdb5262bcaba8018e6345ea391a1 (diff)
downloadgit-bug-24f568a5c61496c516ba103505d7b38eab3d3040.tar.gz
Merge pull request #79 from Steap/feature/bridge-launchpad
WIP: Initial Launchpad bridge.
Diffstat (limited to 'bridge/launchpad')
-rw-r--r--bridge/launchpad/config.go50
-rw-r--r--bridge/launchpad/import.go80
-rw-r--r--bridge/launchpad/launchpad.go24
-rw-r--r--bridge/launchpad/launchpad_api.go178
4 files changed, 332 insertions, 0 deletions
diff --git a/bridge/launchpad/config.go b/bridge/launchpad/config.go
new file mode 100644
index 00000000..8469dbd3
--- /dev/null
+++ b/bridge/launchpad/config.go
@@ -0,0 +1,50 @@
+package launchpad
+
+import (
+ "bufio"
+ "fmt"
+ "os"
+ "strings"
+
+ "github.com/MichaelMure/git-bug/bridge/core"
+ "github.com/MichaelMure/git-bug/repository"
+)
+
+const keyProject = "project"
+
+func (*Launchpad) Configure(repo repository.RepoCommon) (core.Configuration, error) {
+ conf := make(core.Configuration)
+
+ projectName, err := promptProjectName()
+ if err != nil {
+ return nil, err
+ }
+
+ conf[keyProject] = projectName
+
+ return conf, nil
+}
+
+func promptProjectName() (string, error) {
+ for {
+ fmt.Print("Launchpad project name: ")
+
+ line, err := bufio.NewReader(os.Stdin).ReadString('\n')
+ if err != nil {
+ return "", err
+ }
+
+ line = strings.TrimRight(line, "\n")
+
+ if line == "" {
+ fmt.Println("Project name is empty")
+ continue
+ }
+
+ return line, nil
+ }
+}
+
+func (*Launchpad) ValidateConfig(conf core.Configuration) error {
+ return nil
+}
diff --git a/bridge/launchpad/import.go b/bridge/launchpad/import.go
new file mode 100644
index 00000000..074b4bf3
--- /dev/null
+++ b/bridge/launchpad/import.go
@@ -0,0 +1,80 @@
+package launchpad
+
+import (
+ "fmt"
+ "time"
+
+ "github.com/MichaelMure/git-bug/bridge/core"
+ "github.com/MichaelMure/git-bug/bug"
+ "github.com/MichaelMure/git-bug/cache"
+ "github.com/pkg/errors"
+)
+
+type launchpadImporter struct {
+ conf core.Configuration
+}
+
+func (li *launchpadImporter) Init(conf core.Configuration) error {
+ li.conf = conf
+ return nil
+}
+
+const keyLaunchpadID = "launchpad-id"
+
+func (li *launchpadImporter) makePerson(owner LPPerson) bug.Person {
+ return bug.Person{
+ Name: owner.Name,
+ Email: "",
+ Login: owner.Login,
+ AvatarUrl: "",
+ }
+}
+
+func (li *launchpadImporter) ImportAll(repo *cache.RepoCache) error {
+ lpAPI := new(launchpadAPI)
+
+ err := lpAPI.Init()
+ if err != nil {
+ return err
+ }
+
+ lpBugs, err := lpAPI.SearchTasks(li.conf["project"])
+ if err != nil {
+ return err
+ }
+
+ for _, lpBug := range lpBugs {
+ lpBugID := fmt.Sprintf("%d", lpBug.ID)
+ _, err := repo.ResolveBugCreateMetadata(keyLaunchpadID, lpBugID)
+ if err != nil && err != bug.ErrBugNotExist {
+ return err
+ }
+
+ if err == bug.ErrBugNotExist {
+ createdAt, _ := time.Parse(time.RFC3339, lpBug.CreatedAt)
+ _, err := repo.NewBugRaw(
+ li.makePerson(lpBug.Owner),
+ createdAt.Unix(),
+ lpBug.Title,
+ lpBug.Description,
+ nil,
+ map[string]string{
+ keyLaunchpadID: lpBugID,
+ },
+ )
+ if err != nil {
+ return errors.Wrapf(err, "failed to add bug id #%s", lpBugID)
+ }
+ } else {
+ /* TODO: Update bug */
+ fmt.Println("TODO: Update bug")
+ }
+
+ }
+ return nil
+}
+
+func (li *launchpadImporter) Import(repo *cache.RepoCache, id string) error {
+ fmt.Println("IMPORT")
+ return nil
+}
diff --git a/bridge/launchpad/launchpad.go b/bridge/launchpad/launchpad.go
new file mode 100644
index 00000000..f862f24e
--- /dev/null
+++ b/bridge/launchpad/launchpad.go
@@ -0,0 +1,24 @@
+// Package launchad contains the Launchpad bridge implementation
+package launchpad
+
+import (
+ "github.com/MichaelMure/git-bug/bridge/core"
+)
+
+func init() {
+ core.Register(&Launchpad{})
+}
+
+type Launchpad struct{}
+
+func (*Launchpad) Target() string {
+ return "launchpad-preview"
+}
+
+func (*Launchpad) NewImporter() core.Importer {
+ return &launchpadImporter{}
+}
+
+func (*Launchpad) NewExporter() core.Exporter {
+ return nil
+}
diff --git a/bridge/launchpad/launchpad_api.go b/bridge/launchpad/launchpad_api.go
new file mode 100644
index 00000000..09e02bc5
--- /dev/null
+++ b/bridge/launchpad/launchpad_api.go
@@ -0,0 +1,178 @@
+package launchpad
+
+/*
+ * A wrapper around the Launchpad API. The documentation can be found at:
+ * https://launchpad.net/+apidoc/devel.html
+ *
+ * TODO:
+ * - Retrieve all messages associated to bugs
+ * - Retrieve bug status
+ * - Retrieve activity log
+ * - SearchTasks should yield bugs one by one
+ *
+ * TODO (maybe):
+ * - Authentication (this might help retrieving email adresses)
+ */
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/url"
+)
+
+const apiRoot = "https://api.launchpad.net/devel"
+
+// Person describes a person on Launchpad (a bug owner, a message author, ...).
+type LPPerson struct {
+ Name string `json:"display_name"`
+ Login string `json:"name"`
+}
+
+// Caching all the LPPerson we know.
+// The keys are links to an owner page, such as
+// https://api.launchpad.net/devel/~login
+var personCache = make(map[string]LPPerson)
+
+func (owner *LPPerson) UnmarshalJSON(data []byte) error {
+ type LPPersonX LPPerson // Avoid infinite recursion
+ var ownerLink string
+ if err := json.Unmarshal(data, &ownerLink); err != nil {
+ return err
+ }
+
+ // First, try to gather info about the bug owner using our cache.
+ if cachedPerson, hasKey := personCache[ownerLink]; hasKey {
+ *owner = cachedPerson
+ return nil
+ }
+
+ // If the bug owner is not already known, we have to send a request.
+ req, err := http.NewRequest("GET", ownerLink, nil)
+ if err != nil {
+ return nil
+ }
+
+ client := &http.Client{}
+ resp, err := client.Do(req)
+ if err != nil {
+ return nil
+ }
+
+ defer resp.Body.Close()
+
+ var p LPPersonX
+ if err := json.NewDecoder(resp.Body).Decode(&p); err != nil {
+ return nil
+ }
+ *owner = LPPerson(p)
+ // Do not forget to update the cache.
+ personCache[ownerLink] = *owner
+ return nil
+}
+
+// LPBug describes a Launchpad bug.
+type LPBug struct {
+ Title string `json:"title"`
+ ID int `json:"id"`
+ Owner LPPerson `json:"owner_link"`
+ Description string `json:"description"`
+ CreatedAt string `json:"date_created"`
+}
+
+type launchpadBugEntry struct {
+ BugLink string `json:"bug_link"`
+ SelfLink string `json:"self_link"`
+}
+
+type launchpadAnswer struct {
+ Entries []launchpadBugEntry `json:"entries"`
+ Start int `json:"start"`
+ NextLink string `json:"next_collection_link"`
+}
+
+type launchpadAPI struct {
+ client *http.Client
+}
+
+func (lapi *launchpadAPI) Init() error {
+ lapi.client = &http.Client{}
+ return nil
+}
+
+func (lapi *launchpadAPI) SearchTasks(project string) ([]LPBug, error) {
+ var bugs []LPBug
+
+ // First, let us build the URL. Not all statuses are included by
+ // default, so we have to explicitely enumerate them.
+ validStatuses := [13]string{
+ "New", "Incomplete", "Opinion", "Invalid",
+ "Won't Fix", "Expired", "Confirmed", "Triaged",
+ "In Progress", "Fix Committed", "Fix Released",
+ "Incomplete (with response)", "Incomplete (without response)",
+ }
+ queryParams := url.Values{}
+ queryParams.Add("ws.op", "searchTasks")
+ for _, validStatus := range validStatuses {
+ queryParams.Add("status", validStatus)
+ }
+ lpURL := fmt.Sprintf("%s/%s?%s", apiRoot, project, queryParams.Encode())
+
+ for {
+ req, err := http.NewRequest("GET", lpURL, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ resp, err := lapi.client.Do(req)
+ if err != nil {
+ return nil, err
+ }
+
+ defer resp.Body.Close()
+
+ var result launchpadAnswer
+
+ if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
+ return nil, err
+ }
+
+ for _, bugEntry := range result.Entries {
+ bug, err := lapi.queryBug(bugEntry.BugLink)
+ if err == nil {
+ bugs = append(bugs, bug)
+ }
+ }
+
+ // Launchpad only returns 75 results at a time. We get the next
+ // page and run another query, unless there is no other page.
+ lpURL = result.NextLink
+ if lpURL == "" {
+ break
+ }
+ }
+
+ return bugs, nil
+}
+
+func (lapi *launchpadAPI) queryBug(url string) (LPBug, error) {
+ var bug LPBug
+
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ return bug, err
+ }
+
+ resp, err := lapi.client.Do(req)
+ if err != nil {
+ return bug, err
+ }
+
+ defer resp.Body.Close()
+
+ if err := json.NewDecoder(resp.Body).Decode(&bug); err != nil {
+ return bug, err
+ }
+
+ return bug, nil
+}