3 from __future__
import absolute_import
, division
, unicode_literals
4 from __future__
import print_function
15 sys
.path
.append(os
.path
.dirname(os
.path
.dirname(os
.path
.abspath(__file__
))))
18 from cola
import gitcmds
19 from cola
import gitcfg
23 parser
= argparse
.ArgumentParser()
24 parser
.add_argument('filename', metavar
='<travis.yaml>',
25 help='yaml travis config')
26 parser
.add_argument('--build-versions', '-V', action
='append',
27 help='build the specified versions')
28 parser
.add_argument('--dry-run', '-k', default
=False, action
='store_true',
29 help='dry-run, do nothing')
30 parser
.add_argument('--sudo', default
=False, action
='store_true',
31 help='allow commands to use sudo')
32 parser
.add_argument('--skip-missing', default
=False, action
='store_true',
33 help='skip missing interpreters')
34 parser
.add_argument('--no-prompt', '-y', default
=False, action
='store_true',
35 help='do not prompt before each command')
36 parser
.add_argument('--default', default
='y', metavar
='<cmd>',
37 help='default command for empty input (default: y)')
38 parser
.add_argument('--remote', default
='origin',
39 help='remote name whose URL will used for slugs')
40 parser
.add_argument('--before-install', '-b', default
=False,
42 help='run the before_install step')
43 parser
.add_argument('--before-script', '-B', default
=False,
45 help='run the before_script step')
46 parser
.add_argument('--slug', '-s', metavar
='<slug>',
47 help='specify the URL slug')
48 parser
.add_argument('--start', metavar
='<int>', type=int, default
=0,
49 help='specify the starting command index')
50 parser
.add_argument('--test', default
=False, action
='store_true',
51 help='run built-in unit tests')
52 parser
.add_argument('--verbose', '-v', default
=False, action
='store_true',
53 help='increase verbosity')
54 return parser
.parse_args()
57 def load_yaml(filename
):
58 with core
.xopen(filename
, 'r') as f
:
62 def find_interpreter(language
, version
):
63 return core
.find_executable(language
+ version
)
66 def get_platform(platform
):
67 if platform
.lower().startswith('linux'):
75 core
.print_stderr('error: %s' % msg
)
85 if url
.endswith('.git'):
86 url
= url
[:-len('.git')]
89 'https://github.com/',
93 for prefix
in github_prefixes
:
94 if url
.startswith(prefix
):
95 return url
[len(prefix
):]
98 # ssh+git://user:password@host/slug
99 r
'^ssh\+git://[^@]+:[^@]+@[^@/]+/([^:]+)$',
100 # ssh+git://user@host/slug
101 r
'^ssh\+git://[^@]+@[^@]+?/([^:]+)$',
102 # user@host:slug (no ports, etc
103 r
'^[^@]+@[^@]+:([^:]+)$',
105 for pattern
in patterns
:
106 rgx
= re
.compile(pattern
)
107 match
= rgx
.match(url
)
109 return match
.group(1)
114 def guess_slug(context
, remote
):
115 key
= 'remote.%s.url' % remote
116 url
= context
.cfg
.get(key
)
118 default_slug
= 'git-cola/git-cola'
120 slug
= parse_slug(url
) or default_slug
127 def get_slug(context
, args
):
131 slug
= guess_slug(context
, args
.remote
)
135 class ApplicationContext(object):
137 def __init__(self
, args
):
143 def new_context(args
):
144 """Create top-level ApplicationContext objects"""
145 context
= ApplicationContext(args
)
146 context
.git
= git
.create()
147 context
.cfg
= gitcfg
.create(context
)
151 def setup_environment(args
, language
, version
):
152 env
= os
.environ
.copy()
155 if not repo
.is_valid():
156 error('%s is not a Git repository' % core
.getcwd())
158 context
= new_context(args
)
160 version_var
= 'TRAVIS_%s_VERSION' % language
.upper()
161 env
.setdefault(version_var
, version
)
162 env
.setdefault('CI', 'true')
163 env
.setdefault('TRAVIS', 'true')
164 env
.setdefault('CONTINUOUS_INTEGRATION', 'true')
165 env
.setdefault('DEBIAN_FRONTEND', 'noninteractive')
166 env
.setdefault('HAS_DAVID_A_SEAL_OF_APPROVAL', 'true')
167 env
.setdefault('USER', 'travis')
168 env
.setdefault('HOME', '/home/travis')
169 env
.setdefault('LANG', 'en_US.UTF-8')
170 env
.setdefault('LC_ALL', 'en_US.UTF-8')
171 env
.setdefault('RAILS_ENV', 'test')
172 env
.setdefault('RACK_ENV', 'test')
173 env
.setdefault('MERB_ENV', 'test')
174 env
.setdefault('TRAVIS_BRANCH', gitcmds
.current_branch(context
))
175 env
.setdefault('TRAVIS_BUILD_DIR', core
.getcwd())
176 env
.setdefault('TRAVIS_BUILD_ID', '1')
177 env
.setdefault('TRAVIS_BUILD_NUMBER', '1')
178 env
.setdefault('TRAVIS_COMMIT', 'HEAD')
179 env
.setdefault('TRAVIS_COMMIT_RANGE', 'HEAD^..HEAD')
180 env
.setdefault('TRAVIS_JOB_ID', '1')
181 env
.setdefault('TRAVIS_JOB_NUMBER', '1.1')
182 env
.setdefault('TRAVIS_PULL_REQUEST', 'false')
183 env
.setdefault('TRAVIS_SECURE_ENV_VARS', 'false')
184 env
.setdefault('TRAVIS_REPO_SLUG', get_slug(context
, args
))
185 env
.setdefault('TRAVIS_OS_NAME', get_platform(sys
.platform
))
186 env
.setdefault('TRAVIS_TAG', '')
192 return json
.dumps(obj
, sort_keys
=True, indent
=2)
195 def print_environment(environ
):
196 print('# Environment')
197 print(jsonify(environ
))
200 def expandvars(path
, environ
=None):
202 Like os.path.expandvars, but operates on a provided dictionary
204 `os.environ` is used when `environ` is None.
206 >>> expandvars('$foo', environ={'foo': 'bar'}) == 'bar'
209 >>> environ = {'foo': 'a', 'bar': 'b'}
210 >>> expandvars('$foo:$bar:${foo}${bar}', environ=environ) == 'a:b:ab'
217 environ
= os
.environ
.copy()
218 # pylint: disable=protected-access
220 expandvars_re
= expandvars
._re
_cache
221 except AttributeError:
222 expandvars_re
= expandvars
._re
_cache
= re
.compile(r
'\$(\w+|\{[^}]*\})')
225 m
= expandvars_re
.search(path
, i
)
230 if name
.startswith('{') and name
.endswith('}'):
234 path
= path
[:i
] + environ
[name
]
243 shell_env
= os
.getenv('SHELL')
248 for sh
in ('bash', 'zsh', 'ksh', 'sh'):
249 executable
= core
.find_executable(sh
)
256 class RunCommands(object):
258 def __init__(self
, state
, cmds
=None):
261 self
.idx
= state
.start
265 self
.idx
= min(self
.idx
, len(cmds
)-1)
267 def loop(self
, cmds
=None):
275 self
.eval_input(self
.read_input())
280 def show_initial_prompt(self
, title
=None):
282 prompt
= '# %s %s' % (state
.language
.title(), state
.version
)
284 prompt
+= ' - ' + title
286 dashes
= '#' * len(prompt
)
292 def print_help(self
):
296 #<command> ... repeat <command> # times, ex: "5s" skips 5
297 y, r ... yes/run current command (default)
298 b, k, p ... back/prev
299 f, j, n, s ... forward/next/no/skip
300 g, goto # ... goto command at index
301 rw, begin ... start over from the beginning
302 ff, end ... fast-forward to end
303 ls, list ... list commands
304 shell ... enter a subshell (also "bash", "zsh")
305 cd <dir> ... change directories
306 env ... print config-provided environment variables
307 environ ... print current environment variables
308 pwd ... print current working directory
309 h, help ... show help
313 def prep_command(self
, cmd
):
314 sudo
= self
.state
.sudo
315 if not sudo
and cmd
.startswith('sudo '):
316 cmd
= cmd
[len('sudo '):]
319 def current_command(self
):
320 cmd
= self
.cmds
[self
.idx
]
321 return self
.prep_command(cmd
)
323 def expandvars(self
, string
):
324 return expandvars(string
, environ
=self
.state
.environ
)
326 def show_prompt(self
):
328 prompt
= state
.prompt
330 cmd
= self
.current_command()
331 expanded
= self
.expandvars(cmd
)
332 print('$ %s' % expanded
)
334 sys
.stdout
.write('? ')
336 def read_input(self
):
337 if self
.state
.prompt
:
338 answer
= sys
.stdin
.readline().strip()
343 def chdir(self
, directory
):
345 self
.state
.environ
['PWD'] = os
.getcwd()
348 top
= len(self
.cmds
) - 1
349 self
.idx
= max(0, min(idx
, top
))
355 self
.goto(self
.idx
+ 1)
358 self
.goto(self
.idx
- 1)
363 def fast_forward(self
):
364 self
.goto(len(self
.cmds
) - 1)
367 return self
.idx
== len(self
.cmds
) - 1
369 def list_commands(self
):
370 for idx
, cmd
in enumerate(self
.cmds
):
371 cmd
= self
.prep_command(cmd
)
376 print('%03d - %s %s' % (idx
, decoration
, self
.expandvars(cmd
)))
378 def show_status(self
):
379 cmd
= self
.current_command()
380 print('%03d - $ %s' % (self
.idx
, self
.expandvars(cmd
)))
382 def run_current_command(self
):
384 dry_run
= state
.dry_run
386 cmd
= self
.current_command()
387 argv
= shlex
.split(cmd
)
389 error('empty command at index %s' % self
.idx
- 1)
391 if len(argv
) == 2 and argv
[0] == 'cd':
392 directory
= self
.expandvars(argv
[1])
393 self
.chdir(directory
)
397 self
.run_command(cmd
)
399 def run_command(self
, cmd
):
400 environ
= self
.state
.environ
401 status
, _
, _
= core
.run_command(cmd
, env
=environ
, shell
=True,
402 stdin
=None, stdout
=None, stderr
=None)
403 expanded
= self
.expandvars(cmd
)
404 print('The command "%s" exited with %d.' % (expanded
, status
))
406 self
.errors
.append(expanded
)
408 def run_shell(self
, shell
):
409 self
.run_command(shell
)
411 def eval_input(self
, answer
):
412 # Accept the action by default
414 answer
= self
.state
.default
416 words
= shlex
.split(answer
)
419 # Multi-command, e.g. "5s" skips 5 commands
420 rgx
= re
.compile(r
'^(\d+)(.*)$')
422 if answer
in ('f', 'j', 'n', 'N', 'next', 'no', 's', 'skip'):
425 elif answer
in ('b', 'k', 'p', 'back', 'prev'):
428 elif answer
in ('r', 'y', 'Y', 'yes', 'run'):
429 self
.run_current_command()
432 elif answer
in ('rw', 'rewind'):
435 elif answer
in ('ff', 'last', 'end'):
438 elif answer
in ('ls', 'list'):
441 elif answer
in ('shell',):
442 self
.run_shell(default_shell())
444 elif answer
in ('bash', 'sh', 'zsh', 'ash', 'dash', 'ksh', 'csh'):
445 self
.run_shell(answer
)
447 elif answer
in ('st', 'status'):
450 elif answer
in ('pwd',):
453 elif answer
in ('env',):
454 print_environment(self
.state
.env
)
456 elif answer
in ('environ',):
457 print_environment(self
.state
.environ
)
459 elif first
in ('g', 'go', 'goto'):
460 self
.goto(int(words
[1]))
462 elif first
in ('cd', 'chdir'):
463 self
.chdir(self
.expandvars(words
[1]))
465 elif answer
in ('h', 'help', '?'):
468 elif answer
in ('q', 'quit'):
471 elif rgx
.match(answer
):
472 match
= rgx
.match(answer
)
473 count
= match
.group(1)
474 action
= match
.group(2)
475 for _
in range(int(count
)):
476 self
.eval_input(action
)
478 print_error('%s: unknown command' % answer
)
484 def __init__(self
, **kwargs
):
485 for k
, v
in kwargs
.items():
490 def expand_config_environment(envs
, environ
):
491 # Operate on a copy so that we can add to it as we expand the variables.
492 # This allows subsequent variables to reference previous variables.
494 environ
= environ
.copy()
498 # Entries look like "VAR1=x VAR2=x$VAR1" so we split using
499 # the shell lexer to get the individual VAR=value entries
500 env_entries
= shlex
.split(env
)
501 for entry
in env_entries
:
502 key
, value
= entry
.split('=', 1)
503 expanded_value
= expandvars(value
, environ
=environ
)
504 # Add to the returned environment
505 env_values
[key
] = expanded_value
506 # Also add to the outer environ to allow subsequent lookups
507 # to use a previously defined value
508 environ
[key
] = expanded_value
509 expanded
.append(env_values
)
511 # Ensure that we have at least a single environment
518 def build_version(args
, data
, language
, version
):
520 environ
= setup_environment(args
, language
, version
)
522 # Get the possibly config-specified execution environments
523 envs
= expand_config_environment(data
.get('env', []), environ
)
525 environ
= environ
.copy()
528 state
= State(default
=args
.default
,
529 dry_run
=args
.dry_run
,
533 prompt
=not args
.no_prompt
,
536 verbose
=args
.verbose
,
539 # pylint: disable=no-member
541 print_environment(environ
)
544 if args
.before_install
:
545 cmds
.extend(data
.get('before_install', []))
546 cmds
.extend(data
.get('install', []))
548 if args
.before_script
:
549 cmds
.extend(data
.get('before_script', []))
550 cmds
.extend(data
.get('script', []))
552 run_commands
= RunCommands(state
, cmds
=cmds
)
553 run_commands
.show_initial_prompt()
555 errors
.extend(run_commands
.errors
)
560 def build(args
, data
):
561 language
= data
['language']
562 if args
.build_versions
:
563 versions
= args
.build_versions
565 versions
= data
[language
]
567 if args
.skip_missing
:
568 versions
= [v
for v
in versions
if find_interpreter(language
, v
)]
572 for version
in versions
:
573 errors
= build_version(args
, data
, language
, version
)
580 class TravisBuildTestCase(unittest
.TestCase
):
582 def test_parse_slug(self
):
584 'https://github.com/example/slug',
585 'https://github.com/example/slug.git',
586 'git://github.com/example/slug',
587 'git://github.com/example/slug.git',
588 'git@github.com:example/slug',
589 'git@github.com:example/slug.git',
590 'git@example.com:example/slug',
591 'git@example.com:example/slug.git',
592 'ssh+git://git@example.com/example/slug.git',
593 'ssh+git://user:password@example.com/example/slug.git',
594 'ssh+git://user:password@example.com:66/example/slug.git',
596 expect
= 'example/slug'
598 actual
= parse_slug(url
)
599 self
.assertEqual(expect
, actual
)
603 suite
= unittest
.TestLoader().loadTestsFromTestCase(TravisBuildTestCase
)
604 runner
= unittest
.TextTestRunner(verbosity
=2)
605 result
= runner
.run(suite
)
606 if result
.errors
or result
.failures
:
615 signal
.signal(signal
.SIGINT
, signal
.SIG_DFL
)
620 data
= load_yaml(args
.filename
)
621 return build(args
, data
)
624 if __name__
== '__main__':