Fixes a bug, a performance issue, and use 'M' instead of 'U' to be consistent with...
[mw] / src / mw / clicommands.py
1 ###
2 # mw - VCS-like nonsense for MediaWiki websites
3 # Copyright (C) 2010  Ian Weller <ian@ianweller.org>
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_file = 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_file)
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         for these_pages in [pages[i:i + 25] for i in range(0, len(pages), 25)]:
165             data = {
166                     'action': 'query',
167                     'titles': '|'.join(these_pages),
168                     'prop': 'info|revisions',
169                     'rvprop': 'ids|flags|timestamp|user|comment|content',
170             }
171             response = self.api.call(data)['query']['pages']
172             for pageid in response.keys():
173                 pagename = response[pageid]['title']
174                 if 'missing' in response[pageid].keys():
175                     print '%s: %s: page does not exist, file not created' % \
176                             (self.me, pagename)
177                     continue
178                 revids = [x['revid'] for x in response[pageid]['revisions']]
179                 revids.sort()
180                 self.metadir.pagedict_add(pagename, pageid, revids[-1])
181                 self.metadir.pages_add_rv(int(pageid),
182                                           response[pageid]['revisions'][0])
183                 filename = mw.metadir.pagename_to_filename(pagename)
184                 with file(os.path.join(self.metadir.root, filename + '.wiki'),
185                           'w') as fd:
186                     data = response[pageid]['revisions'][0]['*']
187                     data = data.encode('utf-8')
188                     fd.write(data)
189
190
191 class StatusCommand(CommandBase):
192
193     def __init__(self):
194         CommandBase.__init__(self, 'status', 'check repo status')
195         self.shortcuts.append('st')
196
197     def _do_command(self):
198         self._die_if_no_init()
199         status = self.metadir.working_dir_status()
200         for file in status:
201             print '%s %s' % (status[file], file)
202
203
204 class DiffCommand(CommandBase):
205
206     def __init__(self):
207         CommandBase.__init__(self, 'diff', 'diff wiki to working directory')
208
209     def _do_command(self):
210         self._die_if_no_init()
211         status = self.metadir.working_dir_status()
212         for file in status:
213             if status[file] == 'M':
214                 print self.metadir.diff_rv_to_working(
215                         mw.metadir.filename_to_pagename(file[:-5])),
216
217
218 class CommitCommand(CommandBase):
219
220     def __init__(self):
221         usage = '[FILES]'
222         CommandBase.__init__(self, 'commit', 'commit changes to wiki', usage)
223         self.shortcuts.append('ci')
224         self.parser.add_option('-m', '--message', dest='edit_summary',
225                                help='don\'t prompt for edit summary and '
226                                'use this instead')
227         self.parser.add_option('-b', '--bot', dest='bot', action='store_true',
228                                help='mark actions as a bot (won\'t affect '
229                                'anything if you don\'t have the bot right',
230                                default=False)
231
232     def _do_command(self):
233         self._die_if_no_init()
234         self._api_setup()
235         status = self.metadir.working_dir_status(files=self.args)
236         nothing_to_commit = True
237         for file in status:
238             print '%s %s' % (status[file], file)
239             if status[file] in ['M']:
240                 nothing_to_commit = False
241         if nothing_to_commit:
242             print 'nothing to commit'
243             sys.exit()
244         if self.options.edit_summary == None:
245             print 'Edit summary:',
246             edit_summary = raw_input()
247         else:
248             edit_summary = self.options.edit_summary
249         for file_num, file in enumerate(status):
250             if status[file] in ['M']:
251                 # get edit token
252                 data = {
253                         'action': 'query',
254                         'prop': 'info|revisions',
255                         'intoken': 'edit',
256                         'titles': mw.metadir.filename_to_pagename(file[:-5]),
257                 }
258                 response = self.api.call(data)
259                 pages = response['query']['pages']
260                 pageid = pages.keys()[0]
261                 revid = pages[pageid]['revisions'][0]['revid']
262                 awaitedrevid = \
263                         self.metadir.pages_get_rv_list({'id': pageid})[0]
264                 if revid != awaitedrevid:
265                     print 'warning: edit conflict detected on %s (%s -> %s) ' \
266                             '-- skipping!' % (file, awaitedrevid, revid)
267                     continue
268                 edittoken = pages[pageid]['edittoken']
269                 filename = os.path.join(self.metadir.root, file)
270                 text = codecs.open(filename, 'r', 'utf-8').read()
271                 text = text.encode('utf-8')
272                 if (len(text) != 0) and (text[-1] == '\n'):
273                     text = text[:-1]
274                 md5 = hashlib.md5()
275                 md5.update(text)
276                 textmd5 = md5.hexdigest()
277                 data = {
278                         'action': 'edit',
279                         'title': mw.metadir.filename_to_pagename(file[:-5]),
280                         'token': edittoken,
281                         'text': text,
282                         'md5': textmd5,
283                         'summary': edit_summary,
284                 }
285                 if self.options.bot:
286                     data['bot'] = 'bot'
287                 response = self.api.call(data)
288                 if response['edit']['result'] == 'Success':
289                     if 'nochange' in response['edit']:
290                         print 'warning: no changes detected in %s - ' \
291                                 'skipping and removing ending LF' % file
292                         self.metadir.clean_page(file[:-5])
293                         continue
294                     if response['edit']['oldrevid'] != revid:
295                         print 'warning: edit conflict detected on %s -- ' \
296                                 'skipping!' % file
297                         continue
298                     data = {
299                             'action': 'query',
300                             'revids': response['edit']['newrevid'],
301                             'prop': 'info|revisions',
302                             'rvprop':
303                                     'ids|flags|timestamp|user|comment|content',
304                     }
305                     response = self.api.call(data)['query']['pages']
306                     self.metadir.pages_add_rv(int(pageid),
307                                               response[pageid]['revisions'][0])
308                     if file_num != len(status) - 1:
309                         print 'waiting 10s before processing the next file'
310                         time.sleep(10)
311                 else:
312                     print 'error: committing %s failed: %s' % \
313                             (file, response['edit']['result'])

Benjamin Mako Hill || Want to submit a patch?