Fix user_self calling editGet with a wrong parameter
[Melange.git] / thirdparty / chromium / gcl.py
blob5adbb51c2c3af589be47ba3d86f60c279ad65d2f
1 #!/usr/bin/python
2 # Wrapper script around Rietveld's upload.py that groups files into
3 # changelists.
5 import getpass
6 import linecache
7 import os
8 import random
9 import re
10 import string
11 import subprocess
12 import sys
13 import tempfile
14 import upload
15 import urllib2
17 CODEREVIEW_SETTINGS = {
18 # Default values.
19 "CODE_REVIEW_SERVER": "codereviews.googleopensourceprograms.com",
20 "CC_LIST": "melange-soc-dev@googlegroups.com",
21 "VIEW_VC": "http://code.google.com/p/soc/source/detail?r=",
24 # Use a shell for subcommands on Windows to get a PATH search, and because svn
25 # may be a batch file.
26 use_shell = sys.platform.startswith("win")
29 # globals that store the root of the current repositary and the directory where
30 # we store information about changelists.
31 repository_root = ""
32 gcl_info_dir = ""
35 def GetSVNFileInfo(file, field):
36 """Returns a field from the svn info output for the given file."""
37 output = RunShell(["svn", "info", file])
38 for line in output.splitlines():
39 search = field + ": "
40 if line.startswith(search):
41 return line[len(search):]
42 return ""
45 def GetRepositoryRoot():
46 """Returns the top level directory of the current repository."""
47 global repository_root
48 if not repository_root:
49 cur_dir_repo_root = GetSVNFileInfo(os.getcwd(), "Repository Root")
50 if not cur_dir_repo_root:
51 ErrorExit("gcl run outside of repository")
53 repository_root = os.getcwd()
54 while True:
55 parent = os.path.dirname(repository_root)
56 if GetSVNFileInfo(parent, "Repository Root") != cur_dir_repo_root:
57 break
58 repository_root = parent
59 # Now read the code review settings for this repository.
60 settings_file = os.path.join(repository_root, "codereview.settings")
61 if os.path.exists(settings_file):
62 output = ReadFile(settings_file)
63 for line in output.splitlines():
64 if not line or line.startswith("#"):
65 continue
66 key, value = line.split(": ", 1)
67 CODEREVIEW_SETTINGS[key] = value
68 return repository_root
71 def GetCodeReviewSetting(key):
72 """Returns a value for the given key for this repository."""
73 return CODEREVIEW_SETTINGS.get(key, "")
76 def GetInfoDir():
77 """Returns the directory where gcl info files are stored."""
78 global gcl_info_dir
79 if not gcl_info_dir:
80 gcl_info_dir = os.path.join(GetRepositoryRoot(), '.svn', 'gcl_info')
81 return gcl_info_dir
84 def ErrorExit(msg):
85 """Print an error message to stderr and exit."""
86 print >>sys.stderr, msg
87 sys.exit(1)
90 def RunShell(command, print_output=False):
91 """Executes a command and returns the output."""
92 p = subprocess.Popen(command, stdout = subprocess.PIPE,
93 stderr = subprocess.STDOUT, shell = use_shell,
94 universal_newlines=True)
95 if print_output:
96 output_array = []
97 while True:
98 line = p.stdout.readline()
99 if not line:
100 break
101 if print_output:
102 print line.strip('\n')
103 output_array.append(line)
104 output = "".join(output_array)
105 else:
106 output = p.stdout.read()
107 p.wait()
108 p.stdout.close()
109 return output
112 def ReadFile(filename):
113 """Returns the contents of a file."""
114 file = open(filename, 'r')
115 result = file.read()
116 file.close()
117 return result
120 def WriteFile(filename, contents):
121 """Overwrites the file with the given contents."""
122 file = open(filename, 'w')
123 file.write(contents)
124 file.close()
127 class ChangeInfo:
128 """Holds information about a changelist.
130 issue: the Rietveld issue number, of "" if it hasn't been uploaded yet.
131 description: the description.
132 files: a list of 2 tuple containing (status, filename) of changed files,
133 with paths being relative to the top repository directory.
135 def __init__(self, name="", issue="", description="", files=[]):
136 self.name = name
137 self.issue = issue
138 self.description = description
139 self.files = files
141 def FileList(self):
142 """Returns a list of files."""
143 return [file[1] for file in self.files]
145 def Save(self):
146 """Writes the changelist information to disk."""
147 data = SEPARATOR.join([self.issue,
148 "\n".join([f[0] + f[1] for f in self.files]),
149 self.description])
150 WriteFile(GetChangelistInfoFile(self.name), data)
152 def Delete(self):
153 """Removes the changelist information from disk."""
154 os.remove(GetChangelistInfoFile(self.name))
156 def CloseIssue(self):
157 """Closes the Rietveld issue for this changelist."""
158 data = [("description", self.description),]
159 ctype, body = upload.EncodeMultipartFormData(data, [])
160 SendToRietveld("/" + self.issue + "/close", body, ctype)
162 def UpdateRietveldDescription(self):
163 """Sets the description for an issue on Rietveld."""
164 data = [("description", self.description),]
165 ctype, body = upload.EncodeMultipartFormData(data, [])
166 SendToRietveld("/" + self.issue + "/description", body, ctype)
169 SEPARATOR = "\n-----\n"
170 # The info files have the following format:
171 # issue_id\n
172 # SEPARATOR\n
173 # filepath1\n
174 # filepath2\n
177 # filepathn\n
178 # SEPARATOR\n
179 # description
182 def GetChangelistInfoFile(changename):
183 """Returns the file that stores information about a changelist."""
184 if not changename or re.search(r'\W', changename):
185 ErrorExit("Invalid changelist name: " + changename)
186 return os.path.join(GetInfoDir(), changename)
189 def LoadChangelistInfo(changename, fail_on_not_found=True,
190 update_status=False):
191 """Gets information about a changelist.
193 Args:
194 fail_on_not_found: if True, this function will quit the program if the
195 changelist doesn't exist.
196 update_status: if True, the svn status will be updated for all the files
197 and unchanged files will be removed.
199 Returns: a ChangeInfo object.
201 info_file = GetChangelistInfoFile(changename)
202 if not os.path.exists(info_file):
203 if fail_on_not_found:
204 ErrorExit("Changelist " + changename + " not found.")
205 return ChangeInfo(changename)
206 data = ReadFile(info_file)
207 split_data = data.split(SEPARATOR, 2)
208 if len(split_data) != 3:
209 os.remove(info_file)
210 ErrorExit("Changelist file %s was corrupt and deleted" % info_file)
211 issue = split_data[0]
212 files = []
213 for line in split_data[1].splitlines():
214 status = line[:7]
215 file = line[7:]
216 files.append((status, file))
217 description = split_data[2]
218 save = False
219 if update_status:
220 for file in files:
221 filename = os.path.join(GetRepositoryRoot(), file[1])
222 status = RunShell(["svn", "status", filename])[:7]
223 if not status: # File has been reverted.
224 save = True
225 files.remove(file)
226 elif status != file[0]:
227 save = True
228 files[files.index(file)] = (status, file[1])
229 change_info = ChangeInfo(changename, issue, description, files)
230 if save:
231 change_info.Save()
232 return change_info
235 def GetCLs():
236 """Returns a list of all the changelists in this repository."""
237 return os.listdir(GetInfoDir())
240 def GenerateChangeName():
241 """Generate a random changelist name."""
242 random.seed()
243 current_cl_names = GetCLs()
244 while True:
245 cl_name = (random.choice(string.ascii_lowercase) +
246 random.choice(string.digits) +
247 random.choice(string.ascii_lowercase) +
248 random.choice(string.digits))
249 if cl_name not in current_cl_names:
250 return cl_name
253 def GetModifiedFiles():
254 """Returns a set that maps from changelist name to (status,filename) tuples.
256 Files not in a changelist have an empty changelist name. Filenames are in
257 relation to the top level directory of the current repositary. Note that
258 only the current directory and subdirectories are scanned, in order to
259 improve performance while still being flexible.
261 files = {}
263 # Since the files are normalized to the root folder of the repositary, figure
264 # out what we need to add to the paths.
265 dir_prefix = os.getcwd()[len(GetRepositoryRoot()):].strip(os.sep)
267 # Get a list of all files in changelists.
268 files_in_cl = {}
269 for cl in GetCLs():
270 change_info = LoadChangelistInfo(cl)
271 for status, filename in change_info.files:
272 files_in_cl[filename] = change_info.name
274 # Get all the modified files.
275 status = RunShell(["svn", "status"])
276 for line in status.splitlines():
277 if not len(line) or line[0] == "?":
278 continue
279 status = line[:7]
280 filename = line[7:]
281 if dir_prefix:
282 filename = os.path.join(dir_prefix, filename)
283 change_list_name = ""
284 if filename in files_in_cl:
285 change_list_name = files_in_cl[filename]
286 files.setdefault(change_list_name, []).append((status, filename))
288 return files
291 def GetFilesNotInCL():
292 """Returns a list of tuples (status,filename) that aren't in any changelists.
294 See docstring of GetModifiedFiles for information about path of files and
295 which directories are scanned.
297 modified_files = GetModifiedFiles()
298 if "" not in modified_files:
299 return []
300 return modified_files[""]
303 def SendToRietveld(request_path, payload=None,
304 content_type="application/octet-stream"):
305 """Send a POST/GET to Rietveld. Returns the response body."""
306 def GetUserCredentials():
307 """Prompts the user for a username and password."""
308 email = raw_input("Email: ").strip()
309 password = getpass.getpass("Password for %s: " % email)
310 return email, password
312 server = GetCodeReviewSetting("CODE_REVIEW_SERVER")
313 rpc_server = upload.HttpRpcServer(server,
314 GetUserCredentials,
315 host_override=server,
316 save_cookies=True)
317 return rpc_server.Send(request_path, payload, content_type)
320 def GetIssueDescription(issue):
321 """Returns the issue description from Rietveld."""
322 return SendToRietveld("/" + issue + "/description")
325 def UnknownFiles(extra_args):
326 """Runs svn status and prints unknown files.
328 Any args in |extra_args| are passed to the tool to support giving alternate
329 code locations.
331 args = ["svn", "status"]
332 args += extra_args
333 p = subprocess.Popen(args, stdout = subprocess.PIPE,
334 stderr = subprocess.STDOUT, shell = use_shell)
335 while 1:
336 line = p.stdout.readline()
337 if not line:
338 break
339 if line[0] != '?':
340 continue # Not an unknown file to svn.
341 # The lines look like this:
342 # "? foo.txt"
343 # and we want just "foo.txt"
344 print line[7:].strip()
345 p.wait()
346 p.stdout.close()
349 def Opened():
350 """Prints a list of modified files in the current directory down."""
351 files = GetModifiedFiles()
352 cl_keys = files.keys()
353 cl_keys.sort()
354 for cl_name in cl_keys:
355 if cl_name:
356 note = ""
357 if len(LoadChangelistInfo(cl_name).files) != len(files[cl_name]):
358 note = " (Note: this changelist contains files outside this directory)"
359 print "\n--- Changelist " + cl_name + note + ":"
360 for file in files[cl_name]:
361 print "".join(file)
364 def Help():
365 print ("GCL is a wrapper for Subversion that simplifies working with groups "
366 "of files.\n")
367 print "Basic commands:"
368 print "-----------------------------------------"
369 print " gcl change change_name"
370 print (" Add/remove files to a changelist. Only scans the current "
371 "directory and subdirectories.\n")
372 print (" gcl upload change_name [-r reviewer1@gmail.com,"
373 "reviewer2@gmail.com,...] [--send_mail]")
374 print " Uploads the changelist to the server for review.\n"
375 print " gcl commit change_name"
376 print " Commits the changelist to the repository.\n"
377 print "Advanced commands:"
378 print "-----------------------------------------"
379 print " gcl delete change_name"
380 print " Deletes a changelist.\n"
381 print " gcl diff change_name"
382 print " Diffs all files in the changelist.\n"
383 print " gcl diff"
384 print (" Diffs all files in the current directory and subdirectories "
385 "that aren't in a changelist.\n")
386 print " gcl changes"
387 print " Lists all the the changelists and the files in them.\n"
388 print " gcl nothave [optional directory]"
389 print " Lists files unknown to Subversion.\n"
390 print " gcl opened"
391 print (" Lists modified files in the current directory and "
392 "subdirectories.\n")
393 print " gcl try change_name"
394 print (" Sends the change to the tryserver so a trybot can do a test"
395 " run on your code.\n")
398 def GetEditor():
399 editor = os.environ.get("SVN_EDITOR")
400 if not editor:
401 editor = os.environ.get("EDITOR")
403 if not editor:
404 if sys.platform.startswith("win"):
405 editor = "notepad"
406 else:
407 editor = "vi"
409 return editor
412 def GenerateDiff(files):
413 """Returns a string containing the diff for the given file list."""
414 diff = []
415 for file in files:
416 # Use svn info output instead of os.path.isdir because the latter fails
417 # when the file is deleted.
418 if GetSVNFileInfo(file, "Node Kind") == "directory":
419 continue
420 # If the user specified a custom diff command in their svn config file,
421 # then it'll be used when we do svn diff, which we don't want to happen
422 # since we want the unified diff. Using --diff-cmd=diff doesn't always
423 # work, since they can have another diff executable in their path that
424 # gives different line endings. So we use a bogus temp directory as the
425 # config directory, which gets around these problems.
426 if sys.platform.startswith("win"):
427 parent_dir = tempfile.gettempdir()
428 else:
429 parent_dir = sys.path[0] # tempdir is not secure.
430 bogus_dir = os.path.join(parent_dir, "temp_svn_config")
431 if not os.path.exists(bogus_dir):
432 os.mkdir(bogus_dir)
433 diff.append(RunShell(["svn", "diff", "--config-dir", bogus_dir, file]))
434 return "".join(diff)
437 def UploadCL(change_info, args):
438 if not change_info.FileList():
439 print "Nothing to upload, changelist is empty."
440 return
442 upload_arg = ["upload.py", "-y", "-l"]
443 upload_arg.append("--server=" + GetCodeReviewSetting("CODE_REVIEW_SERVER"))
444 upload_arg.extend(args)
446 desc_file = ""
447 if change_info.issue: # Uploading a new patchset.
448 upload_arg.append("--message=''")
449 upload_arg.append("--issue=" + change_info.issue)
450 else: # First time we upload.
451 handle, desc_file = tempfile.mkstemp(text=True)
452 os.write(handle, change_info.description)
453 os.close(handle)
455 upload_arg.append("--cc=" + GetCodeReviewSetting("CC_LIST"))
456 upload_arg.append("--description_file=" + desc_file + "")
457 if change_info.description:
458 subject = change_info.description[:77]
459 if subject.find("\r\n") != -1:
460 subject = subject[:subject.find("\r\n")]
461 if subject.find("\n") != -1:
462 subject = subject[:subject.find("\n")]
463 if len(change_info.description) > 77:
464 subject = subject + "..."
465 upload_arg.append("--message=" + subject)
467 # Change the current working directory before calling upload.py so that it
468 # shows the correct base.
469 os.chdir(GetRepositoryRoot())
471 # If we have a lot of files with long paths, then we won't be able to fit
472 # the command to "svn diff". Instead, we generate the diff manually for
473 # each file and concatenate them before passing it to upload.py.
474 issue = upload.RealMain(upload_arg, GenerateDiff(change_info.FileList()))
475 if issue and issue != change_info.issue:
476 change_info.issue = issue
477 change_info.Save()
479 if desc_file:
480 os.remove(desc_file)
483 def TryChange(change_info, args):
484 """Create a diff file of change_info and send it to the try server."""
485 try:
486 import trychange
487 except ImportError:
488 ErrorExit("You need to install trychange.py to use the try server.")
490 trychange.TryChange(args, change_info.name, change_info.FileList())
493 def Commit(change_info):
494 if not change_info.FileList():
495 print "Nothing to commit, changelist is empty."
496 return
498 commit_cmd = ["svn", "commit"]
499 filename = ''
500 if change_info.issue:
501 # Get the latest description from Rietveld.
502 change_info.description = GetIssueDescription(change_info.issue)
504 commit_message = change_info.description.replace('\r\n', '\n')
505 if change_info.issue:
506 commit_message += ('\nReview URL: http://%s/%s' %
507 (GetCodeReviewSetting("CODE_REVIEW_SERVER"),
508 change_info.issue))
510 handle, commit_filename = tempfile.mkstemp(text=True)
511 os.write(handle, commit_message)
512 os.close(handle)
514 handle, targets_filename = tempfile.mkstemp(text=True)
515 os.write(handle, "\n".join(change_info.FileList()))
516 os.close(handle)
518 commit_cmd += ['--file=' + commit_filename]
519 commit_cmd += ['--targets=' + targets_filename]
520 # Change the current working directory before calling commit.
521 os.chdir(GetRepositoryRoot())
522 output = RunShell(commit_cmd, True)
523 os.remove(commit_filename)
524 os.remove(targets_filename)
525 if output.find("Committed revision") != -1:
526 change_info.Delete()
528 if change_info.issue:
529 revision = re.compile(".*?\nCommitted revision (\d+)",
530 re.DOTALL).match(output).group(1)
531 viewvc_url = GetCodeReviewSetting("VIEW_VC")
532 change_info.description = (change_info.description +
533 "\n\nCommitted: " + viewvc_url + revision)
534 change_info.CloseIssue()
537 def Change(change_info):
538 """Creates/edits a changelist."""
539 if change_info.issue:
540 try:
541 description = GetIssueDescription(change_info.issue)
542 except urllib2.HTTPError, err:
543 if err.code == 404:
544 # The user deleted the issue in Rietveld, so forget the old issue id.
545 description = change_info.description
546 change_info.issue = ""
547 change_info.Save()
548 else:
549 ErrorExit("Error getting the description from Rietveld: " + err)
550 else:
551 description = change_info.description
553 other_files = GetFilesNotInCL()
555 separator1 = ("\n---All lines above this line become the description.\n"
556 "---Repository Root: " + GetRepositoryRoot() + "\n"
557 "---Paths in this changelist (" + change_info.name + "):\n")
558 separator2 = "\n\n---Paths modified but not in any changelist:\n\n"
559 text = (description + separator1 + '\n' +
560 '\n'.join([f[0] + f[1] for f in change_info.files]) + separator2 +
561 '\n'.join([f[0] + f[1] for f in other_files]) + '\n')
563 handle, filename = tempfile.mkstemp(text=True)
564 os.write(handle, text)
565 os.close(handle)
567 command = GetEditor() + " " + filename
568 os.system(command)
570 result = ReadFile(filename)
571 os.remove(filename)
573 if not result:
574 return
576 split_result = result.split(separator1, 1)
577 if len(split_result) != 2:
578 ErrorExit("Don't modify the text starting with ---!\n\n" + result)
580 new_description = split_result[0]
581 cl_files_text = split_result[1]
582 if new_description != description:
583 change_info.description = new_description
584 if change_info.issue:
585 # Update the Rietveld issue with the new description.
586 change_info.UpdateRietveldDescription()
588 new_cl_files = []
589 for line in cl_files_text.splitlines():
590 if not len(line):
591 continue
592 if line.startswith("---"):
593 break
594 status = line[:7]
595 file = line[7:]
596 new_cl_files.append((status, file))
597 change_info.files = new_cl_files
599 change_info.Save()
600 print change_info.name + " changelist saved."
603 def Changes():
604 """Print all the changlists and their files."""
605 for cl in GetCLs():
606 change_info = LoadChangelistInfo(cl, True, True)
607 print "\n--- Changelist " + change_info.name + ":"
608 for file in change_info.files:
609 print "".join(file)
612 def main(argv=None):
613 if argv is None:
614 argv = sys.argv
616 if len(argv) == 1:
617 Help()
618 return 0;
620 # Create the directory where we store information about changelists if it
621 # doesn't exist.
622 if not os.path.exists(GetInfoDir()):
623 os.mkdir(GetInfoDir())
625 command = argv[1]
626 if command == "opened":
627 Opened()
628 return 0
629 if command == "nothave":
630 UnknownFiles(argv[2:])
631 return 0
632 if command == "changes":
633 Changes()
634 return 0
635 if command == "diff" and len(argv) == 2:
636 files = GetFilesNotInCL()
637 print GenerateDiff([os.path.join(GetRepositoryRoot(), x[1]) for x in files])
638 return 0
640 if len(argv) == 2:
641 if command == "change":
642 # Generate a random changelist name.
643 changename = GenerateChangeName()
644 elif command == "help":
645 Help()
646 return 0
647 else:
648 ErrorExit("Need a changelist name.")
649 else:
650 changename = argv[2]
652 fail_on_not_found = command != "change"
653 change_info = LoadChangelistInfo(changename, fail_on_not_found, True)
655 if command == "change":
656 Change(change_info)
657 elif command == "upload":
658 UploadCL(change_info, argv[3:])
659 elif command == "commit":
660 Commit(change_info)
661 elif command == "delete":
662 change_info.Delete()
663 elif command == "try":
664 TryChange(change_info, argv[3:])
665 else:
666 # Everything else that is passed into gcl we redirect to svn, after adding
667 # the files. This allows commands such as 'gcl diff xxx' to work.
668 args =["svn", command]
669 root = GetRepositoryRoot()
670 args.extend([os.path.join(root, x) for x in change_info.FileList()])
671 RunShell(args, True)
672 return 0
675 if __name__ == "__main__":
676 sys.exit(main())