Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MM-611]: Added the feature to select the default repository for the channel #806

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 60 additions & 10 deletions server/plugin/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ const (

requestTimeout = 30 * time.Second
oauthCompleteTimeout = 2 * time.Minute

channelIDParam = "channelId"
)

type OAuthState struct {
Expand All @@ -48,6 +50,18 @@ func (e *APIErrorResponse) Error() string {
return e.Message
}

type RepoResponse struct {
Name string `json:"name,omitempty"`
FullName string `json:"full_name,omitempty"`
Permissions map[string]bool `json:"permissions,omitempty"`
}

// Only send down fields to client that are needed
type RepositoryResponse struct {
DefaultRepo RepoResponse `json:"defaultRepo,omitempty"`
Repos []RepoResponse `json:"repos,omitempty"`
}

type PRDetails struct {
URL string `json:"url"`
Number int `json:"number"`
Expand Down Expand Up @@ -1224,11 +1238,27 @@ func getRepositoryListByOrg(c context.Context, org string, githubClient *github.
return allRepos, http.StatusOK, nil
}

func getRepository(c context.Context, org string, repo string, githubClient *github.Client) (*github.Repository, error) {
repository, _, err := githubClient.Repositories.Get(c, org, repo)
if err != nil {
return nil, err
}

return repository, nil
}

func (p *Plugin) getRepositories(c *UserContext, w http.ResponseWriter, r *http.Request) {
githubClient := p.githubConnectUser(c.Context.Ctx, c.GHInfo)

org := p.getConfiguration().GitHubOrg

channelID := r.URL.Query().Get(channelIDParam)
if channelID == "" {
p.client.Log.Warn("Bad request: missing channelId")
p.writeAPIError(w, &APIErrorResponse{Message: "Bad request: missing channelId", StatusCode: http.StatusBadRequest})
raghavaggarwal2308 marked this conversation as resolved.
Show resolved Hide resolved
return
}

var allRepos []*github.Repository
var err error
var statusCode int
Expand Down Expand Up @@ -1259,18 +1289,38 @@ func (p *Plugin) getRepositories(c *UserContext, w http.ResponseWriter, r *http.
}
}

// Only send down fields to client that are needed
type RepositoryResponse struct {
Name string `json:"name,omitempty"`
FullName string `json:"full_name,omitempty"`
Permissions map[string]bool `json:"permissions,omitempty"`
repoResp := make([]RepoResponse, len(allRepos))
for i, r := range allRepos {
repoResp[i].Name = r.GetName()
repoResp[i].FullName = r.GetFullName()
repoResp[i].Permissions = r.GetPermissions()
}

resp := make([]RepositoryResponse, len(allRepos))
for i, r := range allRepos {
resp[i].Name = r.GetName()
resp[i].FullName = r.GetFullName()
resp[i].Permissions = r.GetPermissions()
resp := RepositoryResponse{
Repos: repoResp,
}

defaultRepo, dErr := p.GetDefaultRepo(c.GHInfo.UserID, channelID)
if dErr != nil {
c.Log.WithError(dErr).Warnf("Failed to get the default repo for the channel. UserID: %s. ChannelID: %s", c.GHInfo.UserID, channelID)
}

if defaultRepo != "" {
config := p.getConfiguration()
baseURL := config.getBaseURL()
owner, repo := parseOwnerAndRepo(defaultRepo, baseURL)
defaultRepository, err := getRepository(c.Ctx, owner, repo, githubClient)
if err != nil {
c.Log.WithError(err).Warnf("Failed to get the default repo %s/%s", owner, repo)
}

if defaultRepository != nil {
resp.DefaultRepo = RepoResponse{
Name: *defaultRepository.Name,
FullName: *defaultRepository.FullName,
Permissions: defaultRepository.Permissions,
}
}
}

p.writeJSON(w, resp)
Expand Down
124 changes: 122 additions & 2 deletions server/plugin/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ const (
PerPageValue = 50
)

const DefaultRepoKey string = "%s_%s-default-repo"

var validFeatures = map[string]bool{
featureIssueCreation: true,
featureIssues: true,
Expand Down Expand Up @@ -115,7 +117,7 @@ func (p *Plugin) getCommand(config *Configuration) (*model.Command, error) {
return &model.Command{
Trigger: "github",
AutoComplete: true,
AutoCompleteDesc: "Available commands: connect, disconnect, todo, subscriptions, issue, me, mute, settings, help, about",
AutoCompleteDesc: "Available commands: connect, disconnect, todo, subscriptions, issue, default-repo, me, mute, settings, help, about",
AutoCompleteHint: "[command]",
AutocompleteData: getAutocompleteData(config),
AutocompleteIconData: iconData,
Expand Down Expand Up @@ -702,6 +704,110 @@ func (p *Plugin) handleIssue(_ *plugin.Context, args *model.CommandArgs, paramet
}
}

func (p *Plugin) handleDefaultRepo(c *plugin.Context, args *model.CommandArgs, parameters []string, userInfo *GitHubUserInfo) string {
if len(parameters) == 0 {
return "Invalid action. Available actions are 'set', 'get' and 'unset'."
}

command := parameters[0]
parameters = parameters[1:]

switch {
case command == "set":
return p.handleSetDefaultRepo(args, parameters, userInfo)
case command == "get":
return p.handleGetDefaultRepo(args, userInfo)
case command == "unset":
return p.handleUnSetDefaultRepo(args, userInfo)
default:
return fmt.Sprintf("Unknown subcommand %v", command)
}
}

func (p *Plugin) handleSetDefaultRepo(args *model.CommandArgs, parameters []string, userInfo *GitHubUserInfo) string {
if len(parameters) == 0 {
return "Please specify a repository."
}

repo := parameters[0]
config := p.getConfiguration()
baseURL := config.getBaseURL()
owner, repo := parseOwnerAndRepo(repo, baseURL)
if owner == "" || repo == "" {
return "Please provide a valid repository"
}

owner = strings.ToLower(owner)
repo = strings.ToLower(repo)

if config.GitHubOrg != "" && strings.ToLower(config.GitHubOrg) != owner {
return fmt.Sprintf("Repository is not part of the locked Github organization. Locked Github organization: %s", config.GitHubOrg)
}

ctx := context.Background()
githubClient := p.githubConnectUser(ctx, userInfo)

ghRepo, _, err := githubClient.Repositories.Get(ctx, owner, repo)
if err != nil {
return "Error occurred while getting github repository details"
}
if ghRepo == nil {
return fmt.Sprintf("Unknown repository %s", fullNameFromOwnerAndRepo(owner, repo))
}

if _, err := p.store.Set(fmt.Sprintf(DefaultRepoKey, args.ChannelId, userInfo.UserID), []byte(fmt.Sprintf("%s/%s", owner, repo))); err != nil {
return "Error occurred saving the default repo"
}

repoLink := fmt.Sprintf("%s%s/%s", baseURL, owner, repo)
successMsg := fmt.Sprintf("The default repo has been set to [%s/%s](%s) for this channel", owner, repo, repoLink)

return successMsg
}

func (p *Plugin) GetDefaultRepo(userID, channelID string) (string, error) {
var defaultRepoBytes []byte
if err := p.store.Get(fmt.Sprintf(DefaultRepoKey, channelID, userID), &defaultRepoBytes); err != nil {
return "", err
}

return string(defaultRepoBytes), nil
}

func (p *Plugin) handleGetDefaultRepo(args *model.CommandArgs, userInfo *GitHubUserInfo) string {
defaultRepo, err := p.GetDefaultRepo(userInfo.UserID, args.ChannelId)
if err != nil {
p.client.Log.Warn("Not able to get the default repo", "UserID", userInfo.UserID, "ChannelID", args.ChannelId, "Error", err.Error())
return "Error occurred while getting the default repo"
}

if defaultRepo == "" {
return "You have not set a default repository for this channel"
}

config := p.getConfiguration()
repoLink := config.getBaseURL() + defaultRepo
return fmt.Sprintf("The default repository is [%s](%s)", defaultRepo, repoLink)
}

func (p *Plugin) handleUnSetDefaultRepo(args *model.CommandArgs, userInfo *GitHubUserInfo) string {
defaultRepo, err := p.GetDefaultRepo(userInfo.UserID, args.ChannelId)
if err != nil {
p.client.Log.Warn("Not able to get the default repo", "UserID", userInfo.UserID, "ChannelID", args.ChannelId, "Error", err.Error())
return "Error occurred while getting the default repo"
}

if defaultRepo == "" {
return "You have not set a default repository for this channel"
}

if err := p.store.Delete(fmt.Sprintf(DefaultRepoKey, args.ChannelId, userInfo.UserID)); err != nil {
return "Error occurred while unsetting the repo for this channel"
}

return "The default repository has been unset successfully"
}

func (p *Plugin) handleSetup(c *plugin.Context, args *model.CommandArgs, parameters []string) string {
userID := args.UserId
isSysAdmin, err := p.isAuthorizedSysAdmin(userID)
Expand Down Expand Up @@ -871,7 +977,7 @@ func getAutocompleteData(config *Configuration) *model.AutocompleteData {
return github
}

github := model.NewAutocompleteData("github", "[command]", "Available commands: connect, disconnect, todo, subscriptions, issue, me, mute, settings, help, about")
github := model.NewAutocompleteData("github", "[command]", "Available commands: connect, disconnect, todo, subscriptions, issue, default-repo, me, mute, settings, help, about")

connect := model.NewAutocompleteData("connect", "", "Connect your Mattermost account to your GitHub account")
if config.EnablePrivateRepo {
Expand Down Expand Up @@ -944,6 +1050,20 @@ func getAutocompleteData(config *Configuration) *model.AutocompleteData {

github.AddCommand(issue)

defaultRepo := model.NewAutocompleteData("default-repo", "[command]", "Available commands: set, get, unset")
defaultRepoSet := model.NewAutocompleteData("set", "[owner/repo]", "Set the default repository for the channel")
defaultRepoSet.AddTextArgument("Owner/repo to set as a default repository", "[owner/repo]", "")

defaultRepoGet := model.NewAutocompleteData("get", "", "Get the default repository already set for the channel")

defaultRepoDelete := model.NewAutocompleteData("unset", "", "Unset the default repository set for the channel")

defaultRepo.AddCommand(defaultRepoSet)
defaultRepo.AddCommand(defaultRepoGet)
defaultRepo.AddCommand(defaultRepoDelete)

github.AddCommand(defaultRepo)

me := model.NewAutocompleteData("me", "", "Display the connected GitHub account")
github.AddCommand(me)

Expand Down
1 change: 1 addition & 0 deletions server/plugin/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ func NewPlugin() *Plugin {
"": p.handleHelp,
"settings": p.handleSettings,
"issue": p.handleIssue,
"default-repo": p.handleDefaultRepo,
}

p.createGithubEmojiMap()
Expand Down
6 changes: 5 additions & 1 deletion server/plugin/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,11 @@ Assignees: {{range $i, $el := .Assignees -}} {{- if $i}}, {{end}}{{template "use
" * `/github mute list` - list your muted GitHub users\n" +
" * `/github mute add [username]` - add a GitHub user to your muted list\n" +
" * `/github mute delete [username]` - remove a GitHub user from your muted list\n" +
" * `/github mute delete-all` - unmute all GitHub users\n"))
" * `/github mute delete-all` - unmute all GitHub users\n" +
"* `/github default-repo` - Manage the default repository per channel for the user. The default repository will be auto selected for creating the issues\n" +
" * `/github default-repo set owner[/repo]` - set the default repo for the channel\n" +
" * `/github default-repo get` - get the default repo for the channel\n" +
" * `/github default-repo unset` - unset the default repo for the channel\n"))

template.Must(masterTemplate.New("newRepoStar").Funcs(funcMap).Parse(`
{{template "repo" .GetRepo}}
Expand Down
4 changes: 2 additions & 2 deletions webapp/src/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,11 @@ export function getReviewsDetails(prList: PrsDetailsData[]) {
};
}

export function getRepos() {
export function getRepos(channelId: string) {
return async (dispatch: DispatchFunc) => {
let data;
try {
data = await Client.getRepositories();
data = await Client.getRepositories(channelId);
} catch (error) {
dispatch({
type: ActionTypes.RECEIVED_REPOSITORIES,
Expand Down
4 changes: 2 additions & 2 deletions webapp/src/client/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ export default class Client {
return this.doPost(`${this.url}/user`, {user_id: userID});
}

getRepositories = async () => {
return this.doGet(`${this.url}/repositories`);
getRepositories = async (channelId) => {
return this.doGet(`${this.url}/repositories?channelId=${channelId}`);
}

getLabels = async (repo) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ const initialState = {

export default class GithubRepoSelector extends PureComponent {
static propTypes = {
yourRepos: PropTypes.array.isRequired,
yourRepos: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
value: PropTypes.string,
currentChannelId: PropTypes.string,
addValidate: PropTypes.func,
removeValidate: PropTypes.func,
actions: PropTypes.shape({
Expand All @@ -30,16 +31,24 @@ export default class GithubRepoSelector extends PureComponent {
}

componentDidMount() {
this.props.actions.getRepos();
this.props.actions.getRepos(this.props.currentChannelId);
}

componentDidUpdate() {
const defaultRepo = this.props.yourRepos.defaultRepo;

if (!(this.props.value) && this.props.yourRepos.defaultRepo?.full_name) {
this.onChange(defaultRepo.name, defaultRepo.full_name);
}
}

onChange = (_, name) => {
const repo = this.props.yourRepos.find((r) => r.full_name === name);
const repo = this.props.yourRepos.repos.find((r) => r.full_name === name);
this.props.onChange({name, permissions: repo.permissions});
}

render() {
const repoOptions = this.props.yourRepos.map((item) => ({value: item.full_name, label: item.full_name}));
const repoOptions = this.props.yourRepos.repos.map((item) => ({value: item.full_name, label: item.full_name}));

return (
<div className={'form-group margin-bottom x3'}>
Expand Down
3 changes: 3 additions & 0 deletions webapp/src/components/github_repo_selector/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';

import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';

import manifest from 'manifest';
import {getRepos} from '../../actions';

Expand All @@ -12,6 +14,7 @@ import GithubRepoSelector from './github_repo_selector.jsx';
function mapStateToProps(state) {
return {
yourRepos: state[`plugins-${manifest.id}`].yourRepos,
currentChannelId: getCurrentChannelId(state),
};
}

Expand Down
4 changes: 3 additions & 1 deletion webapp/src/reducers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,9 @@ function sidebarContent(state = {
}
}

function yourRepos(state: YourReposData[] = [], action: {type: string, data: YourReposData[]}) {
function yourRepos(state: YourReposData = {
repo: [],
}, action: {type: string, data: YourReposData}) {
switch (action.type) {
case ActionTypes.RECEIVED_REPOSITORIES:
return action.data;
Expand Down
10 changes: 10 additions & 0 deletions webapp/src/types/github_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,17 @@ export type GithubIssueData = {
repository_url: string;
}

export type DefaultRepo = {
name: string;
full_name: string;
}

export type YourReposData = {
defaultRepo?: DefaultRepo;
repo: ReposData[];
}

export type ReposData = {
name: string;
full_name: string;
}
Expand Down
Loading