0fa122adb0472c6bbe8fbda9eb4cc3aaf3e08f65
[mw] / src / mw / clicommands.py
1 ###
2 # mw - VCS-like nonsense for MediaWiki websites
3 # Copyright (C) 2011  Ian Weller <ian@ianweller.org> and others
4 #
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 2 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License along
16 # with this program.  If not, see <http://www.gnu.org/licenses/>.
17 ###
18
19 import codecs
20 import cookielib
21 import getpass
22 import hashlib
23 import mw.metadir
24 from optparse import OptionParser, OptionGroup
25 import os
26 import simplemediawiki
27 import sys
28 import time
29
30
31 class CommandBase(object):
32
33     def __init__(self, name, description, usage=None):
34         self.me = os.path.basename(sys.argv[0])
35         self.description = description
36         if usage is None:
37             usage = '%prog ' + name
38         else:
39             usage = '%%prog %s %s' % (name, usage)
40         self.parser = OptionParser(usage=usage, description=description)
41         self.name = name
42         self.metadir = mw.metadir.Metadir()
43         self.shortcuts = []
44
45     def main(self):
46         (self.options, self.args) = self.parser.parse_args()
47         self.args = self.args[1:]  # don't need the first thing
48         self._do_command()
49
50     def _do_command(self):
51         pass
52
53     def _login(self):
54         user = raw_input('Username: ')
55         passwd = getpass.getpass()
56         result = self.api.call({'action': 'login',
57                                 'lgname': user,
58                                 'lgpassword': passwd})
59         if result['login']['result'] == 'Success':
60             # cookies are saved to a file
61             print 'Login successful! (yay)'
62         elif result['login']['result'] == 'NeedToken':
63             print 'Login with token'
64             result = self.api.call({'action': 'login',
65                                     'lgname': user,
66                                     'lgpassword': passwd,
67                                     'lgtoken': result['login']['token']})
68             if result['login']['result'] == 'Success':
69                 print 'Login successful! (yay)'
70             else:
71                 print 'Login failed: %s' % result['login']['result']
72         else:
73             print 'Login failed: %s' % result['login']['result']
74
75     def _die_if_no_init(self):
76         if self.metadir.config is None:
77             print '%s: not a mw repo' % self.me
78             sys.exit(1)
79
80     def _api_setup(self):
81         cookie_filename = os.path.join(self.metadir.location, 'cookies')
82         self.api_url = self.metadir.config.get('remote', 'api_url')
83         self.api = simplemediawiki.MediaWiki(self.api_url,
84                                              cookie_file=cookie_filename)
85
86
87 class InitCommand(CommandBase):
88
89     def __init__(self):
90         usage = 'API_URL'
91         CommandBase.__init__(self, 'init', 'start a mw repo', usage)
92
93     def _do_command(self):
94         if len(self.args) < 1:
95             self.parser.error('must have URL to remote api.php')
96         elif len(self.args) > 1:
97             self.parser.error('too many arguments')
98         self.metadir.create(self.args[0])
99
100
101 class LoginCommand(CommandBase):
102
103     def __init__(self):
104         CommandBase.__init__(self, 'login', 'authenticate with wiki')
105
106     def _do_command(self):
107         self._die_if_no_init()
108         self._api_setup()
109         self._login()
110
111
112 class LogoutCommand(CommandBase):
113
114     def __init__(self):
115         CommandBase.__init__(self, 'logout', 'forget authentication')
116
117     def _do_command(self):
118         self._die_if_no_init()
119         try:
120             os.unlink(os.path.join(self.metadir.location, 'cookies'))
121         except OSError:
122             pass
123
124
125 class PullCategoryMembersCommand(CommandBase):
126
127     def __init__(self):
128         usage = '[options] PAGENAME ...'
129         CommandBase.__init__(self, 'pullcat', 'add remote pages to repo '
130                              'belonging to the given category', usage)
131
132     def _do_command(self):
133         self._die_if_no_init()
134         self._api_setup()
135         pages = []
136         pages += self.args
137         for these_pages in [pages[i:i + 25] for i in range(0, len(pages), 25)]:
138             data = {
139                 'action': 'query',
140                 'gcmtitle': '|'.join(these_pages),
141                 'generator': 'categorymembers',
142                 'gcmlimit': 500
143             }
144         response = self.api.call(data)['query']['pages']
145         for pageid in response.keys():
146             pagename = response[pageid]['title']
147             print pagename
148             pullc = PullCommand()
149             pullc.args = [pagename.encode('utf-8')]
150             pullc._do_command()
151
152
153 class PullCommand(CommandBase):
154     
155     def __init__(self):
156         usage = '[options] PAGENAME ...'
157         CommandBase.__init__(self, 'pull', 'add remote pages to repo', usage)
158
159     def _do_command(self):
160         self._die_if_no_init()
161         self._api_setup()
162         pages = []
163         pages += self.args
164
165         # Pull should work with pagename, filename, or working directory
166         converted_pages = []
167         if pages == []:
168             pages = self.metadir.working_dir_status().keys()
169         for pagename in pages:
170             if '.wiki' in pagename:
171                 converted_pages.append(
172                     mw.metadir.filename_to_pagename(pagename[:-5]))
173             else:
174                 converted_pages.append(pagename)
175         pages = converted_pages
176
177         for these_pages in [pages[i:i + 25] for i in range(0, len(pages), 25)]: # ?
178             data = {
179                     'action': 'query',
180                     'titles': '|'.join(these_pages),
181                     'prop': 'info|revisions',
182                     'rvprop': 'ids|flags|timestamp|user|comment|content',
183             }
184             response = self.api.call(data)['query']['pages']
185             for pageid in response.keys():
186                 pagename = response[pageid]['title']
187                 # if pagename exists as file and its status is 'M' warn not pulled
188                 status = self.metadir.working_dir_status()
189                 filename = mw.metadir.pagename_to_filename(pagename)
190                 if filename + '.wiki' in status and \
191                     status[filename + '.wiki' ] in ['M']:
192                     print('%s: "%s" has uncommitted modifications ' 
193                         '-- skipping!' % (self.me, pagename))
194                     continue
195                 if 'missing' in response[pageid].keys():
196                     print '%s: %s: page does not exist, file not created' % \
197                             (self.me, pagename)
198                     continue
199                 revids = [x['revid'] for x in response[pageid]['revisions']]
200                 revids.sort()
201                 self.metadir.pagedict_add(pagename, pageid, revids[-1])
202                 self.metadir.pages_add_rv(int(pageid),
203                                           response[pageid]['revisions'][0])
204                 with file(os.path.join(self.metadir.root, filename + '.wiki'),
205                           'w') as fd:
206                     data = response[pageid]['revisions'][0]['*']
207                     data = data.encode('utf-8')
208                     fd.write(data)
209
210
211 class StatusCommand(CommandBase):
212
213     def __init__(self):
214         CommandBase.__init__(self, 'status', 'check repo status')
215         self.shortcuts.append('st')
216
217     def _do_command(self):
218         self._die_if_no_init()
219         status = self.metadir.working_dir_status()
220         for filename in status:
221             print '%s %s' % (status[filename], filename)
222
223
224 class DiffCommand(CommandBase):
225
226     def __init__(self):
227         CommandBase.__init__(self, 'diff', 'diff wiki to working directory')
228
229     def _do_command(self):
230         self._die_if_no_init()
231         status = self.metadir.working_dir_status()
232         for filename in status:
233             if status[filename] == 'M':
234                 print self.metadir.diff_rv_to_working(
235                         mw.metadir.filename_to_pagename(filename[:-5])),
236
237
238 class CommitCommand(CommandBase):
239
240     def __init__(self):
241         usage = '[FILES]'
242         CommandBase.__init__(self, 'commit', 'commit changes to wiki', usage)
243         self.shortcuts.append('ci')
244         self.parser.add_option('-m', '--message', dest='edit_summary',
245                                help='don\'t prompt for edit summary and '
246                                'use this instead')
247         self.parser.add_option('-b', '--bot', dest='bot', action='store_true',
248                                help='mark actions as a bot (won\'t affect '
249                                'anything if you don\'t have the bot right',
250                                default=False)
251
252     def _do_command(self):
253         self._die_if_no_init()
254         self._api_setup()
255         status = self.metadir.working_dir_status(files=self.args)
256         nothing_to_commit = True
257         for filename in status:
258             print '%s %s' % (status[filename], filename)
259             if status[filename] in ['M']:
260                 nothing_to_commit = False
261         if nothing_to_commit:
262             print 'nothing to commit'
263             sys.exit()
264         if self.options.edit_summary == None:
265             print 'Edit summary:',
266             edit_summary = raw_input()
267         else:
268             edit_summary = self.options.edit_summary
269         for file_num, filename in enumerate(status):
270             if status[filename] in ['M']:
271                 # get edit token
272                 data = {
273                         'action': 'query',
274                         'prop': 'info|revisions',
275                         'intoken': 'edit',
276                         'titles': mw.metadir.filename_to_pagename(filename[:-5]),
277                 }
278                 response = self.api.call(data)
279                 pages = response['query']['pages']
280                 pageid = pages.keys()[0]
281                 revid = pages[pageid]['revisions'][0]['revid']
282                 awaitedrevid = \
283                         self.metadir.pages_get_rv_list({'id': pageid})[0]
284                 if revid != awaitedrevid:
285                     print 'warning: edit conflict detected on %s (%s -> %s) ' \
286                             '-- skipping!' % (file, awaitedrevid, revid)
287                     continue
288                 edittoken = pages[pageid]['edittoken']
289                 filename = os.path.join(self.metadir.root, filename)
290                 text = codecs.open(filename, 'r', 'utf-8').read()
291                 text = text.encode('utf-8')
292                 if (len(text) != 0) and (text[-1] == '\n'):
293                     text = text[:-1]
294                 md5 = hashlib.md5()
295                 md5.update(text)
296                 textmd5 = md5.hexdigest()
297                 data = {
298                         'action': 'edit',
299                         'title': mw.metadir.filename_to_pagename(filename[:-5]),
300                         'token': edittoken,
301                         'text': text,
302                         'md5': textmd5,
303                         'summary': edit_summary,
304                 }
305                 if self.options.bot:
306                     data['bot'] = 'bot'
307                 response = self.api.call(data)
308                 if response['edit']['result'] == 'Success':
309                     if 'nochange' in response['edit']:
310                         print 'warning: no changes detected in %s - ' \
311                                 'skipping and removing ending LF' % filename
312                         self.metadir.clean_page(filename[:-5])
313                         continue
314                     if response['edit']['oldrevid'] != revid:
315                         print 'warning: edit conflict detected on %s -- ' \
316                                 'skipping!' % filename
317                         continue
318                     data = {
319                             'action': 'query',
320                             'revids': response['edit']['newrevid'],
321                             'prop': 'info|revisions',
322                             'rvprop':
323                                     'ids|flags|timestamp|user|comment|content',
324                     }
325                     response = self.api.call(data)['query']['pages']
326                     self.metadir.pages_add_rv(int(pageid),
327                                               response[pageid]['revisions'][0])
328                     if file_num != len(status) - 1:
329                         print 'waiting 3s before processing the next file'
330                         time.sleep(3)
331                 else:
332                     print 'error: committing %s failed: %s' % \
333                             (filename, response['edit']['result'])

Benjamin Mako Hill || Want to submit a patch?