1
1
# -*- coding: utf-8 -*-
2
2
from __future__ import absolute_import , print_function , unicode_literals
3
3
import logging
4
+ import yaml
4
5
from .requirements import RequirementsBundle
5
6
from .providers .github import Provider as GithubProvider
6
7
from .errors import NoPermissionError , BranchExistsError
8
+ from .config import Config
7
9
8
10
logger = logging .getLogger (__name__ )
9
11
10
12
11
13
class Bot (object ):
12
14
def __init__ (self , repo , user_token , bot_token = None ,
13
- provider = GithubProvider , bundle = RequirementsBundle ):
15
+ provider = GithubProvider , bundle = RequirementsBundle , config = Config ):
14
16
self .bot_token = bot_token
15
17
self .req_bundle = bundle ()
16
18
self .provider = provider (self .req_bundle )
@@ -23,8 +25,9 @@ def __init__(self, repo, user_token, bot_token=None,
23
25
self ._user_repo = None
24
26
self ._bot = None
25
27
self ._bot_repo = None
28
+ self .config = config ()
26
29
27
- self ._pull_requests = None
30
+ self ._fetched_prs = False
28
31
29
32
@property
30
33
def user_repo (self ):
@@ -52,37 +55,54 @@ def bot_repo(self):
52
55
53
56
@property
54
57
def pull_requests (self ):
55
- if self ._pull_requests is None :
56
- self ._pull_requests = [pr for pr in self .provider .iter_issues (
58
+ if not self ._fetched_prs :
59
+ self .req_bundle . pull_requests = [pr for pr in self .provider .iter_issues (
57
60
repo = self .user_repo , creator = self .bot if self .bot_token else self .user )]
58
- return self ._pull_requests
61
+ self ._fetched_prs = True
62
+ return self .req_bundle .pull_requests
59
63
60
- def update (self , branch = None , initial = True , pin_unpinned = False , close_stale_prs = False ):
61
-
62
- default_branch = self .provider .get_default_branch (repo = self .user_repo )
63
-
64
- if branch is None :
65
- branch = default_branch
64
+ def get_repo_config (self , repo ):
65
+ try :
66
+ content , _ = self .provider .get_file (repo , "/.pyup.yml" , self .config .branch )
67
+ if content is not None :
68
+ return yaml .load (content )
69
+ except yaml .YAMLError :
70
+ logger .warning ("Unable to parse config file /.pyup.yml" , exc_info = True )
71
+ return None
66
72
67
- self .get_all_requirements (branch = branch )
73
+ def configure (self , ** kwargs ):
74
+ # if the branch is not set, get the default branch
75
+ if kwargs .get ("branch" , False ) in [None , False ]:
76
+ self .config .branch = self .provider .get_default_branch (repo = self .user_repo )
77
+ # set the config for this update run
78
+ self .config .update (kwargs )
79
+ repo_config = self .get_repo_config (repo = self .user_repo )
80
+ if repo_config :
81
+ self .config .update (repo_config )
82
+
83
+ def update (self , ** kwargs ):
84
+ """
85
+ Main entrypoint to kick off an update run.
86
+ :param kwargs:
87
+ :return: RequirementsBundle
88
+ """
89
+ self .configure (** kwargs )
90
+ self .get_all_requirements ()
68
91
self .apply_updates (
69
- branch ,
70
- initial = initial ,
71
- pin_unpinned = pin_unpinned ,
72
- close_stale_prs = close_stale_prs
92
+ initial = kwargs .get ("initial" , False ),
73
93
)
74
94
75
95
return self .req_bundle
76
96
77
- def apply_updates (self , branch , initial , pin_unpinned , close_stale_prs = False ):
97
+ def apply_updates (self , initial ):
78
98
79
99
InitialUpdateClass = self .req_bundle .get_initial_update_class ()
80
100
81
101
if initial :
82
102
# get the list of pending updates
83
103
try :
84
104
_ , _ , _ , updates = list (
85
- self .req_bundle .get_updates (initial = initial , pin_unpinned = pin_unpinned )
105
+ self .req_bundle .get_updates (initial = initial , config = self . config )
86
106
)[0 ]
87
107
except IndexError :
88
108
# need to catch the index error here in case the intial update is completely
@@ -107,13 +127,14 @@ def apply_updates(self, branch, initial, pin_unpinned, close_stale_prs=False):
107
127
False
108
128
)
109
129
110
- for title , body , update_branch , updates in self .iter_updates (initial , pin_unpinned ):
130
+ # todo: This block needs to be refactored
131
+ for title , body , update_branch , updates in self .iter_updates (initial ):
111
132
if initial_pr :
112
133
pull_request = initial_pr
113
134
elif title not in [pr .title for pr in self .pull_requests ]:
114
135
pull_request = self .commit_and_pull (
115
136
initial = initial ,
116
- base_branch = branch ,
137
+ base_branch = self . config . branch ,
117
138
new_branch = update_branch ,
118
139
title = title ,
119
140
body = body ,
@@ -124,8 +145,8 @@ def apply_updates(self, branch, initial, pin_unpinned, close_stale_prs=False):
124
145
125
146
for update in updates :
126
147
update .requirement .pull_request = pull_request
127
- if close_stale_prs and pull_request and not initial :
128
- self .close_stale_prs (update , pull_request )
148
+ if self . config . close_prs and pull_request and not initial :
149
+ self .close_stale_prs (update = update , pull_request = pull_request )
129
150
130
151
def close_stale_prs (self , update , pull_request ):
131
152
"""
@@ -138,8 +159,10 @@ def close_stale_prs(self, update, pull_request):
138
159
:param update:
139
160
:param pull_request:
140
161
"""
162
+ logger .info ("Preparing to close stale PRs for {}" .format (pull_request .title ))
141
163
if self .bot_token and pull_request .type != "initial" :
142
164
for pr in self .pull_requests :
165
+ logger .info ("Checking PR {}" .format (pr .title ))
143
166
# check that, the pr is an update, is open, the titles are not equal and that
144
167
# the requirement matches
145
168
if pr .type == "update" and \
@@ -158,53 +181,75 @@ def close_stale_prs(self, update, pull_request):
158
181
comment = "Closing this in favor of #{}" .format (pull_request .number )
159
182
)
160
183
161
- def commit_and_pull (self , initial , base_branch , new_branch , title , body , updates ):
162
-
184
+ def create_branch (self , base_branch , new_branch , delete_empty = False ):
185
+ """
186
+ Creates a new branch.
187
+ :param base_branch: string name of the base branch
188
+ :param new_branch: string name of the new branch
189
+ :param delete_empty: bool -- delete the branch if it is empty
190
+ :return: bool -- True if successfull
191
+ """
192
+ logger .info ("Preparing to create branch {} from {}" .format (new_branch , base_branch ))
163
193
try :
164
194
# create new branch
165
195
self .provider .create_branch (
166
196
base_branch = base_branch ,
167
197
new_branch = new_branch ,
168
198
repo = self .user_repo
169
199
)
200
+ logger .info ("Created branch {} from {}" .format (new_branch , base_branch ))
201
+ return True
170
202
except BranchExistsError :
171
- # instead of failing loud if the branch already exists, we are going to return
172
- # None here and handle this case on a different layer.
173
- return None
174
-
175
- updated_files = {}
176
- for update in self .iter_changes (initial , updates ):
177
- if update .requirement_file .path in updated_files :
178
- sha = updated_files [update .requirement_file .path ]["sha" ]
179
- content = updated_files [update .requirement_file .path ]["content" ]
180
- else :
181
- sha = update .requirement_file .sha
182
- content = update .requirement_file .content
183
- old_content = content
184
- content = update .requirement .update_content (content )
185
- if content != old_content :
186
- new_sha = self .provider .create_commit (
187
- repo = self .user_repo ,
188
- path = update .requirement_file .path ,
189
- branch = new_branch ,
190
- content = content ,
191
- commit_message = update .commit_message ,
192
- sha = sha ,
193
- committer = self .bot if self .bot_token else self .user ,
194
- )
195
- updated_files [update .requirement_file .path ] = {"sha" : new_sha , "content" : content }
196
- else :
197
- logger .error ("Empty commit at {repo}, unable to update {title}." .format (
198
- repo = self .user_repo .full_name , title = title )
199
- )
203
+ logger .info ("Branch {} exists." .format (new_branch ))
204
+ # if the branch exists, is empty and delete_empty is set, delete it and call
205
+ # this function again
206
+ if delete_empty :
207
+ if self .provider .is_empty_branch (self .user_repo , base_branch , new_branch ):
208
+ self .provider .delete_branch (self .user_repo , new_branch )
209
+ logger .info ("Branch {} was empty and has been deleted" .format (new_branch ))
210
+ return self .create_branch (base_branch , new_branch , delete_empty = False )
211
+ logger .info ("Branch {} is not empty" .format (new_branch ))
212
+ return False
200
213
201
- if updated_files :
202
- return self .create_pull_request (
203
- title = title ,
204
- body = body ,
205
- base_branch = base_branch ,
206
- new_branch = new_branch
207
- )
214
+ def commit_and_pull (self , initial , base_branch , new_branch , title , body , updates ):
215
+ logger .info ("Preparing commit {}" .format (title ))
216
+ if self .create_branch (base_branch , new_branch , delete_empty = True ):
217
+ updated_files = {}
218
+ for update in self .iter_changes (initial , updates ):
219
+ if update .requirement_file .path in updated_files :
220
+ sha = updated_files [update .requirement_file .path ]["sha" ]
221
+ content = updated_files [update .requirement_file .path ]["content" ]
222
+ else :
223
+ sha = update .requirement_file .sha
224
+ content = update .requirement_file .content
225
+ old_content = content
226
+ content = update .requirement .update_content (content )
227
+ if content != old_content :
228
+ new_sha = self .provider .create_commit (
229
+ repo = self .user_repo ,
230
+ path = update .requirement_file .path ,
231
+ branch = new_branch ,
232
+ content = content ,
233
+ commit_message = update .commit_message ,
234
+ sha = sha ,
235
+ committer = self .bot if self .bot_token else self .user ,
236
+ )
237
+ updated_files [update .requirement_file .path ] = {"sha" : new_sha ,
238
+ "content" : content }
239
+ else :
240
+ logger .error ("Empty commit at {repo}, unable to update {title}." .format (
241
+ repo = self .user_repo .full_name , title = title )
242
+ )
243
+
244
+ if updated_files :
245
+ pr = self .create_pull_request (
246
+ title = title ,
247
+ body = body ,
248
+ base_branch = base_branch ,
249
+ new_branch = new_branch
250
+ )
251
+ self .pull_requests .append (pr )
252
+ return pr
208
253
return None
209
254
210
255
def create_issue (self , title , body ):
@@ -242,33 +287,39 @@ def create_pull_request(self, title, body, base_branch, new_branch):
242
287
def iter_git_tree (self , branch ):
243
288
return self .provider .iter_git_tree (branch = branch , repo = self .user_repo )
244
289
245
- def iter_updates (self , initial , pin_unpinned ):
246
- return self .req_bundle .get_updates (initial = initial , pin_unpinned = pin_unpinned )
290
+ def iter_updates (self , initial ):
291
+ return self .req_bundle .get_updates (initial = initial , config = self . config )
247
292
248
293
def iter_changes (self , initial , updates ):
249
294
return iter (updates )
250
295
251
296
# if this function gets updated, the gist at https://gist.github.com/jayfk/45862b05836701b49b01
252
297
# needs to be updated too
253
- def get_all_requirements (self , branch ):
254
- for file_type , path in self .iter_git_tree (branch ):
255
- if file_type == "blob" :
256
- if "requirements" in path :
257
- if path .endswith ("txt" ) or path .endswith ("pip" ):
258
- self .add_requirement_file (path , branch )
298
+ def get_all_requirements (self ):
299
+ if self .config .search :
300
+ logger .info ("Searching requirement files" )
301
+ for file_type , path in self .iter_git_tree (self .config .branch ):
302
+ if file_type == "blob" :
303
+ if "requirements" in path :
304
+ if path .endswith ("txt" ) or path .endswith ("pip" ):
305
+ self .add_requirement_file (path )
306
+ for req_file in self .config .requirements :
307
+ self .add_requirement_file (req_file .path )
259
308
260
309
# if this function gets updated, the gist at https://gist.github.com/jayfk/c6509bbaf4429052ca3f
261
310
# needs to be updated too
262
- def add_requirement_file (self , path , branch ):
311
+ def add_requirement_file (self , path ):
312
+ logger .info ("Adding requirement file at {}" .format (path ))
263
313
if not self .req_bundle .has_file_in_path (path ):
264
314
req_file = self .provider .get_requirement_file (
265
- path = path , repo = self .user_repo , branch = branch )
315
+ path = path , repo = self .user_repo , branch = self . config . branch )
266
316
if req_file is not None :
267
317
self .req_bundle .append (req_file )
268
318
for other_file in req_file .other_files :
269
- self .add_requirement_file (other_file , branch )
319
+ self .add_requirement_file (other_file )
270
320
271
321
272
322
class DryBot (Bot ):
273
- def commit_and_pull (self , base_branch , new_branch , title , body , updates ): # pragma: no cover
323
+ def commit_and_pull (self , initial , base_branch , new_branch , title , body ,
324
+ updates ): # pragma: no cover
274
325
return None
0 commit comments