source: sage/misc/hg.py @ 5583:fee6a3031a20

Revision 5583:fee6a3031a20, 35.9 KB checked in by William Stein <wstein@…>, 6 years ago (diff)

Some improvements to how hg_sage... works.

Line 
1r"""
2SAGE Interface to the HG/Mercurial Revision Control System
3
4These functions make setup and use of source control with SAGE easier, using
5the distributed Mercurial HG source control system.  To learn about Mercurial,
6see http://www.selenic.com/mercurial/wiki/.
7
8This system should all be fully usable from the SAGE notebook (except
9for merging, currently).
10This 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
31import os, shutil
32
33from   viewer import browser
34from   misc   import tmp_filename, branch_current_hg
35from   remote_file import get_remote_file
36from   sage.server.misc import print_open_msg
37
38import sage.server.support
39def embedded():
40    return sage.server.support.EMBEDDED_MODE
41
42def pager():
43    if embedded():
44        return 'cat'
45    else:
46        return 'less'
47
48class 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
911import misc
912
913SAGE_ROOT = misc.SAGE_ROOT
914try:
915    SAGE_SERVER = os.environ['SAGE_SERVER'] + '/hg/'
916except 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
920hg_sage    = HG('%s/devel/sage'%SAGE_ROOT,
921                'SAGE Library Source Code',
922                url='%s/sage-main'%SAGE_SERVER,
923                cloneable=True)
924
925hg_doc     = HG('%s/devel/doc'%SAGE_ROOT,
926                'SAGE Documentation',
927                url='%s/doc-main'%SAGE_SERVER)
928
929hg_scripts = HG('%s/local/bin/'%SAGE_ROOT,
930                'SAGE Scripts',
931                url='%s/scripts-main'%SAGE_SERVER)
932
933hg_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
937hg_c_lib = HG('%s/devel/c_lib'%SAGE_ROOT,
938                'SAGE C-library code',
939                url='%s/extcode-main'%SAGE_SERVER)
Note: See TracBrowser for help on using the repository browser.