├── .gitignore ├── README.md ├── build └── build ├── example.json ├── get ├── get-min └── get.sublime-project /.gitignore: -------------------------------------------------------------------------------- 1 | get.sublime-workspace 2 | 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Get 2 | === 3 | 4 | Description 5 | ----------- 6 | 7 | Get is a simple script for fetching and updating a collection of git repositories and adding them to your path. It is intended for use in build scripts to ensure you always have the latest version of your toolchain when running builds. 8 | 9 | Get should work equally well on Linux and OS X systems but has only been tested on OS X. 10 | 11 | Usage 12 | ----- 13 | 14 | Using get is simply a matter of adding something like the following line to the beginning of your build script. 15 | 16 | $( curl https://raw.github.com/jbmorley/get/master/get-min | python - ) 17 | 18 | Profiles specify the repositories to add to your path. They can be provided inline, or as a local or remote path (see below). 19 | 20 | If you plan to use get as part of a Hudson or Jenkins build system, don't forget to explicitly source your .bash_profile (or equivalent) to ensure all your other tools are on the path. 21 | 22 | source ~/.bash_profile 23 | $( curl https://raw.github.com/jbmorley/get/master/get-min | python - ) 24 | 25 | Profiles 26 | -------- 27 | 28 | Profiles are specified as a JSON mapping of repository name to path: 29 | 30 | { 31 | "neko": "git@github.com:jbmorley/neko.git", 32 | "mnfy": "git@github.com:brettcannon/mnfy.git" 33 | } 34 | 35 | Get supports inline, local and remote paths for profiles. 36 | 37 | ### Inline 38 | 39 | $( curl https://raw.github.com/jbmorley/get/master/get-min | python - '{ "neko": "git@github.com:jbmorley/neko.git" }' ) 40 | 41 | ### Local 42 | 43 | $( curl https://raw.github.com/jbmorley/get/master/get-min | python - profile.json ) 44 | 45 | ### Remote 46 | 47 | $( curl https://raw.github.com/jbmorley/get/master/get-min | python - https://raw.github.com/jbmorley/get/master/example.json ) 48 | 49 | ### Default 50 | 51 | If no profile is specified then the default local path of `~/.get/default.json` will be used. 52 | 53 | 54 | Future 55 | ------ 56 | 57 | * Add some automated tests to ensure future development doesn't break the supported range of profile types. 58 | * Check support for cloning local repositories. This should work but requires testing. 59 | -------------------------------------------------------------------------------- /build/build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 3 | 4 | pushd $DIR/.. 5 | mnfy.py get > get-min || exit 1 6 | popd 7 | -------------------------------------------------------------------------------- /example.json: -------------------------------------------------------------------------------- 1 | { 2 | "neko": "git@github.com:jbmorley/neko.git", 3 | "mnfy": "git@github.com:brettcannon/mnfy.git" 4 | } 5 | -------------------------------------------------------------------------------- /get: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright (c) 2013 Jason Barrie Morley. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | 24 | import os 25 | import subprocess 26 | import argparse 27 | import json 28 | import urllib2 29 | import base64 30 | import sys 31 | 32 | DEFAULT_PROFILE = "~/.get/default.json" 33 | 34 | 35 | def log(message): 36 | if (len(message) > 0): 37 | sys.stderr.write('%s\n' % message) 38 | 39 | 40 | # Rudimentary class for manipulating the path. 41 | # N.B. This is Mac-specific and will need to be updated with special-case 42 | # behaviour for various platforms. 43 | class Path(): 44 | 45 | def __init__(self): 46 | self.items = os.environ["PATH"].split(":") 47 | 48 | def prepend(self, item): 49 | if (not item in self): 50 | self.items.insert(0, item); 51 | 52 | def append(self, item): 53 | if (not item in self): 54 | self.items.append(item) 55 | 56 | def __str__(self): 57 | return ":".join(self.items) 58 | 59 | def __contains__(self, item): 60 | return item in self.items 61 | 62 | 63 | class Chdir(): 64 | 65 | def __init__(self, directory): 66 | self.directory = directory 67 | 68 | def __enter__(self): 69 | self.previous = os.getcwd() 70 | os.chdir(self.directory) 71 | log("Changing directory to '%s'..." % self.directory) 72 | return self 73 | 74 | def __exit__(self, exc_type, exc_val, exc_tb): 75 | log("Restoring directory to '%s'..." % self.previous) 76 | os.chdir(self.previous) 77 | 78 | 79 | def git(parameters): 80 | try: 81 | command = ["git"] + parameters 82 | log("Running: %s" % " ".join(command)) 83 | output = subprocess.check_output(command) 84 | lines = output.split("\n") 85 | for line in lines: 86 | log(line) 87 | except: 88 | log("Unable to clone repository.") 89 | exit(1) 90 | 91 | 92 | def add_repository(url, directory): 93 | 94 | # Expand the path. 95 | target = os.path.expanduser(directory) 96 | target = os.path.abspath(target) 97 | url = os.path.expanduser(url) 98 | 99 | # Ensure the parent path exists. 100 | (parent, file) = os.path.split(target) 101 | if (not os.path.exists(parent)): 102 | os.makedirs(parent) 103 | 104 | # Check to see if the repository exists. 105 | # If the repository exists, attempt to git update. 106 | # If the repository does not exist, check it out. 107 | 108 | if (not os.path.exists(target)): 109 | with Chdir(parent): 110 | log("Checking out repository...") 111 | git(["clone", url, target]) 112 | else: 113 | with Chdir(target): 114 | log("Updating the repository...") 115 | git(["pull"]) 116 | 117 | return target 118 | 119 | def read_profile(filename): 120 | with open(filename) as f: 121 | profile = f.read() 122 | profile = json.loads(profile) 123 | return profile 124 | 125 | 126 | def main(): 127 | parser = argparse.ArgumentParser(description = "Clone or update git repositories and ensure they are available on the path.") 128 | parser.add_argument("profile", nargs = '?', default = DEFAULT_PROFILE, help = "JSON encoded profile as a string, file or URL. If no profile is specified then the default local path of ~/.get/default.json will be used") 129 | parser.add_argument("-u", "--username", help = "Username for basic HTTP authentication") 130 | parser.add_argument("-p", "--password", help = "Username for basic HTTP authentication") 131 | parser.add_argument("-P", "--persist", action = "store_true", default = False, help = "Persist the path modifications by appending to ~/.bash_profile") 132 | parser.add_argument("-s", "--save", action = "store_true", default = False, help = "Merge the profile into the default profile for future updates") 133 | options = parser.parse_args() 134 | 135 | profile = None 136 | 137 | # First attempt to load the profile as a JSON string. 138 | try: 139 | profile = json.loads(options.profile) 140 | except: 141 | 142 | # If the profile fails to load as JSON attempt to load it as a file. 143 | file = os.path.expanduser(options.profile) 144 | try: 145 | profile = read_profile(file) 146 | except: 147 | 148 | # If the profile fails to load from the file, fetch it as a URL. 149 | request = urllib2.Request(options.profile) 150 | 151 | # Get the username. 152 | username = options.username 153 | try: 154 | if not username: 155 | username = os.environ['GET_USERNAME'] 156 | except: 157 | pass 158 | 159 | # Get the password. 160 | password = options.password 161 | try: 162 | if not password: 163 | password = os.environ['GET_PASSWORD'] 164 | except: 165 | pass 166 | 167 | # Add the log in details if specified. 168 | # These can be specified on the command line or as environment variables. 169 | if (username and password): 170 | base64string = base64.encodestring('%s:%s' % (username, password)).replace('\n', '') 171 | request.add_header("Authorization", "Basic %s" % base64string) 172 | 173 | # Fetch the URL. 174 | try: 175 | response = urllib2.urlopen(request) 176 | profile = response.read() 177 | profile = json.loads(profile) 178 | except: 179 | pass 180 | 181 | # Check to see if we've successfully loaded a profile. 182 | if not profile: 183 | log("Unable to load profile '%s'." % options.profile) 184 | sys.exit(0) 185 | 186 | repositories = [] 187 | 188 | # Clone or update the repositories. 189 | for key, value in profile.iteritems(): 190 | repositories.append(add_repository(value, "~/.get/%s" % key)) 191 | 192 | # Get the current path. 193 | path = Path() 194 | 195 | # Update .bash_profile if the user elects to persist the path. 196 | exports = [] 197 | if options.persist: 198 | for repository in repositories: 199 | if not repository in path: 200 | exports.append("export PATH=%s:$PATH\n" % repository) 201 | 202 | if len(exports) > 0: 203 | with open(os.path.expanduser('~/.bash_profile'), 'a') as file: 204 | file.write("\n") 205 | for export in exports: 206 | file.write(export) 207 | 208 | # Add the repositories to the path and echo an export statement so it can be sourced. 209 | for repository in repositories: 210 | path.prepend(repository) 211 | print ('export PATH=%s' % path) 212 | 213 | # Save the profile if requested (and we're not already using the default profile) 214 | if options.save and options.profile != DEFAULT_PROFILE: 215 | log("Saving profile...") 216 | default = {} 217 | default_filename = os.path.expanduser(DEFAULT_PROFILE) 218 | try: 219 | default = read_profile(default_filename) 220 | except: 221 | pass 222 | default = dict(default.items() + profile.items()) 223 | with open(default_filename, 'w') as file: 224 | json.dump(default, file) 225 | 226 | 227 | if __name__ == "__main__": 228 | main() 229 | 230 | -------------------------------------------------------------------------------- /get-min: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import argparse 4 | import json 5 | import urllib2 6 | import base64 7 | import sys 8 | DEFAULT_PROFILE='~/.get/default.json' 9 | def log(message): 10 | if len(message)>0:sys.stderr.write('%s\n'%message) 11 | class Path: 12 | def __init__(self):self.items=os.environ['PATH'].split(':') 13 | def prepend(self,item): 14 | if not item in self:self.items.insert(0,item) 15 | def append(self,item): 16 | if not item in self:self.items.append(item) 17 | def __str__(self):return ':'.join(self.items) 18 | def __contains__(self,item):return item in self.items 19 | class Chdir: 20 | def __init__(self,directory):self.directory=directory 21 | def __enter__(self):self.previous=os.getcwd();os.chdir(self.directory);log("Changing directory to '%s'..."%self.directory);return self 22 | def __exit__(self,exc_type,exc_val,exc_tb):log("Restoring directory to '%s'..."%self.previous);os.chdir(self.previous) 23 | def git(parameters): 24 | try: 25 | command=['git']+parameters;log('Running: %s'%' '.join(command));output=subprocess.check_output(command);lines=output.split('\n') 26 | for line in lines:log(line) 27 | except:log('Unable to clone repository.');exit(1) 28 | def add_repository(url,directory): 29 | target=os.path.expanduser(directory);target=os.path.abspath(target);url=os.path.expanduser(url);parent,file=os.path.split(target) 30 | if not os.path.exists(parent):os.makedirs(parent) 31 | if not os.path.exists(target): 32 | with Chdir(parent):log('Checking out repository...');git(['clone',url,target]) 33 | else: 34 | with Chdir(target):log('Updating the repository...');git(['pull']) 35 | return target 36 | def read_profile(filename): 37 | with open(filename) as f:profile=f.read();profile=json.loads(profile) 38 | return profile 39 | def main(): 40 | parser=argparse.ArgumentParser(description='Clone or update git repositories and ensure they are available on the path.');parser.add_argument('profile',nargs='?',default=DEFAULT_PROFILE,help='JSON encoded profile as a string, file or URL. If no profile is specified then the default local path of ~/.get/default.json will be used');parser.add_argument('-u','--username',help='Username for basic HTTP authentication');parser.add_argument('-p','--password',help='Username for basic HTTP authentication');parser.add_argument('-P','--persist',action='store_true',default=False,help='Persist the path modifications by appending to ~/.bash_profile');parser.add_argument('-s','--save',action='store_true',default=False,help='Merge the profile into the default profile for future updates');options=parser.parse_args();profile=None 41 | try:profile=json.loads(options.profile) 42 | except: 43 | file=os.path.expanduser(options.profile) 44 | try:profile=read_profile(file) 45 | except: 46 | request=urllib2.Request(options.profile);username=options.username 47 | try: 48 | if not username:username=os.environ['GET_USERNAME'] 49 | except:pass 50 | password=options.password 51 | try: 52 | if not password:password=os.environ['GET_PASSWORD'] 53 | except:pass 54 | if username and password:base64string=base64.encodestring('%s:%s'%(username,password)).replace('\n','');request.add_header('Authorization','Basic %s'%base64string) 55 | try:response=urllib2.urlopen(request);profile=response.read();profile=json.loads(profile) 56 | except:pass 57 | if not profile:log("Unable to load profile '%s'."%options.profile);sys.exit(0) 58 | repositories=[] 59 | for key,value in profile.iteritems():repositories.append(add_repository(value,'~/.get/%s'%key)) 60 | path=Path();exports=[] 61 | if options.persist: 62 | for repository in repositories: 63 | if not repository in path:exports.append('export PATH=%s:$PATH\n'%repository) 64 | if len(exports)>0: 65 | with open(os.path.expanduser('~/.bash_profile'),'a') as file: 66 | file.write('\n') 67 | for export in exports:file.write(export) 68 | for repository in repositories:path.prepend(repository) 69 | print('export PATH=%s'%path) 70 | if options.save and options.profile!=DEFAULT_PROFILE: 71 | log('Saving profile...');default={};default_filename=os.path.expanduser(DEFAULT_PROFILE) 72 | try:default=read_profile(default_filename) 73 | except:pass 74 | default=dict(default.items()+profile.items()) 75 | with open(default_filename,'w') as file:json.dump(default,file) 76 | if __name__=='__main__':main() 77 | -------------------------------------------------------------------------------- /get.sublime-project: -------------------------------------------------------------------------------- 1 | { 2 | "folders": 3 | [ 4 | { 5 | "path": "." 6 | } 7 | ] 8 | } 9 | --------------------------------------------------------------------------------