1#! /usr/bin/env python 2 3"""Remote CVS -- command line interface""" 4 5# XXX To do: 6# 7# Bugs: 8# - if the remote file is deleted, "rcvs update" will fail 9# 10# Functionality: 11# - cvs rm 12# - descend into directories (alraedy done for update) 13# - conflict resolution 14# - other relevant commands? 15# - branches 16# 17# - Finesses: 18# - retain file mode's x bits 19# - complain when "nothing known about filename" 20# - edit log message the way CVS lets you edit it 21# - cvs diff -rREVA -rREVB 22# - send mail the way CVS sends it 23# 24# Performance: 25# - cache remote checksums (for every revision ever seen!) 26# - translate symbolic revisions to numeric revisions 27# 28# Reliability: 29# - remote locking 30# 31# Security: 32# - Authenticated RPC? 33 34 35from cvslib import CVS, File 36import md5 37import os 38import string 39import sys 40from cmdfw import CommandFrameWork 41 42 43DEF_LOCAL = 1 # Default -l 44 45 46class MyFile(File): 47 48 def action(self): 49 """Return a code indicating the update status of this file. 50 51 The possible return values are: 52 53 '=' -- everything's fine 54 '0' -- file doesn't exist anywhere 55 '?' -- exists locally only 56 'A' -- new locally 57 'R' -- deleted locally 58 'U' -- changed remotely, no changes locally 59 (includes new remotely or deleted remotely) 60 'M' -- changed locally, no changes remotely 61 'C' -- conflict: changed locally as well as remotely 62 (includes cases where the file has been added 63 or removed locally and remotely) 64 'D' -- deleted remotely 65 'N' -- new remotely 66 'r' -- get rid of entry 67 'c' -- create entry 68 'u' -- update entry 69 70 (and probably others :-) 71 """ 72 if not self.lseen: 73 self.getlocal() 74 if not self.rseen: 75 self.getremote() 76 if not self.eseen: 77 if not self.lsum: 78 if not self.rsum: return '0' # Never heard of 79 else: 80 return 'N' # New remotely 81 else: # self.lsum 82 if not self.rsum: return '?' # Local only 83 # Local and remote, but no entry 84 if self.lsum == self.rsum: 85 return 'c' # Restore entry only 86 else: return 'C' # Real conflict 87 else: # self.eseen 88 if not self.lsum: 89 if self.edeleted: 90 if self.rsum: return 'R' # Removed 91 else: return 'r' # Get rid of entry 92 else: # not self.edeleted 93 if self.rsum: 94 print "warning:", 95 print self.file, 96 print "was lost" 97 return 'U' 98 else: return 'r' # Get rid of entry 99 else: # self.lsum 100 if not self.rsum: 101 if self.enew: return 'A' # New locally 102 else: return 'D' # Deleted remotely 103 else: # self.rsum 104 if self.enew: 105 if self.lsum == self.rsum: 106 return 'u' 107 else: 108 return 'C' 109 if self.lsum == self.esum: 110 if self.esum == self.rsum: 111 return '=' 112 else: 113 return 'U' 114 elif self.esum == self.rsum: 115 return 'M' 116 elif self.lsum == self.rsum: 117 return 'u' 118 else: 119 return 'C' 120 121 def update(self): 122 code = self.action() 123 if code == '=': return 124 print code, self.file 125 if code in ('U', 'N'): 126 self.get() 127 elif code == 'C': 128 print "%s: conflict resolution not yet implemented" % \ 129 self.file 130 elif code == 'D': 131 remove(self.file) 132 self.eseen = 0 133 elif code == 'r': 134 self.eseen = 0 135 elif code in ('c', 'u'): 136 self.eseen = 1 137 self.erev = self.rrev 138 self.enew = 0 139 self.edeleted = 0 140 self.esum = self.rsum 141 self.emtime, self.ectime = os.stat(self.file)[-2:] 142 self.extra = '' 143 144 def commit(self, message = ""): 145 code = self.action() 146 if code in ('A', 'M'): 147 self.put(message) 148 return 1 149 elif code == 'R': 150 print "%s: committing removes not yet implemented" % \ 151 self.file 152 elif code == 'C': 153 print "%s: conflict resolution not yet implemented" % \ 154 self.file 155 156 def diff(self, opts = []): 157 self.action() # To update lseen, rseen 158 flags = '' 159 rev = self.rrev 160 # XXX should support two rev options too! 161 for o, a in opts: 162 if o == '-r': 163 rev = a 164 else: 165 flags = flags + ' ' + o + a 166 if rev == self.rrev and self.lsum == self.rsum: 167 return 168 flags = flags[1:] 169 fn = self.file 170 data = self.proxy.get((fn, rev)) 171 sum = md5.new(data).digest() 172 if self.lsum == sum: 173 return 174 import tempfile 175 tf = tempfile.NamedTemporaryFile() 176 tf.write(data) 177 tf.flush() 178 print 'diff %s -r%s %s' % (flags, rev, fn) 179 sts = os.system('diff %s %s %s' % (flags, tf.name, fn)) 180 if sts: 181 print '='*70 182 183 def commitcheck(self): 184 return self.action() != 'C' 185 186 def put(self, message = ""): 187 print "Checking in", self.file, "..." 188 data = open(self.file).read() 189 if not self.enew: 190 self.proxy.lock(self.file) 191 messages = self.proxy.put(self.file, data, message) 192 if messages: 193 print messages 194 self.setentry(self.proxy.head(self.file), self.lsum) 195 196 def get(self): 197 data = self.proxy.get(self.file) 198 f = open(self.file, 'w') 199 f.write(data) 200 f.close() 201 self.setentry(self.rrev, self.rsum) 202 203 def log(self, otherflags): 204 print self.proxy.log(self.file, otherflags) 205 206 def add(self): 207 self.eseen = 0 # While we're hacking... 208 self.esum = self.lsum 209 self.emtime, self.ectime = 0, 0 210 self.erev = '' 211 self.enew = 1 212 self.edeleted = 0 213 self.eseen = 1 # Done 214 self.extra = '' 215 216 def setentry(self, erev, esum): 217 self.eseen = 0 # While we're hacking... 218 self.esum = esum 219 self.emtime, self.ectime = os.stat(self.file)[-2:] 220 self.erev = erev 221 self.enew = 0 222 self.edeleted = 0 223 self.eseen = 1 # Done 224 self.extra = '' 225 226 227SENDMAIL = "/usr/lib/sendmail -t" 228MAILFORM = """To: %s 229Subject: CVS changes: %s 230 231...Message from rcvs... 232 233Committed files: 234 %s 235 236Log message: 237 %s 238""" 239 240 241class RCVS(CVS): 242 243 FileClass = MyFile 244 245 def __init__(self): 246 CVS.__init__(self) 247 248 def update(self, files): 249 for e in self.whichentries(files, 1): 250 e.update() 251 252 def commit(self, files, message = ""): 253 list = self.whichentries(files) 254 if not list: return 255 ok = 1 256 for e in list: 257 if not e.commitcheck(): 258 ok = 0 259 if not ok: 260 print "correct above errors first" 261 return 262 if not message: 263 message = raw_input("One-liner: ") 264 committed = [] 265 for e in list: 266 if e.commit(message): 267 committed.append(e.file) 268 self.mailinfo(committed, message) 269 270 def mailinfo(self, files, message = ""): 271 towhom = "sjoerd@cwi.nl, jack@cwi.nl" # XXX 272 mailtext = MAILFORM % (towhom, string.join(files), 273 string.join(files), message) 274 print '-'*70 275 print mailtext 276 print '-'*70 277 ok = raw_input("OK to mail to %s? " % towhom) 278 if string.lower(string.strip(ok)) in ('y', 'ye', 'yes'): 279 p = os.popen(SENDMAIL, "w") 280 p.write(mailtext) 281 sts = p.close() 282 if sts: 283 print "Sendmail exit status %s" % str(sts) 284 else: 285 print "Mail sent." 286 else: 287 print "No mail sent." 288 289 def report(self, files): 290 for e in self.whichentries(files): 291 e.report() 292 293 def diff(self, files, opts): 294 for e in self.whichentries(files): 295 e.diff(opts) 296 297 def add(self, files): 298 if not files: 299 raise RuntimeError, "'cvs add' needs at least one file" 300 list = [] 301 for e in self.whichentries(files, 1): 302 e.add() 303 304 def rm(self, files): 305 if not files: 306 raise RuntimeError, "'cvs rm' needs at least one file" 307 raise RuntimeError, "'cvs rm' not yet imlemented" 308 309 def log(self, files, opts): 310 flags = '' 311 for o, a in opts: 312 flags = flags + ' ' + o + a 313 for e in self.whichentries(files): 314 e.log(flags) 315 316 def whichentries(self, files, localfilestoo = 0): 317 if files: 318 list = [] 319 for file in files: 320 if self.entries.has_key(file): 321 e = self.entries[file] 322 else: 323 e = self.FileClass(file) 324 self.entries[file] = e 325 list.append(e) 326 else: 327 list = self.entries.values() 328 for file in self.proxy.listfiles(): 329 if self.entries.has_key(file): 330 continue 331 e = self.FileClass(file) 332 self.entries[file] = e 333 list.append(e) 334 if localfilestoo: 335 for file in os.listdir(os.curdir): 336 if not self.entries.has_key(file) \ 337 and not self.ignored(file): 338 e = self.FileClass(file) 339 self.entries[file] = e 340 list.append(e) 341 list.sort() 342 if self.proxy: 343 for e in list: 344 if e.proxy is None: 345 e.proxy = self.proxy 346 return list 347 348 349class rcvs(CommandFrameWork): 350 351 GlobalFlags = 'd:h:p:qvL' 352 UsageMessage = \ 353"usage: rcvs [-d directory] [-h host] [-p port] [-q] [-v] [subcommand arg ...]" 354 PostUsageMessage = \ 355 "If no subcommand is given, the status of all files is listed" 356 357 def __init__(self): 358 """Constructor.""" 359 CommandFrameWork.__init__(self) 360 self.proxy = None 361 self.cvs = RCVS() 362 363 def close(self): 364 if self.proxy: 365 self.proxy._close() 366 self.proxy = None 367 368 def recurse(self): 369 self.close() 370 names = os.listdir(os.curdir) 371 for name in names: 372 if name == os.curdir or name == os.pardir: 373 continue 374 if name == "CVS": 375 continue 376 if not os.path.isdir(name): 377 continue 378 if os.path.islink(name): 379 continue 380 print "--- entering subdirectory", name, "---" 381 os.chdir(name) 382 try: 383 if os.path.isdir("CVS"): 384 self.__class__().run() 385 else: 386 self.recurse() 387 finally: 388 os.chdir(os.pardir) 389 print "--- left subdirectory", name, "---" 390 391 def options(self, opts): 392 self.opts = opts 393 394 def ready(self): 395 import rcsclient 396 self.proxy = rcsclient.openrcsclient(self.opts) 397 self.cvs.setproxy(self.proxy) 398 self.cvs.getentries() 399 400 def default(self): 401 self.cvs.report([]) 402 403 def do_report(self, opts, files): 404 self.cvs.report(files) 405 406 def do_update(self, opts, files): 407 """update [-l] [-R] [file] ...""" 408 local = DEF_LOCAL 409 for o, a in opts: 410 if o == '-l': local = 1 411 if o == '-R': local = 0 412 self.cvs.update(files) 413 self.cvs.putentries() 414 if not local and not files: 415 self.recurse() 416 flags_update = '-lR' 417 do_up = do_update 418 flags_up = flags_update 419 420 def do_commit(self, opts, files): 421 """commit [-m message] [file] ...""" 422 message = "" 423 for o, a in opts: 424 if o == '-m': message = a 425 self.cvs.commit(files, message) 426 self.cvs.putentries() 427 flags_commit = 'm:' 428 do_com = do_commit 429 flags_com = flags_commit 430 431 def do_diff(self, opts, files): 432 """diff [difflags] [file] ...""" 433 self.cvs.diff(files, opts) 434 flags_diff = 'cbitwcefhnlr:sD:S:' 435 do_dif = do_diff 436 flags_dif = flags_diff 437 438 def do_add(self, opts, files): 439 """add file ...""" 440 if not files: 441 print "'rcvs add' requires at least one file" 442 return 443 self.cvs.add(files) 444 self.cvs.putentries() 445 446 def do_remove(self, opts, files): 447 """remove file ...""" 448 if not files: 449 print "'rcvs remove' requires at least one file" 450 return 451 self.cvs.remove(files) 452 self.cvs.putentries() 453 do_rm = do_remove 454 455 def do_log(self, opts, files): 456 """log [rlog-options] [file] ...""" 457 self.cvs.log(files, opts) 458 flags_log = 'bhLNRtd:s:V:r:' 459 460 461def remove(fn): 462 try: 463 os.unlink(fn) 464 except os.error: 465 pass 466 467 468def main(): 469 r = rcvs() 470 try: 471 r.run() 472 finally: 473 r.close() 474 475 476if __name__ == "__main__": 477 main() 478