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 10 commits into
base: master
Choose a base branch
from
62 changes: 56 additions & 6 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 Down Expand Up @@ -1224,11 +1226,32 @@ 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, int, error) {
repository, resp, err := githubClient.Repositories.Get(c, org, repo)
if err != nil {
return nil, resp.StatusCode, err
Kshitij-Katiyar marked this conversation as resolved.
Show resolved Hide resolved
}

return repository, http.StatusOK, 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)

Kshitij-Katiyar marked this conversation as resolved.
Show resolved Hide resolved
if channelID == "" {
p.writeAPIError(w, &APIErrorResponse{Message: "Bad request: missing channelId", StatusCode: http.StatusBadRequest})
raghavaggarwal2308 marked this conversation as resolved.
Show resolved Hide resolved
return
}

defaultRepo, dErr := p.GetDefaultRepo(c.GHInfo.UserID, channelID)
Kshitij-Katiyar marked this conversation as resolved.
Show resolved Hide resolved
if dErr != nil {
c.Log.Warnf("Failed to get the default repo for the channel")
Kshitij-Katiyar marked this conversation as resolved.
Show resolved Hide resolved
}

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

// Only send down fields to client that are needed
type RepositoryResponse struct {
type RepoResponse struct {
Name string `json:"name,omitempty"`
FullName string `json:"full_name,omitempty"`
Permissions map[string]bool `json:"permissions,omitempty"`
}

resp := make([]RepositoryResponse, len(allRepos))
// Only send down fields to client that are needed
type RepositoryResponse struct {
DefaultRepo RepoResponse `json:"defaultRepo,omitempty"`
Repo []RepoResponse `json:"repo,omitempty"`
}
Kshitij-Katiyar marked this conversation as resolved.
Show resolved Hide resolved

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

resp := RepositoryResponse{
Repo: repoResp,
Kshitij-Katiyar marked this conversation as resolved.
Show resolved Hide resolved
}

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.Warnf("Failed to get the default repo %s/%s", owner, repo)
Kshitij-Katiyar marked this conversation as resolved.
Show resolved Hide resolved
}

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

p.writeJSON(w, resp)
Expand Down
119 changes: 117 additions & 2 deletions server/plugin/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,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 +702,107 @@ 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 issue command. Available command is 'set', 'get' and 'unset'."
Kshitij-Katiyar marked this conversation as resolved.
Show resolved Hide resolved
}

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

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

func (p *Plugin) handleSetDefaultRepo(_ *plugin.Context, 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 == "" {
Kshitij-Katiyar marked this conversation as resolved.
Show resolved Hide resolved
return "invalid repository"
Kshitij-Katiyar marked this conversation as resolved.
Show resolved Hide resolved
}

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

if config.GitHubOrg != "" && strings.ToLower(config.GitHubOrg) != owner {
return "repository is not part of the locked github organization"
Kshitij-Katiyar marked this conversation as resolved.
Show resolved Hide resolved
}

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

ghRepo, _, _ := githubClient.Repositories.Get(ctx, owner, repo)
Kshitij-Katiyar marked this conversation as resolved.
Show resolved Hide resolved
if ghRepo == nil {
return fmt.Sprintf("unknown repository %s", fullNameFromOwnerAndRepo(owner, repo))
}

if _, err := p.store.Set(args.ChannelId+"_"+userInfo.UserID+"-default-repo", []byte(owner+"/"+repo)); err != nil {
Kshitij-Katiyar marked this conversation as resolved.
Show resolved Hide resolved
return "error occurred saving the default repo"
}

repoLink := baseURL + owner + "/" + repo
Kshitij-Katiyar marked this conversation as resolved.
Show resolved Hide resolved
successMsg := fmt.Sprintf("The default repo has been set to [%s/%s](%s)", owner, repo, repoLink)
Kshitij-Katiyar marked this conversation as resolved.
Show resolved Hide resolved

return successMsg
}

func (p *Plugin) GetDefaultRepo(userID string, channelID string) (string, error) {
Kshitij-Katiyar marked this conversation as resolved.
Show resolved Hide resolved
var defaultRepoBytes []byte
if err := p.store.Get(channelID+"_"+userID+"-default-repo", &defaultRepoBytes); err != nil {
Kshitij-Katiyar marked this conversation as resolved.
Show resolved Hide resolved
return "", err
}

return string(defaultRepoBytes), nil
}

func (p *Plugin) handleGetDefaultRepo(_ *plugin.Context, args *model.CommandArgs, parameters []string, 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", "error", err.Error())
Kshitij-Katiyar marked this conversation as resolved.
Show resolved Hide resolved
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(_ *plugin.Context, args *model.CommandArgs, parameters []string, 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", "error", err.Error())
return "error occurred while getting the default repo"
Kshitij-Katiyar marked this conversation as resolved.
Show resolved Hide resolved
}

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

if err := p.store.Delete(args.ChannelId + "_" + userInfo.UserID + "-default-repo"); 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 +972,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 +1045,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) {
Kshitij-Katiyar marked this conversation as resolved.
Show resolved Hide resolved
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.repo.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.repo.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: [],
Kshitij-Katiyar marked this conversation as resolved.
Show resolved Hide resolved
}, 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