Pull command tracks the right branch
[mdk.git] / mdk / fetch.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 """
5 Moodle Development Kit
6
7 Copyright (c) 2014 Frédéric Massart - FMCorz.net
8
9 This program is free software: you can redistribute it and/or modify
10 it under the terms of the GNU General Public License as published by
11 the Free Software Foundation, either version 3 of the License, or
12 (at your option) any later version.
13
14 This program is distributed in the hope that it will be useful,
15 but WITHOUT ANY WARRANTY; without even the implied warranty of
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 GNU General Public License for more details.
18
19 You should have received a copy of the GNU General Public License
20 along with this program. If not, see <http://www.gnu.org/licenses/>.
21
22 http://github.com/FMCorz/mdk
23 """
24
25 import os
26 import jira
27 import logging
28
29
30 class Fetch(object):
31 """Holds the logic and processing to fetch a remote and following actions into an instance"""
32
33 _M = None
34 _ref = None
35 _repo = None
36
37 _canCreateBranch = True
38 _hasstashed = False
39
40 def __init__(self, M, repo=None, ref=None):
41 self._M = M
42 self._repo = repo
43 self._ref = ref
44
45 def checkout(self):
46 """Fetch and checkout the fetched branch"""
47 self.fetch()
48 logging.info('Checking out branch as FETCH_HEAD')
49 if not self.M.git().checkout('FETCH_HEAD'):
50 raise FetchException('Could not checkout FETCH_HEAD')
51
52 def fetch(self):
53 """Perform the fetch"""
54 if not self.repo:
55 raise FetchException('The repository to fetch from is unknown')
56 elif not self.ref:
57 raise FetchException('The ref to fetch is unknown')
58
59 git = self.M.git()
60 logging.info('Fetching %s from %s' % (self.ref, self.repo))
61 result = git.fetch(remote=self.repo, ref=self.ref)
62 if not result:
63 raise FetchException('Error while fetching %s from %s' % (self.ref, self.repo))
64
65 def _merge(self):
66 """Protected method to merge FETCH_HEAD into the current branch"""
67 logging.info('Merging into current branch')
68 if not self.M.git().merge('FETCH_HEAD'):
69 raise FetchException('Merge failed, resolve the conflicts and commit')
70
71 def pull(self, into=None, track=None):
72 """Fetch and merge the fetched branch into a branch passed as param"""
73 self._stash()
74
75 try:
76 self.fetch()
77 git = self.M.git()
78
79 if into:
80 logging.info('Switching to branch %s' % (into))
81
82 if not git.hasBranch(into):
83 if self.canCreateBranch:
84 if not git.createBranch(into, track=track):
85 raise FetchException('Could not create the branch %s' % (into))
86 else:
87 raise FetchException('Branch %s does not exist and create branch is forbidden' % (into))
88
89 if not git.checkout(into):
90 raise FetchException('Could not checkout branch %s' % (into))
91
92 self._merge()
93
94 except FetchException as e:
95 if self._hasstashed:
96 logging.warning('An error occured. Some files may have been left in your stash.')
97 raise e
98
99 self._unstash()
100
101 def setRef(self, ref):
102 """Set the reference to fetch"""
103 self._ref = ref
104
105 def setRepo(self, repo):
106 """Set the repository to fetch from"""
107 self._repo = repo
108
109 def _stash(self):
110 """Protected method to stash"""
111 stash = self.M.git().stash(untracked=True)
112 if stash[0] != 0:
113 raise FetchException('Error while trying to stash your changes')
114 elif not stash[1].startswith('No local changes'):
115 logging.info('Stashed your local changes')
116 self._hasstashed = True
117
118 def _unstash(self):
119 """Protected method to unstash"""
120 if self._hasstashed:
121 pop = self.M.git().stash(command='pop')
122 if pop[0] != 0:
123 logging.error('An error ocured while unstashing your changes')
124 else:
125 logging.info('Popped the stash')
126 self._hasstashed = False
127
128 @property
129 def canCreateBranch(self):
130 return self._canCreateBranch
131
132 @property
133 def into(self):
134 return self._into
135
136 @property
137 def M(self):
138 return self._M
139
140 @property
141 def ref(self):
142 return self._ref
143
144 @property
145 def repo(self):
146 return self._repo
147
148
149 class FetchTracker(Fetch):
150 """Pretty dodgy implementation of Fetch to work with the tracker.
151
152 If a list of patches is set, we override the git methods to fetch from a remote
153 to use the patches instead. I am not super convinced by this design, but at
154 least the logic to fetch/pull/merge is more or less self contained.
155 """
156
157 _J = None
158 _cache = None
159 _patches = None
160
161 def __init__(self, *args, **kwargs):
162 super(FetchTracker, self).__init__(*args, **kwargs)
163 self._J = jira.Jira()
164 self._cache = {}
165
166 def checkout(self):
167 if not self.patches:
168 return super(FetchTracker, self).checkout()
169
170 self.fetch()
171
172 def fetch(self):
173 if not self.patches:
174 return super(FetchTracker, self).fetch()
175
176 for patch in self.patches:
177 j = 0
178 dest = None
179 while True:
180 downloadedTo = patch.get('filename') + (('.' + str(j)) if j > 0 else '')
181 dest = os.path.join(self.M.get('path'), downloadedTo)
182 j += 1
183 if not os.path.isfile(dest):
184 patch['downloadedTo'] = downloadedTo
185 break
186
187 logging.info('Downloading patch as %s' % (patch.get('downloadedTo')))
188 if not dest or not self.J.download(patch.get('url'), dest):
189 raise FetchTrackerException('Failed to download the patch to %s' % (dest))
190
191
192 def _merge(self):
193 if not self.patches:
194 return super(FetchTracker, self)._merge()
195
196 patchList = [patch.get('downloadedTo') for patch in self.patches]
197 git = self.M.git()
198 if not git.apply(patchList):
199 raise FetchTrackerException('Could not apply the patch(es), please apply manually')
200 else:
201 for f in patchList:
202 os.remove(f)
203 logging.info('Patches applied successfully')
204
205 def getPullInfo(self, mdl):
206 """Return the pull information
207
208 This implements its own local cache because we could potentially
209 call it multiple times during the same request. This is bad though.
210 """
211 if not self._cache.has_key(mdl):
212 issueInfo = self.J.getPullInfo(mdl)
213 self._cache[mdl] = issueInfo
214 return self._cache[mdl]
215
216 def setFromTracker(self, mdl, branch):
217 """Sets the repo and ref according to the tracker information"""
218 issueInfo = self.getPullInfo(mdl)
219
220 repo = issueInfo.get('repo', None)
221 if not repo:
222 raise FetchTrackerRepoException('Missing information about the repository to pull from on %s' % (mdl))
223
224 ref = issueInfo.get('branches').get(str(branch), None)
225 if not ref:
226 raise FetchTrackerBranchException('Could not find branch info on %s' % (str(branch), mdl))
227
228 self.setRepo(repo)
229 self.setRef(ref.get('branch'))
230
231 def usePatches(self, patches):
232 """List of patches (returned by jira.Jira.getAttachments) to work with instead of the standard repo and ref"""
233 self._patches = patches
234
235 @property
236 def J(self):
237 return self._J
238
239 @property
240 def patches(self):
241 return self._patches
242
243
244 class FetchException(Exception):
245 pass
246
247
248 class FetchTrackerException(FetchException):
249 pass
250
251
252 class FetchTrackerBranchException(FetchTrackerException):
253 pass
254
255
256 class FetchTrackerRepoException(FetchTrackerException):
257 pass