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 35 from cvslib import CVS, File 36 import md5 37 import os 38 import string 39 import sys 40 from cmdfw import CommandFrameWork 41 42 43 DEF_LOCAL = 1 # Default -l 44 45 46 class 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 227 SENDMAIL = "/usr/lib/sendmail -t" 228 MAILFORM = """To: %s 229 Subject: CVS changes: %s 230 231 ...Message from rcvs... 232 233 Committed files: 234 %s 235 236 Log message: 237 %s 238 """ 239 240 241 class 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 (at] cwi.nl, jack (at] 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 349 class 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 461 def remove(fn): 462 try: 463 os.unlink(fn) 464 except os.error: 465 pass 466 467 468 def main(): 469 r = rcvs() 470 try: 471 r.run() 472 finally: 473 r.close() 474 475 476 if __name__ == "__main__": 477 main() 478