| 1 | r""" |
|---|
| 2 | SAGE Interface to the HG/Mercurial Revision Control System |
|---|
| 3 | |
|---|
| 4 | These functions make setup and use of source control with SAGE easier, using |
|---|
| 5 | the distributed Mercurial HG source control system. To learn about Mercurial, |
|---|
| 6 | see http://www.selenic.com/mercurial/wiki/. |
|---|
| 7 | |
|---|
| 8 | This system should all be fully usable from the SAGE notebook (except |
|---|
| 9 | for merging, currently). |
|---|
| 10 | This system should all be mostly from the SAGE notebook. |
|---|
| 11 | |
|---|
| 12 | \begin{itemize} |
|---|
| 13 | \item Use \code{hg_sage.record()} to record all of your changes. |
|---|
| 14 | \item Use \code{hg_sage.bundle('filename')} to bundle them up to send them. |
|---|
| 15 | \item Use \code{hg_sage.inspect('filename.hg')} to inspect a bundle. |
|---|
| 16 | \item Use \code{hg_sage.unbundle('filename.hg')} to import a bundle into your |
|---|
| 17 | repository. |
|---|
| 18 | \item Use \code{hg_sage.pull()} to synchronize with the latest official |
|---|
| 19 | stable SAGE changesets. |
|---|
| 20 | \end{itemize} |
|---|
| 21 | """ |
|---|
| 22 | |
|---|
| 23 | ######################################################################## |
|---|
| 24 | # Copyright (C) 2006 William Stein <wstein@gmail.com> |
|---|
| 25 | # |
|---|
| 26 | # Distributed under the terms of the GNU General Public License (GPL) |
|---|
| 27 | # |
|---|
| 28 | # http://www.gnu.org/licenses/ |
|---|
| 29 | ######################################################################## |
|---|
| 30 | |
|---|
| 31 | import os, shutil |
|---|
| 32 | |
|---|
| 33 | from viewer import browser |
|---|
| 34 | from misc import tmp_filename, branch_current_hg |
|---|
| 35 | from remote_file import get_remote_file |
|---|
| 36 | from sage.server.misc import print_open_msg |
|---|
| 37 | |
|---|
| 38 | import sage.server.support |
|---|
| 39 | def embedded(): |
|---|
| 40 | return sage.server.support.EMBEDDED_MODE |
|---|
| 41 | |
|---|
| 42 | def pager(): |
|---|
| 43 | if embedded(): |
|---|
| 44 | return 'cat' |
|---|
| 45 | else: |
|---|
| 46 | return 'less' |
|---|
| 47 | |
|---|
| 48 | class HG: |
|---|
| 49 | r""" |
|---|
| 50 | This is an HG (Mercurial) repository. |
|---|
| 51 | |
|---|
| 52 | To learn about Mercurial, see http://www.selenic.com/mercurial/wiki/. |
|---|
| 53 | |
|---|
| 54 | This system should all be fully usable from the SAGE notebook. |
|---|
| 55 | |
|---|
| 56 | Most commands are directly provided as member functions. However, |
|---|
| 57 | you can use the full functionality of hg, i.e., |
|---|
| 58 | \code{hg_sage("command line arguments")} |
|---|
| 59 | is \emph{exactly} the same as typing |
|---|
| 60 | \begin{verbatim} |
|---|
| 61 | cd <SAGE_ROOT>/devel/sage/ && hg command line arguments |
|---|
| 62 | \end{verbatim} |
|---|
| 63 | """ |
|---|
| 64 | def __init__(self, dir, name, url, target=None, cloneable=False): |
|---|
| 65 | """ |
|---|
| 66 | INPUT: |
|---|
| 67 | dir -- directory that will contain the repository |
|---|
| 68 | name -- a friendly name for the repository (only used for printing) |
|---|
| 69 | url -- a default URL to pull or record sends against (e.g., |
|---|
| 70 | this could be a master repository on modular.math.washington.edu) |
|---|
| 71 | target -- if the last part of dir is, e.g., sage-hg, |
|---|
| 72 | create a symlink from sage-hg to target. |
|---|
| 73 | If target=None, this symlink will not be created. |
|---|
| 74 | """ |
|---|
| 75 | self.__dir = os.path.abspath(dir) |
|---|
| 76 | self.__name = name |
|---|
| 77 | self.__url = url |
|---|
| 78 | self.__initialized = False |
|---|
| 79 | self.__target = target |
|---|
| 80 | self.__cloneable = cloneable |
|---|
| 81 | |
|---|
| 82 | def __repr__(self): |
|---|
| 83 | return "Hg repository '%s' in directory %s"%(self.__name, self.__dir) |
|---|
| 84 | |
|---|
| 85 | def status(self): |
|---|
| 86 | print("Getting status of modified or unknown files:") |
|---|
| 87 | self('status') |
|---|
| 88 | print "\n---\n" |
|---|
| 89 | if self.__name == "SAGE Library Source Code": |
|---|
| 90 | b = branch_current_hg() |
|---|
| 91 | if b == '': b='main' |
|---|
| 92 | elif b[-1] == '/': |
|---|
| 93 | b = b[:-1] |
|---|
| 94 | print("Branch: %s"%b) |
|---|
| 95 | |
|---|
| 96 | |
|---|
| 97 | |
|---|
| 98 | def _changed_files(self): |
|---|
| 99 | out, err = self('status', interactive=False) |
|---|
| 100 | v = [x for x in out.split('\n') if (x.strip()[:1] != '?' and x.strip()[:1] != '!') and len(x) != 0] |
|---|
| 101 | return len(v) > 0 |
|---|
| 102 | |
|---|
| 103 | def _ensure_safe(self): |
|---|
| 104 | """ |
|---|
| 105 | Ensure that the repository is in a safe state to have changes |
|---|
| 106 | applied to it, i.e., that all changes to controlled files in |
|---|
| 107 | the working directory are recorded. |
|---|
| 108 | """ |
|---|
| 109 | if self._changed_files(): |
|---|
| 110 | self.ci() |
|---|
| 111 | if self._changed_files(): |
|---|
| 112 | raise RuntimeError, "Refusing to do operation since you still have unrecorded changes. You must check in all changes in your working repository first." |
|---|
| 113 | |
|---|
| 114 | def _warning(self): |
|---|
| 115 | if not os.path.exists(os.environ['HOME'] + '/.hgrc'): |
|---|
| 116 | print "\nWARNING:" |
|---|
| 117 | print "Make sure to create a ~/.hgrc file:" |
|---|
| 118 | print "-"*70 |
|---|
| 119 | print "[ui]" |
|---|
| 120 | print "username = William Stein <wstein@gmail.com>" |
|---|
| 121 | print "-"*70 |
|---|
| 122 | print "\n" |
|---|
| 123 | |
|---|
| 124 | def __call__(self, cmd=None, interactive=True): |
|---|
| 125 | """ |
|---|
| 126 | Run 'hg cmd' where cmd is an arbitrary string |
|---|
| 127 | in the hg repository. |
|---|
| 128 | |
|---|
| 129 | INPUT: |
|---|
| 130 | cmd -- string, the hg command line (everything after 'hg') |
|---|
| 131 | interactive -- If True, runs using os.system, so user can |
|---|
| 132 | interactively interact with hg, i.e., this |
|---|
| 133 | is needed when you record changes because |
|---|
| 134 | the editor pops up. |
|---|
| 135 | If False, popen3 is used to launch hg |
|---|
| 136 | as a subprocess. |
|---|
| 137 | OUTPUT: |
|---|
| 138 | * If interactive is True, returns the exit code of the system call. |
|---|
| 139 | * If interactive is False, returns the output and error text. |
|---|
| 140 | * If cmd is not supplied, returns the output of the 'status' command |
|---|
| 141 | """ |
|---|
| 142 | self._warning() |
|---|
| 143 | if cmd is None: |
|---|
| 144 | cmd = 'status' |
|---|
| 145 | s = 'cd "%s" && hg %s'%(self.__dir, cmd) |
|---|
| 146 | print s |
|---|
| 147 | if interactive: |
|---|
| 148 | e = os.system(s) |
|---|
| 149 | return e |
|---|
| 150 | else: |
|---|
| 151 | x = os.popen3(s) |
|---|
| 152 | x[0].close() |
|---|
| 153 | out = x[1].read() |
|---|
| 154 | err = x[2].read() |
|---|
| 155 | return out, err |
|---|
| 156 | |
|---|
| 157 | def serve(self, port=8200, address='localhost', |
|---|
| 158 | open_viewer=True, options=''): |
|---|
| 159 | """ |
|---|
| 160 | Start a web server for this repository. |
|---|
| 161 | |
|---|
| 162 | This server is very nice -- you can browse all files in the |
|---|
| 163 | repository, see their changelogs, see who wrote any given |
|---|
| 164 | line, etc. Very nice. |
|---|
| 165 | |
|---|
| 166 | INPUT: |
|---|
| 167 | port -- port that the server will listen on |
|---|
| 168 | address -- (default: 'localhost') address to listen on |
|---|
| 169 | open_viewer -- boolean (default: True); whether to pop up the web page |
|---|
| 170 | options -- a string passed directly to hg's serve command. |
|---|
| 171 | """ |
|---|
| 172 | if open_viewer: |
|---|
| 173 | cmd = 'sleep 1; %s http://%s:%s 1>&2 >/dev/null'%(browser(), |
|---|
| 174 | address, port) |
|---|
| 175 | t = tmp_filename() |
|---|
| 176 | open(t,'w').write(cmd) |
|---|
| 177 | P = os.path.abspath(t) |
|---|
| 178 | os.system('chmod +x %s; %s &'%(P, P)) |
|---|
| 179 | |
|---|
| 180 | print_open_msg(address, port) |
|---|
| 181 | self('serve --address %s --port %s %s'%(address, port, options)) |
|---|
| 182 | print_open_msg(address, port) |
|---|
| 183 | |
|---|
| 184 | browse = serve |
|---|
| 185 | |
|---|
| 186 | def unbundle(self, bundle, update=True, options=''): |
|---|
| 187 | """ |
|---|
| 188 | Apply patches from a hg patch to the repository. |
|---|
| 189 | |
|---|
| 190 | If the bundle is a .patch file, instead call the import_patch method. |
|---|
| 191 | To see what is in a bundle before applying it, using self.incoming(bundle). |
|---|
| 192 | |
|---|
| 193 | INPUT: |
|---|
| 194 | bundle -- an hg bundle (created with the bundle command) |
|---|
| 195 | update -- if True (the default), update the working directory after unbundling. |
|---|
| 196 | """ |
|---|
| 197 | if bundle.startswith("http://") or bundle.startswith("https://"): |
|---|
| 198 | bundle = get_remote_file(bundle, verbose=True) |
|---|
| 199 | if bundle[-6:] == '.patch': |
|---|
| 200 | self.import_patch(bundle, options) |
|---|
| 201 | return |
|---|
| 202 | if bundle[-5:] == '.diff': |
|---|
| 203 | return self.import_patch(bundle) |
|---|
| 204 | self._ensure_safe() |
|---|
| 205 | bundle = os.path.abspath(bundle) |
|---|
| 206 | print "Unbundling bundle %s"%bundle |
|---|
| 207 | if update: |
|---|
| 208 | options = '-u' |
|---|
| 209 | else: |
|---|
| 210 | options = '' |
|---|
| 211 | |
|---|
| 212 | print "If you get an error 'abort: unknown parent'" |
|---|
| 213 | print "this just means you need to do an x.pull()," |
|---|
| 214 | print "where x is the hg_ object you just called this method on." |
|---|
| 215 | self('unbundle %s "%s"'%(options, bundle)) |
|---|
| 216 | |
|---|
| 217 | apply = unbundle |
|---|
| 218 | |
|---|
| 219 | def export(self, revs, filename=None, text=False, options=''): |
|---|
| 220 | r""" |
|---|
| 221 | Export patches with the changeset header and diffs for one or |
|---|
| 222 | more revisions. |
|---|
| 223 | |
|---|
| 224 | If multiple revisions are given, one plain text unified diff |
|---|
| 225 | file is generated for each one. These files should be applied |
|---|
| 226 | using import_patch in order from smallest to largest revision |
|---|
| 227 | number. The information shown in the changeset header is: |
|---|
| 228 | author, changeset hash, parent and commit comment. |
|---|
| 229 | |
|---|
| 230 | \note{If you are sending a patch to somebody using export and |
|---|
| 231 | it depends on previous patches, make sure to include those |
|---|
| 232 | revisions too! Alternatively, use the bundle() method, which |
|---|
| 233 | includes enough information to patch against the default |
|---|
| 234 | repository (but is an annoying and mysterious binary file).} |
|---|
| 235 | |
|---|
| 236 | INPUT: |
|---|
| 237 | revs -- integer or list of integers (revision numbers); use the log() |
|---|
| 238 | method to see these numbers. |
|---|
| 239 | filename -- (default: '%R.patch') The name of the file is given using a format |
|---|
| 240 | string. The formatting rules are as follows: |
|---|
| 241 | %% literal "%" character |
|---|
| 242 | %H changeset hash (40 bytes of hexadecimal) |
|---|
| 243 | %N number of patches being generated |
|---|
| 244 | %R changeset revision number |
|---|
| 245 | %b basename of the exporting repository |
|---|
| 246 | %h short-form changeset hash (12 bytes of hexadecimal) |
|---|
| 247 | %n zero-padded sequence number, starting at 1 |
|---|
| 248 | options -- string (default: '') |
|---|
| 249 | -a --text treat all files as text |
|---|
| 250 | --switch-parent diff against the second parent |
|---|
| 251 | * Without the -a option, export will avoid |
|---|
| 252 | generating diffs of files it detects as |
|---|
| 253 | binary. With -a, export will generate a diff |
|---|
| 254 | anyway, probably with undesirable results. |
|---|
| 255 | * With the --switch-parent option, the diff will |
|---|
| 256 | be against the second parent. It can be useful |
|---|
| 257 | to review a merge. |
|---|
| 258 | """ |
|---|
| 259 | if filename is None: |
|---|
| 260 | filename = '%R.patch' |
|---|
| 261 | if not isinstance(revs, list): |
|---|
| 262 | revs = [int(revs)] |
|---|
| 263 | if not isinstance(filename, str): |
|---|
| 264 | raise TypeError, 'filename must be a string' |
|---|
| 265 | if filename[-6:] != '.patch': |
|---|
| 266 | filename += '.patch' |
|---|
| 267 | options += ' -o "%s"'%(os.path.abspath(filename)) |
|---|
| 268 | if filename == '%R.patch': |
|---|
| 269 | print "Output will be written to revision numbered file."%revs |
|---|
| 270 | else: |
|---|
| 271 | print "Output will be written to '%s'"%filename |
|---|
| 272 | if text: |
|---|
| 273 | options += ' -a' |
|---|
| 274 | self('export %s %s'%(options, ' '.join([str(x) for x in revs]))) |
|---|
| 275 | |
|---|
| 276 | def import_patch(self, filename, options=''): |
|---|
| 277 | """ |
|---|
| 278 | Import an ordered set of patches from patch file, i.e., a plain |
|---|
| 279 | text file created using the export command. |
|---|
| 280 | |
|---|
| 281 | If there are outstanding changes in the working directory, import |
|---|
| 282 | will abort unless given the -f flag. |
|---|
| 283 | |
|---|
| 284 | If imported patch was generated by the export command, user |
|---|
| 285 | and description from patch override values from message |
|---|
| 286 | headers and body. Values given as options with -m and -u |
|---|
| 287 | override these. |
|---|
| 288 | |
|---|
| 289 | INPUT: |
|---|
| 290 | filename -- a string |
|---|
| 291 | options -- a string |
|---|
| 292 | options: [-p NUM] [-b BASE] [-m MESSAGE] [-f] PATCH... |
|---|
| 293 | -p --strip directory strip option for patch. This has the same |
|---|
| 294 | meaning as the corresponding patch option (default: 1) |
|---|
| 295 | -m --message use <text> as commit message |
|---|
| 296 | -b --base base path |
|---|
| 297 | -f --force skip check for outstanding uncommitted changes |
|---|
| 298 | |
|---|
| 299 | ALIASES: patch |
|---|
| 300 | """ |
|---|
| 301 | if filename.startswith("http://") or filename.startswith("https://"): |
|---|
| 302 | filename = get_remote_file(filename, verbose=True) |
|---|
| 303 | self._ensure_safe() |
|---|
| 304 | self('import %s "%s"'%(options, os.path.abspath(filename))) |
|---|
| 305 | |
|---|
| 306 | patch = import_patch |
|---|
| 307 | |
|---|
| 308 | def incoming(self, source, options=''): |
|---|
| 309 | """ |
|---|
| 310 | Show new changesets found in the given source. This even |
|---|
| 311 | works if the source is a bundle file (ends in .hg or .bundle). |
|---|
| 312 | |
|---|
| 313 | Show new changesets found in the specified path/URL or the default |
|---|
| 314 | pull location. These are the changesets that would be pulled if a pull |
|---|
| 315 | was requested. |
|---|
| 316 | |
|---|
| 317 | For remote repository, using --bundle avoids downloading the changesets |
|---|
| 318 | twice if the incoming is followed by a pull. |
|---|
| 319 | |
|---|
| 320 | See pull for valid source format details. |
|---|
| 321 | |
|---|
| 322 | ALIAS: inspect |
|---|
| 323 | |
|---|
| 324 | INPUT: |
|---|
| 325 | filename -- string |
|---|
| 326 | options -- string '[-p] [-n] [-M] [-r REV] ...' |
|---|
| 327 | -M --no-merges do not show merges |
|---|
| 328 | -f --force run even when remote repository is unrelated |
|---|
| 329 | --style display using template map file |
|---|
| 330 | -n --newest-first show newest record first |
|---|
| 331 | --bundle file to store the bundles into |
|---|
| 332 | -p --patch show patch |
|---|
| 333 | -r --rev a specific revision you would like to pull |
|---|
| 334 | --template display with template |
|---|
| 335 | -e --ssh specify ssh command to use |
|---|
| 336 | --remotecmd specify hg command to run on the remote side |
|---|
| 337 | """ |
|---|
| 338 | if source.startswith("http://") or source.startswith("https://"): |
|---|
| 339 | source = get_remote_file(source, verbose=True) |
|---|
| 340 | if os.path.exists(source): |
|---|
| 341 | source = os.path.abspath(source) |
|---|
| 342 | if os.path.splitext(source)[1] in ['.hg', '.bundle']: |
|---|
| 343 | source = 'bundle://%s'%source |
|---|
| 344 | self('incoming %s "%s"'%(options, source)) |
|---|
| 345 | |
|---|
| 346 | inspect = incoming |
|---|
| 347 | |
|---|
| 348 | |
|---|
| 349 | def add(self, files, options=''): |
|---|
| 350 | """ |
|---|
| 351 | Add the given list of files (or file) or directories |
|---|
| 352 | to your HG repository. They must exist already. |
|---|
| 353 | |
|---|
| 354 | To see a list of files that haven't been added to the |
|---|
| 355 | repository do self.status(). They will appear with an |
|---|
| 356 | explanation point next them. |
|---|
| 357 | |
|---|
| 358 | Add needs to be called whenever you add a new file or |
|---|
| 359 | directory to your project. Of course, it also needs to be |
|---|
| 360 | called when you first create the project, to let hg know |
|---|
| 361 | which files should be kept track of. |
|---|
| 362 | |
|---|
| 363 | INPUT: |
|---|
| 364 | files -- list or string; name of file or directory. |
|---|
| 365 | options -- string |
|---|
| 366 | """ |
|---|
| 367 | if isinstance(files, str): |
|---|
| 368 | if ' ' in files: |
|---|
| 369 | files = files.split() |
|---|
| 370 | else: |
|---|
| 371 | files = [files] |
|---|
| 372 | for file in files: |
|---|
| 373 | print "Adding file %s"%file |
|---|
| 374 | self('add %s "%s"'%(options, file)) |
|---|
| 375 | |
|---|
| 376 | def remove(self, files, options=''): |
|---|
| 377 | """ |
|---|
| 378 | Remove the given list of files (or file) or directories |
|---|
| 379 | from your HG repository. |
|---|
| 380 | |
|---|
| 381 | INPUT: |
|---|
| 382 | files -- list or string; name of file or directory. |
|---|
| 383 | options -- string (e.g., '-f') |
|---|
| 384 | """ |
|---|
| 385 | if isinstance(files, str): |
|---|
| 386 | files = [files] |
|---|
| 387 | for file in files: |
|---|
| 388 | print "Removing file %s"%file |
|---|
| 389 | self('rm %s "%s"'%(options, file)) |
|---|
| 390 | |
|---|
| 391 | rm = remove |
|---|
| 392 | |
|---|
| 393 | def rename(self, src, dest, options=''): |
|---|
| 394 | """ |
|---|
| 395 | Move (rename) the given file. |
|---|
| 396 | |
|---|
| 397 | INPUT: |
|---|
| 398 | src, dest -- strings that define files, relative to self.dir() |
|---|
| 399 | options -- |
|---|
| 400 | -A --after record a rename that has already occurred |
|---|
| 401 | -f --force forcibly copy over an existing managed file |
|---|
| 402 | -I --include include names matching the given patterns |
|---|
| 403 | -X --exclude exclude names matching the given patterns |
|---|
| 404 | -n --dry-run do not perform actions, just print output |
|---|
| 405 | """ |
|---|
| 406 | if isinstance(files, str): |
|---|
| 407 | files = [files] |
|---|
| 408 | for file in files: |
|---|
| 409 | print "Moving %s --> %s"%file |
|---|
| 410 | self('mv %s "%s"'%(options, file)) |
|---|
| 411 | |
|---|
| 412 | move = rename |
|---|
| 413 | mv = rename |
|---|
| 414 | |
|---|
| 415 | def log(self, branches=None, keyword=None, limit=None, |
|---|
| 416 | rev=None, merges=False, only_merges=False, |
|---|
| 417 | patch=None, template=False, include=None, |
|---|
| 418 | exclude=None, verbose=False): |
|---|
| 419 | """ |
|---|
| 420 | Display the change log for this repository. This is a list of |
|---|
| 421 | changesets ordered by revision number. |
|---|
| 422 | |
|---|
| 423 | By default this command outputs: changeset id and hash, tags, |
|---|
| 424 | non-trivial parents, user, date and time, and a summary for each |
|---|
| 425 | commit. |
|---|
| 426 | |
|---|
| 427 | INPUT: |
|---|
| 428 | branches -- (string, default: None) show given branches |
|---|
| 429 | keyword -- (string, default: None) search for a keyword |
|---|
| 430 | limit -- (integer, default: None, or 20 in notebook mdoe) |
|---|
| 431 | limit number of changes displayed |
|---|
| 432 | rev -- (integer) show the specified revision |
|---|
| 433 | merges -- (bool, default: False) whether or not to show merges |
|---|
| 434 | only_merges -- (bool, default: False) if true, show only merges |
|---|
| 435 | patch -- (string, default: None) show given patch |
|---|
| 436 | template -- (string, default: None) display with template |
|---|
| 437 | include -- (string, default: None) include names matching the given patterns |
|---|
| 438 | exclude -- (string, default: None) exclude names matching the given patterns |
|---|
| 439 | verbose -- (bool, default: False) If true, the list of changed |
|---|
| 440 | files and full commit message is shown. |
|---|
| 441 | """ |
|---|
| 442 | if embedded() and limit is None: |
|---|
| 443 | limit = 20 |
|---|
| 444 | options = '' |
|---|
| 445 | if branches: |
|---|
| 446 | options += '-b %s '%branches |
|---|
| 447 | if keyword: |
|---|
| 448 | options += '-k "%s" '%keyword |
|---|
| 449 | if limit: |
|---|
| 450 | options += '-l %s '%limit |
|---|
| 451 | if rev: |
|---|
| 452 | options += '-r %s '%rev |
|---|
| 453 | if not merges: |
|---|
| 454 | options += '--no-merges ' |
|---|
| 455 | if only_merges: |
|---|
| 456 | options += '-m ' |
|---|
| 457 | if patch: |
|---|
| 458 | options += '-p "%s"'%patch |
|---|
| 459 | if include: |
|---|
| 460 | options += '-I "%s"'%include |
|---|
| 461 | if exclude: |
|---|
| 462 | options += '-X "%s"'%exclude |
|---|
| 463 | if verbose: |
|---|
| 464 | options = '-v ' + options |
|---|
| 465 | |
|---|
| 466 | self('log %s | %s'%(options, pager())) |
|---|
| 467 | |
|---|
| 468 | changes = log |
|---|
| 469 | history = log |
|---|
| 470 | |
|---|
| 471 | def diff(self, files='', rev=None): |
|---|
| 472 | """ |
|---|
| 473 | Show differences between revisions for the specified files as a unified diff. |
|---|
| 474 | |
|---|
| 475 | By default this command tells you exactly what you have |
|---|
| 476 | changed in your working repository since you last commited |
|---|
| 477 | changes. |
|---|
| 478 | |
|---|
| 479 | INPUT: |
|---|
| 480 | files -- space separated list of files (relative to self.dir()) |
|---|
| 481 | rev -- None or a list of integers. |
|---|
| 482 | |
|---|
| 483 | Differences between files are shown using the unified diff format. |
|---|
| 484 | |
|---|
| 485 | When two revision arguments are given, then changes are shown |
|---|
| 486 | between those revisions. If only one revision is specified then |
|---|
| 487 | that revision is compared to the working directory, and, when no |
|---|
| 488 | revisions are specified, the working directory files are compared |
|---|
| 489 | to its parent. |
|---|
| 490 | """ |
|---|
| 491 | if not rev is None: |
|---|
| 492 | if not isinstance(rev, (list, tuple)): |
|---|
| 493 | rev = [rev] |
|---|
| 494 | options = ' '.join(['-r %s'%r for r in rev]) + ' ' + files |
|---|
| 495 | else: |
|---|
| 496 | options = files |
|---|
| 497 | self('diff %s | %s'%(options, pager())) |
|---|
| 498 | |
|---|
| 499 | what = diff |
|---|
| 500 | |
|---|
| 501 | def revert(self, files='', options='', rev=None): |
|---|
| 502 | """ |
|---|
| 503 | Revert files or dirs to their states as of some revision |
|---|
| 504 | |
|---|
| 505 | With no revision specified, revert the named files or |
|---|
| 506 | directories to the contents they had in the parent of the |
|---|
| 507 | working directory. This restores the contents of the |
|---|
| 508 | affected files to an unmodified state. If the working |
|---|
| 509 | directory has two parents, you must explicitly specify the |
|---|
| 510 | revision to revert to. |
|---|
| 511 | |
|---|
| 512 | Modified files are saved with a .orig suffix before |
|---|
| 513 | reverting. To disable these backups, use --no-backup. |
|---|
| 514 | |
|---|
| 515 | Using the -r option, revert the given files or directories |
|---|
| 516 | to their contents as of a specific revision. This can be |
|---|
| 517 | helpful to 'roll back' some or all of a change that should |
|---|
| 518 | not have been committed. |
|---|
| 519 | |
|---|
| 520 | Revert modifies the working directory. It does not commit |
|---|
| 521 | any changes, or change the parent of the working |
|---|
| 522 | directory. If you revert to a revision other than the |
|---|
| 523 | parent of the working directory, the reverted files will |
|---|
| 524 | thus appear modified afterwards. |
|---|
| 525 | |
|---|
| 526 | If a file has been deleted, it is recreated. If the executable |
|---|
| 527 | mode of a file was changed, it is reset. |
|---|
| 528 | |
|---|
| 529 | If names are given, all files matching the names are reverted. |
|---|
| 530 | |
|---|
| 531 | If no arguments are given, all files in the repository are |
|---|
| 532 | reverted. |
|---|
| 533 | |
|---|
| 534 | OPTIONS: |
|---|
| 535 | --no-backup do not save backup copies of files |
|---|
| 536 | -I --include include names matching given patterns |
|---|
| 537 | -X --exclude exclude names matching given patterns |
|---|
| 538 | -n --dry-run do not perform actions, just print output |
|---|
| 539 | """ |
|---|
| 540 | if not rev is None: |
|---|
| 541 | options = ' -r %s %s'%(rev, files) |
|---|
| 542 | else: |
|---|
| 543 | options = files |
|---|
| 544 | self('revert %s'%options) |
|---|
| 545 | |
|---|
| 546 | def dir(self): |
|---|
| 547 | """ |
|---|
| 548 | Return the directory where this repository is located. |
|---|
| 549 | """ |
|---|
| 550 | return self.__dir |
|---|
| 551 | |
|---|
| 552 | def url(self): |
|---|
| 553 | """ |
|---|
| 554 | Return the default 'master url' for this repository. |
|---|
| 555 | """ |
|---|
| 556 | return self.__url |
|---|
| 557 | |
|---|
| 558 | def help(self, cmd=''): |
|---|
| 559 | r""" |
|---|
| 560 | Return help about the given command, or if cmd is omitted |
|---|
| 561 | a list of commands. |
|---|
| 562 | |
|---|
| 563 | If this hg object is called hg_sage, then you |
|---|
| 564 | call a command using |
|---|
| 565 | \code{hg_sage('usual hg command line notation')} |
|---|
| 566 | """ |
|---|
| 567 | self('%s --help | %s'%(cmd, pager())) |
|---|
| 568 | |
|---|
| 569 | def outgoing(self, url=None, opts=''): |
|---|
| 570 | """ |
|---|
| 571 | Use this to find changsets that are in your branch, but not in the |
|---|
| 572 | specified destination repository. If no destination is specified, the |
|---|
| 573 | official repository is used. |
|---|
| 574 | |
|---|
| 575 | From the Mercurial documentation: |
|---|
| 576 | Show changesets not found in the specified destination repository or the |
|---|
| 577 | default push location. These are the changesets that would be pushed if |
|---|
| 578 | a push was requested. |
|---|
| 579 | |
|---|
| 580 | See pull() for valid destination format details. |
|---|
| 581 | |
|---|
| 582 | INPUT: |
|---|
| 583 | url: default: self.url() -- the official repository |
|---|
| 584 | * http://[user@]host[:port]/[path] |
|---|
| 585 | * https://[user@]host[:port]/[path] |
|---|
| 586 | * ssh://[user@]host[:port]/[path] |
|---|
| 587 | * local directory (starting with a /) |
|---|
| 588 | * name of a branch (for hg_sage); no /'s |
|---|
| 589 | options: (default: none) |
|---|
| 590 | -M --no-merges do not show merges |
|---|
| 591 | -f --force run even when remote repository is unrelated |
|---|
| 592 | -p --patch show patch |
|---|
| 593 | --style display using template map file |
|---|
| 594 | -r --rev a specific revision you would like to push |
|---|
| 595 | -n --newest-first show newest record first |
|---|
| 596 | --template display with template |
|---|
| 597 | -e --ssh specify ssh command to use |
|---|
| 598 | --remotecmd specify hg command to run on the remote side |
|---|
| 599 | """ |
|---|
| 600 | if url is None: |
|---|
| 601 | url = self.__url |
|---|
| 602 | |
|---|
| 603 | if not '/' in url: |
|---|
| 604 | url = '%s/devel/sage-%s'%(SAGE_ROOT, url) |
|---|
| 605 | |
|---|
| 606 | self('outgoing %s %s | %s' % (opts, url, pager())) |
|---|
| 607 | |
|---|
| 608 | def pull(self, url=None, options='-u'): |
|---|
| 609 | """ |
|---|
| 610 | Pull all new patches from the repository at the given url, |
|---|
| 611 | or use the default 'official' repository if no url is |
|---|
| 612 | specified. |
|---|
| 613 | |
|---|
| 614 | INPUT: |
|---|
| 615 | url: default: self.url() -- the official repository |
|---|
| 616 | * http://[user@]host[:port]/[path] |
|---|
| 617 | * https://[user@]host[:port]/[path] |
|---|
| 618 | * ssh://[user@]host[:port]/[path] |
|---|
| 619 | * local directory (starting with a /) |
|---|
| 620 | * name of a branch (for hg_sage); no /'s |
|---|
| 621 | options: (default: '-u') |
|---|
| 622 | -u --update update the working directory to tip after pull |
|---|
| 623 | -e --ssh specify ssh command to use |
|---|
| 624 | -f --force run even when remote repository is unrelated |
|---|
| 625 | -r --rev a specific revision you would like to pull |
|---|
| 626 | --remotecmd specify hg command to run on the remote side |
|---|
| 627 | |
|---|
| 628 | Some notes about using SSH with Mercurial: |
|---|
| 629 | - SSH requires an accessible shell account on the destination machine |
|---|
| 630 | and a copy of hg in the remote path or specified with as remotecmd. |
|---|
| 631 | - path is relative to the remote user's home directory by default. |
|---|
| 632 | Use an extra slash at the start of a path to specify an absolute path: |
|---|
| 633 | ssh://example.com//tmp/repository |
|---|
| 634 | - Mercurial doesn't use its own compression via SSH; the right thing |
|---|
| 635 | to do is to configure it in your ~/.ssh/ssh_config, e.g.: |
|---|
| 636 | Host *.mylocalnetwork.example.com |
|---|
| 637 | Compression off |
|---|
| 638 | Host * |
|---|
| 639 | Compression on |
|---|
| 640 | Alternatively specify "ssh -C" as your ssh command in your hgrc or |
|---|
| 641 | with the --ssh command line option. |
|---|
| 642 | """ |
|---|
| 643 | self._ensure_safe() |
|---|
| 644 | |
|---|
| 645 | if url is None: |
|---|
| 646 | url = self.__url |
|---|
| 647 | if not '/' in url: |
|---|
| 648 | url = '%s/devel/sage-%s'%(SAGE_ROOT, url) |
|---|
| 649 | |
|---|
| 650 | self('pull %s %s'%(options, url)) |
|---|
| 651 | if self.__target == 'sage': |
|---|
| 652 | print "" |
|---|
| 653 | print "Now building the new SAGE libraries" |
|---|
| 654 | os.system('sage -b') |
|---|
| 655 | print "You *MUST* restart SAGE in order for the changes to take effect!" |
|---|
| 656 | |
|---|
| 657 | print "If it says use 'hg merge' above, then you should" |
|---|
| 658 | print "type hg_sage.merge(), where hg_sage is the name" |
|---|
| 659 | print "of the repository you are using. This might not" |
|---|
| 660 | print "work with the notebook yet." |
|---|
| 661 | |
|---|
| 662 | def merge(self, options=''): |
|---|
| 663 | """ |
|---|
| 664 | Merge working directory with another revision |
|---|
| 665 | |
|---|
| 666 | Merge the contents of the current working directory and the |
|---|
| 667 | requested revision. Files that changed between either parent are |
|---|
| 668 | marked as changed for the next commit and a commit must be |
|---|
| 669 | performed before any further updates are allowed. |
|---|
| 670 | |
|---|
| 671 | INPUT: |
|---|
| 672 | options -- default: '' |
|---|
| 673 | 'tip' -- tip |
|---|
| 674 | -b --branch merge with head of a specific branch |
|---|
| 675 | -f --force force a merge with outstanding changes |
|---|
| 676 | """ |
|---|
| 677 | self('merge %s'%options) |
|---|
| 678 | |
|---|
| 679 | def update(self, options=''): |
|---|
| 680 | """ |
|---|
| 681 | update or merge working directory |
|---|
| 682 | |
|---|
| 683 | Update the working directory to the specified revision. |
|---|
| 684 | |
|---|
| 685 | If there are no outstanding changes in the working directory and |
|---|
| 686 | there is a linear relationship between the current version and the |
|---|
| 687 | requested version, the result is the requested version. |
|---|
| 688 | |
|---|
| 689 | To merge the working directory with another revision, use the |
|---|
| 690 | merge command. |
|---|
| 691 | |
|---|
| 692 | By default, update will refuse to run if doing so would require |
|---|
| 693 | merging or discarding local changes. |
|---|
| 694 | |
|---|
| 695 | aliases: up, checkout, co |
|---|
| 696 | |
|---|
| 697 | INPUT: |
|---|
| 698 | options -- string (default: '') |
|---|
| 699 | -b --branch checkout the head of a specific branch |
|---|
| 700 | -C --clean overwrite locally modified files |
|---|
| 701 | -f --force force a merge with outstanding changes |
|---|
| 702 | """ |
|---|
| 703 | self('update %s'%options) |
|---|
| 704 | |
|---|
| 705 | up = update |
|---|
| 706 | checkout = update |
|---|
| 707 | co = update |
|---|
| 708 | |
|---|
| 709 | def head(self, options=''): |
|---|
| 710 | """ |
|---|
| 711 | show current repository heads |
|---|
| 712 | |
|---|
| 713 | Show all repository head changesets. |
|---|
| 714 | |
|---|
| 715 | Repository "heads" are changesets that don't have children |
|---|
| 716 | changesets. They are where development generally takes place and |
|---|
| 717 | are the usual targets for update and merge operations. |
|---|
| 718 | |
|---|
| 719 | INPUT: |
|---|
| 720 | options -- string (default: '') |
|---|
| 721 | -b --branches show branches |
|---|
| 722 | --style display using template map file |
|---|
| 723 | -r --rev show only heads which are descendants of rev |
|---|
| 724 | --template display with template |
|---|
| 725 | """ |
|---|
| 726 | self('head %s'%options) |
|---|
| 727 | |
|---|
| 728 | heads = head |
|---|
| 729 | |
|---|
| 730 | def switch(self, name=None): |
|---|
| 731 | r""" |
|---|
| 732 | Switch to a different branch. You must restart SAGE after switching. |
|---|
| 733 | |
|---|
| 734 | Only available for \code{hg_sage.} |
|---|
| 735 | |
|---|
| 736 | INPUT: |
|---|
| 737 | name -- name of a SAGE branch (default: None) |
|---|
| 738 | |
|---|
| 739 | If the name is not given, this function returns a list of all branches. |
|---|
| 740 | """ |
|---|
| 741 | if name is None: |
|---|
| 742 | s = os.popen('ls -l %s/devel/ |grep sage-'%os.environ['SAGE_ROOT']).read() |
|---|
| 743 | t = s.split('\n') |
|---|
| 744 | v = [] |
|---|
| 745 | for X in t: |
|---|
| 746 | i = X.rfind('sage-') |
|---|
| 747 | n = X[i+5:] |
|---|
| 748 | if n != '': |
|---|
| 749 | v.append(n) |
|---|
| 750 | v = list(set(v)) |
|---|
| 751 | v.sort() |
|---|
| 752 | return v |
|---|
| 753 | os.system('sage -b "%s"'%name) |
|---|
| 754 | |
|---|
| 755 | def clone(self, name, rev=None): |
|---|
| 756 | r""" |
|---|
| 757 | Clone the current branch of the SAGE library, and make it active. |
|---|
| 758 | |
|---|
| 759 | Only available for the \code{hg_sage} repository. |
|---|
| 760 | |
|---|
| 761 | Use \code{hg_sage.switch('branch_name')} to switch to a different branch. |
|---|
| 762 | You must restart SAGE after switching. |
|---|
| 763 | |
|---|
| 764 | INPUT: |
|---|
| 765 | name -- string |
|---|
| 766 | rev -- integer or None (default) |
|---|
| 767 | |
|---|
| 768 | If rev is None, clones the latest recorded version of the repository. |
|---|
| 769 | This is very fast, e.g., about 30-60 seconds (including any build). |
|---|
| 770 | If a specific revision is specified, cloning may take much longer |
|---|
| 771 | (e.g., 5 minutes), since all Pyrex code has to be regenerated and |
|---|
| 772 | compiled. |
|---|
| 773 | |
|---|
| 774 | EXAMPLES: |
|---|
| 775 | |
|---|
| 776 | Make a clone of the repository called testing. A copy of the |
|---|
| 777 | current repository will be created in a directory sage-testing, |
|---|
| 778 | then <SAGE_ROOT>/devel/sage will point to sage-testing, and |
|---|
| 779 | when you next restart SAGE that's the version you'll be using. |
|---|
| 780 | |
|---|
| 781 | sage.: hg_sage.clone('testing') |
|---|
| 782 | ... |
|---|
| 783 | |
|---|
| 784 | Make a clone of the repository as it was at revision 1328. |
|---|
| 785 | sage.: hg_sage.clone('testing', 1328) |
|---|
| 786 | ... |
|---|
| 787 | """ |
|---|
| 788 | if not self.__cloneable: |
|---|
| 789 | raise RuntimeError, "only available for hg_sage" |
|---|
| 790 | name = '_'.join(str(name).split()) |
|---|
| 791 | if rev is None: |
|---|
| 792 | os.system('sage -clone %s'%name) |
|---|
| 793 | else: |
|---|
| 794 | os.system('sage -clone %s -r %s'%(name, int(rev))) |
|---|
| 795 | |
|---|
| 796 | def commit(self, files='', comment=None, options='', diff=True): |
|---|
| 797 | """ |
|---|
| 798 | Commit your changes to the repository. |
|---|
| 799 | |
|---|
| 800 | Quit out of the editor without saving to not record your |
|---|
| 801 | changes. |
|---|
| 802 | |
|---|
| 803 | INPUT: |
|---|
| 804 | files -- space separated string of file names (optional) |
|---|
| 805 | If specified only those files are commited. |
|---|
| 806 | The path must be absolute or relative to |
|---|
| 807 | self.dir(). |
|---|
| 808 | |
|---|
| 809 | comment -- optional changeset comment. If you don't give |
|---|
| 810 | it you will be dumped into an editor. If you're |
|---|
| 811 | using the SAGE notebook, you *must* specify a comment. |
|---|
| 812 | |
|---|
| 813 | options -- string: |
|---|
| 814 | -A --addremove mark new/missing files as added/removed before committing |
|---|
| 815 | -m --message use <text> as commit message |
|---|
| 816 | -l --logfile read the commit message from <file> |
|---|
| 817 | -d --date record datecode as commit date |
|---|
| 818 | -u --user record user as commiter |
|---|
| 819 | -I --include include names matching the given patterns |
|---|
| 820 | -X --exclude exclude names matching the given patterns |
|---|
| 821 | |
|---|
| 822 | diff -- (default: True) if True show diffs between your repository |
|---|
| 823 | and your working repository before recording changes. |
|---|
| 824 | |
|---|
| 825 | \note{If you create new files you should first add them with the add method.} |
|---|
| 826 | """ |
|---|
| 827 | if sage.server.support.EMBEDDED_MODE and comment is None: |
|---|
| 828 | raise RuntimeError, "You're using the SAGE notebook, so you *must* explicitly specify the comment in the commit command." |
|---|
| 829 | if diff: |
|---|
| 830 | self.diff(files) |
|---|
| 831 | |
|---|
| 832 | if isinstance(files, (list, tuple)): |
|---|
| 833 | files = ' '.join([str(x) for x in files]) |
|---|
| 834 | |
|---|
| 835 | if comment: |
|---|
| 836 | self('commit %s -m "%s" %s '%(options, comment, files)) |
|---|
| 837 | else: |
|---|
| 838 | self('commit %s %s'%(options, files)) |
|---|
| 839 | |
|---|
| 840 | record = commit |
|---|
| 841 | ci = commit |
|---|
| 842 | |
|---|
| 843 | def rollback(self): |
|---|
| 844 | """ |
|---|
| 845 | Remove recorded patches without changing the working copy. |
|---|
| 846 | """ |
|---|
| 847 | self('rollback') |
|---|
| 848 | |
|---|
| 849 | def bundle(self, filename, options='', url=None, base=None, to=None): |
|---|
| 850 | r""" |
|---|
| 851 | Create an hg changeset bundle with the given filename against the |
|---|
| 852 | repository at the given url (which is by default the |
|---|
| 853 | 'official' SAGE repository). |
|---|
| 854 | |
|---|
| 855 | If you have internet access, it's best to just do |
|---|
| 856 | \code{hg_sage.bundle(filename)}. If you don't |
|---|
| 857 | find a revision r that you and the person unbundling |
|---|
| 858 | both have (by looking at \code{hg_sage.log()}), then |
|---|
| 859 | do \code{hg_sage.bundle(filename, base=r)}. |
|---|
| 860 | |
|---|
| 861 | Use self.inspect('file.bundle') to inspect the resulting bundle. |
|---|
| 862 | |
|---|
| 863 | This is a file that you should probably send to William Stein |
|---|
| 864 | (wstein@gmail.com), post to a web page, or send to sage-devel. |
|---|
| 865 | It will be written to the current directory. |
|---|
| 866 | |
|---|
| 867 | INPUT: |
|---|
| 868 | filename -- output file in which to put bundle |
|---|
| 869 | options -- pass to hg |
|---|
| 870 | url -- url to bundle against (default: SAGE_SERVER) |
|---|
| 871 | base -- a base changeset revision number to bundle |
|---|
| 872 | against (doesn't require internet access) |
|---|
| 873 | """ |
|---|
| 874 | if not base is None: |
|---|
| 875 | url = '' |
|---|
| 876 | options = '--base=%s %s'%(int(base), options) |
|---|
| 877 | |
|---|
| 878 | if url is None: |
|---|
| 879 | url = self.__url |
|---|
| 880 | |
|---|
| 881 | # make sure that we don't accidentally create a file ending in '.hg.hg' |
|---|
| 882 | if filename[-3:] == '.hg': |
|---|
| 883 | filename = filename[:-3] |
|---|
| 884 | # We write to a local tmp file, then move, since unders |
|---|
| 885 | # windows hg has a bug that makes it fail to write |
|---|
| 886 | # to any filename that is at all complicated! |
|---|
| 887 | filename = os.path.abspath(filename) |
|---|
| 888 | if filename[-3:] != '.hg': |
|---|
| 889 | filename += '.hg' |
|---|
| 890 | print 'Writing to %s'%filename |
|---|
| 891 | tmpfile = '%s/tmphg'%self.__dir |
|---|
| 892 | if os.path.exists(tmpfile): |
|---|
| 893 | os.unlink(tmpfile) |
|---|
| 894 | self('bundle %s tmphg %s'%(options, url)) |
|---|
| 895 | if os.path.exists(tmpfile): |
|---|
| 896 | shutil.move(tmpfile, filename) |
|---|
| 897 | print 'Successfully created hg patch bundle %s'%filename |
|---|
| 898 | if not to is None: |
|---|
| 899 | os.system('scp "%s" %s'%(filename, to)) |
|---|
| 900 | else: |
|---|
| 901 | print 'Problem creating hg patch bundle %s'%filename |
|---|
| 902 | |
|---|
| 903 | send = bundle |
|---|
| 904 | save = send |
|---|
| 905 | |
|---|
| 906 | |
|---|
| 907 | ############################################################################## |
|---|
| 908 | # Initialize the actual repositories. |
|---|
| 909 | ############################################################################## |
|---|
| 910 | |
|---|
| 911 | import misc |
|---|
| 912 | |
|---|
| 913 | SAGE_ROOT = misc.SAGE_ROOT |
|---|
| 914 | try: |
|---|
| 915 | SAGE_SERVER = os.environ['SAGE_SERVER'] + '/hg/' |
|---|
| 916 | except KeyError: |
|---|
| 917 | print "Falling back to a hard coded sage server in misc/hg.py" |
|---|
| 918 | SAGE_SERVER = "http://sage.math.washington.edu/sage/hg/" |
|---|
| 919 | |
|---|
| 920 | hg_sage = HG('%s/devel/sage'%SAGE_ROOT, |
|---|
| 921 | 'SAGE Library Source Code', |
|---|
| 922 | url='%s/sage-main'%SAGE_SERVER, |
|---|
| 923 | cloneable=True) |
|---|
| 924 | |
|---|
| 925 | hg_doc = HG('%s/devel/doc'%SAGE_ROOT, |
|---|
| 926 | 'SAGE Documentation', |
|---|
| 927 | url='%s/doc-main'%SAGE_SERVER) |
|---|
| 928 | |
|---|
| 929 | hg_scripts = HG('%s/local/bin/'%SAGE_ROOT, |
|---|
| 930 | 'SAGE Scripts', |
|---|
| 931 | url='%s/scripts-main'%SAGE_SERVER) |
|---|
| 932 | |
|---|
| 933 | hg_extcode = HG('%s/data/extcode'%SAGE_ROOT, |
|---|
| 934 | 'SAGE External System Code (e.g., PARI, MAGMA, etc.)', |
|---|
| 935 | url='%s/extcode-main'%SAGE_SERVER) |
|---|
| 936 | |
|---|
| 937 | hg_c_lib = HG('%s/devel/c_lib'%SAGE_ROOT, |
|---|
| 938 | 'SAGE C-library code', |
|---|
| 939 | url='%s/extcode-main'%SAGE_SERVER) |
|---|