de08cb64a32097780697c3567d7b1974a44547d0
[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         else:
61             print 'Login failed: %s' % result['login']['result']
62
63     def _die_if_no_init(self):
64         if self.metadir.config is None:
65             print '%s: not a mw repo' % self.me
66             sys.exit(1)
67
68     def _api_setup(self):
69         self.api_url = self.metadir.config.get('remote', 'api_url')
70         self.api = mw.api.API(self.api_url, self.metadir)
71
72
73 class InitCommand(CommandBase):
74
75     def __init__(self):
76         usage = 'API_URL'
77         CommandBase.__init__(self, 'init', 'start a mw repo', usage)
78
79     def _do_command(self):
80         if len(self.args) < 1:
81             self.parser.error('must have URL to remote api.php')
82         elif len(self.args) > 1:
83             self.parser.error('too many arguments')
84         self.metadir.create(self.args[0])
85
86
87 class LoginCommand(CommandBase):
88
89     def __init__(self):
90         CommandBase.__init__(self, 'login', 'authenticate with wiki')
91
92     def _do_command(self):
93         self._die_if_no_init()
94         self._api_setup()
95         self._login()
96
97
98 class LogoutCommand(CommandBase):
99
100     def __init__(self):
101         CommandBase.__init__(self, 'logout', 'forget authentication')
102
103     def _do_command(self):
104         self._die_if_no_init()
105         try:
106             os.unlink(os.path.join(self.metadir.location, 'cookies'))
107         except OSError:
108             pass
109
110
111 class PullCommand(CommandBase):
112
113     def __init__(self):
114         usage = '[options] PAGENAME ...'
115         CommandBase.__init__(self, 'pull', 'add remote pages to repo', usage)
116
117     def _do_command(self):
118         self._die_if_no_init()
119         self._api_setup()
120         pages = []
121         pages += self.args
122         for these_pages in [pages[i:i + 25] for i in range(0, len(pages), 25)]:
123             data = {
124                     'action': 'query',
125                     'titles': '|'.join(these_pages),
126                     'prop': 'info|revisions',
127                     'rvprop': 'ids|flags|timestamp|user|comment|content',
128             }
129             response = self.api.call(data)['query']['pages']
130             for pageid in response.keys():
131                 pagename = response[pageid]['title']
132                 if 'missing' in response[pageid].keys():
133                     print '%s: %s: page does not exist, file not created' % \
134                             (self.me, pagename)
135                     continue
136                 revids = [x['revid'] for x in response[pageid]['revisions']]
137                 revids.sort()
138                 self.metadir.pagedict_add(pagename, pageid, revids[-1])
139                 self.metadir.pages_add_rv(int(pageid),
140                                           response[pageid]['revisions'][0])
141                 filename = mw.api.pagename_to_filename(pagename)
142                 fd = file(os.path.join(self.metadir.root, filename + '.wiki'),
143                           'w')
144                 fd.write(response[pageid]['revisions'][0]['*'].encode('utf-8'))
145
146
147 class StatusCommand(CommandBase):
148
149     def __init__(self):
150         CommandBase.__init__(self, 'status', 'check repo status')
151         self.shortcuts.append('st')
152
153     def _do_command(self):
154         self._die_if_no_init()
155         status = self.metadir.working_dir_status()
156         for file in status:
157             print '%s %s' % (status[file], file)
158
159
160 class DiffCommand(CommandBase):
161
162     def __init__(self):
163         CommandBase.__init__(self, 'diff', 'diff wiki to working directory')
164
165     def _do_command(self):
166         self._die_if_no_init()
167         status = self.metadir.working_dir_status()
168         for file in status:
169             if status[file] == 'U':
170                 print self.metadir.diff_rv_to_working(
171                         mw.api.filename_to_pagename(file[:-5])),
172
173
174 class CommitCommand(CommandBase):
175
176     def __init__(self):
177         usage = '[FILES]'
178         CommandBase.__init__(self, 'commit', 'commit changes to wiki', usage)
179         self.shortcuts.append('ci')
180         self.parser.add_option('-m', '--message', dest='edit_summary',
181                                help='don\'t prompt for edit summary and '
182                                'use this instead')
183         self.parser.add_option('--bot', dest='bot', action='store_true',
184                                help='mark actions as a bot (won\'t affect '
185                                'anything if you don\'t have the bot right',
186                                default=False)
187
188     def _do_command(self):
189         self._die_if_no_init()
190         self._api_setup()
191         status = self.metadir.working_dir_status(files=self.args)
192         nothing_to_commit = True
193         for file in status:
194             print '%s %s' % (status[file], file)
195             if status[file] in ['U']:
196                 nothing_to_commit = False
197         if nothing_to_commit:
198             print 'nothing to commit'
199             sys.exit()
200         print
201         print 'WARNING: mw does not do collision detection yet.'
202         print 'Hit ^C now if you haven\'t double checked, otherwise hit Enter'
203         raw_input()
204         if self.options.edit_summary == None:
205             print 'Edit summary:',
206             edit_summary = raw_input()
207         else:
208             edit_summary = self.options.edit_summary
209         for file in status:
210             if status[file] in ['U']:
211                 # get edit token
212                 data = {
213                         'action': 'query',
214                         'prop': 'info',
215                         'intoken': 'edit',
216                         'titles': mw.api.filename_to_pagename(file[:-5]),
217                 }
218                 response = self.api.call(data)
219                 pageid = response['query']['pages'].keys()[0]
220                 edittoken = response['query']['pages'][pageid]['edittoken']
221                 # FIXME use basetimestamp and starttimestamp
222                 filename = os.path.join(self.metadir.root, file)
223                 text = codecs.open(filename, 'r', 'utf-8').read()
224                 text = text.encode('utf-8')
225                 if text[-1] == '\n':
226                     text = text[:-1]
227                 md5 = hashlib.md5()
228                 md5.update(text)
229                 textmd5 = md5.hexdigest()
230                 data = {
231                         'action': 'edit',
232                         'title': mw.api.filename_to_pagename(file[:-5]),
233                         'token': edittoken,
234                         'text': text,
235                         'md5': textmd5,
236                         'summary': edit_summary,
237                 }
238                 if self.options.bot:
239                     data['bot'] = 'bot'
240                 response = self.api.call(data)
241                 if response['edit']['result'] == 'Success':
242                     data = {
243                             'action': 'query',
244                             'revids': response['edit']['newrevid'],
245                             'prop': 'info|revisions',
246                             'rvprop':
247                                     'ids|flags|timestamp|user|comment|content',
248                     }
249                     response = self.api.call(data)['query']['pages']
250                     self.metadir.pages_add_rv(int(pageid),
251                                               response[pageid]['revisions'][0])
252                 else:
253                     print 'committing %s failed: %s' % \
254                             (file, response['edit']['result'])

Benjamin Mako Hill || Want to submit a patch?