Skip to content

Commit 29da42c

Browse files
[MM-611]: Added the feature to select the default repository for the channel (#806)
* [MM-611]: Added the feature to select the default repository for the channel per User * [MM-611]: Updated the help text * [MM-611]: review fixes * [MM-611]: review fixes * [MM-611]: review fixes * [MM-611]: Fixed the lint issue * [MM-611]: resolved map accessing empty array error * [MM-611]: review fixes * updated json for repo data
1 parent 6f5c7bf commit 29da42c

File tree

10 files changed

+221
-22
lines changed

10 files changed

+221
-22
lines changed

server/plugin/api.go

Lines changed: 60 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ const (
3636

3737
requestTimeout = 30 * time.Second
3838
oauthCompleteTimeout = 2 * time.Minute
39+
40+
channelIDParam = "channelId"
3941
)
4042

4143
type OAuthState struct {
@@ -54,6 +56,18 @@ func (e *APIErrorResponse) Error() string {
5456
return e.Message
5557
}
5658

59+
type RepoResponse struct {
60+
Name string `json:"name,omitempty"`
61+
FullName string `json:"full_name,omitempty"`
62+
Permissions map[string]bool `json:"permissions,omitempty"`
63+
}
64+
65+
// Only send down fields to client that are needed
66+
type RepositoryResponse struct {
67+
DefaultRepo RepoResponse `json:"defaultRepo,omitempty"`
68+
Repos []RepoResponse `json:"repos,omitempty"`
69+
}
70+
5771
type PRDetails struct {
5872
URL string `json:"url"`
5973
Number int `json:"number"`
@@ -1354,10 +1368,26 @@ func (p *Plugin) getRepositoryListByOrg(c context.Context, ghInfo *GitHubUserInf
13541368
return allRepos, http.StatusOK, nil
13551369
}
13561370

1371+
func getRepository(c context.Context, org string, repo string, githubClient *github.Client) (*github.Repository, error) {
1372+
repository, _, err := githubClient.Repositories.Get(c, org, repo)
1373+
if err != nil {
1374+
return nil, err
1375+
}
1376+
1377+
return repository, nil
1378+
}
1379+
13571380
func (p *Plugin) getRepositories(c *UserContext, w http.ResponseWriter, r *http.Request) {
13581381
githubClient := p.githubConnectUser(c.Context.Ctx, c.GHInfo)
13591382
org := p.getConfiguration().GitHubOrg
13601383

1384+
channelID := r.URL.Query().Get(channelIDParam)
1385+
if channelID == "" {
1386+
p.client.Log.Warn("Bad request: missing channelId")
1387+
p.writeAPIError(w, &APIErrorResponse{Message: "Bad request: missing channelId", StatusCode: http.StatusBadRequest})
1388+
return
1389+
}
1390+
13611391
var allRepos []*github.Repository
13621392
var err error
13631393

@@ -1395,18 +1425,38 @@ func (p *Plugin) getRepositories(c *UserContext, w http.ResponseWriter, r *http.
13951425
}
13961426
}
13971427

1398-
// Only send down fields to client that are needed
1399-
type RepositoryResponse struct {
1400-
Name string `json:"name,omitempty"`
1401-
FullName string `json:"full_name,omitempty"`
1402-
Permissions map[string]bool `json:"permissions,omitempty"`
1428+
repoResp := make([]RepoResponse, len(allRepos))
1429+
for i, r := range allRepos {
1430+
repoResp[i].Name = r.GetName()
1431+
repoResp[i].FullName = r.GetFullName()
1432+
repoResp[i].Permissions = r.GetPermissions()
14031433
}
14041434

1405-
resp := make([]RepositoryResponse, len(allRepos))
1406-
for i, r := range allRepos {
1407-
resp[i].Name = r.GetName()
1408-
resp[i].FullName = r.GetFullName()
1409-
resp[i].Permissions = r.GetPermissions()
1435+
resp := RepositoryResponse{
1436+
Repos: repoResp,
1437+
}
1438+
1439+
defaultRepo, dErr := p.GetDefaultRepo(c.GHInfo.UserID, channelID)
1440+
if dErr != nil {
1441+
c.Log.WithError(dErr).Warnf("Failed to get the default repo for the channel. UserID: %s. ChannelID: %s", c.GHInfo.UserID, channelID)
1442+
}
1443+
1444+
if defaultRepo != "" {
1445+
config := p.getConfiguration()
1446+
baseURL := config.getBaseURL()
1447+
owner, repo := parseOwnerAndRepo(defaultRepo, baseURL)
1448+
defaultRepository, err := getRepository(c.Ctx, owner, repo, githubClient)
1449+
if err != nil {
1450+
c.Log.WithError(err).Warnf("Failed to get the default repo %s/%s", owner, repo)
1451+
}
1452+
1453+
if defaultRepository != nil {
1454+
resp.DefaultRepo = RepoResponse{
1455+
Name: *defaultRepository.Name,
1456+
FullName: *defaultRepository.FullName,
1457+
Permissions: defaultRepository.Permissions,
1458+
}
1459+
}
14101460
}
14111461

14121462
p.writeJSON(w, resp)

server/plugin/command.go

Lines changed: 122 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ const (
4141
PerPageValue = 50
4242
)
4343

44+
const DefaultRepoKey string = "%s_%s-default-repo"
45+
4446
var validFeatures = map[string]bool{
4547
featureIssueCreation: true,
4648
featureIssues: true,
@@ -127,7 +129,7 @@ func (p *Plugin) getCommand(config *Configuration) (*model.Command, error) {
127129
return &model.Command{
128130
Trigger: "github",
129131
AutoComplete: true,
130-
AutoCompleteDesc: "Available commands: connect, disconnect, todo, subscriptions, issue, me, mute, settings, help, about",
132+
AutoCompleteDesc: "Available commands: connect, disconnect, todo, subscriptions, issue, default-repo, me, mute, settings, help, about",
131133
AutoCompleteHint: "[command]",
132134
AutocompleteData: getAutocompleteData(config),
133135
AutocompleteIconData: iconData,
@@ -743,6 +745,110 @@ func (p *Plugin) handleIssue(_ *plugin.Context, args *model.CommandArgs, paramet
743745
}
744746
}
745747

748+
func (p *Plugin) handleDefaultRepo(c *plugin.Context, args *model.CommandArgs, parameters []string, userInfo *GitHubUserInfo) string {
749+
if len(parameters) == 0 {
750+
return "Invalid action. Available actions are 'set', 'get' and 'unset'."
751+
}
752+
753+
command := parameters[0]
754+
parameters = parameters[1:]
755+
756+
switch {
757+
case command == "set":
758+
return p.handleSetDefaultRepo(args, parameters, userInfo)
759+
case command == "get":
760+
return p.handleGetDefaultRepo(args, userInfo)
761+
case command == "unset":
762+
return p.handleUnSetDefaultRepo(args, userInfo)
763+
default:
764+
return fmt.Sprintf("Unknown subcommand %v", command)
765+
}
766+
}
767+
768+
func (p *Plugin) handleSetDefaultRepo(args *model.CommandArgs, parameters []string, userInfo *GitHubUserInfo) string {
769+
if len(parameters) == 0 {
770+
return "Please specify a repository."
771+
}
772+
773+
repo := parameters[0]
774+
config := p.getConfiguration()
775+
baseURL := config.getBaseURL()
776+
owner, repo := parseOwnerAndRepo(repo, baseURL)
777+
if owner == "" || repo == "" {
778+
return "Please provide a valid repository"
779+
}
780+
781+
owner = strings.ToLower(owner)
782+
repo = strings.ToLower(repo)
783+
784+
if config.GitHubOrg != "" && strings.ToLower(config.GitHubOrg) != owner {
785+
return fmt.Sprintf("Repository is not part of the locked Github organization. Locked Github organization: %s", config.GitHubOrg)
786+
}
787+
788+
ctx := context.Background()
789+
githubClient := p.githubConnectUser(ctx, userInfo)
790+
791+
ghRepo, _, err := githubClient.Repositories.Get(ctx, owner, repo)
792+
if err != nil {
793+
return "Error occurred while getting github repository details"
794+
}
795+
if ghRepo == nil {
796+
return fmt.Sprintf("Unknown repository %s", fullNameFromOwnerAndRepo(owner, repo))
797+
}
798+
799+
if _, err := p.store.Set(fmt.Sprintf(DefaultRepoKey, args.ChannelId, userInfo.UserID), []byte(fmt.Sprintf("%s/%s", owner, repo))); err != nil {
800+
return "Error occurred saving the default repo"
801+
}
802+
803+
repoLink := fmt.Sprintf("%s%s/%s", baseURL, owner, repo)
804+
successMsg := fmt.Sprintf("The default repo has been set to [%s/%s](%s) for this channel", owner, repo, repoLink)
805+
806+
return successMsg
807+
}
808+
809+
func (p *Plugin) GetDefaultRepo(userID, channelID string) (string, error) {
810+
var defaultRepoBytes []byte
811+
if err := p.store.Get(fmt.Sprintf(DefaultRepoKey, channelID, userID), &defaultRepoBytes); err != nil {
812+
return "", err
813+
}
814+
815+
return string(defaultRepoBytes), nil
816+
}
817+
818+
func (p *Plugin) handleGetDefaultRepo(args *model.CommandArgs, userInfo *GitHubUserInfo) string {
819+
defaultRepo, err := p.GetDefaultRepo(userInfo.UserID, args.ChannelId)
820+
if err != nil {
821+
p.client.Log.Warn("Not able to get the default repo", "UserID", userInfo.UserID, "ChannelID", args.ChannelId, "Error", err.Error())
822+
return "Error occurred while getting the default repo"
823+
}
824+
825+
if defaultRepo == "" {
826+
return "You have not set a default repository for this channel"
827+
}
828+
829+
config := p.getConfiguration()
830+
repoLink := config.getBaseURL() + defaultRepo
831+
return fmt.Sprintf("The default repository is [%s](%s)", defaultRepo, repoLink)
832+
}
833+
834+
func (p *Plugin) handleUnSetDefaultRepo(args *model.CommandArgs, userInfo *GitHubUserInfo) string {
835+
defaultRepo, err := p.GetDefaultRepo(userInfo.UserID, args.ChannelId)
836+
if err != nil {
837+
p.client.Log.Warn("Not able to get the default repo", "UserID", userInfo.UserID, "ChannelID", args.ChannelId, "Error", err.Error())
838+
return "Error occurred while getting the default repo"
839+
}
840+
841+
if defaultRepo == "" {
842+
return "You have not set a default repository for this channel"
843+
}
844+
845+
if err := p.store.Delete(fmt.Sprintf(DefaultRepoKey, args.ChannelId, userInfo.UserID)); err != nil {
846+
return "Error occurred while unsetting the repo for this channel"
847+
}
848+
849+
return "The default repository has been unset successfully"
850+
}
851+
746852
func (p *Plugin) handleSetup(_ *plugin.Context, args *model.CommandArgs, parameters []string) string {
747853
userID := args.UserId
748854
isSysAdmin, err := p.isAuthorizedSysAdmin(userID)
@@ -912,7 +1018,7 @@ func getAutocompleteData(config *Configuration) *model.AutocompleteData {
9121018
return github
9131019
}
9141020

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

9171023
connect := model.NewAutocompleteData("connect", "", "Connect your Mattermost account to your GitHub account")
9181024
if config.EnablePrivateRepo {
@@ -985,6 +1091,20 @@ func getAutocompleteData(config *Configuration) *model.AutocompleteData {
9851091

9861092
github.AddCommand(issue)
9871093

1094+
defaultRepo := model.NewAutocompleteData("default-repo", "[command]", "Available commands: set, get, unset")
1095+
defaultRepoSet := model.NewAutocompleteData("set", "[owner/repo]", "Set the default repository for the channel")
1096+
defaultRepoSet.AddTextArgument("Owner/repo to set as a default repository", "[owner/repo]", "")
1097+
1098+
defaultRepoGet := model.NewAutocompleteData("get", "", "Get the default repository already set for the channel")
1099+
1100+
defaultRepoDelete := model.NewAutocompleteData("unset", "", "Unset the default repository set for the channel")
1101+
1102+
defaultRepo.AddCommand(defaultRepoSet)
1103+
defaultRepo.AddCommand(defaultRepoGet)
1104+
defaultRepo.AddCommand(defaultRepoDelete)
1105+
1106+
github.AddCommand(defaultRepo)
1107+
9881108
me := model.NewAutocompleteData("me", "", "Display the connected GitHub account")
9891109
github.AddCommand(me)
9901110

server/plugin/plugin.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ func NewPlugin() *Plugin {
129129
"": p.handleHelp,
130130
"settings": p.handleSettings,
131131
"issue": p.handleIssue,
132+
"default-repo": p.handleDefaultRepo,
132133
}
133134

134135
p.createGithubEmojiMap()

server/plugin/template.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -461,7 +461,11 @@ Reviewers: {{range $i, $el := .RequestedReviewers -}} {{- if $i}}, {{end}}{{temp
461461
" * `/github mute list` - list your muted GitHub users\n" +
462462
" * `/github mute add [username]` - add a GitHub user to your muted list\n" +
463463
" * `/github mute delete [username]` - remove a GitHub user from your muted list\n" +
464-
" * `/github mute delete-all` - unmute all GitHub users\n"))
464+
" * `/github mute delete-all` - unmute all GitHub users\n" +
465+
"* `/github default-repo` - Manage the default repository per channel for the user. The default repository will be auto selected for creating the issues\n" +
466+
" * `/github default-repo set owner[/repo]` - set the default repo for the channel\n" +
467+
" * `/github default-repo get` - get the default repo for the channel\n" +
468+
" * `/github default-repo unset` - unset the default repo for the channel\n"))
465469

466470
template.Must(masterTemplate.New("newRepoStar").Funcs(funcMap).Parse(`
467471
{{template "repo" .GetRepo}}

webapp/src/actions/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,11 @@ export function getReviewsDetails(prList: PrsDetailsData[]) {
7070
};
7171
}
7272

73-
export function getRepos() {
73+
export function getRepos(channelId: string) {
7474
return async (dispatch: DispatchFunc) => {
7575
let data;
7676
try {
77-
data = await Client.getRepositories();
77+
data = await Client.getRepositories(channelId);
7878
} catch (error) {
7979
dispatch({
8080
type: ActionTypes.RECEIVED_REPOSITORIES,

webapp/src/client/client.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ export default class Client {
3131
return this.doPost(`${this.url}/user`, {user_id: userID});
3232
}
3333

34-
getRepositories = async () => {
35-
return this.doGet(`${this.url}/repositories`);
34+
getRepositories = async (channelId) => {
35+
return this.doGet(`${this.url}/repositories?channelId=${channelId}`);
3636
}
3737

3838
getLabels = async (repo) => {

webapp/src/components/github_repo_selector/github_repo_selector.jsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@ const initialState = {
1313

1414
export default class GithubRepoSelector extends PureComponent {
1515
static propTypes = {
16-
yourRepos: PropTypes.array.isRequired,
16+
yourRepos: PropTypes.object.isRequired,
1717
theme: PropTypes.object.isRequired,
1818
onChange: PropTypes.func.isRequired,
1919
value: PropTypes.string,
20+
currentChannelId: PropTypes.string,
2021
addValidate: PropTypes.func,
2122
removeValidate: PropTypes.func,
2223
actions: PropTypes.shape({
@@ -30,16 +31,24 @@ export default class GithubRepoSelector extends PureComponent {
3031
}
3132

3233
componentDidMount() {
33-
this.props.actions.getRepos();
34+
this.props.actions.getRepos(this.props.currentChannelId);
35+
}
36+
37+
componentDidUpdate() {
38+
const defaultRepo = this.props.yourRepos.defaultRepo;
39+
40+
if (!(this.props.value) && defaultRepo?.full_name) {
41+
this.onChange(defaultRepo.name, defaultRepo.full_name);
42+
}
3443
}
3544

3645
onChange = (_, name) => {
37-
const repo = this.props.yourRepos.find((r) => r.full_name === name);
46+
const repo = this.props.yourRepos.repos.find((r) => r.full_name === name);
3847
this.props.onChange({name, permissions: repo.permissions});
3948
}
4049

4150
render() {
42-
const repoOptions = this.props.yourRepos.map((item) => ({value: item.full_name, label: item.full_name}));
51+
const repoOptions = this.props.yourRepos.repos.map((item) => ({value: item.full_name, label: item.full_name}));
4352

4453
return (
4554
<div className={'form-group x3'}>

webapp/src/components/github_repo_selector/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import {connect} from 'react-redux';
55
import {bindActionCreators} from 'redux';
66

7+
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
8+
79
import manifest from '@/manifest';
810

911
import {getRepos} from '../../actions';
@@ -13,6 +15,7 @@ import GithubRepoSelector from './github_repo_selector.jsx';
1315
function mapStateToProps(state) {
1416
return {
1517
yourRepos: state[`plugins-${manifest.id}`].yourRepos,
18+
currentChannelId: getCurrentChannelId(state),
1619
};
1720
}
1821

webapp/src/reducers/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,9 @@ function sidebarContent(state = {
106106
}
107107
}
108108

109-
function yourRepos(state: YourReposData[] = [], action: {type: string, data: YourReposData[]}) {
109+
function yourRepos(state: YourReposData = {
110+
repos: [],
111+
}, action: {type: string, data: YourReposData}) {
110112
switch (action.type) {
111113
case ActionTypes.RECEIVED_REPOSITORIES:
112114
return action.data;

webapp/src/types/github_types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,17 @@ export type GithubIssueData = {
8989
repository_url: string;
9090
}
9191

92+
export type DefaultRepo = {
93+
name: string;
94+
full_name: string;
95+
}
96+
9297
export type YourReposData = {
98+
defaultRepo?: DefaultRepo;
99+
repos: ReposData[];
100+
}
101+
102+
export type ReposData = {
93103
name: string;
94104
full_name: string;
95105
}

0 commit comments

Comments
 (0)