Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemath
GitHub Repository: sagemath/sagesmc
Path: blob/master/src/sage/dev/sagedev.py
8815 views
1
r"""
2
SageDev
3
4
This module provides :class:`SageDev`, the central object of the developer
5
scripts for sage.
6
7
AUTHORS:
8
9
- David Roe, Frej Drejhammar, Julian Rueth, Martin Raum, Nicolas M. Thiery,
10
R. Andrew Ohana, Robert Bradshaw, Timo Kluck: initial version
11
12
"""
13
#*****************************************************************************
14
# Copyright (C) 2013 David Roe <[email protected]>
15
# Frej Drejhammar <[email protected]>
16
# Julian Rueth <[email protected]>
17
# Martin Raum <[email protected]>
18
# Nicolas M. Thiery <[email protected]>
19
# R. Andrew Ohana <[email protected]>
20
# Robert Bradshaw <[email protected]>
21
# Timo Kluck <[email protected]>
22
# Volker Braun <[email protected]>
23
#
24
# Distributed under the terms of the GNU General Public License (GPL)
25
# as published by the Free Software Foundation; either version 2 of
26
# the License, or (at your option) any later version.
27
# http://www.gnu.org/licenses/
28
#*****************************************************************************
29
30
import os
31
import urllib, urlparse
32
import re
33
34
from user_interface_error import OperationCancelledError
35
from trac_error import TracConnectionError, TracInternalError, TracError
36
from git_error import GitError
37
from patch import MercurialPatchMixin
38
39
from sage.env import TRAC_SERVER_URI
40
41
# regular expression to check validity of git options
42
# http://stackoverflow.com/questions/12093748/how-do-i-check-for-valid-git-branch-names
43
GIT_BRANCH_REGEX = re.compile(
44
r'^(?!.*/\.)(?!.*\.\.)(?!/)(?!.*//)(?!.*@\{)(?!.*\\)'
45
r'[^\040\177 ~^:?*[]+(?<!\.lock)(?<!/)(?<!\.)$')
46
47
# the name of the branch which holds the vanilla clone of sage
48
MASTER_BRANCH = "master"
49
USER_BRANCH = re.compile(r"^u/([^/]+)/")
50
51
COMMIT_GUIDE=r"""
52
53
54
# Please type your commit message above.
55
#
56
# The first line should contain a short summary of your changes, the
57
# following lines should contain a more detailed description. Lines
58
# starting with '#' are ignored.
59
#
60
# An empty file aborts the commit.
61
"""
62
63
class SageDev(MercurialPatchMixin):
64
r"""
65
The developer interface for sage.
66
67
This class facilitates access to git and trac.
68
69
INPUT:
70
71
- ``config`` -- a :class:`~sage.dev.config.Config` or ``None``
72
(default: ``None``), the configuration of this object; the
73
defaults uses the configuration stored in ``DOT_SAGE/devrc``.
74
75
- ``UI`` -- a :class:`~sage.dev.user_interface.UserInterface` or ``None`` (default:
76
``None``), the default creates a
77
:class:`cmd_line_interface.CmdLineInterface` from ``config['UI']``.
78
79
- ``trac`` -- a :class:`trac_interface.TracInterface` or ``None`` (default:
80
``None``), the default creates a :class:`trac_interface.TracInterface`
81
from ``config['trac']``.
82
83
- ``git`` -- a :class:`git_interface.GitInterface` or ``None`` (default:
84
``None``), the default creates a :class:`git_interface.GitInterface` from
85
``config['git']``.
86
87
EXAMPLES::
88
89
sage: dev._sagedev
90
SageDev()
91
"""
92
def __init__(self, config=None, UI=None, trac=None, git=None):
93
r"""
94
Initialization.
95
96
TESTS::
97
98
sage: type(dev._sagedev)
99
<class 'sage.dev.test.sagedev.DoctestSageDev'>
100
"""
101
self.config = config
102
if self.config is None:
103
from config import Config
104
self.config = Config()
105
106
# create some empty config sections if they do not yet exist
107
for section in ['UI','trac','git','sagedev']:
108
if section not in self.config:
109
self.config[section] = {}
110
111
self._UI = UI
112
if self._UI is None:
113
from cmd_line_interface import CmdLineInterface
114
self._UI = CmdLineInterface(self.config['UI'])
115
116
self.trac = trac
117
if self.trac is None:
118
from trac_interface import TracInterface
119
self.trac = TracInterface(self.config['trac'], self._UI)
120
121
self.git = git
122
if self.git is None:
123
from git_interface import GitInterface
124
self.git = GitInterface(self.config['git'], self._UI)
125
126
# create some SavingDicts to store the relations between branches and tickets
127
from sage.env import DOT_SAGE
128
import os
129
def move_legacy_saving_dict(key, old_file, new_file):
130
'''
131
We used to have these files in DOT_SAGE - this is not a good idea
132
because a user might have multiple copies of sage which should each
133
have their own set of files.
134
135
This method moves an existing file mentioned in the config to its
136
new position to support repositories created earlier.
137
'''
138
import sage.doctest
139
if sage.doctest.DOCTEST_MODE:
140
return
141
import shutil
142
if not os.path.exists(new_file) and os.path.exists(old_file):
143
shutil.move(old_file, new_file)
144
self._UI.show('The developer scripts used to store some of their data in "{0}".'
145
' This file has now moved to "{1}". I moved "{0}" to "{1}". This might'
146
' cause trouble if this is a fresh clone of the repository in which'
147
' you never used the developer scripts before. In that case you'
148
' should manually delete "{1}" now.', old_file, new_file)
149
if key in self.config['sagedev']:
150
del self.config['sagedev'][key]
151
152
ticket_file = os.path.join(self.git._dot_git, 'branch_to_ticket')
153
move_legacy_saving_dict('ticketfile', self.config['sagedev'].get(
154
'ticketfile', os.path.join(DOT_SAGE, 'branch_to_ticket')), ticket_file)
155
branch_file = os.path.join(self.git._dot_git, 'ticket_to_branch')
156
move_legacy_saving_dict('branchfile', self.config['sagedev'].get(
157
'branchfile', os.path.join(DOT_SAGE, 'ticket_to_branch')), branch_file)
158
dependencies_file = os.path.join(self.git._dot_git, 'dependencies')
159
move_legacy_saving_dict('dependenciesfile', self.config['sagedev'].get(
160
'dependenciesfile', os.path.join(DOT_SAGE, 'dependencies')), dependencies_file)
161
remote_branches_file = os.path.join(self.git._dot_git, 'remote_branches')
162
move_legacy_saving_dict('remotebranchesfile', self.config['sagedev'].get(
163
'remotebranchesfile', os.path.join(DOT_SAGE, 'remote_branches')), remote_branches_file)
164
165
# some people dislike double underscore fields; here you can very
166
# seriously screw up your setup if you put something invalid into
167
# these. Ideally these fields should only be touched by single
168
# underscore methods such as _set_remote_branch which do some checking
169
# on the parameters
170
from saving_dict import SavingDict
171
self.__branch_to_ticket = SavingDict(ticket_file)
172
self.__ticket_to_branch = SavingDict(branch_file, paired=self.__branch_to_ticket)
173
self.__ticket_dependencies = SavingDict(dependencies_file, default=tuple)
174
self.__branch_to_remote_branch = SavingDict(remote_branches_file)
175
176
@property
177
def tmp_dir(self):
178
r"""
179
A lazy property to provide a temporary directory
180
181
TESTS::
182
183
sage: import os
184
sage: os.path.isdir(dev._sagedev.tmp_dir)
185
True
186
"""
187
try:
188
return self._tmp_dir
189
except AttributeError:
190
from sage.dev.misc import tmp_dir
191
self._tmp_dir = tmp_dir()
192
import atexit, shutil
193
atexit.register(shutil.rmtree, self._tmp_dir)
194
return self._tmp_dir
195
196
def __repr__(self):
197
r"""
198
Return a printable representation of this object.
199
200
TESTS::
201
202
sage: dev # indirect doctest
203
SageDev()
204
"""
205
return "SageDev()"
206
207
def create_ticket(self):
208
r"""
209
Create a new ticket on trac.
210
211
OUTPUT:
212
213
Returns the number of the newly created ticket as an int.
214
215
.. SEEALSO::
216
217
:meth:`checkout`, :meth:`pull`, :meth:`edit_ticket`
218
219
TESTS:
220
221
Set up a single user environment::
222
223
sage: from sage.dev.test.sagedev import single_user_setup
224
sage: dev, config, UI, server = single_user_setup()
225
sage: dev._wrap("_dependencies_for_ticket")
226
227
Create some tickets::
228
229
sage: UI.append("Summary: ticket1\ndescription")
230
sage: dev.create_ticket()
231
Created ticket #1 at https://trac.sagemath.org/1.
232
<BLANKLINE>
233
# (use "sage --dev checkout --ticket=1" to create a new local branch)
234
1
235
236
sage: UI.append("Summary: ticket2\ndescription")
237
sage: dev.create_ticket()
238
Created ticket #2 at https://trac.sagemath.org/2.
239
<BLANKLINE>
240
# (use "sage --dev checkout --ticket=2" to create a new local branch)
241
2
242
243
This fails if the internet connection is broken::
244
245
sage: dev.trac._connected = False
246
sage: UI.append("Summary: ticket7\ndescription")
247
sage: dev.create_ticket()
248
A network error ocurred, ticket creation aborted.
249
Your command failed because no connection to trac could be established.
250
sage: dev.trac._connected = True
251
"""
252
try:
253
ticket = self.trac.create_ticket_interactive()
254
except OperationCancelledError:
255
self._UI.debug("Ticket creation aborted.")
256
raise
257
except TracConnectionError as e:
258
self._UI.error("A network error ocurred, ticket creation aborted.")
259
raise
260
ticket_url = urlparse.urljoin(self.trac._config.get('server', TRAC_SERVER_URI), str(ticket))
261
self._UI.show("Created ticket #{0} at {1}.".format(ticket, ticket_url))
262
self._UI.info(['',
263
'(use "{0}" to create a new local branch)'
264
.format(self._format_command("checkout", ticket=ticket))])
265
return ticket
266
267
def checkout(self, ticket=None, branch=None, base=''):
268
r"""
269
Checkout another branch.
270
271
If ``ticket`` is specified, and ``branch`` is an existing local branch,
272
then ``ticket`` will be associated to it, and ``branch`` will be
273
checked out into the working directory.
274
Otherwise, if there is no local branch for ``ticket``, the branch
275
specified on trac will be pulled to ``branch`` unless ``base`` is
276
set to something other than the empty string ``''``. If the trac ticket
277
does not specify a branch yet or if ``base`` is not the empty string,
278
then a new one will be created from ``base`` (per default, the master
279
branch).
280
281
If ``ticket`` is not specified, then checkout the local branch
282
``branch`` into the working directory.
283
284
INPUT:
285
286
- ``ticket`` -- a string or an integer identifying a ticket or ``None``
287
(default: ``None``)
288
289
- ``branch`` -- a string, the name of a local branch; if ``ticket`` is
290
specified, then this defaults to ticket/``ticket``.
291
292
- ``base`` -- a string or ``None``, a branch on which to base a new
293
branch if one is going to be created (default: the empty string
294
``''`` to create the new branch from the master branch), or a ticket;
295
if ``base`` is set to ``None``, then the current ticket is used. If
296
``base`` is a ticket, then the corresponding dependency will be
297
added. Must be ``''`` if ``ticket`` is not specified.
298
299
.. SEEALSO::
300
301
:meth:`pull`, :meth:`create_ticket`, :meth:`vanilla`
302
303
TESTS:
304
305
Set up a single user for doctesting::
306
307
sage: from sage.dev.test.sagedev import single_user_setup
308
sage: dev, config, UI, server = single_user_setup()
309
310
Create a few branches::
311
312
sage: dev.git.silent.branch("branch1")
313
sage: dev.git.silent.branch("branch2")
314
315
Checking out a branch::
316
317
sage: dev.checkout(branch="branch1")
318
On local branch "branch1" without associated ticket.
319
<BLANKLINE>
320
# Use "sage --dev merge" to include another ticket/branch.
321
# Use "sage --dev commit" to save changes into a new commit.
322
sage: dev.git.current_branch()
323
'branch1'
324
325
Create a ticket and checkout a branch for it::
326
327
sage: UI.append("Summary: summary\ndescription")
328
sage: dev.create_ticket()
329
Created ticket #1 at https://trac.sagemath.org/1.
330
<BLANKLINE>
331
# (use "sage --dev checkout --ticket=1" to create a new local branch)
332
1
333
sage: dev.checkout(ticket=1)
334
On ticket #1 with associated local branch "ticket/1".
335
<BLANKLINE>
336
# Use "sage --dev merge" to include another ticket/branch.
337
# Use "sage --dev commit" to save changes into a new commit.
338
sage: dev.git.current_branch()
339
'ticket/1'
340
"""
341
if ticket is not None:
342
self.checkout_ticket(ticket=ticket, branch=branch, base=base)
343
elif branch is not None:
344
if base != '':
345
raise SageDevValueError("base must not be specified if no ticket is specified.")
346
self.checkout_branch(branch=branch)
347
else:
348
raise SageDevValueError("at least one of ticket or branch must be specified.")
349
350
ticket = self._current_ticket()
351
branch = self.git.current_branch()
352
if ticket:
353
self._UI.show(['On ticket #{0} with associated local branch "{1}".'], ticket, branch)
354
else:
355
self._UI.show(['On local branch "{0}" without associated ticket.'], branch)
356
self._UI.info(['',
357
'Use "{0}" to include another ticket/branch.',
358
'Use "{1}" to save changes into a new commit.'],
359
self._format_command("merge"),
360
self._format_command("commit"))
361
362
363
def checkout_ticket(self, ticket, branch=None, base=''):
364
r"""
365
Checkout the branch associated to ``ticket``.
366
367
If ``branch`` is an existing local branch, then ``ticket`` will be
368
associated to it, and ``branch`` will be checked out into the working directory.
369
370
Otherwise, if there is no local branch for ``ticket``, the branch
371
specified on trac will be pulled to ``branch`` unless ``base`` is
372
set to something other than the empty string ``''``. If the trac ticket
373
does not specify a branch yet or if ``base`` is not the empty string,
374
then a new one will be created from ``base`` (per default, the master
375
branch).
376
377
INPUT:
378
379
- ``ticket`` -- a string or an integer identifying a ticket
380
381
- ``branch`` -- a string, the name of the local branch that stores
382
changes for ``ticket`` (default: ticket/``ticket``)
383
384
- ``base`` -- a string or ``None``, a branch on which to base a new
385
branch if one is going to be created (default: the empty string
386
``''`` to create the new branch from the master branch), or a ticket;
387
if ``base`` is set to ``None``, then the current ticket is used. If
388
``base`` is a ticket, then the corresponding dependency will be
389
added.
390
391
.. SEEALSO::
392
393
:meth:`pull`, :meth:`create_ticket`, :meth:`vanilla`
394
395
TESTS:
396
397
Create a doctest setup with two users::
398
399
sage: from sage.dev.test.sagedev import two_user_setup
400
sage: alice, config_alice, bob, config_bob, server = two_user_setup()
401
402
Alice tries to checkout ticket #1 which does not exist yet::
403
404
sage: alice._chdir()
405
sage: alice.checkout(ticket=1)
406
Ticket name "1" is not valid or ticket does not exist on trac.
407
408
Bob creates that ticket::
409
410
sage: bob._chdir()
411
sage: bob._UI.append("Summary: summary1\ndescription")
412
sage: bob.create_ticket()
413
Created ticket #1 at https://trac.sagemath.org/1.
414
<BLANKLINE>
415
# (use "sage --dev checkout --ticket=1" to create a new local branch)
416
1
417
sage: bob.checkout(ticket=1)
418
On ticket #1 with associated local branch "ticket/1".
419
<BLANKLINE>
420
# Use "sage --dev merge" to include another ticket/branch.
421
# Use "sage --dev commit" to save changes into a new commit.
422
423
Now alice can check it out, even though there is no branch on the
424
ticket description::
425
426
sage: alice._chdir()
427
sage: alice.checkout(ticket=1)
428
On ticket #1 with associated local branch "ticket/1".
429
<BLANKLINE>
430
# Use "sage --dev merge" to include another ticket/branch.
431
# Use "sage --dev commit" to save changes into a new commit.
432
433
If Bob commits something to the ticket, a ``checkout`` by Alice
434
does not take his changes into account::
435
436
sage: bob._chdir()
437
sage: bob.git.super_silent.commit(allow_empty=True,message="empty commit")
438
sage: bob._UI.append("y")
439
sage: bob.push()
440
The branch "u/bob/ticket/1" does not exist on the remote server.
441
Create new remote branch? [Yes/no] y
442
443
sage: alice._chdir()
444
sage: alice.checkout(ticket=1)
445
On ticket #1 with associated local branch "ticket/1".
446
<BLANKLINE>
447
# Use "sage --dev merge" to include another ticket/branch.
448
# Use "sage --dev commit" to save changes into a new commit.
449
sage: alice.git.echo.log('--pretty=%s')
450
initial commit
451
452
If Alice had not checked that ticket out before, she would of course
453
see Bob's changes (this also checks that we can handle a corrupt ticket
454
database and a detached HEAD)::
455
456
sage: alice.git.super_silent.checkout('HEAD', detach=True)
457
sage: alice.git.super_silent.branch('-d','ticket/1')
458
sage: alice.checkout(ticket=1) # ticket #1 refers to the non-existant branch 'ticket/1'
459
Ticket #1 refers to the non-existant local branch "ticket/1". If you have not
460
manually interacted with git, then this is a bug in sagedev. Removing the
461
association from ticket #1 to branch "ticket/1".
462
On ticket #1 with associated local branch "ticket/1".
463
<BLANKLINE>
464
# Use "sage --dev merge" to include another ticket/branch.
465
# Use "sage --dev commit" to save changes into a new commit.
466
sage: alice.git.current_branch()
467
'ticket/1'
468
sage: alice.git.echo.log('--pretty=%s')
469
empty commit
470
initial commit
471
472
Checking out a ticket with untracked files::
473
474
sage: alice._UI.append("Summary: summary2\ndescription")
475
sage: alice.create_ticket()
476
Created ticket #2 at https://trac.sagemath.org/2.
477
<BLANKLINE>
478
# (use "sage --dev checkout --ticket=2" to create a new local branch)
479
2
480
sage: alice.checkout(ticket=2)
481
On ticket #2 with associated local branch "ticket/2".
482
<BLANKLINE>
483
# Use "sage --dev merge" to include another ticket/branch.
484
# Use "sage --dev commit" to save changes into a new commit.
485
sage: alice.git.echo.log('--pretty=%s')
486
initial commit
487
sage: open("untracked","w").close()
488
sage: alice.checkout(ticket=1)
489
On ticket #1 with associated local branch "ticket/1".
490
<BLANKLINE>
491
# Use "sage --dev merge" to include another ticket/branch.
492
# Use "sage --dev commit" to save changes into a new commit.
493
sage: alice.git.echo.log('--pretty=%s')
494
empty commit
495
initial commit
496
497
Checking out a ticket with untracked files which make a checkout
498
impossible::
499
500
sage: alice.git.super_silent.add("untracked")
501
sage: alice.git.super_silent.commit(message="added untracked")
502
sage: alice.checkout(ticket=2)
503
On ticket #2 with associated local branch "ticket/2".
504
<BLANKLINE>
505
# Use "sage --dev merge" to include another ticket/branch.
506
# Use "sage --dev commit" to save changes into a new commit.
507
sage: open("untracked","w").close()
508
sage: alice.checkout(ticket=1)
509
GitError: git exited with a non-zero exit code (1).
510
This happened while executing "git -c [email protected] -c
511
user.name=alice checkout ticket/1".
512
git printed nothing to STDOUT.
513
git printed the following to STDERR:
514
error: The following untracked working tree files would be overwritten by checkout:
515
untracked
516
Please move or remove them before you can switch branches.
517
Aborting
518
519
Checking out a ticket with uncommited changes::
520
521
sage: open("tracked", "w").close()
522
sage: alice.git.super_silent.add("tracked")
523
sage: alice._UI.append('d')
524
sage: alice.checkout(ticket=2)
525
The following files in your working directory contain uncommitted changes:
526
<BLANKLINE>
527
tracked
528
<BLANKLINE>
529
Discard changes? [discard/Keep/stash] d
530
On ticket #2 with associated local branch "ticket/2".
531
<BLANKLINE>
532
# Use "sage --dev merge" to include another ticket/branch.
533
# Use "sage --dev commit" to save changes into a new commit.
534
535
Now follow some single user tests to check that the parameters are interpreted correctly::
536
537
sage: from sage.dev.test.sagedev import single_user_setup
538
sage: dev, config, UI, server = single_user_setup()
539
sage: dev._wrap("_dependencies_for_ticket")
540
541
First, create some tickets::
542
543
sage: UI.append("Summary: ticket1\ndescription")
544
sage: dev.create_ticket()
545
Created ticket #1 at https://trac.sagemath.org/1.
546
<BLANKLINE>
547
# (use "sage --dev checkout --ticket=1" to create a new local branch)
548
1
549
sage: dev.checkout(ticket=1)
550
On ticket #1 with associated local branch "ticket/1".
551
<BLANKLINE>
552
# Use "sage --dev merge" to include another ticket/branch.
553
# Use "sage --dev commit" to save changes into a new commit.
554
sage: UI.append("Summary: ticket2\ndescription")
555
sage: dev.create_ticket()
556
Created ticket #2 at https://trac.sagemath.org/2.
557
<BLANKLINE>
558
# (use "sage --dev checkout --ticket=2" to create a new local branch)
559
2
560
sage: dev.checkout(ticket=2)
561
On ticket #2 with associated local branch "ticket/2".
562
<BLANKLINE>
563
# Use "sage --dev merge" to include another ticket/branch.
564
# Use "sage --dev commit" to save changes into a new commit.
565
sage: dev.git.silent.commit(allow_empty=True, message="second commit")
566
sage: dev.git.commit_for_branch('ticket/2') != dev.git.commit_for_branch('ticket/1')
567
True
568
569
Check that ``base`` works::
570
571
sage: UI.append("Summary: ticket3\ndescription")
572
sage: dev.create_ticket()
573
Created ticket #3 at https://trac.sagemath.org/3.
574
<BLANKLINE>
575
# (use "sage --dev checkout --ticket=3" to create a new local branch)
576
3
577
sage: dev.checkout(ticket=3, base=2)
578
On ticket #3 with associated local branch "ticket/3".
579
<BLANKLINE>
580
# Use "sage --dev merge" to include another ticket/branch.
581
# Use "sage --dev commit" to save changes into a new commit.
582
sage: dev.git.commit_for_branch('ticket/3') == dev.git.commit_for_branch('ticket/2')
583
True
584
sage: dev._dependencies_for_ticket(3)
585
(2,)
586
sage: UI.append("Summary: ticket4\ndescription")
587
sage: dev.create_ticket()
588
Created ticket #4 at https://trac.sagemath.org/4.
589
<BLANKLINE>
590
# (use "sage --dev checkout --ticket=4" to create a new local branch)
591
4
592
sage: dev.checkout(ticket=4, base='ticket/2')
593
On ticket #4 with associated local branch "ticket/4".
594
<BLANKLINE>
595
# Use "sage --dev merge" to include another ticket/branch.
596
# Use "sage --dev commit" to save changes into a new commit.
597
sage: dev.git.commit_for_branch('ticket/4') == dev.git.commit_for_branch('ticket/2')
598
True
599
sage: dev._dependencies_for_ticket(4)
600
()
601
602
In this example ``base`` does not exist::
603
604
sage: UI.append("Summary: ticket5\ndescription")
605
sage: dev.create_ticket()
606
Created ticket #5 at https://trac.sagemath.org/5.
607
<BLANKLINE>
608
# (use "sage --dev checkout --ticket=5" to create a new local branch)
609
5
610
sage: dev.checkout(ticket=5, base=1000)
611
Ticket name "1000" is not valid or ticket does not exist on trac.
612
613
In this example ``base`` does not exist locally::
614
615
sage: UI.append("Summary: ticket6\ndescription")
616
sage: dev.create_ticket()
617
Created ticket #6 at https://trac.sagemath.org/6.
618
<BLANKLINE>
619
# (use "sage --dev checkout --ticket=6" to create a new local branch)
620
6
621
sage: dev.checkout(ticket=6, base=5)
622
Branch field is not set for ticket #5 on trac.
623
624
Creating a ticket when in detached HEAD state::
625
626
sage: dev.git.super_silent.checkout('HEAD', detach=True)
627
sage: UI.append("Summary: ticket detached\ndescription")
628
sage: dev.create_ticket()
629
Created ticket #7 at https://trac.sagemath.org/7.
630
<BLANKLINE>
631
# (use "sage --dev checkout --ticket=7" to create a new local branch)
632
7
633
sage: dev.checkout(ticket=7)
634
On ticket #7 with associated local branch "ticket/7".
635
<BLANKLINE>
636
# Use "sage --dev merge" to include another ticket/branch.
637
# Use "sage --dev commit" to save changes into a new commit.
638
sage: dev.git.current_branch()
639
'ticket/7'
640
641
Creating a ticket when in the middle of a merge::
642
643
sage: dev.git.super_silent.checkout('-b','merge_branch')
644
sage: with open('merge', 'w') as f: f.write("version 0")
645
sage: dev.git.silent.add('merge')
646
sage: dev.git.silent.commit('-m','some change')
647
sage: dev.git.super_silent.checkout('ticket/7')
648
sage: with open('merge', 'w') as f: f.write("version 1")
649
sage: dev.git.silent.add('merge')
650
sage: dev.git.silent.commit('-m','conflicting change')
651
sage: from sage.dev.git_error import GitError
652
sage: try:
653
....: dev.git.silent.merge('merge_branch')
654
....: except GitError: pass
655
sage: UI.append("Summary: ticket merge\ndescription")
656
sage: dev.create_ticket()
657
Created ticket #8 at https://trac.sagemath.org/8.
658
<BLANKLINE>
659
# (use "sage --dev checkout --ticket=8" to create a new local branch)
660
8
661
sage: UI.append("cancel")
662
sage: dev.checkout(ticket=8)
663
Repository is in an unclean state (merge). Resetting the state will discard any
664
uncommited changes.
665
Reset repository? [reset/Cancel] cancel
666
Aborting checkout of branch "ticket/8".
667
<BLANKLINE>
668
# (use "sage --dev commit" to save changes in a new commit)
669
sage: dev.git.reset_to_clean_state()
670
671
Creating a ticket with uncommitted changes::
672
673
sage: open('tracked', 'w').close()
674
sage: dev.git.silent.add('tracked')
675
sage: UI.append("Summary: ticket merge\ndescription")
676
sage: dev.create_ticket()
677
Created ticket #9 at https://trac.sagemath.org/9.
678
<BLANKLINE>
679
# (use "sage --dev checkout --ticket=9" to create a new local branch)
680
9
681
682
The new branch is based on master which is not the same commit
683
as the current branch ``ticket/7``, so it is not a valid
684
option to ``'keep'`` changes::
685
686
sage: UI.append("cancel")
687
sage: dev.checkout(ticket=9)
688
The following files in your working directory contain uncommitted changes:
689
<BLANKLINE>
690
tracked
691
<BLANKLINE>
692
Discard changes? [discard/Cancel/stash] cancel
693
Aborting checkout of branch "ticket/9".
694
<BLANKLINE>
695
# (use "sage --dev commit" to save changes in a new commit)
696
697
Finally, in this case we can keep changes because the base is
698
the same commit as the current branch::
699
700
sage: UI.append("Summary: ticket merge\ndescription")
701
sage: dev.create_ticket()
702
Created ticket #10 at https://trac.sagemath.org/10.
703
<BLANKLINE>
704
# (use "sage --dev checkout --ticket=10" to create a new local branch)
705
10
706
sage: UI.append("keep")
707
sage: dev.checkout(ticket=10, base='ticket/7')
708
The following files in your working directory contain uncommitted changes:
709
<BLANKLINE>
710
tracked
711
<BLANKLINE>
712
Discard changes? [discard/Keep/stash] keep
713
On ticket #10 with associated local branch "ticket/10".
714
<BLANKLINE>
715
# Use "sage --dev merge" to include another ticket/branch.
716
# Use "sage --dev commit" to save changes into a new commit.
717
"""
718
self._check_ticket_name(ticket, exists=True)
719
ticket = self._ticket_from_ticket_name(ticket)
720
721
# if branch points to an existing branch make it the ticket's branch and check it out
722
if branch is not None and self._is_local_branch_name(branch, exists=True):
723
if base != MASTER_BRANCH:
724
raise SageDevValueError("base must not be specified if branch is an existing branch")
725
if branch == MASTER_BRANCH:
726
raise SageDevValueError("branch must not be the master branch")
727
728
self._set_local_branch_for_ticket(ticket, branch)
729
self._UI.debug('The branch for ticket #{0} is now "{1}".', ticket, branch)
730
self._UI.debug('Now checking out branch "{0}".', branch)
731
self.checkout_branch(branch)
732
return
733
734
# if there is a branch for ticket locally, check it out
735
if branch is None:
736
if self._has_local_branch_for_ticket(ticket):
737
branch = self._local_branch_for_ticket(ticket)
738
self._UI.debug('Checking out branch "{0}".', branch)
739
self.checkout_branch(branch)
740
return
741
else:
742
branch = self._new_local_branch_for_ticket(ticket)
743
744
# branch does not exist, so we have to create a new branch for ticket
745
# depending on the value of base, this will either be base or a copy of
746
# the branch mentioned on trac if any
747
dependencies = self.trac.dependencies(ticket)
748
if base is None:
749
base = self._current_ticket()
750
if base is None:
751
raise SageDevValueError('currently on no ticket, "base" must not be None')
752
if self._is_ticket_name(base):
753
base = self._ticket_from_ticket_name(base)
754
dependencies = [base] # we create a new branch for this ticket - ignore the dependencies which are on trac
755
base = self._local_branch_for_ticket(base, pull_if_not_found=True)
756
757
remote_branch = self.trac._branch_for_ticket(ticket)
758
try:
759
if base == '':
760
base = MASTER_BRANCH
761
if remote_branch is None: # branch field is not set on ticket
762
# create a new branch off master
763
self._UI.debug('The branch field on ticket #{0} is not set. Creating a new branch'
764
' "{1}" off the master branch "{2}".', ticket, branch, MASTER_BRANCH)
765
self.git.silent.branch(branch, MASTER_BRANCH)
766
else:
767
# pull the branch mentioned on trac
768
if not self._is_remote_branch_name(remote_branch, exists=True):
769
self._UI.error('The branch field on ticket #{0} is set to the non-existent "{1}".'
770
' Please set the field on trac to a field value.',
771
ticket, remote_branch)
772
self._UI.info(['', '(use "{0}" to edit the ticket description)'],
773
self._format_command("edit-ticket", ticket=ticket))
774
raise OperationCancelledError("remote branch does not exist")
775
776
self.git.super_silent.fetch(self.git._repository_anonymous, remote_branch)
777
self.git.super_silent.branch(branch, 'FETCH_HEAD')
778
else:
779
self._check_local_branch_name(base, exists=True)
780
if remote_branch is not None:
781
self._UI.show('About to create a new branch for #{0} based on "{1}". However, the trac'
782
' ticket for #{0} already refers to the branch "{2}". The new branch will'
783
' not contain any work that has already been done on "{2}".',
784
ticket, base, remote_branch)
785
if not self._UI.confirm('Create fresh branch?', default=False):
786
command = ""
787
if self._has_local_branch_for_ticket(ticket):
788
command += self._format_command("abandon", self._local_branch_for_ticket(ticket)) + "; "
789
command += self._format_command("checkout", ticket=ticket)
790
self._UI.info(['', 'Use "{1}" to work on a local copy of the existing remote branch "{0}".'],
791
remote_branch, command)
792
raise OperationCancelledError("user requested")
793
794
self._UI.debug('Creating a new branch for #{0} based on "{1}".', ticket, base)
795
self.git.silent.branch(branch, base)
796
except:
797
if self._is_local_branch_name(branch, exists=True):
798
self._UI.debug('Deleting local branch "{0}".', branch)
799
self.git.super_silent.branch(branch, D=True)
800
raise
801
802
self._set_local_branch_for_ticket(ticket, branch)
803
if dependencies:
804
self._UI.debug("Locally recording dependency on {0} for #{1}.",
805
", ".join(["#"+str(dep) for dep in dependencies]), ticket)
806
self._set_dependencies_for_ticket(ticket, dependencies)
807
self._set_remote_branch_for_branch(branch, self._remote_branch_for_ticket(ticket))
808
self._UI.debug('Checking out to newly created branch "{0}".'.format(branch))
809
self.checkout_branch(branch)
810
811
def checkout_branch(self, branch, helpful=True):
812
r"""
813
Checkout to the local branch ``branch``.
814
815
INPUT:
816
817
- ``branch`` -- a string, the name of a local branch
818
819
TESTS:
820
821
Set up a single user for doctesting::
822
823
sage: from sage.dev.test.sagedev import single_user_setup
824
sage: dev, config, UI, server = single_user_setup()
825
826
Create a few branches::
827
828
sage: dev.git.silent.branch("branch1")
829
sage: dev.git.silent.branch("branch2")
830
831
Checking out a branch::
832
833
sage: dev.checkout(branch="branch1")
834
On local branch "branch1" without associated ticket.
835
<BLANKLINE>
836
# Use "sage --dev merge" to include another ticket/branch.
837
# Use "sage --dev commit" to save changes into a new commit.
838
sage: dev.git.current_branch()
839
'branch1'
840
841
The branch must exist::
842
843
sage: dev.checkout(branch="branch3")
844
Branch "branch3" does not exist locally.
845
<BLANKLINE>
846
# (use "sage --dev tickets" to list local branches)
847
848
Checking out branches with untracked files::
849
850
sage: open("untracked", "w").close()
851
sage: dev.checkout(branch="branch2")
852
On local branch "branch2" without associated ticket.
853
<BLANKLINE>
854
# Use "sage --dev merge" to include another ticket/branch.
855
# Use "sage --dev commit" to save changes into a new commit.
856
857
Checking out a branch with uncommitted changes::
858
859
sage: open("tracked", "w").close()
860
sage: dev.git.silent.add("tracked")
861
sage: dev.git.silent.commit(message="added tracked")
862
sage: with open("tracked", "w") as f: f.write("foo")
863
sage: UI.append("cancel")
864
sage: dev.checkout(branch="branch1")
865
The following files in your working directory contain uncommitted changes:
866
<BLANKLINE>
867
tracked
868
<BLANKLINE>
869
Discard changes? [discard/Cancel/stash] cancel
870
Aborting checkout of branch "branch1".
871
<BLANKLINE>
872
# (use "sage --dev commit" to save changes in a new commit)
873
874
We can stash uncommitted changes::
875
876
sage: UI.append("s")
877
sage: dev.checkout(branch="branch1")
878
The following files in your working directory contain uncommitted changes:
879
<BLANKLINE>
880
tracked
881
<BLANKLINE>
882
Discard changes? [discard/Cancel/stash] s
883
Your changes have been moved to the git stash stack. To re-apply your changes
884
later use "git stash apply".
885
On local branch "branch1" without associated ticket.
886
<BLANKLINE>
887
# Use "sage --dev merge" to include another ticket/branch.
888
# Use "sage --dev commit" to save changes into a new commit.
889
890
And retrieve the stashed changes later::
891
892
sage: dev.checkout(branch='branch2')
893
On local branch "branch2" without associated ticket.
894
<BLANKLINE>
895
# Use "sage --dev merge" to include another ticket/branch.
896
# Use "sage --dev commit" to save changes into a new commit.
897
sage: dev.git.echo.stash('apply')
898
# On branch branch2
899
# Changes not staged for commit:
900
# (use "git add <file>..." to update what will be committed)
901
# (use "git checkout -- <file>..." to discard changes in working directory)
902
#
903
# modified: tracked
904
#
905
# Untracked files:
906
# (use "git add <file>..." to include in what will be committed)
907
#
908
# untracked
909
no changes added to commit (use "git add" and/or "git commit -a")
910
911
Or we can just discard the changes::
912
913
sage: UI.append("discard")
914
sage: dev.checkout(branch="branch1")
915
The following files in your working directory contain uncommitted changes:
916
<BLANKLINE>
917
tracked
918
<BLANKLINE>
919
Discard changes? [discard/Cancel/stash] discard
920
On local branch "branch1" without associated ticket.
921
<BLANKLINE>
922
# Use "sage --dev merge" to include another ticket/branch.
923
# Use "sage --dev commit" to save changes into a new commit.
924
925
Checking out a branch when in the middle of a merge::
926
927
sage: dev.git.super_silent.checkout('-b','merge_branch')
928
sage: with open('merge', 'w') as f: f.write("version 0")
929
sage: dev.git.silent.add('merge')
930
sage: dev.git.silent.commit('-m','some change')
931
sage: dev.git.super_silent.checkout('branch1')
932
sage: with open('merge', 'w') as f: f.write("version 1")
933
sage: dev.git.silent.add('merge')
934
sage: dev.git.silent.commit('-m','conflicting change')
935
sage: from sage.dev.git_error import GitError
936
sage: try:
937
....: dev.git.silent.merge('merge_branch')
938
....: except GitError: pass
939
sage: UI.append('r')
940
sage: dev.checkout(branch='merge_branch')
941
Repository is in an unclean state (merge). Resetting the state will discard any
942
uncommited changes.
943
Reset repository? [reset/Cancel] r
944
On local branch "merge_branch" without associated ticket.
945
<BLANKLINE>
946
# Use "sage --dev merge" to include another ticket/branch.
947
# Use "sage --dev commit" to save changes into a new commit.
948
949
Checking out a branch when in a detached HEAD::
950
951
sage: dev.git.super_silent.checkout('branch2', detach=True)
952
sage: dev.checkout(branch='branch1')
953
On local branch "branch1" without associated ticket.
954
<BLANKLINE>
955
# Use "sage --dev merge" to include another ticket/branch.
956
# Use "sage --dev commit" to save changes into a new commit.
957
958
With uncommitted changes::
959
960
sage: dev.git.super_silent.checkout('branch2', detach=True)
961
sage: with open('tracked', 'w') as f: f.write("boo")
962
sage: UI.append("discard")
963
sage: dev.checkout(branch='branch1')
964
The following files in your working directory contain uncommitted changes:
965
<BLANKLINE>
966
tracked
967
<BLANKLINE>
968
Discard changes? [discard/Cancel/stash] discard
969
On local branch "branch1" without associated ticket.
970
<BLANKLINE>
971
# Use "sage --dev merge" to include another ticket/branch.
972
# Use "sage --dev commit" to save changes into a new commit.
973
974
Checking out a branch with untracked files that would be overwritten by
975
the checkout::
976
977
sage: with open('tracked', 'w') as f: f.write("boo")
978
sage: dev.checkout(branch='branch2')
979
GitError: git exited with a non-zero exit code (1).
980
This happened while executing "git -c [email protected] -c
981
user.name=doctest checkout branch2".
982
git printed nothing to STDOUT.
983
git printed the following to STDERR:
984
error: The following untracked working tree files would be overwritten
985
by checkout:
986
tracked
987
Please move or remove them before you can switch branches.
988
Aborting
989
"""
990
self._check_local_branch_name(branch, exists=True)
991
992
try:
993
self.reset_to_clean_state(helpful=False)
994
except OperationCancelledError:
995
if helpful:
996
self._UI.show('Aborting checkout of branch "{0}".', branch)
997
self._UI.info(['', '(use "{0}" to save changes in a new commit)'],
998
self._format_command("commit"))
999
raise
1000
1001
current_commit = self.git.commit_for_ref('HEAD')
1002
target_commit = self.git.commit_for_ref(branch)
1003
try:
1004
self.clean(error_unless_clean=(current_commit != target_commit))
1005
except OperationCancelledError:
1006
if helpful:
1007
self._UI.show('Aborting checkout of branch "{0}".', branch)
1008
self._UI.info(['', '(use "{0}" to save changes in a new commit)'],
1009
self._format_command("commit"))
1010
raise
1011
1012
try:
1013
# this leaves locally modified files intact (we only allow this to happen
1014
# if current_commit == target_commit
1015
self.git.super_silent.checkout(branch)
1016
except GitError as e:
1017
# the error message should be self explanatory
1018
raise
1019
1020
def pull(self, ticket_or_remote_branch=None):
1021
r"""
1022
Pull ``ticket_or_remote_branch`` to ``branch``.
1023
1024
INPUT:
1025
1026
- ``ticket_or_remote_branch`` -- a string or an integer or ``None`` (default:
1027
``None``), a ticket or a remote branch name; setting this to ``None``
1028
has the same effect as setting it to the :meth:`current_ticket`.
1029
1030
TESTS:
1031
1032
Create a doctest setup with two users::
1033
1034
sage: from sage.dev.test.sagedev import two_user_setup
1035
sage: alice, config_alice, bob, config_bob, server = two_user_setup()
1036
1037
Alice creates ticket 1::
1038
1039
sage: alice._chdir()
1040
sage: alice._UI.append("Summary: summary1\ndescription")
1041
sage: alice.create_ticket()
1042
Created ticket #1 at https://trac.sagemath.org/1.
1043
<BLANKLINE>
1044
# (use "sage --dev checkout --ticket=1" to create a new local branch)
1045
1
1046
sage: alice.checkout(ticket=1)
1047
On ticket #1 with associated local branch "ticket/1".
1048
<BLANKLINE>
1049
# Use "sage --dev merge" to include another ticket/branch.
1050
# Use "sage --dev commit" to save changes into a new commit.
1051
1052
Bob attempts to pull for the ticket but fails because there is no
1053
branch for the ticket yet::
1054
1055
sage: bob._chdir()
1056
sage: bob.pull(1)
1057
Branch field is not set for ticket #1 on trac.
1058
1059
So, Bob starts to work on the ticket on a new branch::
1060
1061
sage: bob.checkout(ticket=1)
1062
On ticket #1 with associated local branch "ticket/1".
1063
<BLANKLINE>
1064
# Use "sage --dev merge" to include another ticket/branch.
1065
# Use "sage --dev commit" to save changes into a new commit.
1066
1067
Alice pushes a commit::
1068
1069
sage: alice._chdir()
1070
sage: alice.git.super_silent.commit(allow_empty=True, message="alice: empty commit")
1071
sage: alice._UI.append("y")
1072
sage: alice.push()
1073
The branch "u/alice/ticket/1" does not exist on the remote server.
1074
Create new remote branch? [Yes/no] y
1075
1076
Bob pulls the changes for ticket 1::
1077
1078
sage: bob._chdir()
1079
sage: bob.pull()
1080
Merging the remote branch "u/alice/ticket/1" into the local branch "ticket/1".
1081
Automatic merge successful.
1082
<BLANKLINE>
1083
# (use "sage --dev commit" to commit your merge)
1084
sage: bob.git.echo.log('--pretty=%s')
1085
alice: empty commit
1086
initial commit
1087
1088
Bob commits a change::
1089
1090
sage: open("bobs_file","w").close()
1091
sage: bob.git.silent.add("bobs_file")
1092
sage: bob.git.super_silent.commit(message="bob: added bobs_file")
1093
sage: bob._UI.append("y")
1094
sage: bob._UI.append("y")
1095
sage: bob.push()
1096
The branch "u/bob/ticket/1" does not exist on the remote server.
1097
Create new remote branch? [Yes/no] y
1098
The branch field of ticket #1 needs to be updated from its current value
1099
"u/alice/ticket/1" to "u/bob/ticket/1"
1100
Change the "Branch:" field? [Yes/no] y
1101
1102
Alice commits non-conflicting changes::
1103
1104
sage: alice._chdir()
1105
sage: with open("alices_file","w") as f: f.write("1")
1106
sage: alice.git.silent.add("alices_file")
1107
sage: alice.git.super_silent.commit(message="alice: added alices_file")
1108
1109
Alice can now pull the changes by Bob without the need to merge
1110
manually::
1111
1112
sage: alice.pull()
1113
Merging the remote branch "u/bob/ticket/1" into the local branch "ticket/1".
1114
Automatic merge successful.
1115
<BLANKLINE>
1116
# (use "sage --dev commit" to commit your merge)
1117
sage: alice.git.echo.log('--pretty=%s')
1118
Merge branch 'u/bob/ticket/1' of ... into ticket/1
1119
alice: added alices_file
1120
bob: added bobs_file
1121
alice: empty commit
1122
initial commit
1123
1124
Now, Bob commits some conflicting changes::
1125
1126
sage: bob._chdir()
1127
sage: with open("alices_file","w") as f: f.write("2")
1128
sage: bob.git.silent.add("alices_file")
1129
sage: bob.git.super_silent.commit(message="bob: added alices_file")
1130
sage: bob._UI.append('y')
1131
sage: bob.push()
1132
Local commits that are not on the remote branch "u/bob/ticket/1":
1133
<BLANKLINE>
1134
...: bob: added alices_file
1135
<BLANKLINE>
1136
Push to remote branch? [Yes/no] y
1137
1138
Now, the pull fails; one would have to use :meth:`merge`::
1139
1140
sage: alice._chdir()
1141
sage: alice._UI.append("abort")
1142
sage: alice.pull()
1143
Merging the remote branch "u/bob/ticket/1" into the local branch "ticket/1".
1144
Automatic merge failed, there are conflicting commits.
1145
<BLANKLINE>
1146
Auto-merging alices_file
1147
CONFLICT (add/add): Merge conflict in alices_file
1148
<BLANKLINE>
1149
Please edit the affected files to resolve the conflicts. When you are finished,
1150
your resolution will be commited.
1151
Finished? [ok/Abort] abort
1152
1153
Undo the latest commit by alice, so we can pull again::
1154
1155
sage: alice.git.super_silent.reset('HEAD~~', hard=True)
1156
sage: alice.pull()
1157
Merging the remote branch "u/bob/ticket/1" into the local branch "ticket/1".
1158
Automatic merge successful.
1159
<BLANKLINE>
1160
# (use "sage --dev commit" to commit your merge)
1161
sage: alice.git.echo.log('--pretty=%s')
1162
bob: added alices_file
1163
bob: added bobs_file
1164
alice: empty commit
1165
initial commit
1166
1167
Now, Alice creates an untracked file which makes a trivial merge
1168
impossible::
1169
1170
sage: alice._chdir()
1171
sage: open("bobs_other_file","w").close()
1172
1173
sage: bob._chdir()
1174
sage: open("bobs_other_file","w").close()
1175
sage: bob.git.super_silent.add("bobs_other_file")
1176
sage: bob.git.super_silent.commit(message="bob: added bobs_other_file")
1177
sage: bob._UI.append('y')
1178
sage: bob.push()
1179
Local commits that are not on the remote branch "u/bob/ticket/1":
1180
<BLANKLINE>
1181
...: bob: added bobs_other_file
1182
<BLANKLINE>
1183
Push to remote branch? [Yes/no] y
1184
1185
sage: alice._chdir()
1186
sage: alice._UI.append("abort")
1187
sage: alice.pull()
1188
Merging the remote branch "u/bob/ticket/1" into the local branch "ticket/1".
1189
Automatic merge failed, there are conflicting commits.
1190
<BLANKLINE>
1191
Updating ...
1192
error: The following untracked working tree files would be overwritten by merge:
1193
bobs_other_file
1194
Please move or remove them before you can merge.
1195
<BLANKLINE>
1196
Please edit the affected files to resolve the conflicts. When you are finished,
1197
your resolution will be commited.
1198
Finished? [ok/Abort] abort
1199
"""
1200
if ticket_or_remote_branch is None:
1201
ticket_or_remote_branch = self._current_ticket()
1202
1203
if self._is_ticket_name(ticket_or_remote_branch):
1204
ticket = self._ticket_from_ticket_name(ticket_or_remote_branch)
1205
self._check_ticket_name(ticket, exists=True)
1206
1207
remote_branch = self.trac._branch_for_ticket(ticket)
1208
if remote_branch is None:
1209
raise SageDevValueError("Branch field is not set for ticket #{0} on trac.".format(ticket))
1210
else:
1211
remote_branch = ticket_or_remote_branch
1212
1213
self.merge(remote_branch, pull=True)
1214
1215
def commit(self, message=None, interactive=False):
1216
r"""
1217
Create a commit from the pending changes on the current branch.
1218
1219
This is most akin to mercurial's commit command, not git's,
1220
since we do not require users to add files.
1221
1222
INPUT:
1223
1224
- ``message`` -- the message of the commit (default: ``None``), if
1225
``None``, prompt for a message.
1226
1227
- ``interactive`` -- if set, interactively select which part of the
1228
changes should be part of the commit
1229
1230
.. SEEALSO::
1231
1232
- :meth:`push` -- Push changes to the remote server. This
1233
is the next step once you've committed some changes.
1234
1235
- :meth:`diff` -- Show changes that will be committed.
1236
1237
TESTS:
1238
1239
Set up a single user for doctesting::
1240
1241
sage: from sage.dev.test.sagedev import single_user_setup
1242
sage: dev, config, UI, server = single_user_setup()
1243
1244
Commit an untracked file::
1245
1246
sage: dev.git.super_silent.checkout('-b', 'branch1')
1247
sage: open("tracked","w").close()
1248
sage: dev._UI.extend(["y", "added tracked", "y", "y"])
1249
sage: dev.commit()
1250
The following files in your working directory are not tracked by git:
1251
<BLANKLINE>
1252
tracked
1253
<BLANKLINE>
1254
Start tracking any of these files? [yes/No] y
1255
Start tracking "tracked"? [yes/No] y
1256
Commit your changes to branch "branch1"? [Yes/no] y
1257
<BLANKLINE>
1258
# Use "sage --dev push" to push your commits to the trac server once you are done.
1259
1260
Commit a tracked file::
1261
1262
sage: with open("tracked", "w") as F: F.write("foo")
1263
sage: dev._UI.append('y')
1264
sage: dev.commit(message='modified tracked')
1265
Commit your changes to branch "branch1"? [Yes/no] y
1266
<BLANKLINE>
1267
# Use "sage --dev push" to push your commits to the trac server once you are done.
1268
"""
1269
from git_error import DetachedHeadError
1270
try:
1271
branch = self.git.current_branch()
1272
except DetachedHeadError:
1273
self._UI.error("Cannot commit changes when not on any branch.")
1274
self._UI.info(['',
1275
'(use "{0}" to checkout a branch)'
1276
.format(self._format_command("checkout"))])
1277
raise OperationCancelledError("cannot proceed in detached HEAD mode")
1278
1279
# make sure the index is clean
1280
self.git.super_silent.reset()
1281
1282
try:
1283
self._UI.debug('Committing pending changes to branch "{0}".'.format(branch))
1284
1285
try:
1286
untracked_files = self.git.untracked_files()
1287
if untracked_files:
1288
self._UI.show(['The following files in your working directory are not tracked by git:', ''] +
1289
[' ' + f for f in untracked_files ] +
1290
[''])
1291
if self._UI.confirm('Start tracking any of these files?', default=False):
1292
for file in untracked_files:
1293
if self._UI.confirm('Start tracking "{0}"?'.format(file), default=False):
1294
self.git.add(file)
1295
1296
if interactive:
1297
self.git.echo.add(patch=True)
1298
else:
1299
self.git.echo.add(self.git._src, update=True)
1300
1301
if message is None:
1302
from sage.dev.misc import tmp_filename
1303
commit_message = tmp_filename()
1304
with open(commit_message, 'w') as f:
1305
f.write(COMMIT_GUIDE)
1306
self._UI.edit(commit_message)
1307
message = "\n".join([line for line in open(commit_message).read().splitlines()
1308
if not line.startswith("#")]).strip()
1309
if not message:
1310
raise OperationCancelledError("empty commit message")
1311
1312
if not self._UI.confirm('Commit your changes to branch "{0}"?'.format(branch), default=True):
1313
self._UI.info(['', 'Run "{0}" first if you want to commit to a different branch or ticket.'],
1314
self._format_command("checkout"))
1315
raise OperationCancelledError("user does not want to create a commit")
1316
self.git.commit(message=message)
1317
self._UI.debug("A commit has been created.")
1318
self._UI.info(['', 'Use "{0}" to push your commits to the trac server once you are done.'],
1319
self._format_command("push"))
1320
except OperationCancelledError:
1321
self._UI.debug("Not creating a commit.")
1322
raise
1323
except:
1324
self._UI.error("No commit has been created.")
1325
raise
1326
1327
finally:
1328
# do not leave a non-clean index behind
1329
self.git.super_silent.reset()
1330
1331
def set_remote(self, branch_or_ticket, remote_branch):
1332
r"""
1333
Set the remote branch to push to for ``branch_or_ticket`` to
1334
``remote_branch``.
1335
1336
INPUT:
1337
1338
- ``branch_or_ticket`` -- a string, the name of a local branch, or a
1339
string or an integer identifying a ticket or ``None``; if ``None``,
1340
the current branch is used.
1341
1342
- ``remote_branch`` -- a string, the name of a remote branch (this
1343
branch may not exist yet)
1344
1345
.. SEEALSO::
1346
1347
- :meth:`push` -- To push changes after setting the remote
1348
branch
1349
1350
TESTS:
1351
1352
Set up a single user for doctesting::
1353
1354
sage: from sage.dev.test.sagedev import single_user_setup
1355
sage: dev, config, UI, server = single_user_setup()
1356
sage: dev._wrap("_remote_branch_for_ticket")
1357
1358
Create a new branch::
1359
1360
sage: UI.append("Summary: ticket1\ndescription")
1361
sage: dev.create_ticket()
1362
Created ticket #1 at https://trac.sagemath.org/1.
1363
<BLANKLINE>
1364
# (use "sage --dev checkout --ticket=1" to create a new local branch)
1365
1
1366
sage: dev.checkout(ticket=1)
1367
On ticket #1 with associated local branch "ticket/1".
1368
<BLANKLINE>
1369
# Use "sage --dev merge" to include another ticket/branch.
1370
# Use "sage --dev commit" to save changes into a new commit.
1371
1372
Modify the remote branch for this ticket's branch::
1373
1374
sage: dev._remote_branch_for_ticket(1)
1375
'u/doctest/ticket/1'
1376
sage: dev.set_remote('ticket/1', 'u/doctest/foo')
1377
sage: dev._remote_branch_for_ticket(1)
1378
'u/doctest/foo'
1379
sage: dev.set_remote('ticket/1', 'foo')
1380
sage: dev._remote_branch_for_ticket(1)
1381
'foo'
1382
sage: dev.set_remote('#1', 'u/doctest/foo')
1383
sage: dev._remote_branch_for_ticket(1)
1384
'u/doctest/foo'
1385
"""
1386
if branch_or_ticket is None:
1387
from git_error import DetachedHeadError
1388
try:
1389
branch = self.git.current_branch()
1390
except DetachedHeadError:
1391
self._UI.error('You must specify "branch" in detached HEAD state.')
1392
self._UI.info(['', 'Use "{0}" to checkout a branch'],
1393
self._format_command('checkout'))
1394
raise OperationCancelledError("detached head state")
1395
elif self._is_ticket_name(branch_or_ticket):
1396
ticket = self._ticket_from_ticket_name(branch_or_ticket)
1397
if not self._has_local_branch_for_ticket(ticket):
1398
self._UI.error('no local branch for ticket #{0} found. Cannot set remote branch'
1399
' for that ticket.', ticket)
1400
raise OperationCancelledError("no such ticket")
1401
branch = self._local_branch_for_ticket(ticket)
1402
else:
1403
branch = branch_or_ticket
1404
1405
self._check_local_branch_name(branch, exists=True)
1406
self._check_remote_branch_name(remote_branch)
1407
1408
# If we add restrictions on which branches users may push to, we should append them here.
1409
m = USER_BRANCH.match(remote_branch)
1410
if remote_branch == 'master' or m and m.groups()[0] != self.trac._username:
1411
self._UI.warning('The remote branch "{0}" is not in your user scope. You probably'
1412
' do not have permission to push to that branch.', remote_branch)
1413
self._UI.info(['', 'You can always use "u/{1}/{0}" as the remote branch name.'],
1414
remote_branch, self.trac._username)
1415
1416
self._set_remote_branch_for_branch(branch, remote_branch)
1417
1418
def push(self, ticket=None, remote_branch=None, force=False):
1419
r"""
1420
Push the current branch to the Sage repository.
1421
1422
INPUT:
1423
1424
- ``ticket`` -- an integer or string identifying a ticket or ``None``
1425
(default: ``None``), if ``None`` and currently working on a ticket or
1426
if ``ticket`` specifies a ticket, then the branch on that ticket is
1427
set to ``remote_branch`` after the current branch has been pushed there.
1428
1429
- ``remote_branch`` -- a string or ``None`` (default: ``None``), the remote
1430
branch to push to; if ``None``, then a default is chosen
1431
1432
- ``force`` -- a boolean (default: ``False``), whether to push if
1433
this is not a fast-forward.
1434
1435
.. SEEALSO::
1436
1437
- :meth:`commit` -- Save changes to the local repository.
1438
1439
- :meth:`pull` -- Update a ticket with changes from the remote
1440
repository.
1441
1442
TESTS:
1443
1444
Create a doctest setup with two users::
1445
1446
sage: from sage.dev.test.sagedev import two_user_setup
1447
sage: alice, config_alice, bob, config_bob, server = two_user_setup()
1448
1449
Alice tries to push to ticket 1 which does not exist yet::
1450
1451
sage: alice._chdir()
1452
sage: alice.push(ticket=1)
1453
Ticket name "1" is not valid or ticket does not exist on trac.
1454
1455
Alice creates ticket 1 and pushes some changes to it::
1456
1457
sage: alice._UI.append("Summary: summary1\ndescription")
1458
sage: alice.create_ticket()
1459
Created ticket #1 at https://trac.sagemath.org/1.
1460
<BLANKLINE>
1461
# (use "sage --dev checkout --ticket=1" to create a new local branch)
1462
1
1463
sage: alice.checkout(ticket=1)
1464
On ticket #1 with associated local branch "ticket/1".
1465
<BLANKLINE>
1466
# Use "sage --dev merge" to include another ticket/branch.
1467
# Use "sage --dev commit" to save changes into a new commit.
1468
sage: open("tracked", "w").close()
1469
sage: alice.git.super_silent.add("tracked")
1470
sage: alice.git.super_silent.commit(message="alice: added tracked")
1471
sage: alice._UI.append("y")
1472
sage: alice.push()
1473
The branch "u/alice/ticket/1" does not exist on the remote server.
1474
Create new remote branch? [Yes/no] y
1475
1476
Now Bob can check that ticket out and push changes himself::
1477
1478
sage: bob._chdir()
1479
sage: bob.checkout(ticket=1)
1480
On ticket #1 with associated local branch "ticket/1".
1481
<BLANKLINE>
1482
# Use "sage --dev merge" to include another ticket/branch.
1483
# Use "sage --dev commit" to save changes into a new commit.
1484
sage: with open("tracked", "w") as f: f.write("bob")
1485
sage: bob.git.super_silent.add("tracked")
1486
sage: bob.git.super_silent.commit(message="bob: modified tracked")
1487
sage: bob._UI.append("y")
1488
sage: bob._UI.append("y")
1489
sage: bob.push()
1490
The branch "u/bob/ticket/1" does not exist on the remote server.
1491
Create new remote branch? [Yes/no] y
1492
The branch field of ticket #1 needs to be updated from its current value
1493
"u/alice/ticket/1" to "u/bob/ticket/1"
1494
Change the "Branch:" field? [Yes/no] y
1495
1496
Now Alice can pull these changes::
1497
1498
sage: alice._chdir()
1499
sage: alice.pull()
1500
Merging the remote branch "u/bob/ticket/1" into the local branch "ticket/1".
1501
Automatic merge successful.
1502
<BLANKLINE>
1503
# (use "sage --dev commit" to commit your merge)
1504
1505
Alice and Bob make non-conflicting changes simultaneously::
1506
1507
sage: with open("tracked", "w") as f: f.write("alice")
1508
sage: alice.git.super_silent.add("tracked")
1509
sage: alice.git.super_silent.commit(message="alice: modified tracked")
1510
1511
sage: bob._chdir()
1512
sage: open("tracked2", "w").close()
1513
sage: bob.git.super_silent.add("tracked2")
1514
sage: bob.git.super_silent.commit(message="bob: added tracked2")
1515
1516
After Alice pushed her changes, Bob can not set the branch field anymore::
1517
1518
sage: alice._chdir()
1519
sage: alice._UI.append("y")
1520
sage: alice._UI.append("y")
1521
sage: alice.push()
1522
Local commits that are not on the remote branch "u/alice/ticket/1":
1523
<BLANKLINE>
1524
...: alice: modified tracked
1525
...: bob: modified tracked
1526
<BLANKLINE>
1527
Push to remote branch? [Yes/no] y
1528
The branch field of ticket #1 needs to be updated from its current value
1529
"u/bob/ticket/1" to "u/alice/ticket/1"
1530
Change the "Branch:" field? [Yes/no] y
1531
1532
sage: bob._chdir()
1533
sage: bob._UI.append("y")
1534
sage: bob.push()
1535
Local commits that are not on the remote branch "u/bob/ticket/1":
1536
<BLANKLINE>
1537
....: bob: added tracked2
1538
<BLANKLINE>
1539
Push to remote branch? [Yes/no] y
1540
Not setting the branch field for ticket #1 to "u/bob/ticket/1" because
1541
"u/bob/ticket/1" and the current value of the branch field "u/alice/ticket/1"
1542
have diverged.
1543
<BLANKLINE>
1544
# Use "sage --dev pull --ticket=1" to merge the changes introduced by the remote "u/alice/ticket/1" into your local branch.
1545
1546
After merging the changes, this works again::
1547
1548
sage: bob.pull()
1549
Merging the remote branch "u/alice/ticket/1" into the local branch "ticket/1".
1550
Automatic merge successful.
1551
<BLANKLINE>
1552
# (use "sage --dev commit" to commit your merge)
1553
sage: bob._UI.append("y")
1554
sage: bob._UI.append("y")
1555
sage: bob.push()
1556
Local commits that are not on the remote branch "u/bob/ticket/1":
1557
<BLANKLINE>
1558
...: Merge branch 'u/alice/ticket/1' of ... into ticket/1
1559
...: alice: modified tracked
1560
<BLANKLINE>
1561
Push to remote branch? [Yes/no] y
1562
The branch field of ticket #1 needs to be updated from its current value
1563
"u/alice/ticket/1" to "u/bob/ticket/1"
1564
Change the "Branch:" field? [Yes/no] y
1565
1566
Check that ``ticket`` works::
1567
1568
sage: bob.push(2)
1569
Ticket name "2" is not valid or ticket does not exist on trac.
1570
1571
After creating the ticket, this works with a warning::
1572
1573
sage: bob._UI.append("Summary: summary2\ndescription")
1574
sage: bob.create_ticket()
1575
Created ticket #2 at https://trac.sagemath.org/2.
1576
<BLANKLINE>
1577
# (use "sage --dev checkout --ticket=2" to create a new local branch)
1578
2
1579
sage: bob.checkout(ticket=2)
1580
On ticket #2 with associated local branch "ticket/2".
1581
<BLANKLINE>
1582
# Use "sage --dev merge" to include another ticket/branch.
1583
# Use "sage --dev commit" to save changes into a new commit.
1584
sage: bob.checkout(ticket=1)
1585
On ticket #1 with associated local branch "ticket/1".
1586
<BLANKLINE>
1587
# Use "sage --dev merge" to include another ticket/branch.
1588
# Use "sage --dev commit" to save changes into a new commit.
1589
sage: bob._UI.append("y")
1590
sage: bob._UI.append("y")
1591
sage: bob.push(2)
1592
About to push the branch "ticket/1" to "u/bob/ticket/2" for ticket #2. However,
1593
your local branch for ticket #2 seems to be "ticket/2".
1594
Do you really want to proceed? [yes/No] y
1595
<BLANKLINE>
1596
# Use "sage --dev checkout --ticket=2 --branch=ticket/1" to permanently set "ticket/1" as the branch associated to ticket #2.
1597
The branch "u/bob/ticket/2" does not exist on the remote server.
1598
Create new remote branch? [Yes/no] y
1599
1600
Check that ``remote_branch`` works::
1601
1602
sage: bob._UI.append("y")
1603
sage: bob._UI.append("y")
1604
sage: bob.push(remote_branch="u/bob/branch1")
1605
The branch "u/bob/branch1" does not exist on the remote server.
1606
Create new remote branch? [Yes/no] y
1607
The branch field of ticket #1 needs to be updated from its current value
1608
"u/bob/ticket/1" to "u/bob/branch1"
1609
Change the "Branch:" field? [Yes/no] y
1610
1611
Check that dependencies are pushed correctly::
1612
1613
sage: bob.merge(2)
1614
Merging the remote branch "u/bob/ticket/2" into the local branch "ticket/1".
1615
Automatic merge successful.
1616
<BLANKLINE>
1617
# (use "sage --dev commit" to commit your merge)
1618
<BLANKLINE>
1619
Added dependency on #2 to #1.
1620
sage: with open("another_file", "w") as f: f.write("bob after merge(2)")
1621
sage: bob._UI.append('n')
1622
sage: bob.push()
1623
The branch field of ticket #1 needs to be updated from its current value
1624
"u/bob/branch1" to "u/bob/ticket/1"
1625
Change the "Branch:" field? [Yes/no] n
1626
sage: bob._UI.extend(['y', 'y', 'y'])
1627
sage: bob.commit(message="Bob's merge") # oops
1628
The following files in your working directory are not tracked by git:
1629
<BLANKLINE>
1630
another_file
1631
<BLANKLINE>
1632
Start tracking any of these files? [yes/No] y
1633
Start tracking "another_file"? [yes/No] y
1634
Commit your changes to branch "ticket/1"? [Yes/no] y
1635
<BLANKLINE>
1636
# Use "sage --dev push" to push your commits to the trac server once you are done.
1637
sage: bob._UI.extend(['y', 'y'])
1638
sage: bob.push()
1639
Local commits that are not on the remote branch "u/bob/ticket/1":
1640
<BLANKLINE>
1641
...: Bob's merge
1642
<BLANKLINE>
1643
Push to remote branch? [Yes/no] y
1644
The branch field of ticket #1 needs to be updated from its current value
1645
"u/bob/branch1" to "u/bob/ticket/1"
1646
Change the "Branch:" field? [Yes/no] y
1647
Uploading your dependencies for ticket #1: "" => "#2"
1648
sage: bob._sagedev._set_dependencies_for_ticket(1,())
1649
sage: with open("another_file", "w") as f: f.write("bob after push")
1650
sage: bob._UI.extend(['y', 'y', 'y'])
1651
sage: bob.commit(message='another commit')
1652
Commit your changes to branch "ticket/1"? [Yes/no] y
1653
<BLANKLINE>
1654
# Use "sage --dev push" to push your commits to the trac server once you are done.
1655
sage: bob._UI.extend(['y', "keep", 'y'])
1656
sage: bob.push()
1657
Local commits that are not on the remote branch "u/bob/ticket/1":
1658
<BLANKLINE>
1659
...: another commit
1660
<BLANKLINE>
1661
Push to remote branch? [Yes/no] y
1662
Trac ticket #1 depends on #2 while your local branch depends on no tickets.
1663
Updating dependencies is recommended but optional.
1664
Action for dependencies? [upload/download/keep] keep
1665
sage: with open("another_file", "w") as f: f.write("bob after 2nd push")
1666
sage: bob._UI.append('y')
1667
sage: bob.commit(message='final commit')
1668
Commit your changes to branch "ticket/1"? [Yes/no] y
1669
<BLANKLINE>
1670
# Use "sage --dev push" to push your commits to the trac server once you are done.
1671
1672
sage: bob._UI.extend(['y', 'download', 'y'])
1673
sage: bob.push()
1674
Local commits that are not on the remote branch "u/bob/ticket/1":
1675
<BLANKLINE>
1676
...: final commit
1677
<BLANKLINE>
1678
Push to remote branch? [Yes/no] y
1679
Trac ticket #1 depends on #2 while your local branch depends on no tickets.
1680
Updating dependencies is recommended but optional.
1681
Action for dependencies? [upload/download/keep] download
1682
"""
1683
if ticket is None:
1684
ticket = self._current_ticket()
1685
if ticket is not None:
1686
ticket = self._ticket_from_ticket_name(ticket)
1687
self._check_ticket_name(ticket, exists=True)
1688
1689
from git_error import DetachedHeadError
1690
try:
1691
branch = self.git.current_branch()
1692
except DetachedHeadError:
1693
self._UI.error("Cannot push while in detached HEAD state.")
1694
raise OperationCancelledError("cannot push while in detached HEAD state")
1695
1696
if remote_branch is None:
1697
if ticket:
1698
remote_branch = self._remote_branch_for_ticket(ticket)
1699
if remote_branch is None:
1700
raise SageDevValueError("remote_branch must be specified since #{0}"
1701
" has no remote branch set.".format(ticket))
1702
else:
1703
remote_branch = self._remote_branch_for_branch(branch)
1704
if remote_branch is None:
1705
raise SageDevValueError("remote_branch must be specified since the"
1706
" current branch has no remote branch set.")
1707
self._check_remote_branch_name(remote_branch)
1708
1709
# whether the user already confirmed that he really wants to push and set the branch field
1710
user_confirmation = False
1711
1712
if ticket is not None:
1713
if self._has_local_branch_for_ticket(ticket) and self._local_branch_for_ticket(ticket) == branch:
1714
pass
1715
elif self._has_local_branch_for_ticket(ticket) and self._local_branch_for_ticket(ticket) != branch:
1716
self._UI.show('About to push the branch "{0}" to "{1}" for ticket #{2}.'
1717
' However, your local branch for ticket #{2} seems to be "{3}".',
1718
branch, remote_branch, ticket, self._local_branch_for_ticket(ticket))
1719
user_confirmation = self._UI.confirm(' Do you really want to proceed?', default=False)
1720
if user_confirmation:
1721
self._UI.info(['',
1722
'Use "{2}" to permanently set "{1}" as the branch'
1723
' associated to ticket #{0}.'],
1724
ticket, branch, self._format_command("checkout",ticket=ticket,branch=branch))
1725
else:
1726
raise OperationCancelledError("user requsted")
1727
elif self._has_ticket_for_local_branch(branch) and self._ticket_for_local_branch(branch) != ticket:
1728
self._UI.show('About to push the local branch "{0}" to remote branch "{1}" for'
1729
' ticket #{2}. However, that branch is already associated to ticket #{3}.',
1730
branch, remote_branch, ticket, self._ticket_for_local_branch(branch))
1731
user_confirmation = self._UI.confirm(' Do you really want to proceed?', default=False)
1732
if user_confirmation:
1733
self._UI.info(['', 'Use "{2}" to permanently set the branch associated to'
1734
' ticket #{0} to "{1}". To create a new branch from "{1}" for'
1735
' #{0}, use "{3}" and "{4}".'],
1736
ticket, branch,
1737
self._format_command("checkout",ticket=ticket,branch=branch),
1738
self._format_command("checkout",ticket=ticket),
1739
self._format_command("merge", branch=branch))
1740
1741
self._UI.debug('Pushing your changes in "{0}" to "{1}".'.format(branch, remote_branch))
1742
try:
1743
remote_branch_exists = self._is_remote_branch_name(remote_branch, exists=True)
1744
if not remote_branch_exists:
1745
self._UI.show('The branch "{0}" does not exist on the remote server.', remote_branch)
1746
if not self._UI.confirm('Create new remote branch?', default=True):
1747
raise OperationCancelledError("User did not want to create remote branch.")
1748
else:
1749
self.git.super_silent.fetch(self.git._repository_anonymous, remote_branch)
1750
1751
# check whether force is necessary
1752
if remote_branch_exists and not self.git.is_child_of(branch, 'FETCH_HEAD'):
1753
if not force:
1754
self._UI.error('Not pushing your changes because they would discard some of'
1755
' the commits on the remote branch "{0}".', remote_branch)
1756
raise OperationCancelledError("not a fast-forward")
1757
1758
# check whether this is a nop
1759
if remote_branch_exists and not force and \
1760
self.git.commit_for_branch(branch) == self.git.commit_for_ref('FETCH_HEAD'):
1761
self._UI.debug('Remote branch "{0}" is idential to your local branch "{1}',
1762
remote_branch, branch)
1763
self._UI.debug(['', '(use "{0}" to commit changes before pushing)'],
1764
self._format_command("commit"))
1765
else:
1766
try:
1767
if not force:
1768
if remote_branch_exists:
1769
commits = self.git.log("{0}..{1}".format('FETCH_HEAD', branch), '--pretty=%h: %s')
1770
self._UI.show(['Local commits that are not on the remote branch "{0}":', ''] +
1771
[' ' + c for c in commits.splitlines()] +
1772
[''], remote_branch)
1773
if not self._UI.confirm('Push to remote branch?', default=True):
1774
raise OperationCancelledError("user requested")
1775
1776
self._upload_ssh_key() # make sure that we have access to the repository
1777
self.git.super_silent.push(self.git._repository,
1778
"{0}:{1}".format(branch, remote_branch),
1779
force=force)
1780
except GitError as e:
1781
# can we give any advice if this fails?
1782
raise
1783
self._UI.debug('Changes in "{0}" have been pushed to "{1}".'.format(branch, remote_branch))
1784
except OperationCancelledError:
1785
self._UI.debug("Did not push any changes.")
1786
raise
1787
1788
if ticket:
1789
current_remote_branch = self.trac._branch_for_ticket(ticket)
1790
if current_remote_branch == remote_branch:
1791
self._UI.debug('Not setting the branch field for ticket #{0} because it already'
1792
' points to your branch "{1}".'.format(ticket, remote_branch))
1793
else:
1794
self._UI.debug('Setting the branch field of ticket #{0} to "{1}".'.format(ticket, remote_branch))
1795
if current_remote_branch is not None:
1796
self.git.super_silent.fetch(self.git._repository_anonymous, current_remote_branch)
1797
if force or self.git.is_ancestor_of('FETCH_HEAD', branch):
1798
pass
1799
else:
1800
self._UI.error('Not setting the branch field for ticket #{0} to "{1}" because'
1801
' "{1}" and the current value of the branch field "{2}" have diverged.'
1802
.format(ticket, remote_branch, current_remote_branch))
1803
self._UI.info(['',
1804
'Use "{0}" to merge the changes introduced by'
1805
' the remote "{1}" into your local branch.'],
1806
self._format_command("pull", ticket=ticket),
1807
current_remote_branch)
1808
raise OperationCancelledError("not a fast-forward")
1809
1810
if current_remote_branch is not None and not force and not user_confirmation:
1811
self._UI.show('The branch field of ticket #{0} needs to be'
1812
' updated from its current value "{1}" to "{2}"'
1813
,ticket, current_remote_branch, remote_branch)
1814
if not self._UI.confirm('Change the "Branch:" field?', default=True):
1815
raise OperationCancelledError("user requested")
1816
1817
attributes = self.trac._get_attributes(ticket)
1818
attributes['branch'] = remote_branch
1819
self.trac._authenticated_server_proxy.ticket.update(ticket, "", attributes)
1820
1821
if ticket and self._has_ticket_for_local_branch(branch):
1822
old_dependencies_ = self.trac.dependencies(ticket)
1823
old_dependencies = ", ".join(["#"+str(dep) for dep in old_dependencies_])
1824
new_dependencies_ = self._dependencies_for_ticket(self._ticket_for_local_branch(branch))
1825
new_dependencies = ", ".join(["#"+str(dep) for dep in new_dependencies_])
1826
1827
upload = True
1828
if old_dependencies != new_dependencies:
1829
if old_dependencies:
1830
self._UI.show('Trac ticket #{0} depends on {1} while your local branch depends'
1831
' on {2}. Updating dependencies is recommended but optional.',
1832
ticket, old_dependencies, new_dependencies or "no tickets"),
1833
sel = self._UI.select('Action for dependencies?', options=("upload", "download", "keep"))
1834
if sel == "keep":
1835
upload = False
1836
elif sel == "download":
1837
self._set_dependencies_for_ticket(ticket, old_dependencies_)
1838
self._UI.debug("Setting dependencies for #{0} to {1}.", ticket, old_dependencies)
1839
upload = False
1840
elif sel == "upload":
1841
pass
1842
else:
1843
raise NotImplementedError
1844
else:
1845
self._UI.debug("Not uploading your dependencies for ticket #{0} because the"
1846
" dependencies on trac are already up-to-date.", ticket)
1847
upload = False
1848
1849
if upload:
1850
self._UI.show('Uploading your dependencies for ticket #{0}: "{1}" => "{2}"',
1851
ticket, old_dependencies, new_dependencies)
1852
attributes = self.trac._get_attributes(ticket)
1853
attributes['dependencies'] = new_dependencies
1854
# Don't send an e-mail notification
1855
self.trac._authenticated_server_proxy.ticket.update(ticket, "", attributes)
1856
1857
def reset_to_clean_state(self, error_unless_clean=True, helpful=True):
1858
r"""
1859
Reset the current working directory to a clean state.
1860
1861
INPUT:
1862
1863
- ``error_unless_clean`` -- a boolean (default: ``True``),
1864
whether to raise an
1865
:class:`user_interface_error.OperationCancelledError` if the
1866
directory remains in an unclean state; used internally.
1867
1868
TESTS:
1869
1870
Set up a single user for doctesting::
1871
1872
sage: from sage.dev.test.sagedev import single_user_setup
1873
sage: dev, config, UI, server = single_user_setup()
1874
sage: dev._wrap("reset_to_clean_state")
1875
1876
Nothing happens if the directory is already clean::
1877
1878
sage: dev.reset_to_clean_state()
1879
1880
Bring the directory into a non-clean state::
1881
1882
sage: dev.git.super_silent.checkout(b="branch1")
1883
sage: with open("tracked", "w") as f: f.write("boo")
1884
sage: dev.git.silent.add("tracked")
1885
sage: dev.git.silent.commit(message="added tracked")
1886
1887
sage: dev.git.super_silent.checkout('HEAD~')
1888
sage: dev.git.super_silent.checkout(b="branch2")
1889
sage: with open("tracked", "w") as f: f.write("foo")
1890
sage: dev.git.silent.add("tracked")
1891
sage: dev.git.silent.commit(message="added tracked")
1892
sage: from sage.dev.git_error import GitError
1893
sage: try:
1894
....: dev.git.silent.merge("branch1")
1895
....: except GitError: pass
1896
sage: UI.append("cancel")
1897
sage: dev.reset_to_clean_state()
1898
Repository is in an unclean state (merge). Resetting the state will discard any
1899
uncommited changes.
1900
Reset repository? [reset/Cancel] cancel
1901
<BLANKLINE>
1902
# (use "sage --dev commit" to save changes in a new commit)
1903
sage: UI.append("reset")
1904
sage: dev.reset_to_clean_state()
1905
Repository is in an unclean state (merge). Resetting the state will discard any
1906
uncommited changes.
1907
Reset repository? [reset/Cancel] reset
1908
sage: dev.reset_to_clean_state()
1909
1910
A detached HEAD does not count as a non-clean state::
1911
1912
sage: dev.git.super_silent.checkout('HEAD', detach=True)
1913
sage: dev.reset_to_clean_state()
1914
"""
1915
states = self.git.get_state()
1916
if not states:
1917
return
1918
self._UI.show('Repository is in an unclean state ({0}).'
1919
' Resetting the state will discard any uncommited changes.',
1920
', '.join(states))
1921
sel = self._UI.select('Reset repository?',
1922
options=('reset', 'cancel'), default=1)
1923
if sel == 'cancel':
1924
if not error_unless_clean:
1925
return
1926
if helpful:
1927
self._UI.info(['', '(use "{0}" to save changes in a new commit)'],
1928
self._format_command("commit"))
1929
raise OperationCancelledError("User requested not to clean the current state.")
1930
elif sel == 'reset':
1931
self.git.reset_to_clean_state()
1932
else:
1933
assert False
1934
1935
def clean(self, error_unless_clean=True):
1936
r"""
1937
Restore the working directory to the most recent commit.
1938
1939
INPUT:
1940
1941
- ``error_unless_clean`` -- a boolean (default: ``True``),
1942
whether to raise an
1943
:class:`user_interface_error.OperationCancelledError` if the
1944
directory remains in an unclean state; used internally.
1945
1946
TESTS:
1947
1948
Set up a single user for doctesting::
1949
1950
sage: from sage.dev.test.sagedev import single_user_setup
1951
sage: dev, config, UI, server = single_user_setup()
1952
1953
Check that nothing happens if there no changes::
1954
1955
sage: dev.clean()
1956
1957
Check that nothing happens if there are only untracked files::
1958
1959
sage: open("untracked","w").close()
1960
sage: dev.clean()
1961
1962
Uncommitted changes can simply be dropped::
1963
1964
sage: open("tracked","w").close()
1965
sage: dev.git.silent.add("tracked")
1966
sage: dev.git.silent.commit(message="added tracked")
1967
sage: with open("tracked", "w") as f: f.write("foo")
1968
sage: UI.append("discard")
1969
sage: dev.clean()
1970
The following files in your working directory contain uncommitted changes:
1971
<BLANKLINE>
1972
tracked
1973
<BLANKLINE>
1974
Discard changes? [discard/Cancel/stash] discard
1975
sage: dev.clean()
1976
1977
Uncommitted changes can be kept::
1978
1979
sage: with open("tracked", "w") as f: f.write("foo")
1980
sage: UI.append("cancel")
1981
sage: dev.clean()
1982
The following files in your working directory contain uncommitted changes:
1983
<BLANKLINE>
1984
tracked
1985
<BLANKLINE>
1986
Discard changes? [discard/Cancel/stash] cancel
1987
1988
Or stashed::
1989
1990
sage: UI.append("stash")
1991
sage: dev.clean()
1992
The following files in your working directory contain uncommitted changes:
1993
<BLANKLINE>
1994
tracked
1995
<BLANKLINE>
1996
Discard changes? [discard/Cancel/stash] stash
1997
Your changes have been moved to the git stash stack. To re-apply your changes
1998
later use "git stash apply".
1999
sage: dev.clean()
2000
"""
2001
try:
2002
self.reset_to_clean_state(error_unless_clean)
2003
except OperationCancelledError:
2004
self._UI.error("Can not clean the working directory unless in a clean state.")
2005
raise
2006
2007
if not self.git.has_uncommitted_changes():
2008
return
2009
2010
files = [line[2:] for line in self.git.status(porcelain=True).splitlines()
2011
if not line.startswith('?')]
2012
2013
self._UI.show(
2014
['The following files in your working directory contain uncommitted changes:'] +
2015
[''] +
2016
[' ' + f for f in files ] +
2017
[''])
2018
cancel = 'cancel' if error_unless_clean else 'keep'
2019
sel = self._UI.select('Discard changes?',
2020
options=('discard', cancel, 'stash'), default=1)
2021
if sel == 'discard':
2022
self.git.clean_wrapper()
2023
elif sel == cancel:
2024
if error_unless_clean:
2025
raise OperationCancelledError("User requested not to clean the working directory.")
2026
elif sel == 'stash':
2027
self.git.super_silent.stash()
2028
self._UI.show('Your changes have been moved to the git stash stack. '
2029
'To re-apply your changes later use "git stash apply".')
2030
else:
2031
assert False
2032
2033
def edit_ticket(self, ticket=None):
2034
r"""
2035
Edit the description of ``ticket`` on trac.
2036
2037
INPUT:
2038
2039
- ``ticket`` -- an integer or string identifying a ticket or ``None``
2040
(default: ``None``), the number of the ticket to edit. If ``None``,
2041
edit the :meth:`_current_ticket`.
2042
2043
.. SEEALSO::
2044
2045
:meth:`create_ticket`, :meth:`comment`,
2046
:meth:`set_needs_review`, :meth:`set_needs_work`,
2047
:meth:`set_positive_review`, :meth:`set_needs_info`
2048
2049
TESTS:
2050
2051
Set up a single user for doctesting::
2052
2053
sage: from sage.dev.test.sagedev import single_user_setup
2054
sage: dev, config, UI, server = single_user_setup()
2055
2056
Create a ticket and edit it::
2057
2058
sage: UI.append("Summary: summary1\ndescription")
2059
sage: dev.create_ticket()
2060
Created ticket #1 at https://trac.sagemath.org/1.
2061
<BLANKLINE>
2062
# (use "sage --dev checkout --ticket=1" to create a new local branch)
2063
1
2064
sage: dev.checkout(ticket=1)
2065
On ticket #1 with associated local branch "ticket/1".
2066
<BLANKLINE>
2067
# Use "sage --dev merge" to include another ticket/branch.
2068
# Use "sage --dev commit" to save changes into a new commit.
2069
sage: UI.append("Summary: summary1\ndescription...")
2070
sage: dev.edit_ticket()
2071
sage: dev.trac._get_attributes(1)
2072
{'description': 'description...', 'summary': 'summary1'}
2073
"""
2074
if ticket is None:
2075
ticket = self._current_ticket()
2076
2077
if ticket is None:
2078
raise SageDevValueError("ticket must be specified if not currently on a ticket.")
2079
2080
self._check_ticket_name(ticket, exists=True)
2081
ticket = self._ticket_from_ticket_name(ticket)
2082
self.trac.edit_ticket_interactive(ticket)
2083
2084
def needs_review(self, ticket=None, comment=''):
2085
r"""
2086
Set a ticket on trac to ``needs_review``.
2087
2088
INPUT:
2089
2090
- ``ticket`` -- an integer or string identifying a ticket or
2091
``None`` (default: ``None``), the number of the ticket to
2092
edit. If ``None``, edit the :meth:`_current_ticket`.
2093
2094
- ``comment`` -- a comment to go with the status change.
2095
2096
.. SEEALSO::
2097
2098
:meth:`edit_ticket`, :meth:`set_needs_work`,
2099
:meth:`set_positive_review`, :meth:`comment`,
2100
:meth:`set_needs_info`
2101
2102
TESTS:
2103
2104
Set up a single user for doctesting::
2105
2106
sage: from sage.dev.test.sagedev import single_user_setup
2107
sage: dev, config, UI, server = single_user_setup()
2108
2109
Create a ticket and set it to needs_review::
2110
2111
sage: UI.append("Summary: summary1\ndescription")
2112
sage: dev.create_ticket()
2113
Created ticket #1 at https://trac.sagemath.org/1.
2114
<BLANKLINE>
2115
# (use "sage --dev checkout --ticket=1" to create a new local branch)
2116
1
2117
sage: dev.checkout(ticket=1)
2118
On ticket #1 with associated local branch "ticket/1".
2119
<BLANKLINE>
2120
# Use "sage --dev merge" to include another ticket/branch.
2121
# Use "sage --dev commit" to save changes into a new commit.
2122
sage: open("tracked", "w").close()
2123
sage: dev.git.super_silent.add("tracked")
2124
sage: dev.git.super_silent.commit(message="alice: added tracked")
2125
sage: dev._UI.append("y")
2126
sage: dev.push()
2127
The branch "u/doctest/ticket/1" does not exist on the remote server.
2128
Create new remote branch? [Yes/no] y
2129
sage: dev.needs_review(comment='Review my ticket!')
2130
sage: dev.trac._get_attributes(1)['status']
2131
'needs_review'
2132
"""
2133
if ticket is None:
2134
ticket = self._current_ticket()
2135
if ticket is None:
2136
raise SageDevValueError("ticket must be specified if not currently on a ticket.")
2137
self._check_ticket_name(ticket, exists=True)
2138
self.trac.set_attributes(ticket, comment, notify=True, status='needs_review')
2139
self._UI.debug("Ticket #%s marked as needing review"%ticket)
2140
2141
def needs_work(self, ticket=None, comment=''):
2142
r"""
2143
Set a ticket on trac to ``needs_work``.
2144
2145
INPUT:
2146
2147
- ``ticket`` -- an integer or string identifying a ticket or
2148
``None`` (default: ``None``), the number of the ticket to
2149
edit. If ``None``, edit the :meth:`_current_ticket`.
2150
2151
- ``comment`` -- a comment to go with the status change.
2152
2153
.. SEEALSO::
2154
2155
:meth:`edit_ticket`, :meth:`set_needs_review`,
2156
:meth:`set_positive_review`, :meth:`comment`,
2157
:meth:`set_needs_info`
2158
2159
TESTS:
2160
2161
Create a doctest setup with two users::
2162
2163
sage: from sage.dev.test.sagedev import two_user_setup
2164
sage: alice, config_alice, bob, config_bob, server = two_user_setup()
2165
2166
Alice creates a ticket and set it to needs_review::
2167
2168
sage: alice._chdir()
2169
sage: alice._UI.append("Summary: summary1\ndescription")
2170
sage: alice.create_ticket()
2171
Created ticket #1 at https://trac.sagemath.org/1.
2172
<BLANKLINE>
2173
# (use "sage --dev checkout --ticket=1" to create a new local branch)
2174
1
2175
sage: alice.checkout(ticket=1)
2176
On ticket #1 with associated local branch "ticket/1".
2177
<BLANKLINE>
2178
# Use "sage --dev merge" to include another ticket/branch.
2179
# Use "sage --dev commit" to save changes into a new commit.
2180
sage: open("tracked", "w").close()
2181
sage: alice.git.super_silent.add("tracked")
2182
sage: alice.git.super_silent.commit(message="alice: added tracked")
2183
sage: alice._UI.append("y")
2184
sage: alice.push()
2185
The branch "u/alice/ticket/1" does not exist on the remote server.
2186
Create new remote branch? [Yes/no] y
2187
sage: alice.needs_review(comment='Review my ticket!')
2188
2189
Bob reviews the ticket and finds it lacking::
2190
2191
sage: bob._chdir()
2192
sage: bob.checkout(ticket=1)
2193
On ticket #1 with associated local branch "ticket/1".
2194
<BLANKLINE>
2195
# Use "sage --dev merge" to include another ticket/branch.
2196
# Use "sage --dev commit" to save changes into a new commit.
2197
sage: bob.needs_work(comment='Need to add an untracked file!')
2198
sage: bob.trac._get_attributes(1)['status']
2199
'needs_work'
2200
"""
2201
if ticket is None:
2202
ticket = self._current_ticket()
2203
if ticket is None:
2204
raise SageDevValueError("ticket must be specified if not currently on a ticket.")
2205
self._check_ticket_name(ticket, exists=True)
2206
if not comment:
2207
comment = self._UI.get_input("Please add a comment for the author:")
2208
self.trac.set_attributes(ticket, comment, notify=True, status='needs_work')
2209
self._UI.debug("Ticket #%s marked as needing work"%ticket)
2210
2211
def needs_info(self, ticket=None, comment=''):
2212
r"""
2213
Set a ticket on trac to ``needs_info``.
2214
2215
INPUT:
2216
2217
- ``ticket`` -- an integer or string identifying a ticket or
2218
``None`` (default: ``None``), the number of the ticket to
2219
edit. If ``None``, edit the :meth:`_current_ticket`.
2220
2221
- ``comment`` -- a comment to go with the status change.
2222
2223
.. SEEALSO::
2224
2225
:meth:`edit_ticket`, :meth:`needs_review`,
2226
:meth:`positive_review`, :meth:`comment`,
2227
:meth:`needs_work`
2228
2229
TESTS:
2230
2231
Create a doctest setup with two users::
2232
2233
sage: from sage.dev.test.sagedev import two_user_setup
2234
sage: alice, config_alice, bob, config_bob, server = two_user_setup()
2235
2236
Alice creates a ticket and set it to needs_review::
2237
2238
sage: alice._chdir()
2239
sage: alice._UI.append("Summary: summary1\ndescription")
2240
sage: alice.create_ticket()
2241
Created ticket #1 at https://trac.sagemath.org/1.
2242
<BLANKLINE>
2243
# (use "sage --dev checkout --ticket=1" to create a new local branch)
2244
1
2245
sage: alice.checkout(ticket=1)
2246
On ticket #1 with associated local branch "ticket/1".
2247
<BLANKLINE>
2248
# Use "sage --dev merge" to include another ticket/branch.
2249
# Use "sage --dev commit" to save changes into a new commit.
2250
sage: open("tracked", "w").close()
2251
sage: alice.git.super_silent.add("tracked")
2252
sage: alice.git.super_silent.commit(message="alice: added tracked")
2253
sage: alice._UI.append("y")
2254
sage: alice.push()
2255
The branch "u/alice/ticket/1" does not exist on the remote server.
2256
Create new remote branch? [Yes/no] y
2257
sage: alice.needs_review(comment='Review my ticket!')
2258
2259
Bob reviews the ticket and finds it lacking::
2260
2261
sage: bob._chdir()
2262
sage: bob.checkout(ticket=1)
2263
On ticket #1 with associated local branch "ticket/1".
2264
<BLANKLINE>
2265
# Use "sage --dev merge" to include another ticket/branch.
2266
# Use "sage --dev commit" to save changes into a new commit.
2267
sage: bob.needs_info(comment='Why is a tracked file enough?')
2268
sage: bob.trac._get_attributes(1)['status']
2269
'needs_info'
2270
"""
2271
if ticket is None:
2272
ticket = self._current_ticket()
2273
if ticket is None:
2274
raise SageDevValueError("ticket must be specified if not currently on a ticket.")
2275
self._check_ticket_name(ticket, exists=True)
2276
if not comment:
2277
comment = self._UI.get_input("Please specify what information is required from the author:")
2278
self.trac.set_attributes(ticket, comment, notify=True, status='needs_info')
2279
self._UI.debug("Ticket #%s marked as needing info"%ticket)
2280
2281
def positive_review(self, ticket=None, comment=''):
2282
r"""
2283
Set a ticket on trac to ``positive_review``.
2284
2285
INPUT:
2286
2287
- ``ticket`` -- an integer or string identifying a ticket or
2288
``None`` (default: ``None``), the number of the ticket to
2289
edit. If ``None``, edit the :meth:`_current_ticket`.
2290
2291
- ``comment`` -- a comment to go with the status change.
2292
2293
.. SEEALSO::
2294
2295
:meth:`edit_ticket`, :meth:`needs_review`,
2296
:meth:`needs_info`, :meth:`comment`,
2297
:meth:`needs_work`
2298
2299
TESTS:
2300
2301
Create a doctest setup with two users::
2302
2303
sage: from sage.dev.test.sagedev import two_user_setup
2304
sage: alice, config_alice, bob, config_bob, server = two_user_setup()
2305
2306
Alice creates a ticket and set it to needs_review::
2307
2308
sage: alice._chdir()
2309
sage: alice._UI.append("Summary: summary1\ndescription")
2310
sage: alice.create_ticket()
2311
Created ticket #1 at https://trac.sagemath.org/1.
2312
<BLANKLINE>
2313
# (use "sage --dev checkout --ticket=1" to create a new local branch)
2314
1
2315
sage: alice.checkout(ticket=1)
2316
On ticket #1 with associated local branch "ticket/1".
2317
<BLANKLINE>
2318
# Use "sage --dev merge" to include another ticket/branch.
2319
# Use "sage --dev commit" to save changes into a new commit.
2320
sage: open("tracked", "w").close()
2321
sage: alice.git.super_silent.add("tracked")
2322
sage: alice.git.super_silent.commit(message="alice: added tracked")
2323
sage: alice._UI.append("y")
2324
sage: alice.push()
2325
The branch "u/alice/ticket/1" does not exist on the remote server.
2326
Create new remote branch? [Yes/no] y
2327
sage: alice.needs_review(comment='Review my ticket!')
2328
2329
Bob reviews the ticket and finds it good::
2330
2331
sage: bob._chdir()
2332
sage: bob.checkout(ticket=1)
2333
On ticket #1 with associated local branch "ticket/1".
2334
<BLANKLINE>
2335
# Use "sage --dev merge" to include another ticket/branch.
2336
# Use "sage --dev commit" to save changes into a new commit.
2337
sage: bob.positive_review()
2338
sage: bob.trac._get_attributes(1)['status']
2339
'positive_review'
2340
"""
2341
if ticket is None:
2342
ticket = self._current_ticket()
2343
if ticket is None:
2344
raise SageDevValueError("ticket must be specified if not currently on a ticket.")
2345
self._check_ticket_name(ticket, exists=True)
2346
self.trac.set_attributes(ticket, comment, notify=True, status='positive_review')
2347
self._UI.debug("Ticket #%s reviewed!"%ticket)
2348
2349
def comment(self, ticket=None):
2350
r"""
2351
Add a comment to ``ticket`` on trac.
2352
2353
INPUT:
2354
2355
- ``ticket`` -- an integer or string identifying a ticket or ``None``
2356
(default: ``None``), the number of the ticket to edit. If ``None``,
2357
edit the :meth:`_current_ticket`.
2358
2359
.. SEEALSO::
2360
2361
:meth:`create_ticket`, :meth:`edit_ticket`
2362
2363
TESTS:
2364
2365
Set up a single user for doctesting::
2366
2367
sage: from sage.dev.test.sagedev import single_user_setup
2368
sage: dev, config, UI, server = single_user_setup()
2369
2370
Create a ticket and add a comment::
2371
2372
sage: UI.append("Summary: summary1\ndescription")
2373
sage: dev.create_ticket()
2374
Created ticket #1 at https://trac.sagemath.org/1.
2375
<BLANKLINE>
2376
# (use "sage --dev checkout --ticket=1" to create a new local branch)
2377
1
2378
sage: dev.checkout(ticket=1)
2379
On ticket #1 with associated local branch "ticket/1".
2380
<BLANKLINE>
2381
# Use "sage --dev merge" to include another ticket/branch.
2382
# Use "sage --dev commit" to save changes into a new commit.
2383
sage: UI.append("comment")
2384
sage: dev.comment()
2385
sage: server.tickets[1].comments
2386
['comment']
2387
"""
2388
if ticket is None:
2389
ticket = self._current_ticket()
2390
2391
if ticket is None:
2392
raise SageDevValueError("ticket must be specified if not currently on a ticket.")
2393
2394
self._check_ticket_name(ticket, exists=True)
2395
ticket = self._ticket_from_ticket_name(ticket)
2396
self.trac.add_comment_interactive(ticket)
2397
2398
def browse_ticket(self, ticket=None):
2399
r"""
2400
Start a webbrowser at the ticket page on trac.
2401
2402
INPUT:
2403
2404
- ``ticket`` -- an integer or string identifying a ticket or ``None``
2405
(default: ``None``), the number of the ticket to edit. If ``None``,
2406
browse the :meth:`_current_ticket`.
2407
2408
.. SEEALSO::
2409
2410
:meth:`edit_ticket`, :meth:`comment`,
2411
:meth:`sage.dev.trac_interface.TracInterface.show_ticket`,
2412
:meth:`sage.dev.trac_interface.TracInterface.show_comments`
2413
2414
EXAMPLES::
2415
2416
sage: dev.browse_ticket(10000) # not tested
2417
"""
2418
if ticket is None:
2419
ticket = self._current_ticket()
2420
2421
if ticket is None:
2422
raise SageDevValueError("ticket must be specified if not currently on a ticket.")
2423
2424
self._check_ticket_name(ticket, exists=True)
2425
ticket = self._ticket_from_ticket_name(ticket)
2426
2427
from sage.misc.viewer import browser
2428
from sage.env import TRAC_SERVER_URI
2429
browser_cmdline = browser() + ' ' + TRAC_SERVER_URI + '/ticket/' + str(ticket)
2430
import os
2431
os.system(browser_cmdline)
2432
2433
def remote_status(self, ticket=None):
2434
r"""
2435
Show information about the status of ``ticket``.
2436
2437
INPUT:
2438
2439
- ``ticket`` -- an integer or string identifying a ticket or ``None``
2440
(default: ``None``), the number of the ticket to edit. If ``None``,
2441
show information for the :meth:`_current_ticket`.
2442
2443
TESTS:
2444
2445
Set up a single user for doctesting::
2446
2447
sage: from sage.dev.test.sagedev import single_user_setup
2448
sage: dev, config, UI, server = single_user_setup()
2449
2450
It is an error to call this without parameters if not on a ticket::
2451
2452
sage: dev.remote_status()
2453
ticket must be specified if not currently on a ticket.
2454
2455
Create a ticket and show its remote status::
2456
2457
sage: UI.append("Summary: ticket1\ndescription")
2458
sage: dev.create_ticket()
2459
Created ticket #1 at https://trac.sagemath.org/1.
2460
<BLANKLINE>
2461
# (use "sage --dev checkout --ticket=1" to create a new local branch)
2462
1
2463
sage: dev.checkout(ticket=1)
2464
On ticket #1 with associated local branch "ticket/1".
2465
<BLANKLINE>
2466
# Use "sage --dev merge" to include another ticket/branch.
2467
# Use "sage --dev commit" to save changes into a new commit.
2468
sage: dev.remote_status()
2469
Ticket #1 (https://trac.sagemath.org/ticket/1)
2470
==============================================
2471
Your branch "ticket/1" has 0 commits.
2472
No branch has been set on the trac ticket yet.
2473
You have not created a remote branch yet.
2474
2475
After pushing the local branch::
2476
2477
sage: UI.append("y")
2478
sage: dev.push()
2479
The branch "u/doctest/ticket/1" does not exist on the remote server.
2480
Create new remote branch? [Yes/no] y
2481
sage: dev.remote_status()
2482
Ticket #1 (https://trac.sagemath.org/ticket/1)
2483
==============================================
2484
Your branch "ticket/1" has 0 commits.
2485
The trac ticket points to the branch "u/doctest/ticket/1" which has 0 commits. It does not differ from "ticket/1".
2486
2487
Making local changes::
2488
2489
sage: open("tracked", "w").close()
2490
sage: dev.git.silent.add("tracked")
2491
sage: dev.git.silent.commit(message="added tracked")
2492
sage: dev.remote_status()
2493
Ticket #1 (https://trac.sagemath.org/ticket/1)
2494
==============================================
2495
Your branch "ticket/1" has 1 commits.
2496
The trac ticket points to the branch "u/doctest/ticket/1" which has 0 commits. "ticket/1" is ahead of "u/doctest/ticket/1" by 1 commits:
2497
...: added tracked
2498
2499
Pushing them::
2500
2501
sage: UI.append("y")
2502
sage: dev.push()
2503
Local commits that are not on the remote branch "u/doctest/ticket/1":
2504
<BLANKLINE>
2505
...: added tracked
2506
<BLANKLINE>
2507
Push to remote branch? [Yes/no] y
2508
sage: dev.remote_status()
2509
Ticket #1 (https://trac.sagemath.org/ticket/1)
2510
==============================================
2511
Your branch "ticket/1" has 1 commits.
2512
The trac ticket points to the branch "u/doctest/ticket/1" which has 1 commits. It does not differ from "ticket/1".
2513
2514
The branch on the ticket is ahead of the local branch::
2515
2516
sage: dev.git.silent.reset('HEAD~', hard=True)
2517
sage: dev.remote_status()
2518
Ticket #1 (https://trac.sagemath.org/ticket/1)
2519
==============================================
2520
Your branch "ticket/1" has 0 commits.
2521
The trac ticket points to the branch "u/doctest/ticket/1" which has 1 commits. "u/doctest/ticket/1" is ahead of "ticket/1" by 1 commits:
2522
...: added tracked
2523
2524
A mixed case::
2525
2526
sage: open("tracked2", "w").close()
2527
sage: dev.git.silent.add("tracked2")
2528
sage: dev.git.silent.commit(message="added tracked2")
2529
sage: open("tracked3", "w").close()
2530
sage: dev.git.silent.add("tracked3")
2531
sage: dev.git.silent.commit(message="added tracked3")
2532
sage: open("tracked4", "w").close()
2533
sage: dev.git.silent.add("tracked4")
2534
sage: dev.git.silent.commit(message="added tracked4")
2535
sage: dev._UI.append("y")
2536
sage: dev.push(remote_branch="u/doctest/branch1", force=True)
2537
The branch "u/doctest/branch1" does not exist on the remote server.
2538
Create new remote branch? [Yes/no] y
2539
sage: dev.git.silent.reset('HEAD~', hard=True)
2540
sage: dev.remote_status()
2541
Ticket #1 (https://trac.sagemath.org/ticket/1)
2542
==============================================
2543
Your branch "ticket/1" has 2 commits.
2544
The trac ticket points to the branch "u/doctest/branch1" which has 3 commits. "u/doctest/branch1" is ahead of "ticket/1" by 1 commits:
2545
...: added tracked4
2546
Your remote branch "u/doctest/ticket/1" has 1 commits. The branches "u/doctest/ticket/1" and "ticket/1" have diverged.
2547
"u/doctest/ticket/1" is ahead of "ticket/1" by 1 commits:
2548
...: added tracked
2549
"ticket/1" is ahead of "u/doctest/ticket/1" by 2 commits:
2550
...: added tracked2
2551
...: added tracked3
2552
"""
2553
if ticket is None:
2554
ticket = self._current_ticket()
2555
2556
if ticket is None:
2557
raise SageDevValueError("ticket must be specified if not currently on a ticket.")
2558
2559
self._check_ticket_name(ticket, exists=True)
2560
ticket = self._ticket_from_ticket_name(ticket)
2561
2562
self._is_master_uptodate(action_if_not="warning")
2563
2564
from sage.env import TRAC_SERVER_URI
2565
header = "Ticket #{0} ({1})".format(ticket, TRAC_SERVER_URI + '/ticket/' + str(ticket))
2566
underline = "="*len(header)
2567
2568
commits = lambda a, b: list(reversed(
2569
self.git.log("{0}..{1}".format(a,b), "--pretty=%an <%ae>: %s").splitlines()))
2570
2571
def detail(a, b, a_to_b, b_to_a):
2572
if not a_to_b and not b_to_a:
2573
return 'It does not differ from "{0}".'.format(b)
2574
elif not a_to_b:
2575
return '"{0}" is ahead of "{1}" by {2} commits:\n{3}'.format(a,b,len(b_to_a), "\n".join(b_to_a))
2576
elif not b_to_a:
2577
return '"{0}" is ahead of "{1}" by {2} commits:\n{3}'.format(b,a,len(a_to_b),"\n".join(a_to_b))
2578
else:
2579
return ('The branches "{0}" and "{1}" have diverged.\n"{0}" is ahead of'
2580
' "{1}" by {2} commits:\n{3}\n"{1}" is ahead of "{0}" by {4}'
2581
' commits:\n{5}'.format(a, b, len(b_to_a), "\n".join(b_to_a),
2582
len(a_to_b), "\n".join(a_to_b)))
2583
2584
branch = None
2585
merge_base_local = None
2586
if self._has_local_branch_for_ticket(ticket):
2587
branch = self._local_branch_for_ticket(ticket)
2588
merge_base_local = self.git.merge_base(MASTER_BRANCH, branch).splitlines()[0]
2589
master_to_branch = commits(merge_base_local, branch)
2590
local_summary = 'Your branch "{0}" has {1} commits.'.format(branch, len(master_to_branch))
2591
else:
2592
local_summary = "You have no local branch for this ticket"
2593
2594
ticket_branch = self.trac._branch_for_ticket(ticket)
2595
if ticket_branch:
2596
ticket_to_local = None
2597
local_to_ticket = None
2598
if not self._is_remote_branch_name(ticket_branch, exists=True):
2599
ticket_summary = 'The trac ticket points to the branch "{0}" which does not exist.'
2600
else:
2601
self.git.super_silent.fetch(self.git._repository_anonymous, ticket_branch)
2602
merge_base_ticket = self.git.merge_base(MASTER_BRANCH, 'FETCH_HEAD').splitlines()[0]
2603
master_to_ticket = commits(merge_base_ticket, 'FETCH_HEAD')
2604
ticket_summary = 'The trac ticket points to the' \
2605
' branch "{0}" which has {1} commits.'.format(ticket_branch, len(master_to_ticket))
2606
if branch is not None:
2607
if merge_base_local != merge_base_ticket:
2608
ticket_summary += ' The branch can not be compared to your local' \
2609
' branch "{0}" because the branches are based on different versions' \
2610
' of sage (i.e. the "master" branch).'
2611
else:
2612
ticket_to_local = commits('FETCH_HEAD', branch)
2613
local_to_ticket = commits(branch, 'FETCH_HEAD')
2614
ticket_summary += " "+detail(ticket_branch, branch, ticket_to_local, local_to_ticket)
2615
else:
2616
ticket_summary = "No branch has been set on the trac ticket yet."
2617
2618
remote_branch = self._remote_branch_for_ticket(ticket)
2619
if self._is_remote_branch_name(remote_branch, exists=True):
2620
remote_to_local = None
2621
local_to_remote = None
2622
self.git.super_silent.fetch(self.git._repository_anonymous, remote_branch)
2623
merge_base_remote = self.git.merge_base(MASTER_BRANCH, 'FETCH_HEAD').splitlines()[0]
2624
master_to_remote = commits(merge_base_remote, 'FETCH_HEAD')
2625
remote_summary = 'Your remote branch "{0}" has {1} commits.'.format(
2626
remote_branch, len(master_to_remote))
2627
if branch is not None:
2628
if merge_base_remote != merge_base_local:
2629
remote_summary += ' The branch can not be compared to your local' \
2630
' branch "{0}" because the branches are based on different version' \
2631
' of sage (i.e. the "master" branch).'
2632
else:
2633
remote_to_local = commits('FETCH_HEAD', branch)
2634
local_to_remote = commits(branch, 'FETCH_HEAD')
2635
remote_summary += " "+detail(remote_branch, branch, remote_to_local, local_to_remote)
2636
else:
2637
remote_summary = "You have not created a remote branch yet."
2638
2639
show = [header, underline, local_summary, ticket_summary]
2640
if not self._is_remote_branch_name(remote_branch, exists=True) or remote_branch != ticket_branch:
2641
show.append(remote_summary)
2642
self._UI.show("\n".join(show))
2643
2644
def prune_tickets(self):
2645
r"""
2646
Remove branches for tickets that are already merged into master.
2647
2648
.. SEEALSO::
2649
2650
:meth:`abandon` -- Abandon a single ticket or branch.
2651
2652
TESTS:
2653
2654
Create a single user for doctesting::
2655
2656
sage: from sage.dev.test.sagedev import single_user_setup
2657
sage: dev, config, UI, server = single_user_setup()
2658
2659
Create a ticket branch::
2660
2661
sage: UI.append("Summary: summary\ndescription")
2662
sage: dev.create_ticket()
2663
Created ticket #1 at https://trac.sagemath.org/1.
2664
<BLANKLINE>
2665
# (use "sage --dev checkout --ticket=1" to create a new local branch)
2666
1
2667
sage: dev.checkout(ticket=1)
2668
On ticket #1 with associated local branch "ticket/1".
2669
<BLANKLINE>
2670
# Use "sage --dev merge" to include another ticket/branch.
2671
# Use "sage --dev commit" to save changes into a new commit.
2672
sage: dev.tickets()
2673
: master
2674
* #1: ticket/1 summary
2675
2676
With a commit on it, the branch is not abandoned::
2677
2678
sage: open("tracked","w").close()
2679
sage: dev.git.silent.add("tracked")
2680
sage: dev.git.super_silent.commit(message="added tracked")
2681
sage: dev.prune_tickets()
2682
sage: dev.tickets()
2683
: master
2684
* #1: ticket/1 summary
2685
2686
After merging it to the master branch, it is abandoned. This does not
2687
work, because we cannot move the current branch::
2688
2689
sage: dev.git.super_silent.checkout("master")
2690
sage: dev.git.super_silent.merge("ticket/1")
2691
2692
sage: dev.git.super_silent.checkout("ticket/1")
2693
sage: dev.prune_tickets()
2694
Abandoning #1.
2695
Cannot delete "ticket/1": is the current branch.
2696
<BLANKLINE>
2697
# (use "sage --dev vanilla" to switch to the master branch)
2698
2699
Now, the branch is abandoned::
2700
2701
sage: dev.vanilla()
2702
sage: dev.prune_tickets()
2703
Abandoning #1.
2704
Moved your branch "ticket/1" to "trash/ticket/1".
2705
sage: dev.tickets()
2706
: master
2707
sage: dev.prune_tickets()
2708
"""
2709
for branch in self.git.local_branches():
2710
if self._has_ticket_for_local_branch(branch):
2711
ticket = self._ticket_for_local_branch(branch)
2712
if self.git.is_ancestor_of(branch, MASTER_BRANCH):
2713
self._UI.show("Abandoning #{0}.".format(ticket))
2714
self.abandon(ticket, helpful=False)
2715
2716
def abandon(self, ticket_or_branch=None, helpful=True):
2717
r"""
2718
Abandon a ticket or branch.
2719
2720
INPUT:
2721
2722
- ``ticket_or_branch`` -- an integer or string identifying a ticket or
2723
the name of a local branch or ``None`` (default: ``None``), remove
2724
the branch ``ticket_or_branch`` or the branch for the ticket
2725
``ticket_or_branch`` (or the current branch if ``None``). Also
2726
removes the users remote tracking branch.
2727
2728
- ``helpful`` -- boolean (default: ``True``). Whether to print
2729
informational messages to guide new users.
2730
2731
.. SEEALSO::
2732
2733
- :meth:`prune_tickets` -- abandon tickets that have
2734
been closed.
2735
2736
- :meth:`tickets` -- list local non-abandoned tickets.
2737
2738
TESTS:
2739
2740
Create a single user for doctesting::
2741
2742
sage: from sage.dev.test.sagedev import single_user_setup
2743
sage: dev, config, UI, server = single_user_setup()
2744
2745
Create a ticket branch and abandon it::
2746
2747
sage: UI.append("Summary: summary\ndescription")
2748
sage: dev.create_ticket()
2749
Created ticket #1 at https://trac.sagemath.org/1.
2750
<BLANKLINE>
2751
# (use "sage --dev checkout --ticket=1" to create a new local branch)
2752
1
2753
sage: dev.checkout(ticket=1)
2754
On ticket #1 with associated local branch "ticket/1".
2755
<BLANKLINE>
2756
# Use "sage --dev merge" to include another ticket/branch.
2757
# Use "sage --dev commit" to save changes into a new commit.
2758
sage: UI.append("y")
2759
sage: dev.push()
2760
The branch "u/doctest/ticket/1" does not exist on the remote server.
2761
Create new remote branch? [Yes/no] y
2762
sage: dev.abandon(1)
2763
Cannot delete "ticket/1": is the current branch.
2764
<BLANKLINE>
2765
# (use "sage --dev vanilla" to switch to the master branch)
2766
sage: dev.vanilla()
2767
sage: dev.abandon(1)
2768
Moved your branch "ticket/1" to "trash/ticket/1".
2769
<BLANKLINE>
2770
# Use "sage --dev checkout --ticket=1 --base=master" to restart working on #1 with a clean copy of the master branch.
2771
2772
Start to work on a new branch for this ticket::
2773
2774
sage: from sage.dev.sagedev import MASTER_BRANCH
2775
sage: UI.append("y")
2776
sage: dev.checkout(ticket=1, base=MASTER_BRANCH)
2777
About to create a new branch for #1 based on "master". However, the trac ticket
2778
for #1 already refers to the branch "u/doctest/ticket/1". The new branch will
2779
not contain any work that has already been done on "u/doctest/ticket/1".
2780
Create fresh branch? [yes/No] y
2781
On ticket #1 with associated local branch "ticket/1".
2782
<BLANKLINE>
2783
# Use "sage --dev merge" to include another ticket/branch.
2784
# Use "sage --dev commit" to save changes into a new commit.
2785
"""
2786
ticket = None
2787
2788
if self._is_ticket_name(ticket_or_branch):
2789
ticket = self._ticket_from_ticket_name(ticket_or_branch)
2790
2791
if not self._has_local_branch_for_ticket(ticket):
2792
raise SageDevValueError("Cannot abandon #{0}: no local branch for this ticket.", ticket)
2793
ticket_or_branch = self._local_branch_for_ticket(ticket)
2794
2795
if self._has_ticket_for_local_branch(ticket_or_branch):
2796
ticket = self._ticket_for_local_branch(ticket_or_branch)
2797
2798
if self._is_local_branch_name(ticket_or_branch):
2799
branch = ticket_or_branch
2800
self._check_local_branch_name(branch, exists=True)
2801
2802
if branch == MASTER_BRANCH:
2803
self._UI.error("Cannot delete the master branch.")
2804
raise OperationCancelledError("protecting the user")
2805
2806
from git_error import DetachedHeadError
2807
try:
2808
if self.git.current_branch() == branch:
2809
self._UI.error('Cannot delete "{0}": is the current branch.', branch)
2810
self._UI.info(['', '(use "{0}" to switch to the master branch)'],
2811
self._format_command("vanilla"))
2812
raise OperationCancelledError("can not delete current branch")
2813
except DetachedHeadError:
2814
pass
2815
2816
new_branch = self._new_local_branch_for_trash(branch)
2817
self.git.super_silent.branch("-m", branch, new_branch)
2818
self._UI.show('Moved your branch "{0}" to "{1}".', branch, new_branch)
2819
else:
2820
raise SageDevValueError("ticket_or_branch must be the name of a ticket or a local branch")
2821
2822
if ticket:
2823
self._set_local_branch_for_ticket(ticket, None)
2824
self._set_dependencies_for_ticket(ticket, None)
2825
if helpful:
2826
self._UI.info(['',
2827
'Use "{0}" to restart working on #{1} with a clean copy of the master branch.'],
2828
self._format_command("checkout", ticket=ticket, base=MASTER_BRANCH), ticket)
2829
2830
def gather(self, branch, *tickets_or_branches):
2831
r"""
2832
Create a new branch ``branch`` with ``tickets_or_remote_branches``
2833
applied.
2834
2835
This method is not wrapped in the commandline dev scripts. It
2836
does nothing that cannot be done with ``checkout`` and
2837
``merge``, it just steepens the learning curve by introducing
2838
yet another command. Unless a clear use case emerges, it
2839
should be removed.
2840
2841
INPUT:
2842
2843
- ``branch`` -- a string, the name of the new branch
2844
2845
- ``tickets_or_branches`` -- a list of integers and strings; for an
2846
integer or string identifying a ticket, the branch on the trac ticket
2847
gets merged, for the name of a local or remote branch, that branch
2848
gets merged.
2849
2850
.. SEEALSO::
2851
2852
- :meth:`merge` -- merge into the current branch rather
2853
than creating a new one
2854
2855
TESTS:
2856
2857
Create a doctest setup with a single user::
2858
2859
sage: from sage.dev.test.sagedev import single_user_setup
2860
sage: dev, config, UI, server = single_user_setup()
2861
2862
Create tickets and branches::
2863
2864
sage: dev._UI.append("Summary: summary1\ndescription")
2865
sage: dev.create_ticket()
2866
Created ticket #1 at https://trac.sagemath.org/1.
2867
<BLANKLINE>
2868
# (use "sage --dev checkout --ticket=1" to create a new local branch)
2869
1
2870
sage: dev.checkout(ticket=1)
2871
On ticket #1 with associated local branch "ticket/1".
2872
<BLANKLINE>
2873
# Use "sage --dev merge" to include another ticket/branch.
2874
# Use "sage --dev commit" to save changes into a new commit.
2875
sage: open("tracked","w").close()
2876
sage: dev.git.silent.add("tracked")
2877
sage: dev.git.super_silent.commit(message="added tracked")
2878
sage: dev._UI.append("y")
2879
sage: dev._UI.append("y")
2880
sage: dev.push()
2881
The branch "u/doctest/ticket/1" does not exist on the remote server.
2882
Create new remote branch? [Yes/no] y
2883
2884
Gather all these branches::
2885
2886
sage: dev._sagedev.gather("gather_branch", "#1", "ticket/1", "u/doctest/ticket/1")
2887
"""
2888
try:
2889
self.reset_to_clean_state()
2890
self.clean()
2891
except OperationCancelledError:
2892
self._UI.error("Cannot gather branches because working directory is not in a clean state.")
2893
raise OperationCancelledError("working directory not clean")
2894
2895
self._check_local_branch_name(branch, exists=False)
2896
2897
branches = []
2898
for ticket_or_branch in tickets_or_branches:
2899
local_branch = None
2900
remote_branch = None
2901
if self._is_ticket_name(ticket_or_branch):
2902
ticket = self._ticket_from_ticket_name(ticket_or_branch)
2903
remote_branch = self.trac._branch_for_ticket(ticket)
2904
if remote_branch is None:
2905
raise SageDevValueError("Ticket #{0} does not have a branch set yet.".format(ticket))
2906
elif self._is_local_branch_name(ticket_or_branch, exists=True):
2907
local_branch = ticket_or_branch
2908
else:
2909
remote_branch = ticket_or_branch
2910
2911
if local_branch:
2912
self._check_local_branch_name(local_branch, exists=True)
2913
branches.append(("local",local_branch))
2914
if remote_branch:
2915
self._check_remote_branch_name(remote_branch, exists=True)
2916
branches.append(("remote",remote_branch))
2917
2918
self._UI.debug('Creating a new branch "{0}".'.format(branch))
2919
self.git.super_silent.branch(branch, MASTER_BRANCH)
2920
self.git.super_silent.checkout(branch)
2921
2922
try:
2923
for local_remote,branch_name in branches:
2924
self._UI.debug('Merging {2} branch "{0}" into "{1}".'
2925
.format(branch_name, branch, local_remote))
2926
self.merge(branch, pull=local_remote=="remote")
2927
except:
2928
self.git.reset_to_clean_state()
2929
self.git.clean_wrapper()
2930
self.vanilla()
2931
self.git.super_silent.branch("-D", branch)
2932
self._UI.debug('Deleted branch "{0}".'.format(branch))
2933
2934
def merge(self, ticket_or_branch=MASTER_BRANCH, pull=None, create_dependency=None):
2935
r"""
2936
Merge changes from ``ticket_or_branch`` into the current branch.
2937
2938
Incorporate commits from other tickets/branches into the
2939
current branch.
2940
2941
Optionally, you can add the merged ticket to the trac
2942
"Dependency:" field. Note that the merged commits become part
2943
of the current branch, regardless of whether they are noted on
2944
trac. Adding a dependency implies the following:
2945
2946
- the other ticket must be positively reviewed and merged
2947
before this ticket may be merged into the official release
2948
of sage. The commits included from a dependency don't need
2949
to be reviewed in this ticket, whereas commits reviewed in
2950
this ticket from a non-dependency may make reviewing the
2951
other ticket easier.
2952
2953
- you can more easily merge in future changes to dependencies.
2954
So if you need a feature from another ticket it may be
2955
appropriate to create a dependency to that you may more
2956
easily benefit from others' work on that ticket.
2957
2958
- if you depend on another ticket then you need to worry about
2959
the progress on that ticket. If that ticket is still being
2960
actively developed then you may need to make further merges
2961
in the future if conflicts arise.
2962
2963
INPUT:
2964
2965
- ``ticket_or_branch`` -- an integer or strings (default:
2966
``'master'``); for an integer or string identifying a ticket, the
2967
branch on the trac ticket gets merged (or the local branch for the
2968
ticket, if ``pull`` is ``False``), for the name of a local or
2969
remote branch, that branch gets merged. If ``'dependencies'``, the
2970
dependencies are merged in one by one.
2971
2972
- ``pull`` -- a boolean or ``None`` (default: ``None``); if
2973
``ticket_or_branch`` identifies a ticket, whether to pull the
2974
latest branch on the trac ticket (the default); if
2975
``ticket_or_branch`` is a branch name, then ``pull`` controls
2976
whether it should be interpreted as a remote branch (``True``) or as
2977
a local branch (``False``). If it is set to ``None``, then it will
2978
take ``ticket_or_branch`` as a remote branch if it exists, and as a
2979
local branch otherwise.
2980
2981
- ``create_dependency`` -- a boolean or ``None`` (default: ``None``),
2982
whether to create a dependency to ``ticket_or_branch``. If ``None``,
2983
then a dependency is created if ``ticket_or_branch`` identifies a
2984
ticket and if the current branch is associated to a ticket.
2985
2986
.. NOTE::
2987
2988
Dependencies are stored locally and only updated with respect to
2989
the remote server during :meth:`push` and :meth:`pull`.
2990
2991
.. SEEALSO::
2992
2993
- :meth:`show_dependencies` -- see the current
2994
dependencies.
2995
2996
- :meth:`GitInterface.merge` -- git's merge command has
2997
more options and can merge multiple branches at once.
2998
2999
TESTS:
3000
3001
Create a doctest setup with two users::
3002
3003
sage: from sage.dev.test.sagedev import two_user_setup
3004
sage: alice, config_alice, bob, config_bob, server = two_user_setup()
3005
3006
Create tickets and branches::
3007
3008
sage: alice._chdir()
3009
sage: alice._UI.append("Summary: summary1\ndescription")
3010
sage: alice.create_ticket()
3011
Created ticket #1 at https://trac.sagemath.org/1.
3012
<BLANKLINE>
3013
# (use "sage --dev checkout --ticket=1" to create a new local branch)
3014
1
3015
sage: alice._UI.append("Summary: summary2\ndescription")
3016
sage: alice.create_ticket()
3017
Created ticket #2 at https://trac.sagemath.org/2.
3018
<BLANKLINE>
3019
# (use "sage --dev checkout --ticket=2" to create a new local branch)
3020
2
3021
3022
Alice creates two branches and merges them::
3023
3024
sage: alice.checkout(ticket=1)
3025
On ticket #1 with associated local branch "ticket/1".
3026
<BLANKLINE>
3027
# Use "sage --dev merge" to include another ticket/branch.
3028
# Use "sage --dev commit" to save changes into a new commit.
3029
sage: open("alice1","w").close()
3030
sage: alice.git.silent.add("alice1")
3031
sage: alice.git.super_silent.commit(message="added alice1")
3032
sage: alice.checkout(ticket=2)
3033
On ticket #2 with associated local branch "ticket/2".
3034
<BLANKLINE>
3035
# Use "sage --dev merge" to include another ticket/branch.
3036
# Use "sage --dev commit" to save changes into a new commit.
3037
sage: with open("alice2","w") as f: f.write("alice")
3038
sage: alice.git.silent.add("alice2")
3039
sage: alice.git.super_silent.commit(message="added alice2")
3040
3041
When merging for a ticket, the branch on the trac ticket matters::
3042
3043
sage: alice.merge("#1")
3044
Cannot merge remote branch for #1 because no branch has been set on the trac
3045
ticket.
3046
sage: alice.checkout(ticket=1)
3047
On ticket #1 with associated local branch "ticket/1".
3048
<BLANKLINE>
3049
# Use "sage --dev merge" to include another ticket/branch.
3050
# Use "sage --dev commit" to save changes into a new commit.
3051
sage: alice._UI.append("y")
3052
sage: alice.push()
3053
The branch "u/alice/ticket/1" does not exist on the remote server.
3054
Create new remote branch? [Yes/no] y
3055
sage: alice.checkout(ticket=2)
3056
On ticket #2 with associated local branch "ticket/2".
3057
<BLANKLINE>
3058
# Use "sage --dev merge" to include another ticket/branch.
3059
# Use "sage --dev commit" to save changes into a new commit.
3060
sage: alice.merge("#1", pull=False)
3061
Merging the local branch "ticket/1" into the local branch "ticket/2".
3062
Automatic merge successful.
3063
<BLANKLINE>
3064
# (use "sage --dev commit" to commit your merge)
3065
<BLANKLINE>
3066
Added dependency on #1 to #2.
3067
3068
Check that merging dependencies works::
3069
3070
sage: alice.merge("dependencies")
3071
Merging the remote branch "u/alice/ticket/1" into the local branch "ticket/2".
3072
Automatic merge successful.
3073
<BLANKLINE>
3074
# (use "sage --dev commit" to commit your merge)
3075
3076
Merging local branches::
3077
3078
sage: alice.merge("ticket/1")
3079
Merging the local branch "ticket/1" into the local branch "ticket/2".
3080
Automatic merge successful.
3081
<BLANKLINE>
3082
# (use "sage --dev commit" to commit your merge)
3083
3084
A remote branch for a local branch is only merged in if ``pull`` is set::
3085
3086
sage: alice._sagedev._set_remote_branch_for_branch("ticket/1", "nonexistant")
3087
sage: alice.merge("ticket/1")
3088
Merging the local branch "ticket/1" into the local branch "ticket/2".
3089
Automatic merge successful.
3090
<BLANKLINE>
3091
# (use "sage --dev commit" to commit your merge)
3092
sage: alice.merge("ticket/1", pull=True)
3093
Branch "ticket/1" does not exist on the remote system.
3094
3095
Bob creates a conflicting commit::
3096
3097
sage: bob._chdir()
3098
sage: bob.checkout(ticket=1)
3099
On ticket #1 with associated local branch "ticket/1".
3100
<BLANKLINE>
3101
# Use "sage --dev merge" to include another ticket/branch.
3102
# Use "sage --dev commit" to save changes into a new commit.
3103
sage: with open("alice2","w") as f: f.write("bob")
3104
sage: bob.git.silent.add("alice2")
3105
sage: bob.git.super_silent.commit(message="added alice2")
3106
sage: bob._UI.append("y")
3107
sage: bob._UI.append("y")
3108
sage: bob.push()
3109
The branch "u/bob/ticket/1" does not exist on the remote server.
3110
Create new remote branch? [Yes/no] y
3111
The branch field of ticket #1 needs to be updated from its current value
3112
"u/alice/ticket/1" to "u/bob/ticket/1"
3113
Change the "Branch:" field? [Yes/no] y
3114
3115
The merge now requires manual conflict resolution::
3116
3117
sage: alice._chdir()
3118
sage: alice._UI.append("abort")
3119
sage: alice.merge("#1")
3120
Merging the remote branch "u/bob/ticket/1" into the local branch "ticket/2".
3121
Automatic merge failed, there are conflicting commits.
3122
<BLANKLINE>
3123
Auto-merging alice2
3124
CONFLICT (add/add): Merge conflict in alice2
3125
<BLANKLINE>
3126
Please edit the affected files to resolve the conflicts. When you are finished,
3127
your resolution will be commited.
3128
Finished? [ok/Abort] abort
3129
sage: alice._UI.append("ok")
3130
sage: alice.merge("#1")
3131
Merging the remote branch "u/bob/ticket/1" into the local branch "ticket/2".
3132
Automatic merge failed, there are conflicting commits.
3133
<BLANKLINE>
3134
Auto-merging alice2
3135
CONFLICT (add/add): Merge conflict in alice2
3136
<BLANKLINE>
3137
Please edit the affected files to resolve the conflicts. When you are finished,
3138
your resolution will be commited.
3139
Finished? [ok/Abort] ok
3140
Created a commit from your conflict resolution.
3141
3142
We cannot merge a ticket into itself::
3143
3144
sage: alice.merge(2)
3145
cannot merge a ticket into itself
3146
3147
We also cannot merge if the working directory has uncommited changes::
3148
3149
sage: alice._UI.append("cancel")
3150
sage: with open("alice2","w") as f: f.write("uncommited change")
3151
sage: alice.merge(1)
3152
The following files in your working directory contain uncommitted changes:
3153
<BLANKLINE>
3154
alice2
3155
<BLANKLINE>
3156
Discard changes? [discard/Cancel/stash] cancel
3157
Cannot merge because working directory is not in a clean state.
3158
<BLANKLINE>
3159
# (use "sage --dev commit" to commit your changes)
3160
"""
3161
try:
3162
self.reset_to_clean_state()
3163
self.clean()
3164
except OperationCancelledError:
3165
self._UI.error("Cannot merge because working directory is not in a clean state.")
3166
self._UI.info(['', '(use "{0}" to commit your changes)'],
3167
self._format_command('commit'))
3168
raise OperationCancelledError("working directory not clean")
3169
3170
from git_error import DetachedHeadError
3171
try:
3172
current_branch = self.git.current_branch()
3173
except DetachedHeadError:
3174
self._UI.error('Not on any branch.')
3175
self._UI.info(['', '(use "{0}" to checkout a branch)'],
3176
self._format_command("checkout"))
3177
raise OperationCancelledError("detached head")
3178
3179
current_ticket = self._current_ticket()
3180
3181
ticket = None
3182
branch = None
3183
remote_branch = None
3184
3185
if ticket_or_branch == 'dependencies':
3186
if current_ticket == None:
3187
raise SageDevValueError("dependencies can only be merged if currently on a ticket.")
3188
if pull == False:
3189
raise SageDevValueError('"pull" must not be "False" when merging dependencies.')
3190
if create_dependency != None:
3191
raise SageDevValueError('"create_dependency" must not be set when merging dependencies.')
3192
for dependency in self._dependencies_for_ticket(current_ticket):
3193
self._UI.debug("Merging dependency #{0}.".format(dependency))
3194
self.merge(ticket_or_branch=dependency, pull=True)
3195
return
3196
elif self._is_ticket_name(ticket_or_branch):
3197
ticket = self._ticket_from_ticket_name(ticket_or_branch)
3198
if ticket == current_ticket:
3199
raise SageDevValueError("cannot merge a ticket into itself")
3200
self._check_ticket_name(ticket, exists=True)
3201
if pull is None:
3202
pull = True
3203
if create_dependency is None:
3204
create_dependency = True
3205
if self._has_local_branch_for_ticket(ticket):
3206
branch = self._local_branch_for_ticket(ticket)
3207
if pull:
3208
remote_branch = self.trac._branch_for_ticket(ticket)
3209
if remote_branch is None:
3210
self._UI.error("Cannot merge remote branch for #{0} because no branch has"
3211
" been set on the trac ticket.", ticket)
3212
raise OperationCancelledError("remote branch not set on trac")
3213
elif pull == False or (pull is None and not
3214
self._is_remote_branch_name(ticket_or_branch, exists=True)):
3215
# ticket_or_branch should be interpreted as a local branch name
3216
branch = ticket_or_branch
3217
self._check_local_branch_name(branch, exists=True)
3218
pull = False
3219
if create_dependency == True:
3220
if self._has_ticket_for_local_branch(branch):
3221
ticket = self._ticket_for_local_branch(branch)
3222
else:
3223
raise SageDevValueError('"create_dependency" must not be "True" if'
3224
' "ticket_or_branch" is a local branch which'
3225
' is not associated to a ticket.')
3226
else:
3227
create_dependency = False
3228
else:
3229
# ticket_or_branch should be interpreted as a remote branch name
3230
remote_branch = ticket_or_branch
3231
self._check_remote_branch_name(remote_branch, exists=True)
3232
pull = True
3233
if create_dependency == True:
3234
raise SageDevValueError('"create_dependency" must not be "True" if'
3235
' "ticket_or_branch" is a local branch.')
3236
create_dependency = False
3237
3238
if pull:
3239
assert remote_branch
3240
if not self._is_remote_branch_name(remote_branch, exists=True):
3241
self._UI.error('Can not merge remote branch "{0}". It does not exist.',
3242
remote_branch)
3243
raise OperationCancelledError("no such branch")
3244
self._UI.show('Merging the remote branch "{0}" into the local branch "{1}".',
3245
remote_branch, current_branch)
3246
self.git.super_silent.fetch(self.git._repository_anonymous, remote_branch)
3247
local_merge_branch = 'FETCH_HEAD'
3248
else:
3249
assert branch
3250
self._UI.show('Merging the local branch "{0}" into the local branch "{1}".',
3251
branch, current_branch)
3252
local_merge_branch = branch
3253
3254
from git_error import GitError
3255
try:
3256
self.git.super_silent.merge(local_merge_branch)
3257
self._UI.show('Automatic merge successful.')
3258
self._UI.info(['', '(use "{0}" to commit your merge)'],
3259
self._format_command('commit'))
3260
except GitError as e:
3261
try:
3262
self._UI.show('Automatic merge failed, there are conflicting commits.')
3263
excluded = ['Aborting',
3264
"Automatic merge failed; fix conflicts and then commit the result."]
3265
lines = e.stdout.splitlines() + e.stderr.splitlines()
3266
lines = [line for line in lines if line not in excluded]
3267
self._UI.show([''] + lines + [''])
3268
self._UI.show('Please edit the affected files to resolve the conflicts.'
3269
' When you are finished, your resolution will be commited.')
3270
sel = self._UI.select("Finished?", ['ok', 'abort'], default=1)
3271
if sel == 'ok':
3272
self.git.silent.commit(a=True, no_edit=True)
3273
self._UI.show("Created a commit from your conflict resolution.")
3274
elif sel == 'abort':
3275
raise OperationCancelledError("user requested")
3276
else:
3277
assert False
3278
except Exception as e:
3279
self.git.reset_to_clean_state()
3280
self.git.clean_wrapper()
3281
raise
3282
3283
if create_dependency:
3284
assert ticket and current_ticket
3285
dependencies = list(self._dependencies_for_ticket(current_ticket))
3286
if ticket in dependencies:
3287
self._UI.debug("Not recording dependency on #{0} because #{1} already depends on #{0}.",
3288
ticket, current_ticket)
3289
else:
3290
self._UI.show(['', "Added dependency on #{0} to #{1}."], ticket, current_ticket)
3291
self._set_dependencies_for_ticket(current_ticket, dependencies+[ticket])
3292
3293
def tickets(self, include_abandoned=False, cached=True):
3294
r"""
3295
Print the tickets currently being worked on in your local
3296
repository.
3297
3298
This function shows the branch names as well as the ticket numbers for
3299
all active tickets. It also shows local branches that are not
3300
associated to ticket numbers.
3301
3302
INPUT:
3303
3304
- ``include_abandoned`` -- boolean (default: ``False``), whether to
3305
include abandoned branches.
3306
3307
- ``cached`` -- boolean (default: ``True``), whether to try to pull the
3308
summaries from the ticket cache; if ``True``, then the summaries
3309
might not be accurate if they changed since they were last updated.
3310
To update the summaries, set this to ``False``.
3311
3312
.. SEEALSO::
3313
3314
- :meth:`abandon_ticket` -- hide tickets from this method.
3315
3316
- :meth:`remote_status` -- also show status compared to
3317
the trac server.
3318
3319
- :meth:`current_ticket` -- get the current ticket.
3320
3321
TESTS:
3322
3323
Create a doctest setup with a single user::
3324
3325
sage: from sage.dev.test.sagedev import single_user_setup
3326
sage: dev, config, UI, server = single_user_setup()
3327
3328
Create some tickets::
3329
3330
sage: dev.tickets()
3331
* : master
3332
3333
sage: UI.append("Summary: summary\ndescription")
3334
sage: dev.create_ticket()
3335
Created ticket #1 at https://trac.sagemath.org/1.
3336
<BLANKLINE>
3337
# (use "sage --dev checkout --ticket=1" to create a new local branch)
3338
1
3339
sage: dev.checkout(ticket=1)
3340
On ticket #1 with associated local branch "ticket/1".
3341
<BLANKLINE>
3342
# Use "sage --dev merge" to include another ticket/branch.
3343
# Use "sage --dev commit" to save changes into a new commit.
3344
sage: UI.append("Summary: summary\ndescription")
3345
sage: dev.create_ticket()
3346
Created ticket #2 at https://trac.sagemath.org/2.
3347
<BLANKLINE>
3348
# (use "sage --dev checkout --ticket=2" to create a new local branch)
3349
2
3350
sage: dev.checkout(ticket=2)
3351
On ticket #2 with associated local branch "ticket/2".
3352
<BLANKLINE>
3353
# Use "sage --dev merge" to include another ticket/branch.
3354
# Use "sage --dev commit" to save changes into a new commit.
3355
sage: dev.tickets()
3356
: master
3357
#1: ticket/1 summary
3358
* #2: ticket/2 summary
3359
"""
3360
branches = self.git.local_branches()
3361
from git_error import DetachedHeadError
3362
try:
3363
current_branch = self.git.current_branch()
3364
except DetachedHeadError:
3365
current_branch = None
3366
branches = [ branch for branch in branches if include_abandoned or not self._is_trash_name(branch) ]
3367
if not branches:
3368
return
3369
ret = []
3370
for branch in branches:
3371
ticket = None
3372
ticket_summary = ""
3373
extra = " "
3374
if self._has_ticket_for_local_branch(branch):
3375
ticket = self._ticket_for_local_branch(branch)
3376
try:
3377
try:
3378
ticket_summary = self.trac._get_attributes(ticket, cached=cached)['summary']
3379
except KeyError:
3380
ticket_summary = self.trac._get_attributes(ticket, cached=False)['summary']
3381
except TracConnectionError:
3382
ticket_summary = ""
3383
if current_branch == branch:
3384
extra = "*"
3385
ticket_str = "#"+str(ticket) if ticket else ""
3386
ret.append(("{0:>7}: {1} {2}".format(ticket_str, branch, ticket_summary), extra))
3387
while all([info.startswith(' ') for (info, extra) in ret]):
3388
ret = [(info[1:],extra) for (info, extra) in ret]
3389
ret = sorted(ret)
3390
ret = ["{0} {1}".format(extra,info) for (info,extra) in ret]
3391
self._UI.show("\n".join(ret))
3392
3393
def vanilla(self, release=MASTER_BRANCH):
3394
r"""
3395
Return to a clean version of Sage.
3396
3397
INPUT:
3398
3399
- ``release`` -- a string or decimal giving the release name (default:
3400
``'master'``). In fact, any tag, commit or branch will work. If the
3401
tag does not exist locally an attempt to fetch it from the server
3402
will be made.
3403
3404
Git equivalent::
3405
3406
Checks out a given tag, commit or branch in detached head mode.
3407
3408
.. SEEALSO::
3409
3410
- :meth:`checkout` -- checkout another branch, ready to
3411
develop on it.
3412
3413
- :meth:`pull` -- pull a branch from the server and merge
3414
it.
3415
3416
TESTS:
3417
3418
Create a doctest setup with a single user::
3419
3420
sage: from sage.dev.test.sagedev import single_user_setup
3421
sage: dev, config, UI, server = single_user_setup()
3422
3423
Go to a sage release::
3424
3425
sage: dev.git.current_branch()
3426
'master'
3427
sage: dev.vanilla()
3428
sage: dev.git.current_branch()
3429
Traceback (most recent call last):
3430
...
3431
DetachedHeadError: unexpectedly, git is in a detached HEAD state
3432
"""
3433
if hasattr(release, 'literal'):
3434
release = release.literal
3435
release = str(release)
3436
3437
try:
3438
self.reset_to_clean_state()
3439
self.clean()
3440
except OperationCancelledError:
3441
self._UI.error("Cannot checkout a release while your working directory is not clean.")
3442
raise OperationCancelledError("working directory not clean")
3443
3444
# we do not do any checking on the argument here, trying to be liberal
3445
# about what are valid inputs
3446
try:
3447
self.git.super_silent.checkout(release, detach=True)
3448
except GitError as e:
3449
try:
3450
self.git.super_silent.fetch(self.git._repository_anonymous, release)
3451
except GitError as e:
3452
self._UI.error('"{0}" does not exist locally or on the remote server.'.format(release))
3453
raise OperationCancelledError("no such tag/branch/...")
3454
self.git.super_silent.checkout('FETCH_HEAD', detach=True)
3455
3456
def diff(self, base='commit'):
3457
r"""
3458
Show how the current file system differs from ``base``.
3459
3460
INPUT:
3461
3462
- ``base`` -- a string; show the differences against the latest
3463
``'commit'`` (the default), against the branch ``'master'`` (or any
3464
other branch name), or the merge of the ``'dependencies'`` of the
3465
current ticket (if the dependencies merge cleanly)
3466
3467
.. SEEALSO::
3468
3469
- :meth:`commit` -- record changes into the repository.
3470
3471
- :meth:`tickets` -- list local tickets (you may
3472
want to commit your changes to a branch other than the
3473
current one).
3474
3475
TESTS:
3476
3477
Create a doctest setup with a single user::
3478
3479
sage: from sage.dev.test.sagedev import single_user_setup
3480
sage: dev, config, UI, server = single_user_setup()
3481
3482
Create some tickets and make one depend on the others::
3483
3484
sage: UI.append("Summary: summary\ndescription")
3485
sage: dev.create_ticket()
3486
Created ticket #1 at https://trac.sagemath.org/1.
3487
<BLANKLINE>
3488
# (use "sage --dev checkout --ticket=1" to create a new local branch)
3489
1
3490
sage: dev.checkout(ticket=1)
3491
On ticket #1 with associated local branch "ticket/1".
3492
<BLANKLINE>
3493
# Use "sage --dev merge" to include another ticket/branch.
3494
# Use "sage --dev commit" to save changes into a new commit.
3495
sage: UI.append("y")
3496
sage: dev.push()
3497
The branch "u/doctest/ticket/1" does not exist on the remote server.
3498
Create new remote branch? [Yes/no] y
3499
sage: UI.append("Summary: summary\ndescription")
3500
sage: dev.create_ticket()
3501
Created ticket #2 at https://trac.sagemath.org/2.
3502
<BLANKLINE>
3503
# (use "sage --dev checkout --ticket=2" to create a new local branch)
3504
2
3505
sage: dev.checkout(ticket=2)
3506
On ticket #2 with associated local branch "ticket/2".
3507
<BLANKLINE>
3508
# Use "sage --dev merge" to include another ticket/branch.
3509
# Use "sage --dev commit" to save changes into a new commit.
3510
sage: UI.append("y")
3511
sage: dev.push()
3512
The branch "u/doctest/ticket/2" does not exist on the remote server.
3513
Create new remote branch? [Yes/no] y
3514
sage: UI.append("Summary: summary\ndescription")
3515
sage: dev.create_ticket()
3516
Created ticket #3 at https://trac.sagemath.org/3.
3517
<BLANKLINE>
3518
# (use "sage --dev checkout --ticket=3" to create a new local branch)
3519
3
3520
sage: dev.checkout(ticket=3)
3521
On ticket #3 with associated local branch "ticket/3".
3522
<BLANKLINE>
3523
# Use "sage --dev merge" to include another ticket/branch.
3524
# Use "sage --dev commit" to save changes into a new commit.
3525
sage: UI.append("y")
3526
sage: dev.push()
3527
The branch "u/doctest/ticket/3" does not exist on the remote server.
3528
Create new remote branch? [Yes/no] y
3529
sage: dev.merge("#1")
3530
Merging the remote branch "u/doctest/ticket/1" into the local branch "ticket/3".
3531
Automatic merge successful.
3532
<BLANKLINE>
3533
# (use "sage --dev commit" to commit your merge)
3534
<BLANKLINE>
3535
Added dependency on #1 to #3.
3536
sage: dev.merge("#2")
3537
Merging the remote branch "u/doctest/ticket/2" into the local branch "ticket/3".
3538
Automatic merge successful.
3539
<BLANKLINE>
3540
# (use "sage --dev commit" to commit your merge)
3541
<BLANKLINE>
3542
Added dependency on #2 to #3.
3543
3544
Make some non-conflicting changes on the tickets::
3545
3546
sage: dev.checkout(ticket="#1")
3547
On ticket #1 with associated local branch "ticket/1".
3548
<BLANKLINE>
3549
# Use "sage --dev merge" to include another ticket/branch.
3550
# Use "sage --dev commit" to save changes into a new commit.
3551
sage: with open("ticket1","w") as f: f.write("ticket1")
3552
sage: dev.git.silent.add("ticket1")
3553
sage: dev.git.super_silent.commit(message="added ticket1")
3554
3555
sage: dev.checkout(ticket="#2")
3556
On ticket #2 with associated local branch "ticket/2".
3557
<BLANKLINE>
3558
# Use "sage --dev merge" to include another ticket/branch.
3559
# Use "sage --dev commit" to save changes into a new commit.
3560
sage: with open("ticket2","w") as f: f.write("ticket2")
3561
sage: dev.git.silent.add("ticket2")
3562
sage: dev.git.super_silent.commit(message="added ticket2")
3563
sage: UI.append("y")
3564
sage: dev.push()
3565
Local commits that are not on the remote branch "u/doctest/ticket/2":
3566
<BLANKLINE>
3567
...: added ticket2
3568
<BLANKLINE>
3569
Push to remote branch? [Yes/no] y
3570
3571
sage: dev.checkout(ticket="#3")
3572
On ticket #3 with associated local branch "ticket/3".
3573
<BLANKLINE>
3574
# Use "sage --dev merge" to include another ticket/branch.
3575
# Use "sage --dev commit" to save changes into a new commit.
3576
sage: open("ticket3","w").close()
3577
sage: dev.git.silent.add("ticket3")
3578
sage: dev.git.super_silent.commit(message="added ticket3")
3579
sage: UI.append("y")
3580
sage: dev.push()
3581
Local commits that are not on the remote branch "u/doctest/ticket/3":
3582
<BLANKLINE>
3583
...: added ticket3
3584
<BLANKLINE>
3585
Push to remote branch? [Yes/no] y
3586
Uploading your dependencies for ticket #3: "" => "#1, #2"
3587
3588
A diff against the previous commit::
3589
3590
sage: dev.diff()
3591
3592
A diff against a ticket will always take the branch on trac::
3593
3594
sage: dev.diff("#1")
3595
diff --git a/ticket3 b/ticket3
3596
new file mode ...
3597
index ...
3598
sage: dev.diff("ticket/1")
3599
diff --git a/ticket1 b/ticket1
3600
deleted file mode ...
3601
index ...
3602
diff --git a/ticket3 b/ticket3
3603
new file mode ...
3604
index ...
3605
sage: dev.checkout(ticket="#1")
3606
On ticket #1 with associated local branch "ticket/1".
3607
<BLANKLINE>
3608
# Use "sage --dev merge" to include another ticket/branch.
3609
# Use "sage --dev commit" to save changes into a new commit.
3610
sage: UI.append("y")
3611
sage: dev.push()
3612
Local commits that are not on the remote branch "u/doctest/ticket/1":
3613
<BLANKLINE>
3614
...: added ticket1
3615
<BLANKLINE>
3616
Push to remote branch? [Yes/no] y
3617
sage: dev.checkout(ticket="#3")
3618
On ticket #3 with associated local branch "ticket/3".
3619
<BLANKLINE>
3620
# Use "sage --dev merge" to include another ticket/branch.
3621
# Use "sage --dev commit" to save changes into a new commit.
3622
sage: dev.diff("#1")
3623
diff --git a/ticket1 b/ticket1
3624
deleted file mode ...
3625
index ...
3626
diff --git a/ticket3 b/ticket3
3627
new file mode ...
3628
index ...
3629
3630
A diff against the dependencies::
3631
3632
sage: dev.diff("dependencies")
3633
Dependency #1 has not been merged into "ticket/3" (at least not its latest
3634
version).
3635
# (use "sage --dev merge --ticket=1" to merge it)
3636
<BLANKLINE>
3637
Dependency #2 has not been merged into "ticket/3" (at least not its latest
3638
version).
3639
# (use "sage --dev merge --ticket=2" to merge it)
3640
<BLANKLINE>
3641
diff --git a/ticket1 b/ticket1
3642
deleted file mode ...
3643
index ...
3644
diff --git a/ticket2 b/ticket2
3645
deleted file mode ...
3646
index ...
3647
diff --git a/ticket3 b/ticket3
3648
new file mode ...
3649
index ...
3650
sage: dev.merge("#1")
3651
Merging the remote branch "u/doctest/ticket/1" into the local branch "ticket/3".
3652
Automatic merge successful.
3653
<BLANKLINE>
3654
# (use "sage --dev commit" to commit your merge)
3655
sage: dev.merge("#2")
3656
Merging the remote branch "u/doctest/ticket/2" into the local branch "ticket/3".
3657
Automatic merge successful.
3658
<BLANKLINE>
3659
# (use "sage --dev commit" to commit your merge)
3660
sage: dev.diff("dependencies")
3661
diff --git a/ticket3 b/ticket3
3662
new file mode ...
3663
index ...
3664
3665
This does not work if the dependencies do not merge::
3666
3667
sage: dev.checkout(ticket="#1")
3668
On ticket #1 with associated local branch "ticket/1".
3669
<BLANKLINE>
3670
# Use "sage --dev merge" to include another ticket/branch.
3671
# Use "sage --dev commit" to save changes into a new commit.
3672
sage: with open("ticket2","w") as f: f.write("foo")
3673
sage: dev.git.silent.add("ticket2")
3674
sage: dev.git.super_silent.commit(message="added ticket2")
3675
sage: UI.append("y")
3676
sage: dev.push()
3677
Local commits that are not on the remote branch "u/doctest/ticket/1":
3678
<BLANKLINE>
3679
...: added ticket2
3680
<BLANKLINE>
3681
Push to remote branch? [Yes/no] y
3682
3683
sage: dev.checkout(ticket="#3")
3684
On ticket #3 with associated local branch "ticket/3".
3685
<BLANKLINE>
3686
# Use "sage --dev merge" to include another ticket/branch.
3687
# Use "sage --dev commit" to save changes into a new commit.
3688
sage: dev.diff("dependencies")
3689
Dependency #1 has not been merged into "ticket/3" (at least not its latest
3690
version).
3691
# (use "sage --dev merge --ticket=1" to merge it)
3692
<BLANKLINE>
3693
Dependency #2 does not merge cleanly with the other dependencies. Your diff
3694
could not be computed.
3695
"""
3696
if base == "dependencies":
3697
current_ticket = self._current_ticket()
3698
if current_ticket is None:
3699
raise SageDevValueError("'dependencies' are only supported if currently on a ticket.")
3700
try:
3701
self.reset_to_clean_state()
3702
self.clean()
3703
except OperationCancelledError:
3704
self._UI.error("Cannot create merge of dependencies because working directory is not clean.")
3705
raise
3706
3707
self._is_master_uptodate(action_if_not="warning")
3708
3709
branch = self.git.current_branch()
3710
merge_base = self.git.merge_base(branch, MASTER_BRANCH).splitlines()[0]
3711
temporary_branch = self._new_local_branch_for_trash("diff")
3712
self.git.super_silent.branch(temporary_branch, merge_base)
3713
try:
3714
self.git.super_silent.checkout(temporary_branch)
3715
try:
3716
self._UI.debug("Merging dependencies of #{0}.".format(current_ticket))
3717
for dependency in self._dependencies_for_ticket(current_ticket):
3718
self._check_ticket_name(dependency, exists=True)
3719
remote_branch = self.trac._branch_for_ticket(dependency)
3720
if remote_branch is None:
3721
self._UI.warning("Dependency #{0} has no branch field set.".format(dependency))
3722
self._check_remote_branch_name(remote_branch, exists=True)
3723
self.git.super_silent.fetch(self.git._repository_anonymous, remote_branch)
3724
merge_base_dependency = self.git.merge_base(MASTER_BRANCH, 'FETCH_HEAD').splitlines()[0]
3725
if merge_base_dependency != merge_base and \
3726
self.git.is_child_of(merge_base_dependency, merge_base):
3727
self._UI.warning('The remote branch "{0}" is based on a later version of sage'
3728
' compared to the local branch "{1}". The diff might therefore'
3729
' contain unrelated changes.')
3730
self._UI.info(['Use "{2}" to merge latest version of Sage into your branch.', ''],
3731
remote_branch, branch, self._format_command("merge"))
3732
if self.git.is_child_of(merge_base, 'FETCH_HEAD'):
3733
self._UI.debug('Dependency #{0} has already been merged into the master'
3734
' branch of your version of sage.', dependency)
3735
else:
3736
if not self.git.is_child_of(branch, 'FETCH_HEAD'):
3737
self._UI.warning('Dependency #{0} has not been merged into "{1}" (at'
3738
' least not its latest version).', dependency, branch)
3739
self._UI.info(['(use "{0}" to merge it)', ''],
3740
self._format_command("merge", ticket_or_branch=str(dependency)))
3741
from git_error import GitError
3742
try:
3743
self.git.super_silent.merge('FETCH_HEAD')
3744
except GitError as e:
3745
self._UI.error("Dependency #{0} does not merge cleanly with the other"
3746
" dependencies. Your diff could not be computed.", dependency)
3747
raise OperationCancelledError("merge failed")
3748
3749
self.git.echo.diff("{0}..{1}".format(temporary_branch, branch))
3750
return
3751
finally:
3752
self.git.reset_to_clean_state()
3753
self.git.clean_wrapper()
3754
self.git.super_silent.checkout(branch)
3755
finally:
3756
self.git.super_silent.branch("-D", temporary_branch)
3757
3758
if base == "commit":
3759
base = "HEAD"
3760
else:
3761
if self._is_ticket_name(base):
3762
ticket = self._ticket_from_ticket_name(base)
3763
self._check_ticket_name(ticket, exists=True)
3764
base = self.trac._branch_for_ticket(ticket)
3765
if base is None:
3766
self._UI.error("Ticket #{0} has no branch set on trac.".format(ticket))
3767
3768
if self._is_local_branch_name(base, exists=True):
3769
pass
3770
else:
3771
self._check_remote_branch_name(base, exists=True)
3772
self._is_master_uptodate(action_if_not="warning")
3773
self.git.super_silent.fetch(self.git._repository_anonymous, base)
3774
base = 'FETCH_HEAD'
3775
3776
self.git.echo.diff(base)
3777
3778
def show_dependencies(self, ticket=None, all=False, _seen=None): # all = recursive
3779
r"""
3780
Show the dependencies of ``ticket``.
3781
3782
INPUT:
3783
3784
- ``ticket`` -- a string or integer identifying a ticket or ``None``
3785
(default: ``None``), the ticket for which dependencies are displayed.
3786
If ``None``, then the dependencies for the current ticket are
3787
displayed.
3788
3789
- ``all`` -- boolean (default: ``True``), whether to recursively list
3790
all tickets on which this ticket depends (in depth-first order), only
3791
including tickets that have a local branch.
3792
3793
.. NOTE::
3794
3795
Ticket dependencies are stored locally and only updated with
3796
respect to the remote server during :meth:`push` and
3797
:meth:`pull`.
3798
3799
.. SEEALSO::
3800
3801
- :meth:`TracInterface.dependencies` -- Query Trac to find
3802
dependencies.
3803
3804
- :meth:`remote_status` -- will show the status of tickets
3805
with respect to the remote server.
3806
3807
- :meth:`merge` -- Merge in changes from a dependency.
3808
3809
- :meth:`diff` -- Show the changes in this branch over the
3810
dependencies.
3811
3812
TESTS:
3813
3814
Create a doctest setup with a single user::
3815
3816
sage: from sage.dev.test.sagedev import single_user_setup
3817
sage: dev, config, UI, server = single_user_setup()
3818
3819
Create some tickets and add dependencies::
3820
3821
sage: UI.append("Summary: summary\ndescription")
3822
sage: dev.create_ticket()
3823
Created ticket #1 at https://trac.sagemath.org/1.
3824
<BLANKLINE>
3825
# (use "sage --dev checkout --ticket=1" to create a new local branch)
3826
1
3827
sage: dev.checkout(ticket=1)
3828
On ticket #1 with associated local branch "ticket/1".
3829
<BLANKLINE>
3830
# Use "sage --dev merge" to include another ticket/branch.
3831
# Use "sage --dev commit" to save changes into a new commit.
3832
sage: UI.append("Summary: summary\ndescription")
3833
sage: dev.create_ticket()
3834
Created ticket #2 at https://trac.sagemath.org/2.
3835
<BLANKLINE>
3836
# (use "sage --dev checkout --ticket=2" to create a new local branch)
3837
2
3838
sage: dev.checkout(ticket=2)
3839
On ticket #2 with associated local branch "ticket/2".
3840
<BLANKLINE>
3841
# Use "sage --dev merge" to include another ticket/branch.
3842
# Use "sage --dev commit" to save changes into a new commit.
3843
sage: UI.append("Summary: summary\ndescription")
3844
sage: dev.create_ticket()
3845
Created ticket #3 at https://trac.sagemath.org/3.
3846
<BLANKLINE>
3847
# (use "sage --dev checkout --ticket=3" to create a new local branch)
3848
3
3849
sage: dev.checkout(ticket=3)
3850
On ticket #3 with associated local branch "ticket/3".
3851
<BLANKLINE>
3852
# Use "sage --dev merge" to include another ticket/branch.
3853
# Use "sage --dev commit" to save changes into a new commit.
3854
sage: UI.append("Summary: summary\ndescription")
3855
sage: dev.create_ticket()
3856
Created ticket #4 at https://trac.sagemath.org/4.
3857
<BLANKLINE>
3858
# (use "sage --dev checkout --ticket=4" to create a new local branch)
3859
4
3860
sage: dev.checkout(ticket=4)
3861
On ticket #4 with associated local branch "ticket/4".
3862
<BLANKLINE>
3863
# Use "sage --dev merge" to include another ticket/branch.
3864
# Use "sage --dev commit" to save changes into a new commit.
3865
3866
sage: dev.merge('ticket/2',create_dependency=True)
3867
Merging the local branch "ticket/2" into the local branch "ticket/4".
3868
Automatic merge successful.
3869
<BLANKLINE>
3870
# (use "sage --dev commit" to commit your merge)
3871
<BLANKLINE>
3872
Added dependency on #2 to #4.
3873
sage: dev.merge('ticket/3',create_dependency=True)
3874
Merging the local branch "ticket/3" into the local branch "ticket/4".
3875
Automatic merge successful.
3876
<BLANKLINE>
3877
# (use "sage --dev commit" to commit your merge)
3878
<BLANKLINE>
3879
Added dependency on #3 to #4.
3880
sage: dev.checkout(ticket='#2')
3881
On ticket #2 with associated local branch "ticket/2".
3882
<BLANKLINE>
3883
# Use "sage --dev merge" to include another ticket/branch.
3884
# Use "sage --dev commit" to save changes into a new commit.
3885
sage: dev.merge('ticket/1', create_dependency=True)
3886
Merging the local branch "ticket/1" into the local branch "ticket/2".
3887
Automatic merge successful.
3888
<BLANKLINE>
3889
# (use "sage --dev commit" to commit your merge)
3890
<BLANKLINE>
3891
Added dependency on #1 to #2.
3892
sage: dev.checkout(ticket='#3')
3893
On ticket #3 with associated local branch "ticket/3".
3894
<BLANKLINE>
3895
# Use "sage --dev merge" to include another ticket/branch.
3896
# Use "sage --dev commit" to save changes into a new commit.
3897
sage: dev.merge('ticket/1', create_dependency=True)
3898
Merging the local branch "ticket/1" into the local branch "ticket/3".
3899
Automatic merge successful.
3900
<BLANKLINE>
3901
# (use "sage --dev commit" to commit your merge)
3902
<BLANKLINE>
3903
Added dependency on #1 to #3.
3904
3905
Check that the dependencies show correctly::
3906
3907
sage: dev.checkout(ticket='#4')
3908
On ticket #4 with associated local branch "ticket/4".
3909
<BLANKLINE>
3910
# Use "sage --dev merge" to include another ticket/branch.
3911
# Use "sage --dev commit" to save changes into a new commit.
3912
sage: dev.show_dependencies()
3913
Ticket #4 depends on #2, #3.
3914
sage: dev.show_dependencies('#4')
3915
Ticket #4 depends on #2, #3.
3916
sage: dev.show_dependencies('#3')
3917
Ticket #3 depends on #1.
3918
sage: dev.show_dependencies('#2')
3919
Ticket #2 depends on #1.
3920
sage: dev.show_dependencies('#1')
3921
Ticket #1 has no dependencies.
3922
sage: dev.show_dependencies('#4', all=True)
3923
Ticket #4 depends on #3, #1, #2.
3924
"""
3925
if ticket is None:
3926
ticket = self._current_ticket()
3927
3928
if ticket is None:
3929
raise SageDevValueError("ticket must be specified")
3930
3931
self._check_ticket_name(ticket)
3932
ticket = self._ticket_from_ticket_name(ticket)
3933
3934
if not self._has_local_branch_for_ticket(ticket):
3935
raise SageDevValueError('ticket must be a ticket with a local branch. Use "{0}" to checkout the ticket first.'.format(self._format_command("checkout",ticket=ticket)))
3936
3937
branch = self._local_branch_for_ticket(ticket)
3938
if all:
3939
ret = []
3940
stack = [ticket]
3941
while stack:
3942
t = stack.pop()
3943
if t in ret: continue
3944
ret.append(t)
3945
if not self._has_local_branch_for_ticket(t):
3946
self._UI.warning("no local branch for ticket #{0} present, some dependencies might be missing in the output.".format(t))
3947
continue
3948
deps = self._dependencies_for_ticket(t)
3949
for d in deps:
3950
if d not in stack and d not in ret:
3951
stack.append(d)
3952
ret = ret[1:]
3953
else:
3954
ret = self._dependencies_for_ticket(ticket)
3955
3956
if ret:
3957
self._UI.show("Ticket #{0} depends on {1}.".format(ticket,", ".join(["#{0}".format(d) for d in ret])))
3958
else:
3959
self._UI.show("Ticket #{0} has no dependencies.".format(ticket))
3960
3961
def upload_ssh_key(self, public_key=None):
3962
r"""
3963
Upload ``public_key`` to gitolite through the trac interface.
3964
3965
INPUT:
3966
3967
- ``public_key`` -- a string or ``None`` (default: ``None``), the path
3968
of the key file, defaults to ``~/.ssh/id_rsa.pub`` (or
3969
``~/.ssh/id_dsa.pub`` if it exists).
3970
3971
TESTS:
3972
3973
Create a doctest setup with a single user::
3974
3975
sage: from sage.dev.test.sagedev import single_user_setup
3976
sage: dev, config, UI, server = single_user_setup()
3977
3978
Create and upload a key file::
3979
3980
sage: import os
3981
sage: public_key = os.path.join(dev._sagedev.tmp_dir, "id_rsa.pub")
3982
sage: UI.append("no")
3983
sage: UI.append("yes")
3984
sage: dev.upload_ssh_key(public_key=public_key)
3985
The trac git server requires your SSH public key to be able to identify you.
3986
Upload ".../id_rsa.pub" to trac? [Yes/no] yes
3987
File not found: ".../id_rsa.pub"
3988
Create new ssh key pair? [Yes/no] no
3989
<BLANKLINE>
3990
# Use "sage --dev upload-ssh-key" to upload a public key. Or set your key manually at https://trac.sagemath.org/prefs/sshkeys.
3991
sage: UI.append("yes")
3992
sage: UI.append("yes")
3993
sage: dev.upload_ssh_key(public_key=public_key)
3994
The trac git server requires your SSH public key to be able to identify you.
3995
Upload ".../id_rsa.pub" to trac? [Yes/no] yes
3996
File not found: ".../id_rsa.pub"
3997
Create new ssh key pair? [Yes/no] yes
3998
Generating ssh key.
3999
Your key has been uploaded.
4000
sage: UI.append("yes")
4001
sage: dev.upload_ssh_key(public_key=public_key)
4002
The trac git server requires your SSH public key to be able to identify you.
4003
Upload ".../id_rsa.pub" to trac? [Yes/no] yes
4004
Your key has been uploaded.
4005
"""
4006
try:
4007
import os
4008
if public_key is None:
4009
public_key = os.path.expanduser("~/.ssh/id_dsa.pub")
4010
if not os.path.exists(public_key):
4011
public_key = os.path.expanduser("~/.ssh/id_rsa.pub")
4012
if not public_key.endswith(".pub"):
4013
raise SageDevValueError('public key must end with ".pub".')
4014
4015
self._UI.show('The trac git server requires your SSH public key'
4016
' to be able to identify you.')
4017
if not self._UI.confirm('Upload "{0}" to trac?'
4018
.format(public_key), default=True):
4019
raise OperationCancelledError("do not upload key")
4020
4021
if not os.path.exists(public_key):
4022
self._UI.warning('File not found: "{0}"'.format(public_key))
4023
if not self._UI.confirm('Create new ssh key pair?', default=True):
4024
raise OperationCancelledError("no keyfile found")
4025
private_key = public_key[:-4]
4026
self._UI.show("Generating ssh key.")
4027
from subprocess import call
4028
success = call(['sage-native-execute', 'ssh-keygen', '-q',
4029
'-f', private_key, '-P', '', '-t', 'rsa'])
4030
if success == 0:
4031
self._UI.debug("Key generated.")
4032
else:
4033
self._UI.error(["Key generation failed.",
4034
'Please create a key in "{0}" and retry.'.format(public_key)])
4035
raise OperationCancelledError("ssh-keygen failed")
4036
4037
with open(public_key, 'r') as F:
4038
public_key = F.read().strip()
4039
4040
self.trac._authenticated_server_proxy.sshkeys.addkey(public_key)
4041
self._UI.show("Your key has been uploaded.")
4042
except OperationCancelledError:
4043
server = self.config.get('server', TRAC_SERVER_URI)
4044
4045
url = urlparse.urljoin(server, urllib.pathname2url(os.path.join('prefs', 'sshkeys')))
4046
self._UI.info(['',
4047
'Use "{0}" to upload a public key. Or set your key manually at {1}.'
4048
.format(self._format_command("upload_ssh_key"), url)])
4049
raise
4050
4051
def _upload_ssh_key(self):
4052
r"""
4053
Make sure that the public ssh key has been uploaded to the trac server.
4054
4055
.. NOTE::
4056
4057
This is a wrapper for :meth:`upload_ssh_key` which is only called
4058
one the user's first attempt to push to the repository, i.e., on
4059
the first attempt to acces ``SAGE_REPO_AUTHENTICATED``.
4060
4061
TESTS:
4062
4063
Create a doctest setup with a single user::
4064
4065
sage: from sage.dev.test.sagedev import single_user_setup
4066
sage: dev, config, UI, server = single_user_setup()
4067
sage: del dev._sagedev.config['git']['ssh_key_set']
4068
4069
We need to patch :meth:`upload_ssh_key` to get testable results since
4070
it depends on whether the user has an ssh key in ``.ssh/id_rsa.pub``::
4071
4072
sage: from sage.dev.user_interface_error import OperationCancelledError
4073
sage: def upload_ssh_key():
4074
....: print "Uploading ssh key."
4075
....: raise OperationCancelledError("")
4076
sage: dev._sagedev.upload_ssh_key = upload_ssh_key
4077
4078
The ssh key is only uploaded once::
4079
4080
sage: dev._sagedev._upload_ssh_key()
4081
Uploading ssh key.
4082
sage: dev._sagedev._upload_ssh_key()
4083
"""
4084
if self.config['git'].get('ssh_key_set', False):
4085
return
4086
4087
from user_interface_error import OperationCancelledError
4088
try:
4089
self.upload_ssh_key()
4090
except OperationCancelledError:
4091
pass # do not bother the user again, probably the key has been uploaded manually already
4092
self.config['git']['ssh_key_set'] = "True"
4093
4094
def _is_master_uptodate(self, action_if_not=None):
4095
r"""
4096
Check whether the master branch is up to date with respect to the
4097
remote master branch.
4098
4099
INPUT:
4100
4101
- ``action_if_not`` -- one of ``'error'``, ``'warning'``, or ``None``
4102
(default: ``None``), the action to perform if master is not up to
4103
date. If ``'error'``, then this raises a ``SageDevValueError``,
4104
otherwise return a boolean and print a warning if ``'warning'``.
4105
4106
.. NOTE::
4107
4108
In the transitional period from hg to git, this is a nop. This will
4109
change as soon as ``master`` is our actual master branch.
4110
4111
TESTS:
4112
4113
Create a doctest setup with a single user::
4114
4115
sage: from sage.dev.test.sagedev import single_user_setup
4116
sage: dev, config, UI, server = single_user_setup()
4117
sage: dev._wrap("_is_master_uptodate")
4118
4119
Initially ``master`` is up to date::
4120
4121
sage: dev._is_master_uptodate()
4122
True
4123
4124
When the remote ``master`` branches changes, this is not the case
4125
anymore::
4126
4127
sage: server.git.super_silent.commit(allow_empty=True, message="a commit")
4128
sage: dev._is_master_uptodate()
4129
False
4130
sage: dev._is_master_uptodate(action_if_not="warning")
4131
Your version of sage, i.e., your "master" branch, is out of date. Your command might fail or produce unexpected results.
4132
False
4133
sage: dev._is_master_uptodate(action_if_not="error")
4134
Your version of sage, i.e., your "master" branch, is out of date.
4135
4136
We upgrade the local master::
4137
4138
sage: dev.pull(ticket_or_remote_branch="master")
4139
Merging the remote branch "master" into the local branch "master".
4140
Automatic merge successful.
4141
<BLANKLINE>
4142
# (use "sage --dev commit" to commit your merge)
4143
sage: dev._is_master_uptodate()
4144
True
4145
sage: dev._is_master_uptodate(action_if_not="warning")
4146
True
4147
sage: dev._is_master_uptodate(action_if_not="error")
4148
True
4149
"""
4150
remote_master = self._remote_branch_for_branch(MASTER_BRANCH)
4151
if remote_master is not None:
4152
self.git.fetch(self.git._repository_anonymous, remote_master)
4153
# In the transition from hg to git we are using
4154
# public/sage-git/master instead of master on the remote end.
4155
# This check makes sure that we are not printing any confusing
4156
# messages unless master is actually the latest (development)
4157
# version of sage.
4158
if self.git.is_child_of('FETCH_HEAD', MASTER_BRANCH):
4159
if self.git.commit_for_ref('FETCH_HEAD') != self.git.commit_for_branch(MASTER_BRANCH):
4160
msg = ('To upgrade your "{0}" branch to the latest version, use "{1}".',
4161
MASTER_BRANCH, self._format_command("pull", ticket_or_branch=remote_master,
4162
branch=MASTER_BRANCH))
4163
if action_if_not is None:
4164
pass
4165
elif action_if_not == "error":
4166
self._UI.debug(*msg)
4167
raise SageDevValueError('Your version of sage, i.e., your "{0}" branch, is out'
4168
' of date.', MASTER_BRANCH)
4169
elif action_if_not == "warning":
4170
self._UI.warning('Your version of sage, i.e., your "{0}" branch, is out of date.'
4171
' Your command might fail or produce unexpected results.',
4172
MASTER_BRANCH)
4173
self._UI.debug(*msg)
4174
else:
4175
raise ValueError
4176
return False
4177
4178
return True
4179
4180
def _is_ticket_name(self, name, exists=False):
4181
r"""
4182
Return whether ``name`` is a valid ticket name, i.e., an integer.
4183
4184
INPUT:
4185
4186
- ``name`` -- a string or an int
4187
4188
- ``exists`` -- a boolean (default: ``False``), if ``True``, return
4189
whether ``name`` is the name of an existing ticket
4190
4191
EXAMPLES::
4192
4193
sage: dev = dev._sagedev
4194
sage: dev._is_ticket_name(1000)
4195
True
4196
sage: dev._is_ticket_name("1000")
4197
True
4198
sage: dev._is_ticket_name("1 000")
4199
False
4200
sage: dev._is_ticket_name("#1000")
4201
True
4202
sage: dev._is_ticket_name("master")
4203
False
4204
sage: dev._is_ticket_name(1000, exists=True) # optional: internet
4205
True
4206
sage: dev._is_ticket_name(2^30, exists=True) # optional: internet
4207
False
4208
sage: dev._is_ticket_name('')
4209
False
4210
"""
4211
if name is None:
4212
return False
4213
if not isinstance(name, int):
4214
try:
4215
name = self._ticket_from_ticket_name(name)
4216
except SageDevValueError:
4217
return False
4218
if exists:
4219
try:
4220
self.trac._anonymous_server_proxy.ticket.get(name)
4221
except TracInternalError as e:
4222
if e.faultCode == 404: # ticket does not exist
4223
return False
4224
raise
4225
except TracConnectionError as e:
4226
# if we cannot connect to trac, we assume that the ticket
4227
# exists; this makes more of the dev scripts usable in offline
4228
# scenarios
4229
pass
4230
return True
4231
4232
def _check_ticket_name(self, name, exists=False):
4233
r"""
4234
Check that ``name`` is a valid ticket name.
4235
4236
INPUT:
4237
4238
- ``name`` -- a string or int
4239
4240
- ``exists`` -- a boolean (default: ``False``), whether to check that
4241
the ticket exists on trac
4242
4243
TESTS::
4244
4245
sage: dev = dev._sagedev
4246
sage: dev._check_ticket_name(1000)
4247
sage: dev._check_ticket_name("1000")
4248
sage: dev._check_ticket_name("1 000")
4249
Traceback (most recent call last):
4250
...
4251
SageDevValueError: Invalid ticket name "1 000".
4252
sage: dev._check_ticket_name("#1000")
4253
sage: dev._check_ticket_name("master")
4254
Traceback (most recent call last):
4255
...
4256
SageDevValueError: Invalid ticket name "master".
4257
sage: dev._check_ticket_name(1000, exists=True) # optional: internet
4258
sage: dev._check_ticket_name(2^30, exists=True) # optional: internet
4259
Traceback (most recent call last):
4260
...
4261
SageDevValueError: Ticket name "1073741824" is not valid or ticket does not exist on trac.
4262
"""
4263
if not self._is_ticket_name(name, exists=exists):
4264
if exists:
4265
raise SageDevValueError('Ticket name "{0}" is not valid or ticket'
4266
' does not exist on trac.', name)
4267
else:
4268
raise SageDevValueError('Invalid ticket name "{0}".', name)
4269
4270
def _ticket_from_ticket_name(self, name):
4271
r"""
4272
Return the ticket number for the ticket ``name``.
4273
4274
EXAMPLES::
4275
4276
sage: from sage.dev.test.sagedev import single_user_setup
4277
sage: dev, config, UI, server = single_user_setup()
4278
sage: dev = dev._sagedev
4279
4280
sage: dev._ticket_from_ticket_name("1000")
4281
1000
4282
sage: dev._ticket_from_ticket_name("#1000")
4283
1000
4284
sage: dev._ticket_from_ticket_name(1000)
4285
1000
4286
sage: dev._ticket_from_ticket_name(int(1000))
4287
1000
4288
sage: dev._ticket_from_ticket_name("1 000")
4289
Traceback (most recent call last):
4290
...
4291
SageDevValueError: "1 000" is not a valid ticket name.
4292
"""
4293
ticket = name
4294
if not isinstance(ticket, int):
4295
if isinstance(ticket, str) and ticket and ticket[0] == "#":
4296
ticket = ticket[1:]
4297
try:
4298
ticket = int(ticket)
4299
except ValueError:
4300
raise SageDevValueError('"{0}" is not a valid ticket name.'.format(name))
4301
4302
if ticket < 0:
4303
raise SageDevValueError('"{0}" is not a valid ticket name.'.format(name))
4304
4305
return ticket
4306
4307
def _is_local_branch_name(self, name, exists=any):
4308
r"""
4309
Return whether ``name`` is a valid name for a local branch.
4310
4311
INPUT:
4312
4313
- ``name`` -- a string
4314
4315
- ``exists`` -- a boolean or ``any`` (default: ``any``), if ``True``,
4316
check whether ``name`` is the name of an existing local branch; if
4317
``False``, check whether ``name`` is the name of a branch that does
4318
not exist yet.
4319
4320
TESTS::
4321
4322
sage: from sage.dev.test.sagedev import single_user_setup
4323
sage: dev, config, UI, server = single_user_setup()
4324
sage: dev = dev._sagedev
4325
4326
sage: dev._is_local_branch_name('')
4327
False
4328
sage: dev._is_local_branch_name('ticket/1')
4329
True
4330
sage: dev._is_local_branch_name('ticket/1', exists=True)
4331
False
4332
sage: dev._is_local_branch_name('ticket/1', exists=False)
4333
True
4334
sage: dev.git.silent.branch('ticket/1')
4335
sage: dev._is_local_branch_name('ticket/1', exists=True)
4336
True
4337
sage: dev._is_local_branch_name('ticket/1', exists=False)
4338
False
4339
"""
4340
if not isinstance(name, str):
4341
raise ValueError("name must be a string")
4342
4343
if not GIT_BRANCH_REGEX.match(name):
4344
return False
4345
# branches which could be tickets are calling for trouble - cowardly refuse to accept them
4346
if self._is_ticket_name(name):
4347
return False
4348
if name in ["None", "True", "False", "dependencies"]:
4349
return False
4350
4351
if exists == True:
4352
return self.git.commit_for_branch(name) is not None
4353
elif exists == False:
4354
return self.git.commit_for_branch(name) is None
4355
elif exists is any:
4356
return True
4357
else:
4358
raise ValueError("exists")
4359
4360
def _is_trash_name(self, name, exists=any):
4361
r"""
4362
Return whether ``name`` is a valid name for an abandoned branch.
4363
4364
INPUT:
4365
4366
- ``name`` -- a string
4367
4368
- ``exists`` -- a boolean or ``any`` (default: ``any``), if ``True``,
4369
check whether ``name`` is the name of an existing branch; if
4370
``False``, check whether ``name`` is the name of a branch that does
4371
not exist yet.
4372
4373
TESTS::
4374
4375
sage: from sage.dev.test.sagedev import single_user_setup
4376
sage: dev, config, UI, server = single_user_setup()
4377
sage: dev = dev._sagedev
4378
4379
sage: dev._is_trash_name("branch1")
4380
False
4381
sage: dev._is_trash_name("trash")
4382
False
4383
sage: dev._is_trash_name("trash/")
4384
False
4385
sage: dev._is_trash_name("trash/1")
4386
True
4387
sage: dev._is_trash_name("trash/1", exists=True)
4388
False
4389
"""
4390
if not isinstance(name, str):
4391
raise ValueError("name must be a string")
4392
if not name.startswith("trash/"):
4393
return False
4394
return self._is_local_branch_name(name, exists)
4395
4396
def _is_remote_branch_name(self, name, exists=any):
4397
r"""
4398
Return whether ``name`` is a valid name for a remote branch.
4399
4400
INPUT:
4401
4402
- ``name`` -- a string
4403
4404
- ``exists`` -- a boolean or ``any`` (default: ``any``), if ``True``,
4405
check whether ``name`` is the name of an existing remote branch; if
4406
``False``, check whether ``name`` is the name of a branch that does
4407
not exist yet.
4408
4409
.. NOTE::
4410
4411
Currently, this does not check whether name is in accordance with
4412
naming scheme configured on gitolite.
4413
4414
TESTS::
4415
4416
sage: from sage.dev.test.sagedev import single_user_setup
4417
sage: dev, config, UI, server = single_user_setup()
4418
sage: dev = dev._sagedev
4419
4420
sage: dev._is_remote_branch_name('')
4421
False
4422
sage: dev._is_remote_branch_name('ticket/1')
4423
True
4424
4425
sage: dev._is_remote_branch_name('ticket/1', exists=True)
4426
False
4427
sage: dev._is_remote_branch_name('ticket/1', exists=False)
4428
True
4429
"""
4430
if not isinstance(name, str):
4431
raise ValueError("name must be a string")
4432
4433
if not GIT_BRANCH_REGEX.match(name):
4434
return False
4435
# branches which could be tickets are calling for trouble - cowardly refuse to accept them
4436
if self._is_ticket_name(name):
4437
return False
4438
4439
if exists is any:
4440
return True
4441
4442
from git_error import GitError
4443
try:
4444
self.git.super_silent.ls_remote(self.git._repository_anonymous, "refs/heads/"+name, exit_code=True)
4445
remote_exists = True
4446
except GitError as e:
4447
if e.exit_code == 2:
4448
remote_exists = False
4449
else:
4450
raise
4451
4452
if exists == True or exists == False:
4453
return remote_exists == exists
4454
else:
4455
raise ValueError("exists")
4456
4457
def _check_local_branch_name(self, name, exists=any):
4458
r"""
4459
Check whether ``name`` is a valid name for a local branch, raise a
4460
``SageDevValueError`` if it is not.
4461
4462
INPUT:
4463
4464
same as for :meth:`_is_local_branch_name`
4465
4466
TESTS::
4467
4468
sage: from sage.dev.test.sagedev import single_user_setup
4469
sage: dev, config, UI, server = single_user_setup()
4470
sage: dev = dev._sagedev
4471
4472
sage: dev._check_local_branch_name('')
4473
Traceback (most recent call last):
4474
...
4475
SageDevValueError: Invalid branch name "".
4476
sage: dev._check_local_branch_name('ticket/1')
4477
sage: dev._check_local_branch_name('ticket/1', exists=True)
4478
Traceback (most recent call last):
4479
...
4480
SageDevValueError: Branch "ticket/1" does not exist locally.
4481
sage: dev._check_local_branch_name('ticket/1', exists=False)
4482
sage: dev.git.silent.branch('ticket/1')
4483
sage: dev._check_local_branch_name('ticket/1', exists=True)
4484
sage: dev._check_local_branch_name('ticket/1', exists=False)
4485
Traceback (most recent call last):
4486
...
4487
SageDevValueError: Branch "ticket/1" already exists, use a different name.
4488
"""
4489
try:
4490
if not self._is_local_branch_name(name, exists=any):
4491
raise SageDevValueError("caught below")
4492
except SageDevValueError:
4493
raise SageDevValueError('Invalid branch name "{0}".'.format(name))
4494
4495
if exists == any:
4496
return
4497
elif exists == True:
4498
if not self._is_local_branch_name(name, exists=exists):
4499
raise SageDevValueError('Branch "{0}" does not exist locally.', name).info(
4500
['', '(use "{0}" to list local branches)'], self._format_command('tickets'))
4501
elif exists == False:
4502
if not self._is_local_branch_name(name, exists=exists):
4503
raise SageDevValueError('Branch "{0}" already exists, use a different name.'.format(name))
4504
else:
4505
assert False
4506
4507
def _check_remote_branch_name(self, name, exists=any):
4508
r"""
4509
Check whether ``name`` is a valid name for a remote branch, raise a
4510
``SageDevValueError`` if it is not.
4511
4512
INPUT:
4513
4514
same as for :meth:`_is_remote_branch_name`
4515
4516
TESTS::
4517
4518
sage: from sage.dev.test.sagedev import single_user_setup
4519
sage: dev, config, UI, server = single_user_setup()
4520
sage: dev = dev._sagedev
4521
4522
sage: dev._check_remote_branch_name('')
4523
Traceback (most recent call last):
4524
...
4525
SageDevValueError: Invalid name "" for a remote branch.
4526
sage: dev._check_remote_branch_name('ticket/1')
4527
4528
sage: dev._check_remote_branch_name('ticket/1', exists=True)
4529
Traceback (most recent call last):
4530
...
4531
SageDevValueError: Branch "ticket/1" does not exist on the remote system.
4532
sage: dev._check_remote_branch_name('ticket/1', exists=False)
4533
"""
4534
try:
4535
if not self._is_remote_branch_name(name, exists=any):
4536
raise SageDevValueError("caught below")
4537
except SageDevValueError:
4538
raise SageDevValueError('Invalid name "{0}" for a remote branch.'.format(name))
4539
4540
if exists == any:
4541
return
4542
elif exists == True:
4543
if not self._is_remote_branch_name(name, exists=exists):
4544
raise SageDevValueError('Branch "{0}" does not exist on the remote system.'.format(name))
4545
elif exists == False:
4546
if not self._is_remote_branch_name(name, exists=exists):
4547
raise SageDevValueError('Branch "{0}" already exists, use a different name.'.format(name))
4548
else:
4549
assert False
4550
4551
def _remote_branch_for_ticket(self, ticket):
4552
r"""
4553
Return the name of the remote branch for ``ticket``.
4554
4555
INPUT:
4556
4557
- ``ticket`` -- an int or a string identifying a ticket
4558
4559
.. NOTE:
4560
4561
This does not take into account the ``branch`` field of the ticket
4562
on trac.
4563
4564
TESTS::
4565
4566
sage: from sage.dev.test.sagedev import single_user_setup
4567
sage: dev, config, UI, server = single_user_setup()
4568
sage: dev = dev._sagedev
4569
4570
sage: dev._remote_branch_for_ticket(1)
4571
'u/doctest/ticket/1'
4572
sage: dev._remote_branch_for_ticket("#1")
4573
'u/doctest/ticket/1'
4574
sage: dev._remote_branch_for_ticket("1")
4575
'u/doctest/ticket/1'
4576
sage: dev._remote_branch_for_ticket("master")
4577
Traceback (most recent call last):
4578
...
4579
SageDevValueError: "master" is not a valid ticket name.
4580
4581
sage: UI.append("Summary: summary1\ndescription")
4582
sage: dev.create_ticket()
4583
Created ticket #1 at https://trac.sagemath.org/1.
4584
<BLANKLINE>
4585
# (use "sage --dev checkout --ticket=1" to create a new local branch)
4586
1
4587
sage: dev.checkout(ticket=1)
4588
On ticket #1 with associated local branch "ticket/1".
4589
<BLANKLINE>
4590
# Use "sage --dev merge" to include another ticket/branch.
4591
# Use "sage --dev commit" to save changes into a new commit.
4592
4593
sage: dev._set_remote_branch_for_branch("ticket/1", "public/1")
4594
sage: dev._remote_branch_for_ticket(1)
4595
'public/1'
4596
sage: dev._set_remote_branch_for_branch("ticket/1", None)
4597
sage: dev._remote_branch_for_ticket(1)
4598
'u/doctest/ticket/1'
4599
"""
4600
ticket = self._ticket_from_ticket_name(ticket)
4601
4602
default = "u/{0}/ticket/{1}".format(self.trac._username, ticket)
4603
4604
try:
4605
branch = self._local_branch_for_ticket(ticket)
4606
except KeyError: # ticket has no branch yet
4607
return default
4608
4609
ret = self._remote_branch_for_branch(branch)
4610
if ret is None:
4611
return default
4612
return ret
4613
4614
def _ticket_for_local_branch(self, branch):
4615
r"""
4616
Return the ticket associated to the local ``branch``.
4617
4618
INPUT:
4619
4620
- ``branch`` -- a string, the name of a local branch
4621
4622
TESTS::
4623
4624
sage: from sage.dev.test.sagedev import single_user_setup
4625
sage: dev, config, UI, server = single_user_setup()
4626
sage: UI.append("Summary: summary\ndescription")
4627
sage: dev.create_ticket()
4628
Created ticket #1 at https://trac.sagemath.org/1.
4629
<BLANKLINE>
4630
# (use "sage --dev checkout --ticket=1" to create a new local branch)
4631
1
4632
sage: dev.checkout(ticket=1)
4633
On ticket #1 with associated local branch "ticket/1".
4634
<BLANKLINE>
4635
# Use "sage --dev merge" to include another ticket/branch.
4636
# Use "sage --dev commit" to save changes into a new commit.
4637
sage: dev._sagedev._ticket_for_local_branch("ticket/1")
4638
1
4639
"""
4640
self._check_local_branch_name(branch, exists=True)
4641
if not self._has_ticket_for_local_branch(branch):
4642
raise SageDevValueError("branch must be associated to a ticket")
4643
return self.__branch_to_ticket[branch]
4644
4645
def _has_ticket_for_local_branch(self, branch):
4646
r"""
4647
Return whether ``branch`` is associated to a ticket.
4648
4649
INPUT:
4650
4651
- ``branch`` -- a string, the name of a local branch
4652
4653
TESTS::
4654
4655
sage: from sage.dev.test.sagedev import single_user_setup
4656
sage: dev, config, UI, server = single_user_setup()
4657
sage: UI.append("Summary: summary\ndescription")
4658
sage: dev.create_ticket()
4659
Created ticket #1 at https://trac.sagemath.org/1.
4660
<BLANKLINE>
4661
# (use "sage --dev checkout --ticket=1" to create a new local branch)
4662
1
4663
sage: dev.checkout(ticket=1)
4664
On ticket #1 with associated local branch "ticket/1".
4665
<BLANKLINE>
4666
# Use "sage --dev merge" to include another ticket/branch.
4667
# Use "sage --dev commit" to save changes into a new commit.
4668
sage: dev._sagedev._has_ticket_for_local_branch("ticket/1")
4669
True
4670
"""
4671
self._check_local_branch_name(branch, exists=True)
4672
4673
return branch in self.__branch_to_ticket
4674
4675
def _has_local_branch_for_ticket(self, ticket):
4676
r"""
4677
Return whether there is a local branch for ``ticket``.
4678
4679
INPUT:
4680
4681
- ``ticket`` -- an int or a string identifying a ticket
4682
4683
TESTS::
4684
4685
sage: from sage.dev.test.sagedev import single_user_setup
4686
sage: dev, config, UI, server = single_user_setup()
4687
sage: dev._sagedev._has_local_branch_for_ticket(1)
4688
False
4689
"""
4690
ticket = self._ticket_from_ticket_name(ticket)
4691
if ticket not in self.__ticket_to_branch:
4692
return False
4693
4694
branch = self.__ticket_to_branch[ticket]
4695
if not self._is_local_branch_name(branch, exists=True):
4696
self._UI.warning('Ticket #{0} refers to the non-existant local branch "{1}".'
4697
' If you have not manually interacted with git, then this is'
4698
' a bug in sagedev. Removing the association from ticket #{0}'
4699
' to branch "{1}".', ticket, branch)
4700
del self.__ticket_to_branch[ticket]
4701
return False
4702
return True
4703
4704
def _local_branch_for_ticket(self, ticket, pull_if_not_found=False):
4705
r"""
4706
Return the name of the local branch for ``ticket``.
4707
4708
INPUT:
4709
4710
- ``ticket`` -- an int or a string identifying a ticket
4711
4712
- ``pull_if_not_found`` -- a boolean (default: ``False``), whether
4713
to attempt to pull a branch for ``ticket`` from trac if it does
4714
not exist locally
4715
4716
TESTS:
4717
4718
Create a doctest setup with two users::
4719
4720
sage: from sage.dev.test.sagedev import two_user_setup
4721
sage: alice, config_alice, bob, config_bob, server = two_user_setup()
4722
4723
If a local branch for the ticket exists, its name is returned::
4724
4725
sage: alice._chdir()
4726
sage: alice._UI.append("Summary: ticket1\ndescription")
4727
sage: alice.create_ticket()
4728
Created ticket #1 at https://trac.sagemath.org/1.
4729
<BLANKLINE>
4730
# (use "sage --dev checkout --ticket=1" to create a new local branch)
4731
1
4732
sage: alice.checkout(ticket=1)
4733
On ticket #1 with associated local branch "ticket/1".
4734
<BLANKLINE>
4735
# Use "sage --dev merge" to include another ticket/branch.
4736
# Use "sage --dev commit" to save changes into a new commit.
4737
sage: alice._sagedev._local_branch_for_ticket(1)
4738
'ticket/1'
4739
4740
If no local branch exists, the behaviour depends on ``pull_if_not_found``::
4741
4742
sage: bob._chdir()
4743
sage: bob._sagedev._local_branch_for_ticket(1)
4744
Traceback (most recent call last):
4745
...
4746
KeyError: 'No branch for ticket #1 in your repository.'
4747
sage: bob._sagedev._local_branch_for_ticket(1, pull_if_not_found=True)
4748
Traceback (most recent call last):
4749
...
4750
SageDevValueError: Branch field is not set for ticket #1 on trac.
4751
sage: attributes = alice.trac._get_attributes(1)
4752
sage: attributes['branch'] = 'public/ticket/1'
4753
sage: alice.trac._authenticated_server_proxy.ticket.update(1, "", attributes)
4754
'https://trac.sagemath.org/ticket/1#comment:1'
4755
sage: bob._sagedev._local_branch_for_ticket(1, pull_if_not_found=True)
4756
Traceback (most recent call last):
4757
...
4758
SageDevValueError: Branch "public/ticket/1" does not exist on the remote server.
4759
4760
sage: import os
4761
sage: os.chdir(server.git._config['src'])
4762
sage: server.git.silent.branch('public/ticket/1')
4763
sage: bob._chdir()
4764
sage: bob._sagedev._local_branch_for_ticket(1, pull_if_not_found=True)
4765
'ticket/1'
4766
sage: bob._sagedev._local_branch_for_ticket(1)
4767
'ticket/1'
4768
"""
4769
ticket = self._ticket_from_ticket_name(ticket)
4770
4771
if self._has_local_branch_for_ticket(ticket):
4772
return self.__ticket_to_branch[ticket]
4773
4774
if not pull_if_not_found:
4775
raise KeyError("No branch for ticket #{0} in your repository.".format(ticket))
4776
4777
branch = self._new_local_branch_for_ticket(ticket)
4778
self._check_ticket_name(ticket, exists=True)
4779
4780
remote_branch = self.trac._branch_for_ticket(ticket)
4781
if remote_branch is None:
4782
raise SageDevValueError("Branch field is not set for ticket #{0} on trac.".format(ticket))
4783
4784
try:
4785
self.git.super_silent.fetch(self.git._repository_anonymous, remote_branch)
4786
except GitError as e:
4787
raise SageDevValueError('Branch "%s" does not exist on the remote server.'%remote_branch)
4788
4789
self.git.super_silent.branch(branch, 'FETCH_HEAD')
4790
4791
self._set_local_branch_for_ticket(ticket, branch)
4792
4793
return self._local_branch_for_ticket(ticket, pull_if_not_found=False)
4794
4795
def _new_local_branch_for_trash(self, branch):
4796
r"""
4797
Return a new local branch name to trash ``branch``.
4798
4799
TESTS::
4800
4801
sage: from sage.dev.test.sagedev import single_user_setup
4802
sage: dev, config, UI, server = single_user_setup()
4803
sage: dev = dev._sagedev
4804
4805
sage: dev._new_local_branch_for_trash('branch')
4806
'trash/branch'
4807
sage: dev.git.silent.branch('trash/branch')
4808
sage: dev._new_local_branch_for_trash('branch')
4809
'trash/branch_'
4810
"""
4811
while True:
4812
trash_branch = 'trash/{0}'.format(branch)
4813
if self._is_trash_name(trash_branch, exists=False):
4814
return trash_branch
4815
branch = branch + "_"
4816
4817
def _new_local_branch_for_ticket(self, ticket):
4818
r"""
4819
Return a local branch name for ``ticket`` which does not exist yet.
4820
4821
INPUT:
4822
4823
- ``ticket`` -- a string or an int identifying a ticket
4824
4825
TESTS::
4826
4827
sage: from sage.dev.test.sagedev import single_user_setup
4828
sage: dev, config, UI, server = single_user_setup()
4829
sage: dev = dev._sagedev
4830
4831
sage: dev._new_local_branch_for_ticket(1)
4832
'ticket/1'
4833
sage: dev.git.silent.branch('ticket/1')
4834
sage: dev._new_local_branch_for_ticket(1)
4835
'ticket/1_'
4836
"""
4837
ticket = self._ticket_from_ticket_name(ticket)
4838
branch = 'ticket/{0}'.format(ticket)
4839
4840
while self._is_local_branch_name(branch, exists=True):
4841
branch = branch + "_"
4842
assert self._is_local_branch_name(branch, exists=False)
4843
return branch
4844
4845
def _set_dependencies_for_ticket(self, ticket, dependencies):
4846
r"""
4847
Locally record ``dependencies`` for ``ticket``.
4848
4849
INPUT:
4850
4851
- ``ticket`` -- an int or string identifying a ticket
4852
4853
- ``dependencies`` -- an iterable of ticket numbers or ``None`` for no
4854
dependencies
4855
4856
TESTS::
4857
4858
sage: from sage.dev.test.sagedev import single_user_setup
4859
sage: dev, config, UI, server = single_user_setup()
4860
sage: dev = dev._sagedev
4861
4862
sage: UI.append("Summary: ticket1\ndescription")
4863
sage: dev.create_ticket()
4864
Created ticket #1 at https://trac.sagemath.org/1.
4865
<BLANKLINE>
4866
# (use "sage --dev checkout --ticket=1" to create a new local branch)
4867
1
4868
sage: dev.checkout(ticket=1)
4869
On ticket #1 with associated local branch "ticket/1".
4870
<BLANKLINE>
4871
# Use "sage --dev merge" to include another ticket/branch.
4872
# Use "sage --dev commit" to save changes into a new commit.
4873
sage: dev._set_dependencies_for_ticket(1, [2, 3])
4874
sage: dev._dependencies_for_ticket(1)
4875
(2, 3)
4876
sage: dev._set_dependencies_for_ticket(1, None)
4877
sage: dev._dependencies_for_ticket(1)
4878
()
4879
"""
4880
ticket = self._ticket_from_ticket_name(ticket)
4881
if dependencies is None:
4882
dependencies = []
4883
dependencies = [self._ticket_from_ticket_name(dep) for dep in dependencies]
4884
if not(dependencies):
4885
if ticket in self.__ticket_dependencies:
4886
del self.__ticket_dependencies[ticket]
4887
return
4888
4889
if not self._has_local_branch_for_ticket(ticket):
4890
raise KeyError("no local branch for ticket #{0} found.".format(ticket))
4891
self.__ticket_dependencies[ticket] = tuple(sorted(dependencies))
4892
4893
def _dependencies_for_ticket(self, ticket, download_if_not_found=False):
4894
r"""
4895
Return the locally recorded dependencies for ``ticket``.
4896
4897
INPUT:
4898
4899
- ``ticket`` -- an int or string identifying a ticket
4900
4901
- ``download_if_not_found`` -- a boolean (default: ``False``), whether
4902
to take the information from trac if the ticket does not exist
4903
locally
4904
4905
TESTS::
4906
4907
sage: from sage.dev.test.sagedev import single_user_setup
4908
sage: dev, config, UI, server = single_user_setup()
4909
sage: dev = dev._sagedev
4910
4911
sage: UI.append("Summary: ticket1\ndescription")
4912
sage: dev.create_ticket()
4913
Created ticket #1 at https://trac.sagemath.org/1.
4914
<BLANKLINE>
4915
# (use "sage --dev checkout --ticket=1" to create a new local branch)
4916
1
4917
sage: dev.checkout(ticket=1)
4918
On ticket #1 with associated local branch "ticket/1".
4919
<BLANKLINE>
4920
# Use "sage --dev merge" to include another ticket/branch.
4921
# Use "sage --dev commit" to save changes into a new commit.
4922
4923
sage: dev._set_dependencies_for_ticket(1, [2, 3])
4924
sage: dev._dependencies_for_ticket(1)
4925
(2, 3)
4926
sage: dev._set_dependencies_for_ticket(1, None)
4927
sage: dev._dependencies_for_ticket(1)
4928
()
4929
4930
sage: dev._dependencies_for_ticket(2, download_if_not_found=True)
4931
Traceback (most recent call last):
4932
...
4933
NotImplementedError
4934
"""
4935
ticket = self._ticket_from_ticket_name(ticket)
4936
4937
if not self._has_local_branch_for_ticket(ticket):
4938
if download_if_not_found:
4939
raise NotImplementedError
4940
else:
4941
raise KeyError("no local branch for ticket #{0} found.".format(ticket))
4942
else:
4943
ret = self.__ticket_dependencies[ticket]
4944
4945
return tuple(sorted([self._ticket_from_ticket_name(dep) for dep in ret]))
4946
4947
def _set_remote_branch_for_branch(self, branch, remote_branch):
4948
r"""
4949
Set the remote branch of ``branch`` to ``remote_branch``.
4950
4951
INPUT:
4952
4953
- ``branch`` -- a string, a name of a local branch
4954
4955
- ``remote_branch`` -- a string or ``None``, unset the remote branch if
4956
``None``
4957
4958
TESTS::
4959
4960
sage: from sage.dev.test.sagedev import single_user_setup
4961
sage: dev, config, UI, server = single_user_setup()
4962
sage: dev = dev._sagedev
4963
4964
sage: dev.git.silent.branch('ticket/1')
4965
4966
sage: dev._remote_branch_for_ticket(1)
4967
'u/doctest/ticket/1'
4968
sage: dev._set_remote_branch_for_branch("ticket/1", "public/1")
4969
sage: dev._remote_branch_for_ticket(1) # ticket/1 has not been set to be the branch for ticket #1
4970
'u/doctest/ticket/1'
4971
sage: dev._set_local_branch_for_ticket(1, 'ticket/1')
4972
sage: dev._remote_branch_for_ticket(1)
4973
'public/1'
4974
sage: dev._set_remote_branch_for_branch("ticket/1", None)
4975
sage: dev._remote_branch_for_ticket(1)
4976
'u/doctest/ticket/1'
4977
"""
4978
self._check_local_branch_name(branch, exists=any)
4979
4980
if remote_branch is None:
4981
if branch in self.__branch_to_remote_branch:
4982
del self.__branch_to_remote_branch[branch]
4983
return
4984
4985
self._check_local_branch_name(branch, exists=True)
4986
self._check_remote_branch_name(remote_branch)
4987
4988
self.__branch_to_remote_branch[branch] = remote_branch
4989
4990
def _remote_branch_for_branch(self, branch):
4991
r"""
4992
Return the remote branch of ``branch`` or ``None`` if no remote branch is set.
4993
4994
INPUT:
4995
4996
- ``branch`` -- a string, the name of a local branch
4997
4998
TESTS::
4999
5000
sage: from sage.dev.test.sagedev import single_user_setup
5001
sage: dev, config, UI, server = single_user_setup()
5002
sage: dev = dev._sagedev
5003
5004
sage: dev.git.silent.branch('ticket/1')
5005
5006
sage: dev._remote_branch_for_branch('ticket/1') is None
5007
True
5008
sage: dev._set_remote_branch_for_branch("ticket/1", "public/1")
5009
sage: dev._remote_branch_for_branch('ticket/1')
5010
'public/1'
5011
sage: dev._set_remote_branch_for_branch("ticket/1", None)
5012
sage: dev._remote_branch_for_branch('ticket/1') is None
5013
True
5014
"""
5015
self._check_local_branch_name(branch, exists=True)
5016
5017
if branch in self.__branch_to_remote_branch:
5018
return self.__branch_to_remote_branch[branch]
5019
if branch == MASTER_BRANCH:
5020
return MASTER_BRANCH
5021
5022
return None
5023
5024
def _set_local_branch_for_ticket(self, ticket, branch):
5025
r"""
5026
Record that ``branch`` is the local branch associated to ``ticket``.
5027
5028
INPUT:
5029
5030
- ``ticket`` -- a string or int identifying a ticket
5031
5032
- ``branch`` -- a string, the name of a local branch, or ``None`` to
5033
delete the association
5034
5035
TESTS::
5036
5037
sage: from sage.dev.test.sagedev import single_user_setup
5038
sage: dev, config, UI, server = single_user_setup()
5039
sage: dev = dev._sagedev
5040
5041
sage: dev._local_branch_for_ticket(1)
5042
Traceback (most recent call last):
5043
...
5044
KeyError: 'No branch for ticket #1 in your repository.'
5045
5046
sage: dev._set_local_branch_for_ticket(1, 'ticket/1')
5047
Traceback (most recent call last):
5048
...
5049
SageDevValueError: Branch "ticket/1" does not exist locally.
5050
sage: dev.git.silent.branch('ticket/1')
5051
sage: dev._set_local_branch_for_ticket(1, 'ticket/1')
5052
sage: dev._local_branch_for_ticket(1)
5053
'ticket/1'
5054
"""
5055
ticket = self._ticket_from_ticket_name(ticket)
5056
if branch is None:
5057
if ticket in self.__ticket_to_branch:
5058
del self.__ticket_to_branch[ticket]
5059
return
5060
5061
self._check_local_branch_name(branch, exists=True)
5062
self.__ticket_to_branch[ticket] = branch
5063
5064
def _format_command(self, command, *args, **kwargs):
5065
r"""
5066
Helper method for informational messages.
5067
5068
OUTPUT:
5069
5070
A command which the user can run from the command line/sage interactive
5071
shell to execute ``command`` with ``args`` and ``kwargs``.
5072
5073
TESTS::
5074
5075
sage: dev._sagedev._format_command('checkout')
5076
'sage --dev checkout'
5077
5078
sage: dev._sagedev._format_command('checkout', ticket=int(1))
5079
'sage --dev checkout --ticket=1'
5080
"""
5081
try:
5082
__IPYTHON__
5083
except NameError:
5084
args = [str(arg) for arg in args]
5085
kwargs = [ "--{0}{1}".format(str(key.split("_or_")[0]).replace("_","-"),"="+str(kwargs[key]) if kwargs[key] is not True else "") for key in kwargs ]
5086
return "sage --dev {0} {1}".format(command.replace("_","-"), " ".join(args+kwargs)).rstrip()
5087
else:
5088
args = [str(arg) for arg in args]
5089
kwargs = [ "{0}={1}".format(str(key).replace("-","_"),kwargs[key]) for key in kwargs ]
5090
return "dev.{0}({1})".format(command.replace("-","_"), ", ".join(args+kwargs))
5091
5092
def _current_ticket(self):
5093
r"""
5094
Return the ticket corresponding to the current branch or ``None`` if
5095
there is no ticket associated to that branch.
5096
5097
TESTS::
5098
5099
sage: from sage.dev.test.sagedev import single_user_setup
5100
sage: dev, config, UI, server = single_user_setup()
5101
sage: dev = dev._sagedev
5102
5103
sage: dev._current_ticket() is None
5104
True
5105
5106
sage: UI.append("Summary: ticket1\ndescription")
5107
sage: dev.create_ticket()
5108
Created ticket #1 at https://trac.sagemath.org/1.
5109
<BLANKLINE>
5110
# (use "sage --dev checkout --ticket=1" to create a new local branch)
5111
1
5112
sage: dev._current_ticket()
5113
sage: dev.checkout(ticket=1)
5114
On ticket #1 with associated local branch "ticket/1".
5115
<BLANKLINE>
5116
# Use "sage --dev merge" to include another ticket/branch.
5117
# Use "sage --dev commit" to save changes into a new commit.
5118
sage: dev._current_ticket()
5119
1
5120
"""
5121
from git_error import DetachedHeadError
5122
try:
5123
branch = self.git.current_branch()
5124
except DetachedHeadError:
5125
return None
5126
5127
if branch in self.__branch_to_ticket:
5128
return self.__branch_to_ticket[branch]
5129
return None
5130
5131
5132
class SageDevValueError(ValueError):
5133
r"""
5134
A ``ValueError`` to indicate that the user supplied an invaid value.
5135
5136
EXAMPLES::
5137
5138
sage: from sage.dev.test.sagedev import single_user_setup
5139
sage: dev, config, UI, server = single_user_setup()
5140
5141
sage: dev.checkout(ticket=-1)
5142
Ticket name "-1" is not valid or ticket does not exist on trac.
5143
"""
5144
def __init__(self, message, *args):
5145
r"""
5146
Initialization.
5147
5148
TESTS::
5149
5150
sage: from sage.dev.sagedev import SageDevValueError
5151
sage: type(SageDevValueError("message"))
5152
<class 'sage.dev.sagedev.SageDevValueError'>
5153
"""
5154
ValueError.__init__(self, message.format(*args))
5155
self._error = (message,) + args
5156
self._info = None
5157
5158
def show_error(self, user_interface):
5159
"""
5160
Display helpful message if available.
5161
5162
INPUT:
5163
5164
- ``user_interface`` -- an instance of
5165
:class:`~sage.dev.user_interface.UserInterface`.
5166
5167
TESTS::
5168
5169
sage: from sage.dev.sagedev import SageDevValueError
5170
sage: e = SageDevValueError("message >{0}<", 123).info('1{0}3', 2)
5171
sage: e.show_error(dev._sagedev._UI)
5172
message >123<
5173
"""
5174
user_interface.error(*self._error)
5175
5176
def info(self, *args):
5177
"""
5178
Store helpful message to be displayed if the exception is not
5179
caught.
5180
5181
INPUT:
5182
5183
- ``*args`` -- arguments to be passed to
5184
:meth:`~sage.dev.user_interface.UserInterface.info`.
5185
5186
OUTPUT:
5187
5188
Returns the exception.
5189
5190
TESTS::
5191
5192
sage: from sage.dev.sagedev import SageDevValueError
5193
sage: e = SageDevValueError("message").info('1{0}3', 2)
5194
sage: e.show_info(dev._sagedev._UI)
5195
# 123
5196
"""
5197
self._info = args
5198
return self
5199
5200
def show_info(self, user_interface):
5201
"""
5202
Display helpful message if available.
5203
5204
INPUT:
5205
5206
- ``user_interface`` -- an instance of
5207
:class:`~sage.dev.user_interface.UserInterface`.
5208
5209
TESTS::
5210
5211
sage: from sage.dev.sagedev import SageDevValueError
5212
sage: e = SageDevValueError("message").info('1{0}3', 2)
5213
sage: e.show_info(dev._sagedev._UI)
5214
# 123
5215
"""
5216
if self._info:
5217
user_interface.info(*self._info)
5218
5219
5220
5221