aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRagnis Armus <ragnis@armus.ee>2019-05-22 20:46:00 +0300
committerRagnis Armus <ragnis@armus.ee>2019-05-22 20:46:00 +0300
commitb9da8259e79e8888dc81cf2da1013b1ee7f4eec7 (patch)
tree677d27947650a39a4eab651fe259308359f0e472
First commit
-rw-r--r--LICENSE21
-rw-r--r--README.md24
-rwxr-xr-xggoto171
3 files changed, 216 insertions, 0 deletions
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..f9e47fc
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2019 Ragnis Armus
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..830b2cd
--- /dev/null
+++ b/README.md
@@ -0,0 +1,24 @@
+# ggoto
+
+ggoto is a helper for quickly navigating between Git branches.
+
+Examples:
+
+ # Fetch branch "foo" from origin, update the local "foo" branch, and
+ # finally checkout it:
+ ggoto foo
+
+ # Same as above, but acts on the currently checked out branch.
+ ggoto
+
+ # Fetch origin/foo and checkout it:
+ ggoto origin/foo
+
+After checking out a branch, ggoto will also perform some additional actions:
+
+- If there is a package.json file, and it changed during checkout, then ggoto
+ executes `npm i`.
+
+- If there is a composer.json file, and it changed during checkout, then ggoto
+ executes `composer install`. If the file did not change, then ggoto executes
+ `composer dumpautoload`.
diff --git a/ggoto b/ggoto
new file mode 100755
index 0000000..130d826
--- /dev/null
+++ b/ggoto
@@ -0,0 +1,171 @@
+#!/usr/bin/python3
+
+import hashlib
+import os.path
+import subprocess
+import sys
+
+
+def get_file_sha1sum(filename):
+ hash = hashlib.sha1()
+
+ try:
+ with open(filename, 'rb') as f:
+ for chunk in iter(lambda: f.read(4096), b''):
+ hash.update(chunk)
+ except FileNotFoundError:
+ return None
+
+ return hash.hexdigest()
+
+
+class FileStates:
+ def __init__(self, filenames=None):
+ self.states = {}
+
+ if filenames:
+ for f in filenames:
+ self.remember(f)
+
+
+ def remember(self, filename):
+ self.states[filename] = get_file_sha1sum(filename)
+
+
+ def changed(self, filename):
+ sum = get_file_sha1sum(filename)
+ prev = self.states.get(filename, None)
+ return sum != prev
+
+
+ def any_changed(self, filenames):
+ for f in filenames:
+ if self.changed(f):
+ return True
+ return False
+
+
+def parse_args(argv):
+ rv = {
+ 'force': False,
+ 'help': False,
+ }
+
+ for i, arg in enumerate(argv):
+ if arg == '-f':
+ rv['force'] = True
+ elif arg == '-h' or arg == '--help':
+ rv['help'] = True
+ elif 'ref' not in rv:
+ rv['ref'] = arg
+ else:
+ raise ValueError('unexpected argument {} at position #{}'.format(arg, i))
+
+ return rv
+
+
+def parse_git_ref(ref):
+ split = ref.split('/', 1)
+
+ if len(split) == 2:
+ return (split[0], split[1])
+ else:
+ return (None, split[0])
+
+
+def exec(args):
+ cp = subprocess.run(args,
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL)
+ cp.check_returncode()
+ return cp
+
+
+def git_fetch(remote, rev):
+ exec(['git', 'fetch', remote, rev])
+
+
+def git_pull(remote, rev):
+ exec(['git', 'pull', '--rebase', remote, rev])
+
+
+def git_checkout(rev, force=False):
+ args = []
+
+ if force:
+ args.append('-f')
+
+ exec(['git', *args, 'checkout', rev])
+
+
+def git_get_symbolic_ref(name):
+ """
+ Get the short symbolic ref for a name. Returns None if name does not have a
+ symbolic ref.
+ """
+ cp = subprocess.run(['git', 'symbolic-ref', '--short', name],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.DEVNULL,
+ text=True)
+
+ if cp.returncode == 0:
+ return cp.stdout.strip()
+
+
+def needs_npm_install(file_states=None):
+ if not os.path.isfile('package.json'):
+ return False
+
+ if file_states is None:
+ return False
+
+ return file_states.any_changed([
+ 'package.json',
+ 'package-lock.json',
+ ])
+
+
+def needs_composer_install(file_states=None):
+ if not os.path.isfile('composer.json'):
+ return False
+
+ if file_states is None:
+ return False
+
+ return file_states.any_changed([
+ 'composer.json',
+ 'composer.lock',
+ ])
+
+
+if __name__ == '__main__':
+ args = parse_args(sys.argv[1:])
+ file_states = FileStates([
+ 'package.json',
+ 'package-lock.json',
+ ])
+
+ if 'ref' in args:
+ (remote, rev) = parse_git_ref(args['ref'])
+ else:
+ remote = None
+ rev = git_get_symbolic_ref('HEAD')
+
+ if not rev:
+ print('HEAD is not at a symbolic ref', file=sys.stderr)
+ sys.exit(1)
+
+ if remote:
+ git_fetch(remote, rev)
+ git_checkout(remote + '/' + rev, args['force'])
+ else:
+ git_checkout(rev, args['force'])
+ git_pull('origin', rev)
+
+ if needs_npm_install(file_states):
+ exec(['npm', 'i'])
+
+ if needs_composer_install(file_states):
+ exec(['composer', '--ignore-platform-reqs', 'install'])
+ elif os.path.isfile('composer.json'):
+ exec(['composer', '--ignore-platform-reqs', 'dumpautoload'])