Fix unicode errors on commit
[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         #global_options = OptionGroup(self.parser, "Global Options")
42         #global_options.add_option('-u', '--use-auth', action='store_true',
43         #                          dest='use_auth', help='force authentication '
44         #                          'even if not required')
45         #self.parser.add_option_group(global_options)
46         self.shortcuts = []
47
48     def main(self):
49         (self.options, self.args) = self.parser.parse_args()
50         self.args = self.args[1:] # don't need the first thing
51         self._do_command()
52
53     def _do_command(self):
54         pass
55
56     def _login(self):
57         user = raw_input('Username: ')
58         passwd = getpass.getpass()
59         result = self.api.call({'action': 'login',
60                                 'lgname': user,
61                                 'lgpassword': passwd})
62         if result['login']['result'] == 'Success':
63             # cookies are saved to a file
64             print 'Login successful! (yay)'
65         else:
66             print 'Login failed: %s' % result['login']['result']
67
68     def _die_if_no_init(self):
69         if self.metadir.config is None:
70             print '%s: not a mw repo' % self.me
71             sys.exit(1)
72
73     def _api_setup(self):
74         self.api_url = self.metadir.config.get('remote', 'api_url')
75         self.api = mw.api.API(self.api_url, self.metadir)
76
77
78 class InitCommand(CommandBase):
79
80     def __init__(self):
81         usage = 'API_URL'
82         CommandBase.__init__(self, 'init', 'start a mw repo', usage)
83
84     def _do_command(self):
85         if len(self.args) < 1:
86             self.parser.error('must have URL to remote api.php')
87         elif len(self.args) > 1:
88             self.parser.error('too many arguments')
89         self.metadir.create(self.args[0])
90
91
92 class LoginCommand(CommandBase):
93
94     def __init__(self):
95         CommandBase.__init__(self, 'login', 'authenticate with wiki')
96
97     def _do_command(self):
98         self._die_if_no_init()
99         self._api_setup()
100         self._login()
101
102
103 class LogoutCommand(CommandBase):
104
105     def __init__(self):
106         CommandBase.__init__(self, 'logout', 'forget authentication')
107
108     def _do_command(self):
109         self._die_if_no_init()
110         try:
111             os.unlink(os.path.join(self.metadir.location, 'cookies'))
112         except OSError:
113             pass
114
115
116 class PullCommand(CommandBase):
117
118     def __init__(self):
119         usage = '[options] PAGENAME ...'
120         CommandBase.__init__(self, 'pull', 'add remote pages to repo', usage)
121
122     def _do_command(self):
123         self._die_if_no_init()
124         self._api_setup()
125         pages = []
126         pages += self.args
127         for these_pages in [pages[i:i + 25] for i in range(0, len(pages), 25)]:
128             data = {
129                     'action': 'query',
130                     'titles': '|'.join(these_pages),
131                     'prop': 'info|revisions',
132                     'rvprop': 'ids|flags|timestamp|user|comment|content',
133             }
134             response = self.api.call(data)['query']['pages']
135             for pageid in response.keys():
136                 pagename = response[pageid]['title']
137                 if 'missing' in response[pageid].keys():
138                     print '%s: %s: page does not exist, file not created' % \
139                             (self.me, pagename)
140                     continue
141                 revids = [x['revid'] for x in response[pageid]['revisions']]
142                 revids.sort()
143                 self.metadir.pagedict_add(pagename, pageid, revids[-1])
144                 self.metadir.pages_add_rv(int(pageid),
145                                           response[pageid]['revisions'][0])
146                 filename = mw.api.pagename_to_filename(pagename)
147                 fd = file(os.path.join(self.metadir.root, filename + '.wiki'),
148                           'w')
149                 fd.write(response[pageid]['revisions'][0]['*'].encode('utf-8'))
150
151
152 class StatusCommand(CommandBase):
153
154     def __init__(self):
155         CommandBase.__init__(self, 'status', 'check repo status')
156         self.shortcuts.append('st')
157
158     def _do_command(self):
159         self._die_if_no_init()
160         status = self.metadir.working_dir_status()
161         for file in status:
162             print '%s %s' % (status[file], file)
163
164
165 class DiffCommand(CommandBase):
166
167     def __init__(self):
168         CommandBase.__init__(self, 'diff', 'diff wiki to working directory')
169
170     def _do_command(self):
171         self._die_if_no_init()
172         status = self.metadir.working_dir_status()
173         for file in status:
174             if status[file] == 'U':
175                 print self.metadir.diff_rv_to_working(
176                     mw.api.filename_to_pagename(file[:-5])
177                 ),
178
179
180 class CommitCommand(CommandBase):
181
182     def __init__(self):
183         CommandBase.__init__(self, 'commit', 'commit changes to wiki')
184         self.shortcuts.append('ci')
185         self.parser.add_option('-m', '--message', dest='edit_summary',
186                                help='don\'t prompt for edit summary and '
187                                'use this instead')
188
189     def _do_command(self):
190         self._die_if_no_init()
191         self._api_setup()
192         print 'WARNING: mw does not do collision detection yet.'
193         print 'Hit ^C now if you haven\'t double checked, otherwise hit Enter'
194         raw_input()
195         status = self.metadir.working_dir_status()
196         nothing_to_commit = True
197         for file in status:
198             print '%s %s' % (status[file], file)
199             if status[file] in ['U']:
200                 nothing_to_commit = False
201         if nothing_to_commit:
202             print 'nothing to commit'
203         else:
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                 response = self.api.call(data)
239                 if response['edit']['result'] == 'Success':
240                     data = {
241                             'action': 'query',
242                             'revids': response['edit']['newrevid'],
243                             'prop': 'info|revisions',
244                             'rvprop':
245                                     'ids|flags|timestamp|user|comment|content',
246                     }
247                     response = self.api.call(data)['query']['pages']
248                     self.metadir.pages_add_rv(int(pageid),
249                                               response[pageid]['revisions'][0])
250                 else:
251                     print 'committing %s failed: %s' % \
252                             (file, response['edit']['result'])

Benjamin Mako Hill || Want to submit a patch?