3 from __future__
import absolute_import
, division
, unicode_literals
16 sys
.path
.append(os
.path
.dirname(os
.path
.dirname(os
.path
.abspath(__file__
))))
19 from cola
import gitcmds
20 from cola
import gitcfg
24 parser
= argparse
.ArgumentParser()
25 parser
.add_argument('filename', metavar
='<travis.yaml>',
26 help='yaml travis config')
27 parser
.add_argument('--build-versions', '-V', action
='append',
28 help='build the specified versions')
29 parser
.add_argument('--dry-run', '-k', default
=False, action
='store_true',
30 help='dry-run, do nothing')
31 parser
.add_argument('--sudo', default
=False, action
='store_true',
32 help='allow commands to use sudo')
33 parser
.add_argument('--skip-missing', default
=False, action
='store_true',
34 help='skip missing interpreters')
35 parser
.add_argument('--no-prompt', '-y', default
=False, action
='store_true',
36 help='do not prompt before each command')
37 parser
.add_argument('--default', default
='y', metavar
='<cmd>',
38 help='default command for empty input (default: y)')
39 parser
.add_argument('--remote', default
='origin',
40 help='remote name whose URL will used for slugs')
41 parser
.add_argument('--before-install', '-b', default
=False,
43 help='run the before_install step')
44 parser
.add_argument('--before-script', '-B', default
=False,
46 help='run the before_script step')
47 parser
.add_argument('--slug', '-s', metavar
='<slug>',
48 help='specify the URL slug')
49 parser
.add_argument('--start', metavar
='<int>', type=int, default
=0,
50 help='specify the starting command index')
51 parser
.add_argument('--test', default
=False, action
='store_true',
52 help='run built-in unit tests')
53 parser
.add_argument('--verbose', '-v', default
=False, action
='store_true',
54 help='increase verbosity')
55 return parser
.parse_args()
58 def load_yaml(filename
):
59 with core
.xopen(filename
, 'r') as f
:
63 def find_interpreter(language
, version
):
64 return distutils
.spawn
.find_executable(language
+ version
)
67 def get_platform(platform
):
68 if platform
.lower().startswith('linux'):
76 core
.stderr('error: %s' % msg
)
86 if url
.endswith('.git'):
87 url
= url
[:-len('.git')]
90 'https://github.com/',
94 for prefix
in github_prefixes
:
95 if url
.startswith(prefix
):
96 return url
[len(prefix
):]
99 # ssh+git://user:password@host/slug
100 r
'^ssh\+git://[^@]+:[^@]+@[^@/]+/([^:]+)$',
101 # ssh+git://user@host/slug
102 r
'^ssh\+git://[^@]+@[^@]+?/([^:]+)$',
103 # user@host:slug (no ports, etc
104 r
'^[^@]+@[^@]+:([^:]+)$',
106 for pattern
in patterns
:
107 rgx
= re
.compile(pattern
)
108 match
= rgx
.match(url
)
110 return match
.group(1)
115 def guess_slug(remote
):
116 key
= 'remote.%s.url' % remote
117 url
= gitcfg
.current().get(key
)
119 default_slug
= 'git-cola/git-cola'
121 slug
= parse_slug(url
) or default_slug
132 slug
= guess_slug(args
.remote
)
136 def get_current_branch():
137 return gitcmds
.current_branch()
140 def setup_environment(args
, language
, version
):
141 env
= os
.environ
.copy()
144 if not repo
.is_valid():
145 error('%s is not a Git repository' % core
.getcwd())
147 version_var
= 'TRAVIS_%s_VERSION' % language
.upper()
148 env
.setdefault(version_var
, version
)
149 env
.setdefault('CI', 'true')
150 env
.setdefault('TRAVIS', 'true')
151 env
.setdefault('CONTINUOUS_INTEGRATION', 'true')
152 env
.setdefault('DEBIAN_FRONTEND', 'noninteractive')
153 env
.setdefault('HAS_DAVID_A_SEAL_OF_APPROVAL', 'true')
154 env
.setdefault('USER', 'travis')
155 env
.setdefault('HOME', '/home/travis')
156 env
.setdefault('LANG', 'en_US.UTF-8')
157 env
.setdefault('LC_ALL', 'en_US.UTF-8')
158 env
.setdefault('RAILS_ENV', 'test')
159 env
.setdefault('RACK_ENV', 'test')
160 env
.setdefault('MERB_ENV', 'test')
161 env
.setdefault('TRAVIS_BRANCH', gitcmds
.current_branch())
162 env
.setdefault('TRAVIS_BUILD_DIR', core
.getcwd())
163 env
.setdefault('TRAVIS_BUILD_ID', '1')
164 env
.setdefault('TRAVIS_BUILD_NUMBER', '1')
165 env
.setdefault('TRAVIS_COMMIT', 'HEAD')
166 env
.setdefault('TRAVIS_COMMIT_RANGE', 'HEAD^..HEAD')
167 env
.setdefault('TRAVIS_JOB_ID', '1')
168 env
.setdefault('TRAVIS_JOB_NUMBER', '1.1')
169 env
.setdefault('TRAVIS_PULL_REQUEST', 'false')
170 env
.setdefault('TRAVIS_SECURE_ENV_VARS', 'false')
171 env
.setdefault('TRAVIS_REPO_SLUG', get_slug(args
))
172 env
.setdefault('TRAVIS_OS_NAME', get_platform(sys
.platform
))
173 env
.setdefault('TRAVIS_TAG', '')
179 return json
.dumps(obj
, sort_keys
=True, indent
=2)
182 def print_environment(environ
):
183 print('# Environment')
184 print(jsonify(environ
))
187 def expandvars(path
, environ
=None):
189 Like os.path.expandvars, but operates on a provided dictionary
191 `os.environ` is used when `environ` is None.
193 >>> expandvars('$foo', environ={'foo': 'bar'}) == 'bar'
196 >>> environ = {'foo': 'a', 'bar': 'b'}
197 >>> expandvars('$foo:$bar:${foo}${bar}', environ=environ) == 'a:b:ab'
204 environ
= os
.environ
.copy()
206 expandvars_re
= expandvars
._re
_cache
207 except AttributeError:
208 expandvars_re
= expandvars
._re
_cache
= re
.compile(r
'\$(\w+|\{[^}]*\})')
211 m
= expandvars_re
.search(path
, i
)
216 if name
.startswith('{') and name
.endswith('}'):
220 path
= path
[:i
] + environ
[name
]
229 shell_env
= os
.getenv('SHELL')
234 for sh
in ('bash', 'zsh', 'ksh', 'sh'):
235 executable
= distutils
.spawn
.find_executable(sh
)
242 class RunCommands(object):
244 def __init__(self
, state
, cmds
=None):
247 self
.idx
= state
.start
251 self
.idx
= min(self
.idx
, len(cmds
)-1)
253 def loop(self
, cmds
=None):
261 self
.eval_input(self
.read_input())
266 def show_initial_prompt(self
, title
=None):
268 prompt
= '# %s %s' % (state
.language
.title(), state
.version
)
270 prompt
+= ' - ' + title
272 dashes
= '#' * len(prompt
)
278 def print_help(self
):
282 #<command> ... repeat <command> # times, ex: "5s" skips 5
283 y, r ... yes/run current command (default)
284 b, k, p ... back/prev
285 f, j, n, s ... forward/next/no/skip
286 g, goto # ... goto command at index
287 rw, begin ... start over from the beginning
288 ff, end ... fast-forward to end
289 ls, list ... list commands
290 shell ... enter a subshell (also "bash", "zsh")
291 cd <dir> ... change directories
292 env ... print config-provided environment variables
293 environ ... print current environment variables
294 pwd ... print current working directory
295 h, help ... show help
299 def prep_command(self
, cmd
):
300 sudo
= self
.state
.sudo
301 if not sudo
and cmd
.startswith('sudo '):
302 cmd
= cmd
[len('sudo '):]
305 def current_command(self
):
306 cmd
= self
.cmds
[self
.idx
]
307 return self
.prep_command(cmd
)
309 def expandvars(self
, string
):
310 return expandvars(string
, environ
=self
.state
.environ
)
312 def show_prompt(self
):
314 prompt
= state
.prompt
316 cmd
= self
.current_command()
317 expanded
= self
.expandvars(cmd
)
318 print('$ %s' % expanded
)
320 sys
.stdout
.write('? ')
322 def read_input(self
):
323 if self
.state
.prompt
:
324 answer
= sys
.stdin
.readline().strip()
329 def chdir(self
, directory
):
331 self
.state
.environ
['PWD'] = os
.getcwd()
334 top
= len(self
.cmds
) - 1
335 self
.idx
= max(0, min(idx
, top
))
341 self
.goto(self
.idx
+ 1)
344 self
.goto(self
.idx
- 1)
349 def fast_forward(self
):
350 self
.goto(len(self
.cmds
) - 1)
353 return self
.idx
== len(self
.cmds
) - 1
355 def list_commands(self
):
356 for idx
, cmd
in enumerate(self
.cmds
):
357 cmd
= self
.prep_command(cmd
)
362 print('%03d - %s %s' % (idx
, decoration
, self
.expandvars(cmd
)))
364 def show_status(self
):
365 cmd
= self
.current_command()
366 print('%03d - $ %s' % (self
.idx
, self
.expandvars(cmd
)))
368 def run_current_command(self
):
370 dry_run
= state
.dry_run
372 cmd
= self
.current_command()
373 argv
= shlex
.split(cmd
)
375 error('empty command at index %s' % self
.idx
- 1)
377 if len(argv
) == 2 and argv
[0] == 'cd':
378 directory
= self
.expandvars(argv
[1])
379 self
.chdir(directory
)
383 self
.run_command(cmd
)
385 def run_command(self
, cmd
):
386 environ
= self
.state
.environ
387 status
, out
, err
= core
.run_command(cmd
, env
=environ
, shell
=True,
388 stdin
=None, stdout
=None,
390 expanded
= self
.expandvars(cmd
)
391 print('The command "%s" exited with %d.' % (expanded
, status
))
393 self
.errors
.append(expanded
)
395 def run_shell(self
, shell
):
396 self
.run_command(shell
)
398 def eval_input(self
, answer
):
399 # Accept the action by default
401 answer
= self
.state
.default
403 words
= shlex
.split(answer
)
406 # Multi-command, e.g. "5s" skips 5 commands
407 rgx
= re
.compile('^(\d+)(.*)$')
409 if answer
in ('f', 'j', 'n', 'N', 'next', 'no', 's', 'skip'):
413 elif answer
in ('b', 'k', 'p', 'back', 'prev'):
417 elif answer
in ('r', 'y', 'Y', 'yes', 'run'):
418 self
.run_current_command()
421 elif answer
in ('rw', 'rewind'):
424 elif answer
in ('ff', 'last', 'end'):
427 elif answer
in ('ls', 'list'):
430 elif answer
in ('shell',):
431 self
.run_shell(default_shell())
433 elif answer
in ('bash', 'sh', 'zsh', 'ash', 'dash', 'ksh', 'csh'):
434 self
.run_shell(answer
)
436 elif answer
in ('st', 'status'):
439 elif answer
in ('pwd',):
442 elif answer
in ('env',):
443 print_environment(self
.state
.env
)
445 elif answer
in ('environ',):
446 print_environment(self
.state
.environ
)
448 elif first
in ('g', 'go', 'goto'):
449 self
.goto(int(words
[1]))
450 elif first
in ('cd', 'chdir'):
451 self
.chdir(self
.expandvars(words
[1]))
452 elif answer
in ('h', 'help', '?'):
455 elif answer
in ('q', 'quit'):
458 elif rgx
.match(answer
):
459 match
= rgx
.match(answer
)
460 count
= match
.group(1)
461 action
= match
.group(2)
462 for i
in range(int(count
)):
463 self
.eval_input(action
)
466 print('error: %s: unknown command')
472 def __init__(self
, **kwargs
):
473 for k
, v
in kwargs
.items():
478 def expand_config_environment(envs
, environ
):
479 # Operate on a copy so that we can add to it as we expand the variables.
480 # This allows subsequent variables to reference previous variables.
482 environ
= environ
.copy()
486 # Entries look like "VAR1=x VAR2=x$VAR1" so we split using
487 # the shell lexer to get the individual VAR=value entries
488 env_entries
= shlex
.split(env
)
489 for entry
in env_entries
:
490 key
, value
= entry
.split('=', 1)
491 expanded_value
= expandvars(value
, environ
=environ
)
492 # Add to the returned environment
493 env_values
[key
] = expanded_value
494 # Also add to the outer environ to allow subsequent lookups
495 # to use a previously defined value
496 environ
[key
] = expanded_value
497 expanded
.append(env_values
)
499 # Ensure that we have at least a single environment
506 def build_version(args
, data
, language
, version
):
508 environ
= setup_environment(args
, language
, version
)
510 # Get the possibly config-specified execution environments
511 envs
= expand_config_environment(data
.get('env', []), environ
)
513 environ
= environ
.copy()
516 state
= State(default
=args
.default
,
517 dry_run
=args
.dry_run
,
521 prompt
=not args
.no_prompt
,
524 verbose
=args
.verbose
,
528 print_environment(environ
)
531 if args
.before_install
:
532 cmds
.extend(data
.get('before_install', []))
533 cmds
.extend(data
.get('install', []))
535 if args
.before_script
:
536 cmds
.extend(data
.get('before_script', []))
537 cmds
.extend(data
.get('script', []))
539 run_commands
= RunCommands(state
, cmds
=cmds
)
540 run_commands
.show_initial_prompt()
542 errors
.extend(run_commands
.errors
)
547 def build(args
, data
):
548 language
= data
['language']
549 if args
.build_versions
:
550 versions
= args
.build_versions
552 versions
= data
[language
]
554 if args
.skip_missing
:
555 versions
= [v
for v
in versions
if find_interpreter(language
, v
)]
559 for version
in versions
:
560 errors
= build_version(args
, data
, language
, version
)
567 class TravisBuildTestCase(unittest
.TestCase
):
569 def test_parse_slug(self
):
571 'https://github.com/example/slug',
572 'https://github.com/example/slug.git',
573 'git://github.com/example/slug',
574 'git://github.com/example/slug.git',
575 'git@github.com:example/slug',
576 'git@github.com:example/slug.git',
577 'git@example.com:example/slug',
578 'git@example.com:example/slug.git',
579 'ssh+git://git@example.com/example/slug.git',
580 'ssh+git://user:password@example.com/example/slug.git',
581 'ssh+git://user:password@example.com:66/example/slug.git',
583 expect
= 'example/slug'
585 actual
= parse_slug(url
)
586 self
.assertEqual(expect
, actual
)
590 suite
= unittest
.TestLoader().loadTestsFromTestCase(TravisBuildTestCase
)
591 runner
= unittest
.TextTestRunner(verbosity
=2)
592 result
= runner
.run(suite
)
593 if result
.errors
or result
.failures
:
602 signal
.signal(signal
.SIGINT
, signal
.SIG_DFL
)
607 data
= load_yaml(args
.filename
)
608 return build(args
, data
)
611 if __name__
== '__main__':