88import shutil
99import sys
1010
11- PUBLIC_BOT_ROOT_REPO = f'https:/github.com/root-project-bot/root.git'
11+ BOT_REPO_DIR_NAME = "root_bot"
12+ # This is needed for adding the remote branch to apply the patch deriving from the PR
1213OFFICIAL_ROOT_REPO = 'https://github.com/root-project/root'
14+ OFFICIAL_REPO_DIR_NAME = "root_official"
1315
1416def validateTargetBranches (brs ):
17+ '''
18+ Verify the branches are floating point numbers
19+
20+ :param brs: The branches as passed to the tool
21+ '''
1522 for br in brs :
1623 try :
1724 brf = float (br )
@@ -24,8 +31,6 @@ def parse_args():
2431 parser .add_argument ('--to' , action = 'append' , type = str , help = 'The target branches' )
2532 parser .add_argument ('--comment' , type = str , help = 'Comment that contains the info, e.g "/backport to 6.36, 6.42"' )
2633 parser .add_argument ('--requestor' , type = str , help = 'The requestor of the backport"' )
27- parser .add_argument ('--push-token' , type = str , help = 'The secret to push' )
28- parser .add_argument ('--pr-token' , type = str , help = 'The secret to open a PR' )
2934
3035 args = parser .parse_args ()
3136
@@ -55,9 +60,17 @@ def parse_args():
5560
5661 validateTargetBranches (targetBranches )
5762
58- return (args .push_token , args . pr_token , args . requestor , pullN , targetBranches )
63+ return (args .requestor , pullN , targetBranches )
5964
6065def printFirstMessage (requestor , pullNumber , targetBranches ):
66+ '''
67+ Prints a message useful for debugging, declaring what the script will do
68+ for what branches
69+
70+ :param requestor: The user who would like to prepare a backport
71+ :param pullNumber: The number of the PR to backport
72+ :param targetBranches: The branches onto which the backport has to be prepared
73+ '''
6174 nBranches = len (targetBranches )
6275 targetBranchesStr = ''
6376 plural = ''
@@ -70,132 +83,186 @@ def printFirstMessage(requestor, pullNumber, targetBranches):
7083 requestorInfo = f' requested by { requestor } ' if requestor else ''
7184 printInfo (f'Preparing to backport PR #{ pullNumber } to branch{ plural } { targetBranchesStr } { requestorInfo } ' )
7285
86+ def execCommandOfficialRepo (command ):
87+ '''
88+ Execute a shell command in the official ROOT repo
89+
90+ :param command: The command to execute
91+ '''
92+ return execCommand (command , OFFICIAL_REPO_DIR_NAME )
93+
94+ def execCommandBotRepo (command ):
95+ '''
96+ Execute a shell command in the bot ROOT repo
97+
98+ :param command: The command to execute
99+ '''
100+ return execCommand (command , BOT_REPO_DIR_NAME )
101+
73102def shortBranchToRealBranch (shortBranchName ):
103+ '''
104+ Translate branches of the form X.YZ to the names of the branches in the root repository.
105+ For example, 6.40 will become v6-40-00-patches
106+
107+ :param shortBranchName: The branch name in the form X.YZ, e.g. 6.38
108+ '''
74109 major , minor = shortBranchName .split ('.' )
75110 return f'v{ major } -{ minor } -00-patches'
76111
77- def getPRTitle (pullNumber ):
78- out = execCommand (f'gh pr view { pullNumber } --repo root-project/root' )
79- titleLine = out .split ('\n ' )[0 ]
80- _ , title = titleLine .split ('\t ' )
81- return title
82-
83- def createWorkdir (workdir ):
84- if os .path .exists (workdir ):
85- shutil .rmtree (workdir )
86- os .mkdir (workdir )
87-
88- def fetchPatch (pullNumber ):
89- outname = f'../{ pullNumber } .patch'
90- out = execCommand (f'gh pr diff { pullNumber } --patch > { outname } ' )
91- return os .path .abspath (outname )
92-
93- def parseJson (jsonStr , level1 , level2 ):
112+ class OfficialROOTRepoPR :
94113 '''
95- A json of the type
96- {
97- "labels": [
98- {
99- "id": "MDU6TGFiZWwyMzM2MDQ5MDQw",
100- "name": "affects:master",
101- "description": "",
102- "color": "93e0ea"
103- },
104- {
105- "id": "LA_kwDOAKfCqc8AAAACEWlFOQ",
106- "name" ...
107- is returned.
114+ A class that represents a PR to the Official ROOT repository
108115 '''
109- json_object = json .loads (jsonStr )
110- return ',' .join ([ l [level2 ] for l in json_object [level1 ] ])
116+ def __init__ (self , pullNumber ):
117+ self .pullNumber = pullNumber
118+ PRJsonStr = execCommandOfficialRepo (f'gh pr view { pullNumber } --json labels,assignees,title' )
119+ self .PRJsonObject = json .loads (PRJsonStr )
111120
112- def getPRLabels (pullNumber ):
113- jsonStr = execCommand (f'gh pr view { pullNumber } --json labels' )
114- labels = parseJson (jsonStr , 'labels' , 'name' )
115- labels = f'pr:backport,{ labels } '
116- printInfo (f'The label(s) of PR { pullNumber } is(are) { labels } ' )
117- return labels
121+ def fetchPatch (self ):
122+ '''
123+ Obtain the patch a PR to the root repo by its number.
124+ A file is created called <#PR>.patch
125+
126+ :param pullNumber: The number of the PR to the ROOT repo
127+ '''
128+ outname = os .path .join (os .getcwd (), f'{ self .pullNumber } .patch' )
129+ out = execCommandOfficialRepo (f'gh pr diff { self .pullNumber } --patch > { outname } ' )
130+ return outname
118131
119- def getPRAssignees ( pullNumber ):
120- jsonStr = execCommand ( f'gh pr view { pullNumber } --json assignees' )
121- assignees = parseJson ( jsonStr , 'assignees' , 'login' )
122- printInfo ( f'The assignee(s) of PR { pullNumber } is(are) { assignees } ' )
123- return assignees
132+ def _parseJson ( self , level1Label , level2Label = '' ):
133+ '''
134+ Obtain information from a json of the type returned by the GitHub interface as specified
135+ by the level 1 and 2 labels.
136+ The result is a comma separated list of strings.
124137
125- def authenticate (token ):
126- execCommand (f'gh auth login --with-token' , theInput = token )
138+ A typical json returned has the following structure:
139+ ```
140+ {
141+ "labels": [
142+ {
143+ "id": "MDU6TGFiZWwyMzM2MDQ5MDQw",
144+ "name": "affects:master",
145+ "description": "",
146+ "color": "93e0ea"
147+ },
148+ {
149+ "id": "LA_kwDOAKfCqc8AAAACEWlFOQ",
150+ "name" ...
151+ ```
127152
128- def principal ():
129- push_token , pr_token , requestor , pullNumber , targetBranches = parse_args ()
130- printFirstMessage (requestor , pullNumber , targetBranches )
153+ :param jsonStr: The json as string
154+ :param level1Label: The label of the first level of the json
155+ :param level2Label: The label of the second level of the json
156+ '''
157+ if level2Label != '' :
158+ return ',' .join ([ l [level2Label ] for l in self .PRJsonObject [level1Label ] ])
159+ else :
160+ return self .PRJsonObject [level1Label ]
131161
132- BOT_ROOT_REPO = f'https://x-access-token:{ push_token } @github.com/root-project-bot/root.git'
133-
134- # Get some information about the PR
135- authenticate (push_token )
136- assignees = getPRAssignees (pullNumber )
137- labels = getPRLabels (pullNumber )
138- patchName = fetchPatch (pullNumber )
139- prTitle = getPRTitle (pullNumber )
162+ def getPRTitle (self ):
163+ '''
164+ Obtain the name of a PR to the official ROOT repo by its number.
165+ '''
166+ return self ._parseJson ('title' )
140167
141- # We start the automated procedure, assuming to be in a clean root repo
142- os .chdir ('../' )
143- workdir = f'./workdir_{ pullNumber } '
168+ def getPRLabels (self ):
169+ '''
170+ Get the labels of a PR to the ROOT official repository as a comma separated list.
171+
172+ :param pullNumber: The number of the PR to the ROOT repo
173+ '''
174+ labels = self ._parseJson ('labels' , 'name' )
175+ labels = f'pr:backport,{ labels } '
176+ printInfo (f'The label(s) of PR { self .pullNumber } is(are) { labels } ' )
177+ return labels
178+
179+ def getPRAssignees (self ):
180+ '''
181+ Get the assignees of a PR to the ROOT official repository as a comma separated list.
182+
183+ :param pullNumber: The number of the PR to the ROOT repo
184+ '''
185+ assignees = self ._parseJson ('assignees' , 'login' )
186+ printInfo (f'The assignee(s) of PR { self .pullNumber } is(are) { assignees } ' )
187+ return assignees
188+
189+ def postCommentAfterBP (self , bpPRUrlBranch ):
190+ '''
191+ Post a clear message summarising what backports have been created
192+
193+ :param pullNumber: The number of the PR to the ROOT repo
194+ :param bpPRUrlBranch: The list of backport PRs numbers and branch names
195+ '''
196+ # We now prepare a clear message to post on the PR for which backports have been created...
197+ prComment = 'This PR has been backported to'
198+ if len (bpPRUrlBranch ) == 1 :
199+ prUrl , brName = bpPRUrlBranch [0 ]
200+ prComment += f' branch { brName } : { prUrl } '
201+ else :
202+ for prUrl , brName in bpPRUrlBranch :
203+ prComment += f'\n - Branch { brName } :#{ prUrl } '
204+ # ...and post it
205+ execCommandOfficialRepo (f'gh pr comment { self .pullNumber } --body "{ prComment } "' )
206+ return 0
207+
208+ def principal ():
144209
145- createWorkdir ( workdir )
146- os . chdir ( workdir )
210+ # We first obtain the information from the parser
211+ requestor , pullNumber , targetBranches = parse_args ( )
147212
148- cloneCmd = 'git clone --depth 1'
213+ # We declare what we are about to do, for clarity and debugging purposes
214+ printFirstMessage (requestor , pullNumber , targetBranches )
149215
150- execCommand (cmd = f'{ cloneCmd } { BOT_ROOT_REPO } ' , replace = push_token )
216+ # We get some information about the PR
217+ thePR = OfficialROOTRepoPR (pullNumber )
218+ assignees = thePR .getPRAssignees ()
219+ labels = thePR .getPRLabels ()
220+ patchName = thePR .fetchPatch ()
221+ prTitle = thePR .getPRTitle ()
151222
152- os .chdir ('root' )
153- execCommand (f'git remote add root_upstream { OFFICIAL_ROOT_REPO } ' )
154-
155223 requestorInfo = f', requested by @{ requestor } ' if requestor else ''
156224 bpPRUrlBranch = []
157225
158226 labelSwitch = '' if labels == '' else f'--label "{ labels } "'
159227 assigneesSwitch = '' if assignees == '' else f'--assignee "{ assignees } "'
160228
161- execCommand (f'git config user.email "{ requestor } @no-reply.github.com"' )
162- execCommand (f'git config user.name "{ requestor } "' )
229+ execCommandBotRepo (f'git config user.email "{ requestor } @no-reply.github.com"' )
230+ execCommandBotRepo (f'git config user.name "{ requestor } "' )
231+
232+ # Before looping on the target branches to prepare the backport PRs, we
233+ # need to add the ROOT repo as remote to the bot repo.
234+ if 'root_upstream' in execCommandBotRepo ('git remote' ):
235+ execCommandBotRepo (f'git remote remove root_upstream' )
236+ execCommandBotRepo (f'git remote add root_upstream { OFFICIAL_ROOT_REPO } ' )
163237
164238 # Now we loop on the target branches to create one PR for each of them
165239 for targetBranch in targetBranches :
166- printInfo (f'--------- Backporting PR { pullNumber } to branch { targetBranch } ' )
167- realTargetBranch = shortBranchToRealBranch (targetBranch )
168- bpBranchName = f'BP_{ targetBranch } _pull_{ pullNumber } '
169- execCommand (f'git fetch root_upstream { realTargetBranch } ' )
170- execCommand (f'git checkout { realTargetBranch } ' )
171- execCommand (f'git checkout -b { bpBranchName } ' )
172- execCommand (f'git apply --check { patchName } ' )
173- execCommand (f'git am --keep-cr --signoff < { patchName } ' )
174- execCommand (f'git push --set-upstream origin { bpBranchName } ' )
175- authenticate (pr_token )
176- prUrl = execCommand ('gh pr create --repo root-project/root ' \
177- f'--base { realTargetBranch } ' \
178- f'--head root-project-bot:{ bpBranchName } ' \
179- f'--title "[{ targetBranch } ] { prTitle } " ' \
180- f'--body "Backport of #{ pullNumber } { requestorInfo } " ' \
181- f'{ labelSwitch } { assigneesSwitch } ' \
182- '-d' )
183- bpPRUrlBranch .append ((prUrl ,targetBranch ))
240+ printInfo (f'--------- Backporting PR { pullNumber } to branch { targetBranch } ' )
241+ realTargetBranch = shortBranchToRealBranch (targetBranch )
242+ bpBranchName = f'BP_{ targetBranch } _pull_{ pullNumber } '
243+ execCommandBotRepo (f'git fetch root_upstream { realTargetBranch } ' )
244+ execCommandBotRepo (f'git checkout { realTargetBranch } ' )
245+ if bpBranchName in execCommandBotRepo ('git branch' ):
246+ execCommandBotRepo (f'git branch -D { bpBranchName } ' )
247+ if bpBranchName in execCommandBotRepo ('git ls-remote origin' ):
248+ execCommandBotRepo (f'git push -d origin { bpBranchName } ' )
249+ execCommandBotRepo (f'git checkout -b { bpBranchName } ' )
250+ execCommandBotRepo (f'git apply --check { patchName } ' )
251+ execCommandBotRepo (f'git am --keep-cr --signoff < { patchName } ' )
252+ execCommandBotRepo (f'git push --set-upstream origin { bpBranchName } ' )
253+ prUrl = execCommandBotRepo ('gh pr create --repo root-project/root ' \
254+ f'--base { realTargetBranch } ' \
255+ f'--head root-project-bot:{ bpBranchName } ' \
256+ f'--title "[{ targetBranch } ] { prTitle } " ' \
257+ f'--body "Backport of #{ pullNumber } { requestorInfo } " ' \
258+ f'{ labelSwitch } { assigneesSwitch } ' \
259+ '-d || echo \' \' ' )
260+ bpPRUrlBranch .append ((prUrl ,targetBranch ))
184261
185262 if bpPRUrlBranch == []:
186263 Exception ('No backport succeeded!' )
187264
188- prComment = 'This PR has been backported to'
189- if len (bpPRUrlBranch ) == 1 :
190- prUrl , brName = bpPRUrlBranch [0 ]
191- prComment += f' branch { brName } : { prUrl } '
192- else :
193- for prUrl , brName in bpPRUrlBranch :
194- prComment += f'\n - Branch { brName } :#{ prUrl } '
195-
196- authenticate (pr_token )
197- os .chdir ('../../root' ) # we go back to the original root repo dir
198- execCommand (f'gh pr comment { pullNumber } --body "{ prComment } "' )
265+ return thePR .postCommentAfterBP (bpPRUrlBranch )
199266
200267if __name__ == "__main__" :
201268 sys .exit (principal ())
0 commit comments