Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemath
GitHub Repository: sagemath/sagesmc
Path: blob/master/src/sage/dev/git_interface.py
8815 views
1
r"""
2
Git Interface
3
4
This module provides a python interface to Sage's git repository.
5
6
AUTHORS:
7
8
- David Roe, Julian Rueth, Keshav Kini, Nicolas M. Thiery, Robert Bradshaw:
9
initial version
10
11
"""
12
#*****************************************************************************
13
# Copyright (C) 2013 David Roe <[email protected]>
14
# Julian Rueth <[email protected]>
15
# Keshav Kini <[email protected]>
16
# Nicolas M. Thiery <[email protected]>
17
# Robert Bradshaw <[email protected]>
18
#
19
# Distributed under the terms of the GNU General Public License (GPL)
20
# as published by the Free Software Foundation; either version 2 of
21
# the License, or (at your option) any later version.
22
# http://www.gnu.org/licenses/
23
#*****************************************************************************
24
25
import os
26
27
from sage.env import (
28
SAGE_DOT_GIT, SAGE_REPO_AUTHENTICATED, SAGE_ROOT,
29
SAGE_REPO_ANONYMOUS
30
)
31
32
from git_error import GitError, DetachedHeadError
33
34
class GitProxy(object):
35
r"""
36
A proxy object to wrap actual calls to git.
37
38
EXAMPLES::
39
40
sage: from sage.dev.git_interface import GitProxy
41
sage: from sage.dev.test.config import DoctestConfig
42
sage: from sage.dev.test.user_interface import DoctestUserInterface
43
sage: config = DoctestConfig()
44
sage: GitProxy(config['git'], DoctestUserInterface(config['UI']))
45
<sage.dev.git_interface.GitProxy object at 0x...>
46
"""
47
def __init__(self, config, UI):
48
r"""
49
Initialization.
50
51
TESTS::
52
53
sage: from sage.dev.git_interface import GitProxy
54
sage: from sage.dev.test.config import DoctestConfig
55
sage: from sage.dev.test.user_interface import DoctestUserInterface
56
sage: config = DoctestConfig()
57
sage: type(GitProxy(config['git'], DoctestUserInterface(config['UI'])))
58
<class 'sage.dev.git_interface.GitProxy'>
59
"""
60
self._config = config
61
self._UI = UI
62
63
self._src = self._config.get('src', SAGE_ROOT)
64
self._dot_git = self._config.get('dot_git', SAGE_DOT_GIT)
65
self._gitcmd = self._config.get('gitcmd', 'git')
66
self._repository = self._config.get('repository', SAGE_REPO_AUTHENTICATED)
67
self._repository_anonymous = self._config.get('repository_anonymous', SAGE_REPO_ANONYMOUS)
68
69
if not os.path.isabs(self._src):
70
raise ValueError("`%s` is not an absolute path."%self._src)
71
if not os.path.exists(self._src):
72
raise ValueError("`%s` does not point to an existing directory."%self._src)
73
if not os.path.isabs(self._dot_git):
74
raise ValueError("`%s` is not an absolute path."%self._dot_git)
75
if not os.path.exists(self._dot_git):
76
raise ValueError("`%s` does not point to an existing directory."%self._dot_git)
77
78
def _run_git(self, cmd, args, git_kwds, popen_kwds=dict()):
79
r"""
80
Common implementation for :meth:`_execute`, :meth:`_execute_silent`,
81
:meth:`_execute_supersilent`, and :meth:`_read_output`
82
83
INPUT:
84
85
- ``cmd`` - git command run
86
87
- ``args`` - extra arguments for git
88
89
- ``git_kwds`` - extra keywords for git
90
91
- ``popen_kwds`` - extra keywords passed to Popen
92
93
.. WARNING::
94
95
This method does not raise an exception if the git call returns a
96
non-zero exit code.
97
98
EXAMPLES::
99
100
sage: from sage.dev.git_interface import GitInterface
101
sage: from sage.dev.test.config import DoctestConfig
102
sage: from sage.dev.test.user_interface import DoctestUserInterface
103
sage: config = DoctestConfig()
104
sage: git = GitInterface(config["git"], DoctestUserInterface(config["UI"]))
105
sage: os.chdir(config['git']['src'])
106
107
sage: git._run_git('status', (), {})
108
(0,
109
'# On branch master\n#\n# Initial commit\n#\nnothing to commit
110
(create/copy files and use "git add" to track)\n',
111
'',
112
'git -c [email protected] -c user.name=doctest status')
113
114
TESTS:
115
116
Check that we refuse to touch the live source code in doctests::
117
118
sage: git_config = config['git']
119
sage: git_config['dot_git'] = sage.env.DOT_SAGE
120
sage: git_config['src'] = sage.env.SAGE_ROOT
121
sage: git = GitInterface(git_config, DoctestUserInterface(config["UI"]))
122
sage: git.status()
123
Traceback (most recent call last):
124
...
125
AssertionError: working with the sage repository in a doctest
126
"""
127
from sage.doctest import DOCTEST_MODE
128
if DOCTEST_MODE:
129
from sage.misc.misc import SAGE_TMP
130
SAGE_TMP = str(SAGE_TMP)
131
error = "working with the sage repository in a doctest"
132
assert self._dot_git != SAGE_DOT_GIT, error
133
assert self._repository != SAGE_REPO_AUTHENTICATED, error
134
assert os.path.abspath(self._src).startswith(SAGE_TMP), error
135
136
# not sure which commands could possibly create a commit object with
137
# unless there are some crazy flags set - these commands should be safe
138
if cmd not in [
139
"config", "diff", "grep", "log", "ls_remote", "remote", "reset",
140
"show", "show_ref", "status", "symbolic_ref" ]:
141
self._check_user_email()
142
143
s = [self._gitcmd, "--git-dir=%s"%self._dot_git, "--work-tree=%s"%self._src, cmd]
144
if 'user.name' in self._config:
145
s.insert(3, '-c')
146
s.insert(4, 'user.name='+self._config['user.name'])
147
if 'user.email' in self._config:
148
s.insert(3, '-c')
149
s.insert(4, 'user.email='+self._config['user.email'])
150
151
env = popen_kwds.setdefault('env', dict(os.environ))
152
env.update(git_kwds.pop('env', {}))
153
env['LC_ALL'] = 'POSIX' # do not translate git messages
154
155
for k, v in git_kwds.iteritems():
156
if len(k) == 1:
157
k = '-' + k
158
else:
159
k = '--' + k.replace('_', '-')
160
if v is True:
161
s.append(k)
162
elif v is not False:
163
s.extend((k, v))
164
if args:
165
s.extend(a for a in args if a is not None)
166
s = [str(arg) for arg in s]
167
168
# drop --git-dir, --work-tree from debug output
169
complete_cmd = " ".join(s[0:1] + s[3:])
170
self._UI.debug("[git] %s"%complete_cmd)
171
172
if popen_kwds.get('dryrun', False):
173
return s
174
175
import subprocess
176
popen_kwds['stdout'] = subprocess.PIPE
177
popen_kwds['stderr'] = subprocess.PIPE
178
process = subprocess.Popen(s, **popen_kwds)
179
stdout, stderr = process.communicate()
180
retcode = process.poll()
181
return retcode, stdout, stderr, complete_cmd
182
183
def _execute(self, cmd, *args, **kwds):
184
r"""
185
Run git.
186
187
Raises an exception if git has non-zero exit code.
188
189
INPUT:
190
191
- ``cmd`` -- string. git command run
192
193
- ``args`` -- extra arguments for git
194
195
- ``kwds`` -- extra keywords for git
196
197
EXAMPLES::
198
199
sage: from sage.dev.git_interface import GitInterface
200
sage: from sage.dev.test.config import DoctestConfig
201
sage: from sage.dev.test.user_interface import DoctestUserInterface
202
sage: config = DoctestConfig()
203
sage: git = GitInterface(config["git"], DoctestUserInterface(config["UI"]))
204
sage: os.chdir(config['git']['src'])
205
206
sage: git._execute('status')
207
# On branch master
208
#
209
# Initial commit
210
#
211
nothing to commit (create/copy files and use "git add" to track)
212
sage: git._execute('status',foo=True) # --foo is not a valid parameter
213
Traceback (most recent call last):
214
...
215
GitError: git returned with non-zero exit code (129) for
216
"git -c [email protected] -c user.name=doctest status --foo".
217
output to stderr: error: unknown option `foo'
218
usage: git status [options] [--] ...
219
<BLANKLINE>
220
-v, --verbose be verbose
221
-s, --short show status concisely
222
-b, --branch show branch information
223
--porcelain machine-readable output
224
...
225
"""
226
exit_code, stdout, stderr, cmd = self._run_git(cmd, args, kwds)
227
if exit_code:
228
raise GitError(exit_code, cmd, stdout, stderr)
229
if stdout:
230
print(stdout.strip())
231
if stderr:
232
print(stderr.strip())
233
234
def _execute_silent(self, cmd, *args, **kwds):
235
r"""
236
Run git and supress its output to stdout.
237
238
Same input as :meth:`execute`.
239
240
Raises an error if git returns a non-zero exit code.
241
242
EXAMPLES::
243
244
sage: from sage.dev.git_interface import GitInterface
245
sage: from sage.dev.test.config import DoctestConfig
246
sage: from sage.dev.test.user_interface import DoctestUserInterface
247
sage: config = DoctestConfig()
248
sage: git = GitInterface(config["git"], DoctestUserInterface(config["UI"]))
249
sage: os.chdir(config['git']['src'])
250
251
sage: git._execute_silent('status')
252
sage: git._execute_silent('status',foo=True) # --foo is not a valid parameter
253
Traceback (most recent call last):
254
...
255
GitError: git returned with non-zero exit code (129) for
256
"git -c [email protected] -c user.name=doctest status --foo".
257
output to stderr: error: unknown option `foo'
258
usage: git status [options] [--] ...
259
<BLANKLINE>
260
-v, --verbose be verbose
261
-s, --short show status concisely
262
-b, --branch show branch information
263
--porcelain machine-readable output
264
...
265
"""
266
exit_code, stdout, stderr, cmd = self._run_git(cmd, args, kwds)
267
if exit_code:
268
raise GitError(exit_code, cmd, stdout, stderr)
269
if stderr:
270
print(stderr.strip())
271
272
def _execute_supersilent(self, cmd, *args, **kwds):
273
r"""
274
Run git and supress its output to stdout and stderr.
275
276
Same input as :meth:`execute`.
277
278
Raises an error if git returns a non-zero exit code.
279
280
EXAMPLES::
281
282
sage: from sage.dev.git_interface import GitInterface
283
sage: from sage.dev.test.config import DoctestConfig
284
sage: from sage.dev.test.user_interface import DoctestUserInterface
285
sage: config = DoctestConfig()
286
sage: git = GitInterface(config["git"], DoctestUserInterface(config["UI"]))
287
sage: os.chdir(config['git']['src'])
288
289
sage: git._execute_supersilent('status')
290
sage: git._execute_supersilent('status',foo=True) # --foo is not a valid parameter
291
Traceback (most recent call last):
292
...
293
GitError: git returned with non-zero exit code (129) for
294
"git -c [email protected] -c user.name=doctest status --foo".
295
...
296
"""
297
exit_code, stdout, stderr, cmd = self._run_git(cmd, args, kwds)
298
if exit_code:
299
raise GitError(exit_code, cmd, stdout, stderr)
300
301
def _read_output(self, cmd, *args, **kwds):
302
r"""
303
Run git and return its output to stdout.
304
305
Same input as :meth:`execute`.
306
307
Raises an error if git returns a non-zero exit code.
308
309
EXAMPLES::
310
311
sage: import os
312
sage: from sage.dev.git_interface import GitInterface
313
sage: from sage.dev.test.config import DoctestConfig
314
sage: from sage.dev.test.user_interface import DoctestUserInterface
315
sage: config = DoctestConfig()
316
sage: git = GitInterface(config["git"], DoctestUserInterface(config["UI"]))
317
sage: os.chdir(config['git']['src'])
318
319
sage: git._read_output('status')
320
'# On branch master\n#\n# Initial commit\n#\nnothing to
321
commit (create/copy files and use "git add" to track)\n'
322
sage: git._read_output('status',foo=True) # --foo is not a valid parameter
323
Traceback (most recent call last):
324
...
325
GitError: git returned with non-zero exit code (129) for
326
"git -c [email protected] -c user.name=doctest status --foo".
327
...
328
"""
329
exit_code, stdout, stderr, cmd = self._run_git(cmd, args, kwds)
330
if exit_code:
331
raise GitError(exit_code, cmd, stdout, stderr)
332
return stdout
333
334
def _check_user_email(self):
335
r"""
336
Make sure that a real name and an email are set for git. These will
337
show up next to any commit that user creates.
338
339
TESTS::
340
341
sage: import os
342
sage: from sage.dev.git_interface import GitInterface
343
sage: from sage.dev.test.config import DoctestConfig
344
sage: from sage.dev.test.user_interface import DoctestUserInterface
345
sage: config = DoctestConfig()
346
sage: del config['git']['user.name']
347
sage: del config['git']['user.email']
348
sage: UI = DoctestUserInterface(config["UI"])
349
sage: git = GitInterface(config["git"], UI)
350
sage: os.chdir(config['git']['src'])
351
sage: UI.append("Doc Test")
352
sage: UI.append("doc@test")
353
354
The following depends on whether the user has set
355
``user.name`` and ``user.email`` in its ``.gitconfig`` ::
356
357
sage: git._check_user_email() # random output
358
"""
359
if self._config.get('user_email_set', False):
360
return
361
if 'user.name' not in self._config:
362
try:
363
self._execute_supersilent("config","user.name")
364
except GitError as e:
365
if e.exit_code == 1:
366
name = self._UI.get_input("No real name has been set for git. This name"
367
" shows up as the author for any commits you contribute"
368
" to sage. Your real name:")
369
self._execute("config","user.name",name,local=True,add=True)
370
self._UI.info("Your real name has been saved.")
371
else:
372
raise
373
if 'user.email' not in self._config:
374
try:
375
self._execute_supersilent("config", "user.email")
376
except GitError as e:
377
if e.exit_code == 1:
378
email = self._UI._get_input("No email address has been set for git. This email"
379
" shows up as the author for any commits you contribute"
380
" to sage. Your email address:")
381
self._execute("config","user.email",email,local=True,add=True)
382
self._UI.info("Your email has been saved.")
383
else:
384
raise
385
self._config['user_email_set'] = "True"
386
387
388
class ReadStdoutGitProxy(GitProxy):
389
r"""
390
A proxy object to wrap calls to git.
391
392
Calls to git return the stdout of git and raise an error on a non-zero exit
393
code. Output to stderr is supressed.
394
395
EXAMPLES::
396
397
sage: dev.git.status() # not tested
398
"""
399
__call__ = GitProxy._read_output
400
401
402
class SilentGitProxy(GitProxy):
403
r"""
404
A proxy object to wrap calls to git.
405
406
Calls to git do not show any output to stdout and raise an error on a
407
non-zero exit code. Output to stderr is printed.
408
409
EXAMPLES::
410
411
sage: dev.git.silent.status() # not tested
412
"""
413
__call__ = GitProxy._execute_silent
414
415
416
class EchoGitProxy(GitProxy):
417
r"""
418
A proxy object to wrap calls to git.
419
420
Calls to git show output to stdout and stderr as if the command was
421
executed directly and raise an error on a non-zero exit code.
422
423
EXAMPLES::
424
425
sage: dev.git.echo.status() # not tested
426
"""
427
__call__ = GitProxy._execute
428
429
430
class SuperSilentGitProxy(GitProxy):
431
r"""
432
A proxy object to wrap calls to git.
433
434
Calls to git do not show any output to stderr or stdout and raise an error
435
on a non-zero exit code.
436
437
EXAMPLES::
438
439
sage: dev.git.super_silent.status() # not tested
440
"""
441
__call__ = GitProxy._execute_supersilent
442
443
444
class GitInterface(ReadStdoutGitProxy):
445
r"""
446
A wrapper around the ``git`` command line tool.
447
448
Most methods of this class correspond to actual git commands. Some add
449
functionality which is not directly available in git. However, all of the
450
methods should be non-interactive. If interaction is required the method
451
should live in :class:`saged.dev.sagedev.SageDev`.
452
453
EXAMPLES::
454
455
sage: from sage.dev.test.config import DoctestConfig
456
sage: from sage.dev.user_interface import UserInterface
457
sage: from sage.dev.git_interface import GitInterface
458
sage: config = DoctestConfig()
459
sage: GitInterface(config['git'], UserInterface(config['UI']))
460
GitInterface()
461
"""
462
def __init__(self, config, UI):
463
r"""
464
Initialization.
465
466
TESTS::
467
468
sage: from sage.dev.test.config import DoctestConfig
469
sage: from sage.dev.user_interface import UserInterface
470
sage: from sage.dev.git_interface import GitInterface
471
sage: config = DoctestConfig()
472
sage: type(GitInterface(config['git'], UserInterface(config['UI'])))
473
<class 'sage.dev.git_interface.GitInterface'>
474
"""
475
ReadStdoutGitProxy.__init__(self, config, UI)
476
477
self.silent = SilentGitProxy(config, UI)
478
self.super_silent = SuperSilentGitProxy(config, UI)
479
self.echo = EchoGitProxy(config, UI)
480
481
def __repr__(self):
482
r"""
483
Return a printable representation of this object.
484
485
TESTS::
486
487
sage: from sage.dev.test.config import DoctestConfig
488
sage: from sage.dev.user_interface import UserInterface
489
sage: from sage.dev.git_interface import GitInterface
490
sage: config = DoctestConfig()
491
sage: repr(GitInterface(config['git'], UserInterface(config['UI'])))
492
'GitInterface()'
493
"""
494
return "GitInterface()"
495
496
def get_state(self):
497
r"""
498
Get the current state of merge/rebase/am/etc operations.
499
500
OUTPUT:
501
502
A tuple of strings which consists of any of the following:
503
``'rebase'``, ``'am'``, ``'rebase-i'``, ``'rebase-m'``, ``'merge'``,
504
``'bisect'``, ``'cherry-seq'``, ``'cherry'``.
505
506
EXAMPLES:
507
508
Create a :class:`GitInterface` for doctesting::
509
510
sage: import os
511
sage: from sage.dev.git_interface import GitInterface
512
sage: from sage.dev.test.config import DoctestConfig
513
sage: from sage.dev.test.user_interface import DoctestUserInterface
514
sage: config = DoctestConfig()
515
sage: git = GitInterface(config["git"], DoctestUserInterface(config["UI"]))
516
517
Create two conflicting branches::
518
519
sage: os.chdir(config['git']['src'])
520
sage: with open("file","w") as f: f.write("version 0")
521
sage: git.silent.add("file")
522
sage: git.silent.commit("-m","initial commit")
523
sage: git.super_silent.checkout("-b","branch1")
524
sage: with open("file","w") as f: f.write("version 1")
525
sage: git.silent.commit("-am","second commit")
526
sage: git.super_silent.checkout("master")
527
sage: git.super_silent.checkout("-b","branch2")
528
sage: with open("file","w") as f: f.write("version 2")
529
sage: git.silent.commit("-am","conflicting commit")
530
531
A ``merge`` state::
532
533
sage: git.super_silent.checkout("branch1")
534
sage: git.silent.merge('branch2')
535
Traceback (most recent call last):
536
...
537
GitError: git returned with non-zero exit code (1) for
538
"git -c [email protected] -c user.name=doctest merge branch2".
539
...
540
sage: git.get_state()
541
('merge',)
542
sage: git.silent.merge(abort=True)
543
sage: git.get_state()
544
()
545
546
A ``rebase`` state::
547
548
sage: git._execute_supersilent('rebase', 'branch2')
549
Traceback (most recent call last):
550
...
551
GitError: git returned with non-zero exit code (1) for
552
"git -c [email protected] -c user.name=doctest rebase branch2".
553
...
554
sage: git.get_state()
555
('rebase',)
556
sage: git.super_silent.rebase(abort=True)
557
sage: git.get_state()
558
()
559
"""
560
# logic based on zsh's git backend for vcs_info
561
opj = os.path.join
562
p = lambda x: opj(self._dot_git, x)
563
ret = []
564
for d in map(p,("rebase-apply", "rebase", opj("..",".dotest"))):
565
if os.path.isdir(d):
566
if os.path.isfile(opj(d, 'rebasing')) and 'rebase' not in ret:
567
ret.append('rebase')
568
if os.path.isfile(opj(d, 'applying')) and 'am' not in ret:
569
ret.append('am')
570
for f in map(p, (opj('rebase-merge', 'interactive'),
571
opj('.dotest-merge', 'interactive'))):
572
if os.path.isfile(f):
573
ret.append('rebase-i')
574
break
575
else:
576
for d in map(p, ('rebase-merge', '.dotest-merge')):
577
if os.path.isdir(d):
578
ret.append('rebase-m')
579
break
580
if os.path.isfile(p('MERGE_HEAD')):
581
ret.append('merge')
582
if os.path.isfile(p('BISECT_LOG')):
583
ret.append('bisect')
584
if os.path.isfile(p('CHERRY_PICK_HEAD')):
585
if os.path.isdir(p('sequencer')):
586
ret.append('cherry-seq')
587
else:
588
ret.append('cherry')
589
# return in reverse order so reset operations are correctly ordered
590
return tuple(reversed(ret))
591
592
def reset_to_clean_state(self):
593
r"""
594
Get out of a merge/am/rebase/etc state.
595
596
EXAMPLES:
597
598
Create a :class:`GitInterface` for doctesting::
599
600
sage: import os
601
sage: from sage.dev.git_interface import GitInterface
602
sage: from sage.dev.test.config import DoctestConfig
603
sage: from sage.dev.test.user_interface import DoctestUserInterface
604
sage: config = DoctestConfig()
605
sage: git = GitInterface(config["git"], DoctestUserInterface(config["UI"]))
606
607
Create two conflicting branches::
608
609
sage: os.chdir(config['git']['src'])
610
sage: with open("file","w") as f: f.write("version 0")
611
sage: git.silent.add("file")
612
sage: git.silent.commit("-m","initial commit")
613
sage: git.super_silent.checkout("-b","branch1")
614
sage: with open("file","w") as f: f.write("version 1")
615
sage: git.silent.commit("-am","second commit")
616
sage: git.super_silent.checkout("master")
617
sage: git.super_silent.checkout("-b","branch2")
618
sage: with open("file","w") as f: f.write("version 2")
619
sage: git.silent.commit("-am","conflicting commit")
620
621
A merge::
622
623
sage: git.silent.merge('branch1')
624
Traceback (most recent call last):
625
...
626
GitError: git returned with non-zero exit code (1) for
627
"git -c [email protected] -c user.name=doctest merge branch1".
628
...
629
sage: git.get_state()
630
('merge',)
631
632
Get out of this state::
633
634
sage: git.reset_to_clean_state()
635
sage: git.get_state()
636
()
637
"""
638
states = self.get_state()
639
if not states:
640
return
641
642
state = states[0]
643
if state.startswith('rebase'):
644
self.silent.rebase(abort=True)
645
elif state == 'am':
646
self.silent.am(abort=True)
647
elif state == 'merge':
648
self.silent.merge(abort=True)
649
elif state == 'bisect':
650
self.silent.bisect(reset=True)
651
elif state.startswith('cherry'):
652
self.silent.cherry_pick(abort=True)
653
else:
654
raise RuntimeError("'%s' is not a valid state"%state)
655
656
return self.reset_to_clean_state()
657
658
def clean_wrapper(self, remove_untracked_files=False,
659
remove_untracked_directories=False,
660
remove_ignored=False):
661
r"""
662
Clean the working directory.
663
664
This is a convenience wrapper for ``git clean``
665
666
INPUT:
667
668
- ``remove_untracked_files`` -- a boolean (default: ``False``), whether
669
to remove files which are not tracked by git
670
671
- ``remove_untracked_directories`` -- a boolean (default: ``False``),
672
whether to remove directories which are not tracked by git
673
674
- ``remove_ignored`` -- a boolean (default: ``False``), whether to
675
remove files directories which are ignored by git
676
677
EXAMPLES:
678
679
Create a :class:`GitInterface` for doctesting::
680
681
sage: from sage.dev.git_interface import GitInterface
682
sage: from sage.dev.test.config import DoctestConfig
683
sage: from sage.dev.test.user_interface import DoctestUserInterface
684
sage: config = DoctestConfig()
685
sage: git = GitInterface(config["git"], DoctestUserInterface(config["UI"]))
686
687
Set up some files/directories::
688
689
sage: os.chdir(config['git']['src'])
690
sage: open('tracked','w').close()
691
sage: git.silent.add('tracked')
692
sage: with open('.gitignore','w') as f: f.write('ignored\nignored_dir')
693
sage: git.silent.add('.gitignore')
694
sage: git.silent.commit('-m', 'initial commit')
695
696
sage: os.mkdir('untracked_dir')
697
sage: open('untracked_dir/untracked','w').close()
698
sage: open('untracked','w').close()
699
sage: open('ignored','w').close()
700
sage: os.mkdir('ignored_dir')
701
sage: open('ignored_dir/untracked','w').close()
702
sage: with open('tracked','w') as f: f.write('version 0')
703
sage: git.echo.status()
704
# On branch master
705
# Changes not staged for commit:
706
# (use "git add <file>..." to update what will be committed)
707
# (use "git checkout -- <file>..." to discard changes in working directory)
708
#
709
# modified: tracked
710
#
711
# Untracked files:
712
# (use "git add <file>..." to include in what will be committed)
713
#
714
# untracked
715
# untracked_dir/
716
no changes added to commit (use "git add" and/or "git commit -a")
717
718
Some invalid combinations of flags::
719
720
sage: git.clean_wrapper(
721
....: remove_untracked_files=False, remove_untracked_directories=True)
722
Traceback (most recent call last):
723
...
724
ValueError: remove_untracked_directories only valid if remove_untracked_files is set
725
sage: git.clean_wrapper(remove_untracked_files = False, remove_ignored = True)
726
Traceback (most recent call last):
727
...
728
ValueError: remove_ignored only valid if remove_untracked_files is set
729
730
Per default only the tracked modified files are reset to a clean
731
state::
732
733
sage: git.clean_wrapper()
734
sage: git.echo.status()
735
# On branch master
736
# Untracked files:
737
# (use "git add <file>..." to include in what will be committed)
738
#
739
# untracked
740
# untracked_dir/
741
nothing added to commit but untracked files present (use "git add" to track)
742
743
Untracked items can be removed by setting the parameters::
744
745
sage: git.clean_wrapper(remove_untracked_files=True)
746
Removing untracked
747
sage: git.clean_wrapper(
748
....: remove_untracked_files=True, remove_untracked_directories=True)
749
Removing untracked_dir/
750
sage: git.clean_wrapper(
751
....: remove_untracked_files=True, remove_ignored=True)
752
Removing ignored
753
sage: git.clean_wrapper(
754
....: remove_untracked_files=True,
755
....: remove_untracked_directories=True,
756
....: remove_ignored=True)
757
Removing ignored_dir/
758
"""
759
if remove_untracked_directories and not remove_untracked_files:
760
raise ValueError("remove_untracked_directories only valid if remove_untracked_files is set")
761
if remove_ignored and not remove_untracked_files:
762
raise ValueError("remove_ignored only valid if remove_untracked_files is set")
763
764
self.silent.reset(hard=True)
765
if remove_untracked_files:
766
switches = ['-f']
767
if remove_untracked_directories: switches.append("-d")
768
if remove_ignored: switches.append("-x")
769
self.echo.clean(*switches)
770
771
def is_child_of(self, a, b):
772
r"""
773
Return whether ``a`` is a child of ``b``.
774
775
EXAMPLES:
776
777
Create a :class:`GitInterface` for doctesting::
778
779
sage: import os
780
sage: from sage.dev.git_interface import GitInterface
781
sage: from sage.dev.test.config import DoctestConfig
782
sage: from sage.dev.test.user_interface import DoctestUserInterface
783
sage: config = DoctestConfig()
784
sage: git = GitInterface(config["git"], DoctestUserInterface(config["UI"]))
785
786
Create two conflicting branches::
787
788
sage: os.chdir(config['git']['src'])
789
sage: with open("file","w") as f: f.write("version 0")
790
sage: git.silent.add("file")
791
sage: git.silent.commit("-m","initial commit")
792
sage: git.super_silent.checkout("-b","branch1")
793
sage: with open("file","w") as f: f.write("version 1")
794
sage: git.silent.commit("-am","second commit")
795
sage: git.super_silent.checkout("master")
796
sage: git.super_silent.checkout("-b","branch2")
797
sage: with open("file","w") as f: f.write("version 2")
798
sage: git.silent.commit("-am","conflicting commit")
799
800
sage: git.is_child_of('master', 'branch2')
801
False
802
sage: git.is_child_of('branch2', 'master')
803
True
804
sage: git.is_child_of('branch1', 'branch2')
805
False
806
sage: git.is_child_of('master', 'master')
807
True
808
"""
809
return self.is_ancestor_of(b, a)
810
811
def is_ancestor_of(self, a, b):
812
r"""
813
Return whether ``a`` is an ancestor of ``b``.
814
815
EXAMPLES:
816
817
Create a :class:`GitInterface` for doctesting::
818
819
sage: import os
820
sage: from sage.dev.git_interface import GitInterface
821
sage: from sage.dev.test.config import DoctestConfig
822
sage: from sage.dev.test.user_interface import DoctestUserInterface
823
sage: config = DoctestConfig()
824
sage: git = GitInterface(config["git"], DoctestUserInterface(config["UI"]))
825
826
Create two conflicting branches::
827
828
sage: os.chdir(config['git']['src'])
829
sage: with open("file","w") as f: f.write("version 0")
830
sage: git.silent.add("file")
831
sage: git.silent.commit("-m","initial commit")
832
sage: git.super_silent.checkout("-b","branch1")
833
sage: with open("file","w") as f: f.write("version 1")
834
sage: git.silent.commit("-am","second commit")
835
sage: git.super_silent.checkout("master")
836
sage: git.super_silent.checkout("-b","branch2")
837
sage: with open("file","w") as f: f.write("version 2")
838
sage: git.silent.commit("-am","conflicting commit")
839
840
sage: git.is_ancestor_of('master', 'branch2')
841
True
842
sage: git.is_ancestor_of('branch2', 'master')
843
False
844
sage: git.is_ancestor_of('branch1', 'branch2')
845
False
846
sage: git.is_ancestor_of('master', 'master')
847
True
848
"""
849
return self.merge_base(a, b) == self.rev_parse(a)
850
851
def has_uncommitted_changes(self):
852
r"""
853
Return whether there are uncommitted changes, i.e., whether there are
854
modified files which are tracked by git.
855
856
EXAMPLES:
857
858
Create a :class:`GitInterface` for doctesting::
859
860
sage: import os
861
sage: from sage.dev.git_interface import GitInterface
862
sage: from sage.dev.test.config import DoctestConfig
863
sage: from sage.dev.test.user_interface import DoctestUserInterface
864
sage: config = DoctestConfig()
865
sage: git = GitInterface(config["git"], DoctestUserInterface(config["UI"]))
866
867
An untracked file does not count towards uncommited changes::
868
869
sage: os.chdir(config['git']['src'])
870
sage: open('untracked','w').close()
871
sage: git.has_uncommitted_changes()
872
False
873
874
Once added to the index it does::
875
876
sage: git.silent.add('untracked')
877
sage: git.has_uncommitted_changes()
878
True
879
sage: git.silent.commit('-m', 'tracking untracked')
880
sage: git.has_uncommitted_changes()
881
False
882
sage: with open('untracked','w') as f: f.write('version 0')
883
sage: git.has_uncommitted_changes()
884
True
885
"""
886
return bool([line for line in self.status(porcelain=True).splitlines()
887
if not line.startswith('?')])
888
889
def untracked_files(self):
890
r"""
891
Return a list of file names for files that are not tracked by git and
892
not ignored.
893
894
EXAMPLES:
895
896
Create a :class:`GitInterface` for doctesting::
897
898
sage: import os
899
sage: from sage.dev.git_interface import GitInterface
900
sage: from sage.dev.test.config import DoctestConfig
901
sage: from sage.dev.test.user_interface import DoctestUserInterface
902
sage: config = DoctestConfig()
903
sage: git = GitInterface(config["git"], DoctestUserInterface(config["UI"]))
904
905
An untracked file::
906
907
sage: os.chdir(config['git']['src'])
908
sage: git.untracked_files()
909
[]
910
sage: open('untracked','w').close()
911
sage: git.untracked_files()
912
['untracked']
913
914
Directories are not displayed here::
915
916
sage: os.mkdir('untracked_dir')
917
sage: git.untracked_files()
918
['untracked']
919
sage: open('untracked_dir/untracked','w').close()
920
sage: git.untracked_files()
921
['untracked', 'untracked_dir/untracked']
922
"""
923
import os
924
old_cwd = os.getcwd()
925
if 'src' in self._config:
926
os.chdir(self._config['src'])
927
else:
928
from sage.env import SAGE_ROOT
929
os.chdir(SAGE_ROOT)
930
try:
931
fnames = self.ls_files(other=True, exclude_standard=True).splitlines()
932
fnames = [ os.path.abspath(fname) for fname in fnames ]
933
return [ os.path.relpath(fname, old_cwd) for fname in fnames ]
934
finally:
935
os.chdir(old_cwd)
936
937
def local_branches(self):
938
r"""
939
Return a list of local branches sorted by last commit time.
940
941
EXAMPLES:
942
943
Create a :class:`GitInterface` for doctesting::
944
945
sage: import os, time
946
sage: from sage.dev.git_interface import GitInterface
947
sage: from sage.dev.test.config import DoctestConfig
948
sage: from sage.dev.test.user_interface import DoctestUserInterface
949
sage: config = DoctestConfig()
950
sage: git = GitInterface(config["git"], DoctestUserInterface(config["UI"]))
951
952
Create some branches::
953
954
sage: os.chdir(config['git']['src'])
955
sage: env = {'GIT_COMMITTER_DATE': time.strftime("%Y-%m-%dT%H:%M:10")}
956
sage: git.silent.commit('-m','initial commit','--allow-empty', env=env)
957
sage: git.super_silent.checkout('-b', 'branch')
958
sage: env['GIT_COMMITTER_DATE'] = time.strftime("%Y-%m-%dT%H:%M:20")
959
sage: git.silent.commit('-m','second commit','--allow-empty', env=env)
960
sage: git.super_silent.checkout('-b', 'other', 'master')
961
sage: env['GIT_COMMITTER_DATE'] = time.strftime("%Y-%m-%dT%H:%M:30")
962
sage: git.silent.commit('-m','third commit','--allow-empty', env=env)
963
964
Use this repository as a remote repository::
965
966
sage: config2 = DoctestConfig()
967
sage: git2 = GitInterface(config2["git"], DoctestUserInterface(config["UI"]))
968
sage: os.chdir(config2['git']['src'])
969
sage: env['GIT_COMMITTER_DATE'] = time.strftime("%Y-%m-%dT%H:%M:40")
970
sage: git2.silent.commit('-m','initial commit','--allow-empty', env=env)
971
sage: git2.silent.remote('add', 'git', config['git']['src'])
972
sage: git2.super_silent.fetch('git')
973
sage: git2.super_silent.checkout("branch")
974
sage: git2.echo.branch("-a")
975
* branch
976
master
977
remotes/git/branch
978
remotes/git/master
979
remotes/git/other
980
981
sage: git2.local_branches()
982
['master', 'branch']
983
sage: os.chdir(config['git']['src'])
984
sage: git.local_branches()
985
['other', 'branch', 'master']
986
"""
987
result = self.for_each_ref('refs/heads/', sort='-committerdate', format="%(refname)")
988
return [head[11:] for head in result.splitlines()]
989
990
def current_branch(self):
991
r"""
992
Return the current branch
993
994
EXAMPLES:
995
996
Create a :class:`GitInterface` for doctesting::
997
998
sage: import os
999
sage: from sage.dev.git_interface import GitInterface
1000
sage: from sage.dev.test.config import DoctestConfig
1001
sage: from sage.dev.test.user_interface import DoctestUserInterface
1002
sage: config = DoctestConfig()
1003
sage: git = GitInterface(config["git"], DoctestUserInterface(config["UI"]))
1004
1005
Create some branches::
1006
1007
sage: os.chdir(config['git']['src'])
1008
sage: git.silent.commit('-m','initial commit','--allow-empty')
1009
sage: git.silent.commit('-m','second commit','--allow-empty')
1010
sage: git.silent.branch('branch1')
1011
sage: git.silent.branch('branch2')
1012
1013
sage: git.current_branch()
1014
'master'
1015
sage: git.super_silent.checkout('branch1')
1016
sage: git.current_branch()
1017
'branch1'
1018
1019
If ``HEAD`` is detached::
1020
1021
sage: git.super_silent.checkout('master~')
1022
sage: git.current_branch()
1023
Traceback (most recent call last):
1024
...
1025
DetachedHeadError: unexpectedly, git is in a detached HEAD state
1026
"""
1027
try:
1028
return self.symbolic_ref('HEAD', short=True, quiet=True).strip()
1029
except GitError as e:
1030
if e.exit_code == 1:
1031
raise DetachedHeadError()
1032
raise
1033
1034
def commit_for_branch(self, branch):
1035
r"""
1036
Return the commit id of the local ``branch``, or ``None`` if the branch
1037
does not exist
1038
1039
EXAMPLES:
1040
1041
Create a :class:`GitInterface` for doctesting::
1042
1043
sage: import os
1044
sage: from sage.dev.git_interface import GitInterface
1045
sage: from sage.dev.test.config import DoctestConfig
1046
sage: from sage.dev.test.user_interface import DoctestUserInterface
1047
sage: config = DoctestConfig()
1048
sage: git = GitInterface(config["git"], DoctestUserInterface(config["UI"]))
1049
1050
Create some branches::
1051
1052
sage: os.chdir(config['git']['src'])
1053
sage: git.silent.commit('-m','initial commit','--allow-empty')
1054
sage: git.silent.branch('branch1')
1055
sage: git.silent.branch('branch2')
1056
1057
Check existence of branches::
1058
1059
sage: git.commit_for_branch('branch1') # random output
1060
'087e1fdd0fe6f4c596f5db22bc54567b032f5d2b'
1061
sage: git.commit_for_branch('branch2') is not None
1062
True
1063
sage: git.commit_for_branch('branch3') is not None
1064
False
1065
"""
1066
return self.commit_for_ref("refs/heads/%s"%branch)
1067
1068
def commit_for_ref(self, ref):
1069
r"""
1070
Return the commit id of the ``ref``, or ``None`` if the ``ref`` does
1071
not exist.
1072
1073
EXAMPLES:
1074
1075
Create a :class:`GitInterface` for doctesting::
1076
1077
sage: import os
1078
sage: from sage.dev.git_interface import GitInterface
1079
sage: from sage.dev.test.config import DoctestConfig
1080
sage: from sage.dev.test.user_interface import DoctestUserInterface
1081
sage: config = DoctestConfig()
1082
sage: git = GitInterface(config["git"], DoctestUserInterface(config["UI"]))
1083
1084
Create some branches::
1085
1086
sage: os.chdir(config['git']['src'])
1087
sage: git.silent.commit('-m','initial commit','--allow-empty')
1088
sage: git.silent.branch('branch1')
1089
sage: git.silent.branch('branch2')
1090
1091
Check existence of branches::
1092
1093
sage: git.commit_for_ref('refs/heads/branch1') # random output
1094
'087e1fdd0fe6f4c596f5db22bc54567b032f5d2b'
1095
sage: git.commit_for_ref('refs/heads/branch2') is not None
1096
True
1097
sage: git.commit_for_ref('refs/heads/branch3') is not None
1098
False
1099
"""
1100
try:
1101
return self.rev_parse(ref, verify=True).strip()
1102
except GitError:
1103
return None
1104
1105
def rename_branch(self, oldname, newname):
1106
r"""
1107
Rename ``oldname`` to ``newname``.
1108
1109
EXAMPLES:
1110
1111
Create a :class:`GitInterface` for doctesting::
1112
1113
sage: from sage.dev.git_interface import GitInterface
1114
sage: from sage.dev.test.config import DoctestConfig
1115
sage: from sage.dev.test.user_interface import DoctestUserInterface
1116
sage: config = DoctestConfig()
1117
sage: git = GitInterface(config["git"], DoctestUserInterface(config["UI"]))
1118
1119
Create some branches::
1120
1121
sage: os.chdir(config['git']['src'])
1122
sage: git.silent.commit('-m','initial commit','--allow-empty')
1123
sage: git.silent.branch('branch1')
1124
sage: git.silent.branch('branch2')
1125
1126
Rename some branches::
1127
1128
sage: git.rename_branch('branch1', 'branch3')
1129
sage: git.rename_branch('branch2', 'branch3')
1130
Traceback (most recent call last):
1131
...
1132
GitError: git returned with non-zero exit code (128) for
1133
"git -c [email protected] -c user.name=doctest branch --move branch2 branch3".
1134
output to stderr: fatal: A branch named 'branch3' already exists.
1135
"""
1136
self.branch(oldname, newname, move=True)
1137
1138
for git_cmd_ in (
1139
"add",
1140
"am",
1141
"apply",
1142
"bisect",
1143
"branch",
1144
"config",
1145
"checkout",
1146
"cherry_pick",
1147
"clean",
1148
"clone",
1149
"commit",
1150
"diff",
1151
"fetch",
1152
"for_each_ref",
1153
"format_patch",
1154
"grep",
1155
"init",
1156
"log",
1157
"ls_files",
1158
"ls_remote",
1159
"merge",
1160
"merge_base",
1161
"mv",
1162
"pull",
1163
"push",
1164
"rebase",
1165
"remote",
1166
"reset",
1167
"rev_list",
1168
"rev_parse",
1169
"rm",
1170
"show",
1171
"show_ref",
1172
"stash",
1173
"status",
1174
"symbolic_ref",
1175
"tag"
1176
):
1177
def create_wrapper(git_cmd__):
1178
r"""
1179
Create a wrapper for ``git_cmd__``.
1180
1181
EXAMPLES::
1182
1183
sage: import os
1184
sage: from sage.dev.git_interface import GitInterface
1185
sage: from sage.dev.test.config import DoctestConfig
1186
sage: from sage.dev.test.user_interface import DoctestUserInterface
1187
sage: config = DoctestConfig()
1188
sage: git = GitInterface(config["git"], DoctestUserInterface(config["UI"]))
1189
sage: os.chdir(config['git']['src'])
1190
sage: git.echo.status() # indirect doctest
1191
# On branch master
1192
#
1193
# Initial commit
1194
#
1195
nothing to commit (create/copy files and use "git add" to track)
1196
"""
1197
git_cmd = git_cmd__.replace("_","-")
1198
def meth(self, *args, **kwds):
1199
return self(git_cmd, *args, **kwds)
1200
meth.__doc__ = r"""
1201
Call ``git {0}``.
1202
1203
OUTPUT:
1204
1205
See the docstring of ``__call__`` for more information.
1206
1207
EXAMPLES:
1208
1209
sage: dev.git.{1}() # not tested
1210
""".format(git_cmd, git_cmd__)
1211
return meth
1212
setattr(GitProxy, git_cmd_, create_wrapper(git_cmd_))
1213
1214
1215