]> projects.mako.cc - mw/blob - src/mw/clicommands.py
088df16a5bf4c80330fd4d6956a152180af43901
[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 getpass
21 import hashlib
22 import mw.api
23 import mw.metadir
24 from optparse import OptionParser, OptionGroup
25 import os
26 import sys
27
28
29 class CommandBase(object):
30
31     def __init__(self, name, description, usage=None):
32         self.me = os.path.basename(sys.argv[0])
33         self.description = description
34         if usage is None:
35             usage = '%prog ' + name
36         else:
37             usage = '%%prog %s %s' % (name, usage)
38         self.parser = OptionParser(usage=usage, description=description)
39         self.name = name
40         self.metadir = mw.metadir.Metadir()
41         self.shortcuts = []
42
43     def main(self):
44         (self.options, self.args) = self.parser.parse_args()
45         self.args = self.args[1:] # don't need the first thing
46         self._do_command()
47
48     def _do_command(self):
49         pass
50
51     def _login(self):
52         user = raw_input('Username: ')
53         passwd = getpass.getpass()
54         result = self.api.call({'action': 'login',
55                                 'lgname': user,
56                                 'lgpassword': passwd})
57         if result['login']['result'] == 'Success':
58             # cookies are saved to a file
59             print 'Login successful! (yay)'
60         elif result['login']['result'] == 'NeedToken':
61             print 'Login with token'
62             result = self.api.call({'action': 'login',
63                                     'lgname': user,
64                                     'lgpassword': passwd,
65                                     'lgtoken': result['login']['token']})
66             if result['login']['result'] == 'Success':
67                 print 'Login successful! (yay)'
68             else:
69                 print 'Login failed: %s' % result['login']['result']
70         else:
71             print 'Login failed: %s' % result['login']['result']
72
73     def _die_if_no_init(self):
74         if self.metadir.config is None:
75             print '%s: not a mw repo' % self.me
76             sys.exit(1)
77
78     def _api_setup(self):
79         self.api_url = self.metadir.config.get('remote', 'api_url')
80         self.api = mw.api.API(self.api_url, self.metadir)
81
82
83 class InitCommand(CommandBase):
84
85     def __init__(self):
86         usage = 'API_URL'
87         CommandBase.__init__(self, 'init', 'start a mw repo', usage)
88
89     def _do_command(self):
90         if len(self.args) < 1:
91             self.parser.error('must have URL to remote api.php')
92         elif len(self.args) > 1:
93             self.parser.error('too many arguments')
94         self.metadir.create(self.args[0])
95
96
97 class LoginCommand(CommandBase):
98
99     def __init__(self):
100         CommandBase.__init__(self, 'login', 'authenticate with wiki')
101
102     def _do_command(self):
103         self._die_if_no_init()
104         self._api_setup()
105         self._login()
106
107
108 class LogoutCommand(CommandBase):
109
110     def __init__(self):
111         CommandBase.__init__(self, 'logout', 'forget authentication')
112
113     def _do_command(self):
114         self._die_if_no_init()
115         try:
116             os.unlink(os.path.join(self.metadir.location, 'cookies'))
117         except OSError:
118             pass
119
120
121 class PullCommand(CommandBase):
122
123     def __init__(self):
124         usage = '[options] PAGENAME ...'
125         CommandBase.__init__(self, 'pull', 'add remote pages to repo', usage)
126
127     def _do_command(self):
128         self._die_if_no_init()
129         self._api_setup()
130         pages = []
131         pages += self.args
132         for these_pages in [pages[i:i + 25] for i in range(0, len(pages), 25)]:
133             data = {
134                     'action': 'query',
135                     'titles': '|'.join(these_pages),
136                     'prop': 'info|revisions',
137                     'rvprop': 'ids|flags|timestamp|user|comment|content',
138             }
139             response = self.api.call(data)['query']['pages']
140             for pageid in response.keys():
141                 pagename = response[pageid]['title']
142                 if 'missing' in response[pageid].keys():
143                     print '%s: %s: page does not exist, file not created' % \
144                             (self.me, pagename)
145                     continue
146                 revids = [x['revid'] for x in response[pageid]['revisions']]
147                 revids.sort()
148                 self.metadir.pagedict_add(pagename, pageid, revids[-1])
149                 self.metadir.pages_add_rv(int(pageid),
150                                           response[pageid]['revisions'][0])
151                 filename = mw.api.pagename_to_filename(pagename)
152                 with file(os.path.join(self.metadir.root, filename + '.wiki'),
153                           'w') as fd:
154                     data = response[pageid]['revisions'][0]['*'].encode('utf-8')
155                     fd.write(data)
156
157 class StatusCommand(CommandBase):
158
159     def __init__(self):
160         CommandBase.__init__(self, 'status', 'check repo status')
161         self.shortcuts.append('st')
162
163     def _do_command(self):
164         self._die_if_no_init()
165         status = self.metadir.working_dir_status()
166         for file in status:
167             print '%s %s' % (status[file], file)
168
169
170 class DiffCommand(CommandBase):
171
172     def __init__(self):
173         CommandBase.__init__(self, 'diff', 'diff wiki to working directory')
174
175     def _do_command(self):
176         self._die_if_no_init()
177         status = self.metadir.working_dir_status()
178         for file in status:
179             if status[file] == 'U':
180                 print self.metadir.diff_rv_to_working(
181                         mw.api.filename_to_pagename(file[:-5])),
182
183
184 class CommitCommand(CommandBase):
185
186     def __init__(self):
187         usage = '[FILES]'
188         CommandBase.__init__(self, 'commit', 'commit changes to wiki', usage)
189         self.shortcuts.append('ci')
190         self.parser.add_option('-m', '--message', dest='edit_summary',
191                                help='don\'t prompt for edit summary and '
192                                'use this instead')
193         self.parser.add_option('-b', '--bot', dest='bot', action='store_true',
194                                help='mark actions as a bot (won\'t affect '
195                                'anything if you don\'t have the bot right',
196                                default=False)
197
198     def _do_command(self):
199         self._die_if_no_init()
200         self._api_setup()
201         status = self.metadir.working_dir_status(files=self.args)
202         nothing_to_commit = True
203         for file in status:
204             print '%s %s' % (status[file], file)
205             if status[file] in ['U']:
206                 nothing_to_commit = False
207         if nothing_to_commit:
208             print 'nothing to commit'
209             sys.exit()
210         if self.options.edit_summary == None:
211             print 'Edit summary:',
212             edit_summary = raw_input()
213         else:
214             edit_summary = self.options.edit_summary
215         for file in status:
216             if status[file] in ['U']:
217                 # get edit token
218                 data = {
219                         'action': 'query',
220                         'prop': 'info|revisions',
221                         'intoken': 'edit',
222                         'titles': mw.api.filename_to_pagename(file[:-5]),
223                 }
224                 response = self.api.call(data)
225                 pageid = response['query']['pages'].keys()[0]
226                 revid = response['query']['pages'][pageid]['revisions'][0]['revid']
227                 awaitedrevid = self.metadir.pages_get_rv_list( {'id': pageid } )[0]                
228                 if revid != awaitedrevid:
229                     print "warning: edit conflict detected on %s (%s -> %s) " \
230                             "-- skipping!" % (file , awaitedrevid, revid)
231                     continue
232                 edittoken = response['query']['pages'][pageid]['edittoken']
233                 # FIXME use basetimestamp and starttimestamp
234                 filename = os.path.join(self.metadir.root, file)
235                 text = codecs.open(filename, 'r', 'utf-8').read()
236                 text = text.encode('utf-8')
237                 if (len(text) != 0) and (text[-1] == '\n'):
238                     text = text[:-1]
239                 md5 = hashlib.md5()
240                 md5.update(text)
241                 textmd5 = md5.hexdigest()
242                 data = {
243                         'action': 'edit',
244                         'title': mw.api.filename_to_pagename(file[:-5]),
245                         'token': edittoken,
246                         'text': text,
247                         'md5': textmd5,
248                         'summary': edit_summary,
249                 }
250                 if self.options.bot:
251                     data['bot'] = 'bot'
252                 response = self.api.call(data)
253                 if response['edit']['result'] == 'Success':
254                     if response['edit'].has_key('nochange'):
255                         print "warning: no changes detected in %s - " \
256                                 "skipping and removing ending LF" % file
257                         self.metadir.clean_page(file[:-5])
258                         continue
259                     if response['edit']['oldrevid'] != revid:
260                         print "warning: edit conflict detected on %s -- " \
261                                 "skipping!" % file
262                         continue
263                     data = {
264                             'action': 'query',
265                             'revids': response['edit']['newrevid'],
266                             'prop': 'info|revisions',
267                             'rvprop':
268                                     'ids|flags|timestamp|user|comment|content',
269                     }
270                     response = self.api.call(data)['query']['pages']
271                     self.metadir.pages_add_rv(int(pageid),
272                                               response[pageid]['revisions'][0])
273                 else:
274                     print 'error: committing %s failed: %s' % \
275                             (file, response['edit']['result'])

Benjamin Mako Hill || Want to submit a patch?