diff options
-rw-r--r-- | bridge/bridges.go | 1 | ||||
-rw-r--r-- | bridge/launchpad/config.go | 50 | ||||
-rw-r--r-- | bridge/launchpad/import.go | 80 | ||||
-rw-r--r-- | bridge/launchpad/launchpad.go | 24 | ||||
-rw-r--r-- | bridge/launchpad/launchpad_api.go | 178 |
5 files changed, 333 insertions, 0 deletions
diff --git a/bridge/bridges.go b/bridge/bridges.go index 4a0ef9e2..6cbd13fe 100644 --- a/bridge/bridges.go +++ b/bridge/bridges.go @@ -4,6 +4,7 @@ package bridge import ( "github.com/MichaelMure/git-bug/bridge/core" _ "github.com/MichaelMure/git-bug/bridge/github" + _ "github.com/MichaelMure/git-bug/bridge/launchpad" "github.com/MichaelMure/git-bug/cache" "github.com/MichaelMure/git-bug/repository" ) 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 +} |