├── LICENSE ├── git-abort ├── git-amend-to-now ├── git-branch-tree ├── git-buddy ├── git-chain ├── git-continue ├── git-declone ├── git-fancy-branch-list ├── git-find-blob ├── git-fixes-commit-msg-hook ├── git-fixup-to-last ├── git-flip-history ├── git-fzf-diff ├── git-list-clones ├── git-mass-branch-rename ├── git-mru-branch ├── git-multi-cherry-pick ├── git-new-project ├── git-original-rev ├── git-range-compare ├── git-rebase-auto-sink ├── git-rebase-cmd ├── git-reftrack ├── git-remote-clone ├── git-remote-push ├── git-retext ├── git-set-email ├── git-skip ├── git-trash ├── git-undo-amend ├── hooks └── pre-push └── stg-rebase-i /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Dan Aloni 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | -------------------------------------------------------------------------------- /git-abort: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Abort a rebase, merge, `am`, a cherry-pick or a revert, depending on the situation. 4 | 5 | set -e 6 | git_dir="$(git rev-parse --git-dir)" 7 | 8 | if [[ -e "${git_dir}/CHERRY_PICK_HEAD" ]] ; then 9 | exec git cherry-pick --abort "$@" 10 | elif [[ -e "${git_dir}/REVERT_HEAD" ]] ; then 11 | exec git revert --abort "$@" 12 | elif [[ -e "${git_dir}/rebase-apply/applying" ]] ; then 13 | exec git am --abort "$@" 14 | elif [[ -e "${git_dir}/rebase-apply" ]] ; then 15 | exec git rebase --abort "$@" 16 | elif [[ -e "${git_dir}/rebase-merge" ]] ; then 17 | exec git rebase --abort "$@" 18 | elif [[ -e "${git_dir}/MERGE_MODE" ]] ; then 19 | exec git merge --abort "$@" 20 | else 21 | echo git-abort: unknown state >&2 22 | exit -1 23 | fi 24 | -------------------------------------------------------------------------------- /git-amend-to-now: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | exec git commit --amend --no-edit --date="$(date -R)" 4 | -------------------------------------------------------------------------------- /git-branch-tree: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | 3 | """ 4 | A very slow version of 'git branch' that shows a tree, according 5 | to which branch is an ancestor of which other branch. 6 | """ 7 | 8 | import os 9 | import pty 10 | import subprocess as sp 11 | import sys 12 | import re 13 | 14 | def main(): 15 | master, slave = pty.openpty() 16 | cmd = ["git", "branch"] + sys.argv[1:] 17 | tstP = sp.Popen(cmd, 18 | stdin=slave, stdout=slave, 19 | stderr=sp.STDOUT, close_fds=True) 20 | os.close(slave) 21 | f = os.fdopen(master) 22 | data = f.read() 23 | 24 | branches = [] 25 | for line in data.splitlines(): 26 | m = re.match(r"^([ *]+)([\x1b][\[][^m]+m)?([^\x1b]*)([\x1b][\[][^m]*m)?$", line) 27 | (prefix, ansi1, branch_name, ansi2) = m.groups() 28 | if not ansi2: 29 | ansi2 = "" 30 | if not ansi1: 31 | ansi1 = "" 32 | branches.append((branch_name, (prefix, ansi1, ansi2), [])) 33 | 34 | xbranches = [] 35 | for (branch_name, info, lst) in list(branches): 36 | parents = [] 37 | for (other_branch_name, _, _) in branches: 38 | if other_branch_name != branch_name: 39 | cmd = "git merge-base --is-ancestor %s %s" % (other_branch_name, branch_name) 40 | r = os.system(cmd) 41 | if r == 0: 42 | f = os.popen("git log %s..%s --oneline | wc -l" % (other_branch_name, branch_name), "r") 43 | distance = int(f.read()) 44 | parents.append((distance, other_branch_name)) 45 | if not parents: 46 | xbranches.append((branch_name, info, lst)) 47 | else: 48 | parents.sort() 49 | for (other_branch_name, _, other_lst) in list(branches): 50 | if other_branch_name == parents[0][1]: 51 | other_lst.append((branch_name, info, lst)) 52 | break 53 | 54 | def f(lst, level): 55 | for (branch_name, (prefix, ansi1, ansi2), subs) in lst: 56 | print prefix + level + ansi1 + branch_name + ansi2 57 | if subs: 58 | f(subs, level = level + " ") 59 | 60 | f(xbranches, level="") 61 | 62 | if __name__ == "__main__": 63 | main() 64 | -------------------------------------------------------------------------------- /git-buddy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | Bidirectional sync of local branches with another repository. 5 | 6 | This looks whether each is fast forward of each other, and if not, does nothing. 7 | 8 | 1) Useful when you have two dev environments that need syncing, and you don't want 9 | to use rsync. 10 | 2) Remote name to sync is configured as buddy.other. 11 | """ 12 | 13 | import sys 14 | import os 15 | import tempfile 16 | import re 17 | from optparse import OptionParser 18 | 19 | def system(cmd): 20 | return os.system(cmd) 21 | 22 | class Abort(Exception): pass 23 | 24 | def get_other(): 25 | other = os.popen("git config buddy.other", "r").read().strip() 26 | if other == "": 27 | raise Abort("buddy not configured") 28 | return other 29 | 30 | def e_system(cmd): 31 | r = system(cmd) 32 | if r != 0: 33 | raise Abort(f"command {cmd} failed: {r}"); 34 | 35 | def cmd_set(remote_name): 36 | e_system(f"git config buddy.other {remote_name}") 37 | e_system(f"git config receive.denyCurrentBranch updateInstead") 38 | 39 | def cmd_sync(*args): 40 | parser = OptionParser() 41 | 42 | # Take newer branch if exists in both ends 43 | parser.add_option("-f", "--force", dest="force", action="store_true") 44 | 45 | # Delete remote branches that don't locally exist 46 | parser.add_option("-o", "--out", dest="out", action="store_true") 47 | 48 | (options, _args) = parser.parse_args(list(args)) 49 | 50 | other = get_other() 51 | 52 | e_system(f'git fetch -p {other}') 53 | 54 | remote_refs = {} 55 | local_refs = {} 56 | remote_re = re.compile(f"^([^ ]+) refs/remotes/{other}/(.*)") 57 | local_re = re.compile(f"([^ ]+) refs/heads/(.*)") 58 | 59 | for ref in os.popen("git show-ref", "r"): 60 | ref = ref.strip() 61 | m = remote_re.search(ref) 62 | if m: 63 | (ref, name) = m.groups(0) 64 | remote_refs[name] = ref 65 | 66 | m = local_re.search(ref) 67 | if m: 68 | (ref, name) = m.groups(0) 69 | local_refs[name] = ref 70 | 71 | branches = list(set(remote_refs.keys()).union(set(local_refs.keys()))) 72 | branches.sort() 73 | 74 | update = [] 75 | 76 | for item in branches: 77 | if item in remote_refs and item not in local_refs: 78 | if options.out: 79 | update.append(("remote", "remove", item, remote_refs[item])) 80 | else: 81 | update.append(("local", "new", item, remote_refs[item])) 82 | elif item not in remote_refs and item in local_refs: 83 | update.append(("remote", "new", item, local_refs[item])) 84 | else: 85 | if remote_refs[item] == local_refs[item]: 86 | update.append((None, None, item, remote_refs[item])) 87 | continue 88 | 89 | ret = os.system(f"git merge-base --is-ancestor {local_refs[item]} {remote_refs[item]}") 90 | if ret == 0: 91 | update.append(("local", "ff", item, local_refs[item])) 92 | else: 93 | ret = os.system(f"git merge-base --is-ancestor {remote_refs[item]} {local_refs[item]}") 94 | if ret == 0: 95 | update.append(("remote", "ff", item, remote_refs[item])) 96 | else: 97 | remote_date = int(os.popen(f"git show {remote_refs[item]} --no-patch --pretty='%ct'").read().strip()) 98 | local_date = int(os.popen(f"git show {local_refs[item]} --no-patch --pretty='%ct'").read().strip()) 99 | if remote_date > local_date: 100 | update.append(("remote", "override", item, remote_refs[item])) 101 | elif remote_date < local_date: 102 | update.append(("local", "override", item, remote_refs[item])) 103 | else: 104 | update.append(("???", "override", item, remote_refs[item])) 105 | 106 | 107 | fetch = f"git fetch {other}" 108 | fetch_force = f"git fetch -f {other}" 109 | push = f"git push {other}" 110 | push_force = f"git push -f {other}" 111 | fetches = 0 112 | force_fetches = 0 113 | pushes = 0 114 | force_pushes = 0 115 | 116 | for (direction, kind, item, ref) in update: 117 | if not direction: 118 | continue 119 | if direction == "local" and kind in ["ff", "new"]: 120 | fetches += 1 121 | fetch += f" {item}:{item}" 122 | if direction == "remote" and kind in ["ff", "new", "remove"]: 123 | pushes += 1 124 | if kind == "remove": 125 | push += f" :{item}" 126 | else: 127 | push += f" {item}:{item}" 128 | if kind == "override": 129 | if direction == "remote": 130 | print(f"{item}: not fast-forward, remote is newer") 131 | force_fetches += 1 132 | fetch_force += f" {item}:{item}" 133 | elif direction == "local": 134 | print(f"{item}: not fast-forward, local is newer") 135 | force_pushes += 1 136 | push_force += f" {item}:{item}" 137 | else: 138 | print(f"{item}: not fast-forward, not sure what is newer") 139 | 140 | if fetches: 141 | print("Syncing local") 142 | os.system(fetch) 143 | if pushes: 144 | print("Syncing remote") 145 | os.system(push) 146 | if options.force: 147 | if force_fetches: 148 | print("Force syncing local") 149 | os.system(fetch_force) 150 | if force_pushes: 151 | print("Force syncing remote") 152 | os.system(push_force) 153 | 154 | def cmd_rm(args): 155 | other = get_other() 156 | delete_remote = f"git push {other} --delete" 157 | delete_local = f"git branch -D" 158 | 159 | for branch in args: 160 | delete_remote += f" {branch}" 161 | delete_local += f" {branch}" 162 | 163 | e_system(delete_remote) 164 | e_system(delete_local) 165 | 166 | def main(): 167 | parser = OptionParser() 168 | if len(sys.argv) == 1: 169 | print(" set - set remote buddy name") 170 | print(" sync - bidirectional sync on both ends") 171 | print(" rm - remove given branches on both ends") 172 | return 173 | if sys.argv[1] == "set": 174 | cmd_set(sys.argv[2]) 175 | elif sys.argv[1] == "sync": 176 | cmd_sync(*sys.argv[2:]) 177 | elif sys.argv[1] == "rm": 178 | cmd_rm(sys.argv[2:]) 179 | else: 180 | print(f"unknown command {sys.argv[1]}", file=sys.stderr) 181 | sys.exit(-1) 182 | 183 | if __name__ == "__main__": 184 | main() 185 | -------------------------------------------------------------------------------- /git-chain: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from typing import NamedTuple 4 | import subprocess 5 | import datetime 6 | import time 7 | 8 | """ 9 | Helper program to handle ongoing work that is temporarily split to several 10 | commits. 11 | 12 | The purpose is to stop working with amends, and only occasionally squash 13 | tenative work together. 14 | 15 | new - Create a new as commit message with 'Git-chain: ', will pick 16 | color from palette. 17 | link - FZF-pick a chain (see below), and create a new commit for it, with 18 | the 'Git-chain: ' metadata 19 | defrag - Perform minimal re-arragment of commit history using rebase so 20 | that chains are sequitive. Show what we are going to change and 21 | confirm. There can be conflicts to resolve. 22 | kill - FZF-pick a chain, and perform git rebase to remove all its 23 | fragments. Can create conflicts. 24 | squash - Squash and short each chain to a single commit 25 | finish - FZF-pick a chain, move it to be first, then squash it, then remove 26 | the `ChainItem: ' meta-data. The move stage can cause conflicts. 27 | reword - FZF-pick a chain, edit the commit message of a chain (will edit 28 | the first commit, the other ones only keep their subject line) 29 | list - Just list the current chain fragments. 30 | 31 | ----- 32 | 33 | No args - pick one of the commands with FZF 34 | If first arg is '--', take the rest as a chain to match and then only FZF 35 | the command we want to execute on it. 36 | 37 | FZF-pick a chain, either of the following: 38 | - Collect past chains from UUIDs of recent commits from HEAD (stopping at 39 | the first one that does not have a 'ChainItem: UUID' klline) and run FZF 40 | - Uniqely figure out based on matching of commit message with input from 41 | command line 42 | 43 | 44 | """ 45 | 46 | import sys 47 | import os 48 | import tempfile 49 | import re 50 | from optparse import OptionParser 51 | import itertools 52 | import tempfile 53 | import argparse 54 | import inspect 55 | import shutil 56 | 57 | 58 | def system(cmd): 59 | return os.system(cmd) 60 | 61 | 62 | class Abort(Exception): 63 | pass 64 | 65 | 66 | def abort(msg): 67 | print(msg, file=sys.stderr) 68 | sys.exit(-1) 69 | 70 | 71 | def color24(r, g, b1): 72 | return "\x1b[38;2;%d;%d;%dm" % (r, g, b1) 73 | 74 | 75 | def backcolor24(r, g, b1): 76 | return "\x1b[48;2;%d;%d;%dm" % (r, g, b1) 77 | 78 | 79 | COLOR_RE = re.compile("^#?([a-f0-9][a-f0-9])([a-f0-9][a-f0-9])([a-f0-9][a-f0-9])$") 80 | 81 | 82 | def parse_rgb(colorstr): 83 | m = COLOR_RE.match(colorstr) 84 | m = m.groups(0) 85 | return (int(m[0], 16), int(m[1], 16), int(m[2], 16)) 86 | 87 | 88 | def colorreset(): 89 | return "\x1b[0;m" 90 | 91 | 92 | DRY_RUN = "GIT_REBASE_CMD__OPTION_DRY_RUN" 93 | 94 | 95 | class ChainItem(NamedTuple): 96 | githash: str 97 | message: str 98 | chain: str 99 | commitdate: str 100 | 101 | def subject(self) -> str: 102 | return self.message.split("\n", 1)[0] 103 | 104 | 105 | class ChainFragment(NamedTuple): 106 | items: list[ChainItem] 107 | full: bool 108 | index: int 109 | 110 | def subject(self) -> str: 111 | return self.items[0].subject() 112 | 113 | def chain(self) -> str: 114 | return self.items[0].chain 115 | 116 | def message(self) -> str: 117 | return self.items[0].message 118 | 119 | 120 | class ChainInfo(NamedTuple): 121 | frags: list[ChainFragment] 122 | colors: dict[str, str] 123 | chain_squash_count: dict[str, int] 124 | base: str 125 | 126 | def chain(self): 127 | return self.frags[0].chain() 128 | 129 | def reverse(self): 130 | frags = list(self.frags) 131 | frags.reverse() 132 | 133 | for idx, frag in enumerate(frags): 134 | l = list(frag.items) 135 | l.reverse() 136 | frags[idx] = ChainFragment(index=frag.index, full=frag.full, items=l) 137 | 138 | return ChainInfo(colors=self.colors, frags=frags, 139 | base=self.base, 140 | chain_squash_count=self.chain_squash_count) 141 | 142 | def find_first_frag(self, chain: str): 143 | for frag in self.frags: 144 | if frag.chain() == chain: 145 | return frag 146 | 147 | 148 | def get_chain_info() -> ChainInfo: 149 | data = None 150 | with os.popen("git log --oneline --pretty='%H %ct%n%B###' -n 1000") as f: 151 | data = f.read() 152 | 153 | r = re.compile("^Git-chain: ([a-f0-9-]+)$") 154 | r2 = re.compile("^Git-chain-color: #([a-f0-9]+)$") 155 | r3 = re.compile("^Git-squash-count: ([0-9]+)$") 156 | chains = [] 157 | chain_colors = {} 158 | chain_squash_count = {} 159 | base = None 160 | for commit in data.split("\n###\n"): 161 | if not commit: 162 | continue 163 | 164 | (githashdate, message) = commit.split("\n", 1) 165 | (githash, commitdate) = githashdate.split(" ") 166 | chain = None 167 | for line in message.split("\n"): 168 | m = r.match(line) 169 | if m: 170 | chain = m.groups(0)[0] 171 | m = r2.match(line) 172 | if m: 173 | chain_colors[chain] = m.groups(0)[0] 174 | m = r3.match(line) 175 | if m: 176 | chain_squash_count[chain] = int(m.groups(0)[0], 10) 177 | if not chain: 178 | base = githash 179 | break 180 | 181 | chains.append(ChainItem(githash=githash, commitdate=commitdate, message=message, chain=chain)) 182 | 183 | def f(a): 184 | return a.chain 185 | 186 | counts = dict() 187 | out = [] 188 | for (key, l) in itertools.groupby(chains, key=f): 189 | cf = ChainFragment(items=list(l), index=0, full=True) 190 | if cf.chain() not in counts: 191 | counts[cf.chain()] = 1 192 | else: 193 | counts[cf.chain()] += 1 194 | j = counts[cf.chain()] - 1 195 | cf = ChainFragment(items=cf.items, index=j, full=True) 196 | out.append(cf) 197 | 198 | for (chain, frags) in counts.items(): 199 | if frags < 2: 200 | continue 201 | out2 = [] 202 | for cf in out: 203 | if cf.chain() == chain: 204 | out2.append(ChainFragment(items=cf.items, index=cf.index, full=False)) 205 | else: 206 | out2.append(cf) 207 | out = out2 208 | 209 | return ChainInfo(frags=out, colors=chain_colors, base=base, chain_squash_count=chain_squash_count) 210 | 211 | 212 | def list_chains(): 213 | for chain in get_chain_info().frags: 214 | print(chain) 215 | 216 | 217 | 218 | class Selection(NamedTuple): 219 | githash: str 220 | chain_id: str 221 | 222 | 223 | def pick_fzf() -> Selection: 224 | chain_info = get_chain_info() 225 | fzf_options = [ 226 | "--ansi", "-e", "--no-sort", "--height=30%", "--with-nth", "3.." 227 | ] 228 | inp = [] 229 | current_time = time.time() 230 | statm = re.compile("^([^\t]+)\t([^\t]+)\t.*") 231 | red = color24(255, 0, 0) 232 | green = color24(0, 255, 0) 233 | 234 | for chain_frag in chain_info.frags: 235 | len_items = len(chain_frag.items) 236 | for (idx, item) in enumerate(chain_frag.items): 237 | color = color24(*parse_rgb(chain_info.colors.get(item.chain, "#ffffff"))) 238 | if idx == 0: 239 | if len_items == 1: 240 | prefix = "───" 241 | else: 242 | prefix = "└──" 243 | else: 244 | if idx + 1 == len_items: 245 | prefix = "┌──" 246 | else: 247 | prefix = "├──" 248 | 249 | date = int(item.commitdate, 10) 250 | time_diff = current_time - date 251 | brightness = 255 252 | if time_diff > 3600: 253 | brightness = 170 254 | if time_diff > 3600*24: 255 | brightness = 150 256 | if time_diff > 3600*24*7: 257 | brightness = 100 258 | if time_diff > 30*3600*24*7: 259 | brightness = 70 260 | datecolor = color24(brightness, brightness, brightness) 261 | date = datetime.datetime.fromtimestamp(date).strftime('%Y-%m-%d %H:%M:%S') 262 | total_added = 0 263 | total_removed = 0 264 | ministat_monochrome = [] 265 | ministat = [] 266 | with os.popen(f"git show {item.githash} --pretty='' --numstat") as p: 267 | for line in p: 268 | m = statm.match(line) 269 | if not m: 270 | continue 271 | (added, removed) = m.groups(0) 272 | if added != "-": 273 | total_added += int(added, 10) 274 | if removed != "-": 275 | total_removed += int(removed, 10) 276 | if total_added > 0: 277 | ministat += [f"{green}+{total_added}{colorreset()}"] 278 | ministat_monochrome += [f"+{total_added}"] 279 | if total_removed > 0: 280 | ministat += [f"{red}-{total_removed}{colorreset()}"] 281 | ministat_monochrome += [f"-{total_removed}"] 282 | 283 | if ministat: 284 | ministat_monochrome = " ".join(ministat_monochrome) + " " 285 | pad = "" 286 | pad_size = 14 287 | if len(ministat_monochrome) < pad_size: 288 | pad = " "*(pad_size - len(ministat_monochrome)) 289 | ministat = pad + " ".join(ministat) + " " 290 | else: 291 | ministat = "" 292 | 293 | inp.append(f"{item.chain} {item.githash} {prefix} {color}ID:{item.chain.split('-')[0]}{colorreset()} " 294 | f"{item.githash[:12]} {datecolor}{date}{colorreset()} {ministat} {item.subject()}\n") 295 | 296 | 297 | process = subprocess.Popen(["fzf"] + fzf_options, stdin=subprocess.PIPE, stdout=subprocess.PIPE) 298 | for line in inp: 299 | process.stdin.write(line.encode('utf-8')) 300 | process.stdin.close() 301 | 302 | res = process.wait() 303 | if res != 0: 304 | sys.exit(-1) 305 | return 306 | 307 | output_lines = process.stdout.readlines() 308 | for line in output_lines: 309 | tokens = [x for x in line.decode('utf-8').split(' ') if x] 310 | return Selection(chain_id=tokens[0], githash=tokens[1]) 311 | 312 | 313 | def link_chain(selection: Selection, remaining): 314 | """ 'link' command """ 315 | chain_info = get_chain_info() 316 | if remaining: 317 | remaining = ' - ' + ' '.join(remaining) 318 | else: 319 | remaining = '' 320 | chain = chain_info.find_first_frag(selection.chain_id) 321 | system(f'git commit -m "{chain.subject()}{remaining}\n\nGit-chain: {selection.chain_id}"') 322 | 323 | 324 | def reword(selection): 325 | """ 'kill' command - FZF-pick a chain and remove all its fragments """ 326 | chain_info = get_chain_info().reverse() 327 | commands = [] 328 | for frag in chain_info.frags: 329 | for (item_idx, item) in enumerate(frag.items): 330 | if frag.chain() == selection.chain_id and frag.index == 0 and item_idx == 0: 331 | commands.append(f"reword {item.githash}") 332 | else: 333 | commands.append(f"pick {item.githash}") 334 | rebase(chain_info.base, "reword_chain", commands) 335 | 336 | 337 | POSSIBLE_COLORS = [ 338 | "#1177ff", 339 | "#7733ff", 340 | "#11cc66", 341 | "#ff33cc", 342 | "#9999cc", 343 | "#ffffbb", 344 | ] 345 | 346 | 347 | def create_new(remaining): 348 | """ 'new' command """ 349 | chain_info = get_chain_info() 350 | possible_colors = set(POSSIBLE_COLORS) 351 | for (_, color) in chain_info.colors.items(): 352 | if color in possible_colors: 353 | possible_colors.remove(color) 354 | if len(possible_colors) == 0: 355 | raise Exception("no color") 356 | return 357 | 358 | new_color = list(possible_colors)[0] 359 | subject = ' '.join(remaining) 360 | if subject == '': 361 | subject = "WIP" 362 | import uuid 363 | uid = str(uuid.uuid4()) 364 | system(f'git commit -m "{subject}\n\nGit-chain: {uid}\nGit-chain-color: {new_color}"') 365 | 366 | 367 | def rebase(base, reword_func, commands): 368 | tf = tempfile.NamedTemporaryFile() 369 | tf.write("\n".join(commands + []).encode('utf-8')) 370 | tf.flush() 371 | editor_cmd = "" 372 | if reword_func: 373 | editor_cmd = f"-c core.editor='{sys.argv[0]} from-rebase {tf.name} {reword_func}'" 374 | cmd = f"git {editor_cmd} rebase -i {base}" 375 | return system(cmd) 376 | 377 | 378 | def squash(): 379 | chain_info = get_chain_info().reverse() 380 | commands = [] 381 | for frag in chain_info.frags: 382 | for (idx, item) in enumerate(frag.items): 383 | if idx == 0: 384 | commands.append(f"reword {item.githash}") 385 | else: 386 | commands.append(f"fixup {item.githash}") 387 | rebase(chain_info.base, "bump_squash_count", commands) 388 | 389 | 390 | def kill_chain(selection): 391 | """ 'kill' command - FZF-pick a chain and remove all its fragments """ 392 | chain_info = get_chain_info().reverse() 393 | commands = [] 394 | for frag in chain_info.frags: 395 | for item in frag.items: 396 | if frag.chain() == selection.chain_id: 397 | commands.append(f"drop {item.githash}") 398 | else: 399 | commands.append(f"pick {item.githash}") 400 | rebase(chain_info.base, None, commands) 401 | 402 | 403 | def finish_chain(selection): 404 | chain_info = get_chain_info().reverse() 405 | first_commands = [] 406 | commands = [] 407 | for frag in chain_info.frags: 408 | for item in frag.items: 409 | if frag.chain() == selection.chain_id: 410 | if len(first_commands) == 0: 411 | first_commands.append(f"reword {item.githash}") 412 | else: 413 | first_commands.append(f"fixup {item.githash}") 414 | else: 415 | commands.append(f"pick {item.githash}") 416 | commands = first_commands + commands 417 | rebase(chain_info.base, "remove_meta", commands) 418 | 419 | 420 | def bump_squash_count(filename): 421 | # We are going to rewrite the file given by `filename`. 422 | # It is a commit message. 423 | # Search for a line matching `^Git-chain-squash-count: [0-9]+$` in the file. 424 | # If exists, bump the number +1 and rewrite the file. 425 | # Otherwise, insert `Git-chain-squash-count: 1` right after the line matching `^Git-chain: .*$` 426 | with open(filename, 'r') as f: 427 | lines = f.readlines() 428 | 429 | pattern = re.compile(r'^(Git-chain-squash-count: )(\d+)$') 430 | new_lines = [] 431 | found = False 432 | for line in lines: 433 | m = pattern.match(line.rstrip('\n')) 434 | if m: 435 | count = int(m.group(2), 10) + 1 436 | new_lines.append(f"{m.group(1)}{count}\n") 437 | found = True 438 | else: 439 | new_lines.append(line) 440 | 441 | if not found: 442 | inserted = False 443 | init_line = "Git-chain-squash-count: 2\n" 444 | for i, line in enumerate(new_lines): 445 | if re.match(r'^Git-chain: .*$', 446 | line.rstrip('\n')): 447 | new_lines.insert(i + 1, init_line) 448 | inserted = True 449 | break 450 | if not inserted: 451 | new_lines.append(init_line) 452 | with open(filename, 'w') as f: 453 | f.writelines(new_lines) 454 | 455 | 456 | def remove_meta(filename): 457 | with open(filename, 'r') as f: 458 | lines = f.readlines() 459 | 460 | pattern = re.compile(r'^Git-chain.*:.*') 461 | new_lines = [] 462 | for line in lines: 463 | m = pattern.match(line.rstrip('\n')) 464 | if not m: 465 | new_lines.append(line) 466 | 467 | with open(filename, 'w') as f: 468 | f.writelines(new_lines) 469 | 470 | 471 | GIT_PUSH_HOOK = """#!/bin/bash 472 | 473 | if [[ "${ALLOW_GIT_CHAIN_PUSH:-}" == "y" ]]; then 474 | exit 0 475 | fi 476 | 477 | # Read commit range from stdin (local and remote ref pairs) 478 | while read -r local_ref local_sha remote_ref remote_sha; do 479 | # Determine commits to push 480 | if [ "$remote_sha" = "0000000000000000000000000000000000000000" ]; then 481 | # New branch or force push, check all history 482 | commit_range="$local_sha" 483 | else 484 | commit_range="$remote_sha..$local_sha" 485 | fi 486 | 487 | # Check commit messages 488 | problematic_commits=$(git log --pretty=format:"%h %b" "$commit_range" -E --grep='^Git-chain(-[a-z]+)?:') 489 | 490 | if [[ -n "$problematic_commits" ]]; then 491 | echo "Push rejected: found unfinished 'git chain' commits" 492 | echo "Add ALLOW_GIT_CHAIN_PUSH=y to environment to bypass" 493 | exit 1 494 | fi 495 | done 496 | 497 | exit 0 498 | """ 499 | 500 | 501 | def install_prepush(): 502 | gitdir = None 503 | with os.popen("git rev-parse --git-dir") as f: 504 | gitdir = f.read().strip() 505 | if not gitdir: 506 | return 507 | path = f"{gitdir}/hooks/pre-push" 508 | print(f"git-chain: installing {path}") 509 | with open(path, "w") as f: 510 | f.write(GIT_PUSH_HOOK) 511 | system(f"chmod a+x {path}") 512 | print(f"git-chain: done") 513 | 514 | 515 | def main(): 516 | if sys.argv[1:] and sys.argv[1] == "from-rebase": 517 | script = sys.argv[2] 518 | reword_func = sys.argv[3] 519 | output = sys.argv[4] 520 | if output.endswith('git-rebase-todo'): 521 | shutil.copy(script, output) 522 | else: 523 | if reword_func == "bump_squash_count": 524 | bump_squash_count(output) 525 | elif reword_func == "remove_meta": 526 | remove_meta(output) 527 | elif reword_func == "reword_chain": 528 | sys.exit(os.system(f"$EDITOR {output}")) 529 | 530 | sys.exit(0) 531 | 532 | parser = argparse.ArgumentParser(description='') 533 | parser.add_argument("-a", action="store_true", help="perform git add -a after command") 534 | subparsers = parser.add_subparsers(dest="top_level") 535 | 536 | commands = ['new', 'link', 'list', 'squash', 'kill', 'finish', 'prepush', "reword"] 537 | for command in commands: 538 | func = subparsers.add_parser(command) 539 | func.add_argument('remaining', nargs=argparse.REMAINDER, help='') 540 | func.set_defaults() 541 | 542 | funcs = dict(list=list_chains, link=link_chain, new=create_new, squash=squash, kill=kill_chain, 543 | finish=finish_chain, prepush=install_prepush, reword=reword) 544 | 545 | args = parser.parse_args() 546 | if args.top_level: 547 | func = funcs[args.top_level] 548 | params = [] 549 | for (param_name, _) in list(inspect.signature(func).parameters.items()): 550 | if param_name == 'selection': 551 | params.append(pick_fzf()) 552 | elif param_name == 'remaining': 553 | params.append(args.remaining) 554 | else: 555 | raise Exception(param_name) 556 | if args.a: 557 | system("git add -A") 558 | func(*params) 559 | 560 | 561 | if __name__ == "__main__": 562 | main() 563 | -------------------------------------------------------------------------------- /git-continue: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Continue a rebase, `am`, a cherry-pick or a revert, depending on the situation. 4 | set -e 5 | 6 | git_dir="$(git rev-parse --git-dir)" 7 | if [[ -e "${git_dir}/CHERRY_PICK_HEAD" ]] ; then 8 | exec git cherry-pick --continue "$@" 9 | elif [[ -e "${git_dir}/REVERT_HEAD" ]] ; then 10 | exec git revert --continue "$@" 11 | elif [[ -e "${git_dir}/rebase-apply/applying" ]] ; then 12 | exec git am --continue "$@" 13 | elif [[ -e "${git_dir}/rebase-apply" ]] ; then 14 | exec git rebase --continue "$@" 15 | elif [[ -e "${git_dir}/rebase-merge" ]] ; then 16 | exec git rebase --continue "$@" 17 | else 18 | echo git-continue: unknown state >&2 19 | exit -1 20 | fi 21 | -------------------------------------------------------------------------------- /git-declone: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # Old `git clone` of repositories lying about with or without changes, take 5 | # up space. What if we can get rid of these clones without losing our work? 6 | # 7 | # This is a git 'unclone' in a sense that it also tries to preserve unpushed 8 | # revisions. So, it turns the local branches into patches, so that the clone 9 | # can be safely deleted. This does not take reflogs into account. 10 | # 11 | 12 | output=$1 13 | 14 | if [[ "$output" == "" ]] ; then 15 | exit -1 16 | fi 17 | 18 | git branch -v > ${output}/local-branch-state 19 | git branch -rv > ${output}/remote-branch-state 20 | git remote -v > ${output}/remote-state 21 | 22 | for branch in $(git show-ref | grep ' refs/heads/' | cut -c53-) ; do 23 | mkdir -p ${output}/${branch} 24 | git format-patch $(for i in $(git show-ref | grep ' refs/remotes/' | cut -c41- | grep -v HEAD); do echo $i..${branch}; done) -o ${output}/heads/${branch} 25 | rmdir ${output}/${branch} 2>/dev/null 26 | 27 | if [[ -e ${output}/${branch} ]] ; then 28 | echo ${branch} 29 | ls -l ${output}/${branch} 30 | fi 31 | done 32 | -------------------------------------------------------------------------------- /git-fancy-branch-list: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | 5 | Show remote and local branch sorted by last commit time, using colors to indicate Githash. 6 | 7 | If '-g' is provided, show branches that are identical to ones in remote. 8 | 9 | Remote branches can be filtered by configuring fancy-branch-list.prefix, e.g.: 10 | 11 | git config fancy-branch-list.prefix origin/dan/ 12 | 13 | """ 14 | 15 | import os 16 | import sys 17 | import datetime 18 | import time 19 | from dateutil import parser 20 | 21 | f = os.popen("git config fancy-branch-list.prefix", "r") 22 | remote_prefix = f.read().strip() 23 | f.close() 24 | 25 | cmd = ( 26 | """git for-each-ref "$@" --sort=-committerdate refs/heads/ refs/remotes/""" 27 | + remote_prefix + 28 | """ --format="%(committerdate) ;; %(refname) ;; %(objectname) ;; %(subject)" """ 29 | ) 30 | 31 | branch_list = os.popen(cmd, "r").read() 32 | def color24(r, g, b1): 33 | return "\x1b[38;2;%d;%d;%dm" % (r, g, b1) 34 | def backcolor24(r, g, b1): 35 | return "\x1b[48;2;%d;%d;%dm" % (r, g, b1) 36 | def colorreset(): 37 | return "\x1b[0;m" 38 | 39 | def pretty_date(diff): 40 | """ 41 | Get a datetime object or a int() Epoch timestamp and return a 42 | pretty string like 'an hour ago', 'Yesterday', '3 months ago', 43 | 'just now', etc 44 | """ 45 | second_diff = int(diff) 46 | day_diff = int(diff / 86400) 47 | 48 | if day_diff < 0: 49 | return '' 50 | 51 | if day_diff == 0: 52 | if second_diff < 10: 53 | return "just now" 54 | if second_diff < 60: 55 | return str(second_diff) + " seconds ago" 56 | if second_diff < 120: 57 | return "a minute ago" 58 | if second_diff < 3600: 59 | return str(int(second_diff / 60)) + " minutes ago" 60 | if second_diff < 7200: 61 | return "an hour ago" 62 | if second_diff < 86400: 63 | return str(int((second_diff / 3600))) + " hours ago" 64 | if day_diff == 1: 65 | return "Yesterday" 66 | if day_diff < 7: 67 | return str(day_diff) + " days ago" 68 | if day_diff < 31: 69 | return str(int(day_diff / 7)) + " weeks ago" 70 | if day_diff < 365: 71 | return str(int(day_diff / 30)) + " months ago" 72 | return str(int(day_diff / 365)) + " years ago" 73 | 74 | lst = [] 75 | nb = 0 76 | branch_type = {} 77 | branch_hash = {} 78 | for line in branch_list.splitlines(): 79 | (ts, b, h, s) = line.split(' ;; ') 80 | place = " " 81 | is_remote = False 82 | if b.startswith('refs/heads/'): 83 | b = b[len('refs/heads/'):] 84 | if b.startswith('refs/remotes/origin/'): 85 | b = b[len('refs/remotes/origin/'):] 86 | place = "(remote)" 87 | is_remote = True 88 | if b not in branch_type: 89 | if is_remote: 90 | branch_type[b] = "remote-single" 91 | else: 92 | branch_type[b] = "local-single" 93 | else: 94 | if branch_hash[b] == h: 95 | branch_type[b] = "remote-and-local-ident" 96 | else: 97 | if branch_type[b] == "remote-single": 98 | if not is_remote: 99 | branch_type[b] = "remote-and-local-older" 100 | else: 101 | if is_remote: 102 | branch_type[b] = "local-and-remote-older" 103 | 104 | branch_hash[b] = h 105 | lst.append((ts, b, h, s, place)) 106 | nb = max(nb, len(b)) 107 | 108 | branchDictTypes = { 109 | "local-single" : color24( 95, 185, 235), 110 | "remote-single" : color24(168, 168, 255), 111 | "local-and-remote-older" : color24(255, 255, 55), 112 | "remote-and-local-ident" : color24( 0, 255, 0), 113 | "remote-and-local-older" : color24(255, 50, 50), 114 | } 115 | 116 | show_remote_and_local_ident = '-g' in sys.argv 117 | 118 | lst.reverse() 119 | for (ts, b, h, s, place) in lst: 120 | h = h[:12] 121 | if branch_type[b] == "remote-and-local-ident" and not show_remote_and_local_ident: 122 | continue 123 | branch_color = branchDictTypes[branch_type[b]] 124 | back_color = '' 125 | ts_v = parser.parse(ts) 126 | age = (time.time() - time.mktime(ts_v.timetuple())) 127 | if age > 86400 * 31: 128 | back_color = backcolor24(40, 40, 40) 129 | print(("%s %s %-16s %s %s %s%-" + str(nb) + "s%s %s") % ( 130 | back_color, parser.parse(ts), pretty_date(age), 131 | h, place, branch_color, b, colorreset(), s)) 132 | -------------------------------------------------------------------------------- /git-find-blob: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | # sudo dnf install -y perl-Memoize 4 | 5 | use 5.008; 6 | use strict; 7 | use Memoize; 8 | 9 | my $obj_name; 10 | 11 | sub check_tree { 12 | my ( $tree ) = @_; 13 | my @subtree; 14 | 15 | { 16 | open my $ls_tree, '-|', git => 'ls-tree' => $tree 17 | or die "Couldn't open pipe to git-ls-tree: $!\n"; 18 | 19 | while ( <$ls_tree> ) { 20 | /\A[0-7]{6} (\S+) (\S+)/ 21 | or die "unexpected git-ls-tree output"; 22 | return 1 if $2 eq $obj_name; 23 | push @subtree, $2 if $1 eq 'tree'; 24 | } 25 | } 26 | 27 | check_tree( $_ ) && return 1 for @subtree; 28 | 29 | return; 30 | } 31 | 32 | memoize 'check_tree'; 33 | 34 | die "usage: git-find-blob []\n" 35 | if not @ARGV; 36 | 37 | my $obj_short = shift @ARGV; 38 | $obj_name = do { 39 | local $ENV{'OBJ_NAME'} = $obj_short; 40 | `git rev-parse --verify \$OBJ_NAME`; 41 | } or die "Couldn't parse $obj_short: $!\n"; 42 | chomp $obj_name; 43 | 44 | open my $log, '-|', git => log => @ARGV, '--pretty=format:%T %h %s' 45 | or die "Couldn't open pipe to git-log: $!\n"; 46 | 47 | while ( <$log> ) { 48 | chomp; 49 | my ( $tree, $commit, $subject ) = split " ", $_, 3; 50 | print "$commit $subject\n" if check_tree( $tree ); 51 | } 52 | -------------------------------------------------------------------------------- /git-fixes-commit-msg-hook: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # 4 | # Commit-msg hook that automatically expandes Fixes: lines with 5 | # the relevant commit message. 6 | # 7 | # Fixes: 13d847a0d3 8 | # 9 | # to: 10 | # 11 | # Fixes: 13d847a0d3 ("adadad asdasd") 12 | # 13 | 14 | import sys 15 | import os 16 | import re 17 | 18 | RE = re.compile("^Fixes: ([a-f0-9_]+)$") 19 | 20 | RESET = '\033[0m' 21 | GREEN = '\033[01m\033[32m' 22 | YELLOW = '\033[01m\033[33m' 23 | WHITE = '\033[37m' 24 | 25 | def main(): 26 | if os.getenv("GIT_FIXES_COMMIT_MSG", "") == "disable": 27 | # Script is disabled 28 | return 29 | 30 | myself = os.popen("git config user.name").read().strip() 31 | if os.getenv("GIT_AUTHOR_NAME", "") != myself: 32 | # Don't fix commits of others 33 | return 34 | 35 | commit_msg_file = sys.argv[1] 36 | commit_msg_file_tmp = commit_msg_file + ".mod.tmp" 37 | 38 | lines = [] 39 | modified = False 40 | for line in open(commit_msg_file, "r").readlines(): 41 | m = RE.match(line) 42 | if m: 43 | line = line.strip() 44 | rev = m.groups(0)[0] 45 | new_line = os.popen(f'git show {rev} --pretty="%h %s" -s 2>/dev/null')\ 46 | .read().strip() 47 | if new_line: 48 | (commithash, subject) = new_line.split(' ', 1) 49 | new_line = "Fixes: %s (%r)" % (commithash, subject) 50 | print(f"{GREEN}Auto-modified: {WHITE}{new_line}{RESET}") 51 | line = new_line 52 | else: 53 | print(f"{YELLOW}No ref for {rev}{RESET}") 54 | line = line + "\n" 55 | modified = True 56 | lines.append(line) 57 | 58 | if not modified: 59 | return 60 | 61 | print() 62 | 63 | f = open(commit_msg_file_tmp, "w") 64 | f.write("".join(lines)) 65 | f.close() 66 | 67 | os.rename(commit_msg_file_tmp, commit_msg_file) 68 | 69 | if __name__ == "__main__": 70 | main() 71 | -------------------------------------------------------------------------------- /git-fixup-to-last: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # Takes all existing modifications and fixups for each file, according the last 5 | # commit that touched the file. 6 | # 7 | # Syntax: 8 | # 9 | # fix-to-last (BASE) 10 | # 11 | # Optional [BASE] commitish limits the lookup for commits. 12 | # 13 | # By https://github.com/sinelaw 14 | # 15 | 16 | cd $(git rev-parse --git-dir)/.. || exit -1 17 | 18 | if [[ "$1" != "" ]] ; then 19 | BASE="${1}.." 20 | else 21 | BASE= 22 | fi 23 | 24 | git diff --cached --exit-code > /dev/null 25 | 26 | if [[ "$?" != "0" ]] ; then 27 | echo "fix-to-last: modified but uncommited changes, doing nothing" 28 | exit -1 29 | fi 30 | 31 | git status --porcelain -uno | cut -d' ' -f3- | while read filename; do 32 | last=$(git log -1 --format="%H" ${BASE}HEAD -- $filename) 33 | if [[ "$last" == "" ]] ; then 34 | echo fix-to-last: skipped $filename 35 | else 36 | git commit --fixup=${last} $filename || exit -1 37 | fi 38 | done 39 | -------------------------------------------------------------------------------- /git-flip-history: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # This script flips the history of a branch that is structured in the following way. 5 | # 6 | # For example, let's have a history, where the first commit adds 4 features, and 7 | # each of the 3 proceeding commits reverts 3 of the features one by one: 8 | # 9 | # Revert: A 10 | # Revert: B 11 | # Revert: C 12 | # First: D 13 | # 14 | # The result branch after running this command will be the following: 15 | # 16 | # C 17 | # B 18 | # A 19 | # D 20 | # 21 | 22 | set -euo pipefail 23 | 24 | if [[ $(git diff --exit-code) ]] ; then 25 | echo "Unclean state" 26 | exit 1 27 | fi 28 | if [[ $(git diff --cached --exit-code) ]] ; then 29 | echo "Unclean state" 30 | exit 1 31 | fi 32 | 33 | # Gather versions with 'Revert: ', until bumping into a commit that does not have 'Revert: '. 34 | 35 | versions=() 36 | 37 | last_feature_commitmsg=$(mktemp /tmp/commitmsg.XXXXXX) 38 | 39 | echo "# Commit message to describe the feature added by this history:" >> ${last_feature_commitmsg} 40 | echo "#" >> ${last_feature_commitmsg} 41 | 42 | commit=$(git rev-parse HEAD) 43 | while [ 1 ] ; do 44 | set +e 45 | git show --pretty=%B --no-patch $commit | head -n 1 | grep ^Revert: 46 | result=$? 47 | set -e 48 | 49 | versions+=($commit) 50 | 51 | echo "# "$(git show --pretty=%B --no-patch $commit | head -n 1) >> ${last_feature_commitmsg} 52 | if [[ "$result" != "0" ]] ; then 53 | set +e 54 | git show --pretty=%B --no-patch $commit | head -n 1 | grep ^First: 55 | result=$? 56 | set -e 57 | 58 | if [[ "$result" != "0" ]] ; then 59 | echo "Expected earliest commit to start with 'First: '" 60 | exit -1 61 | fi 62 | 63 | last_commit=${commit} 64 | base_commit=$(git rev-parse $commit~1) 65 | break 66 | fi 67 | 68 | commit=$(git rev-parse $commit~1) 69 | done 70 | 71 | parent=${base_commit} 72 | i=0 73 | while [ "$i" != ${#versions[@]} ] ; do 74 | commit=${versions[$i]} 75 | 76 | if [[ $i == "0" ]] ; then 77 | refcommit=${last_commit} 78 | else 79 | refcommit=${versions[$(( $i - 1 ))]} 80 | fi 81 | 82 | commitmsg=$(mktemp /tmp/commitmsg.XXXXXX) 83 | tree=$(git show --pretty=%T --no-patch ${commit}) 84 | if [[ "${refcommit}" == ${last_commit} ]] ; then 85 | git show --pretty=%B --no-patch ${refcommit} | sed -E '1s/^First: //g' > ${commitmsg} 86 | else 87 | git show --pretty=%B --no-patch ${refcommit} | sed -E '1s/^Revert: //g' > ${commitmsg} 88 | fi 89 | parent=$(git commit-tree ${tree} -p ${parent} -F ${commitmsg}) 90 | rm -f ${commitmsg} 91 | i=$(($i + 1)) 92 | done 93 | 94 | rm -f ${last_feature_commitmsg} 95 | 96 | echo 97 | echo "Changed to different history:" 98 | echo 99 | 100 | git --no-pager log --no-decorate --oneline --oneline ${base_commit}.. 101 | 102 | echo 103 | echo "New history: ${parent}" 104 | echo 105 | 106 | git --no-pager log --no-decorate --oneline --oneline ${base_commit}..${parent} 107 | git reset --hard ${parent} 108 | -------------------------------------------------------------------------------- /git-fzf-diff: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | Show hunks in FZF multi-select or single-select and do stuff with these hunks. 5 | 6 | The output of `git diff ` is processed through this command, then it 7 | lets you do FZF things with these hunks. 8 | 9 | FZF Actions: 10 | 11 | Insert do 'git add -p' on the selected hunks and refresh. Works well if 12 | for the output of `git diff`. Works regardless of '-a' command line 13 | option. 14 | 15 | Ctrl-Insert do 'git commit' for staged hunks. 16 | 17 | Return Unless '-a' is specified, open an editor on a hunk if we selected 18 | a single one. Otherwise, for '-a', do 'git add -p' on the selected 19 | hunks and return. If '-c' is specified, also do a 'git commit'. 20 | 21 | Command line options: 22 | 23 | -a Add selected hunks on 'Return'. 24 | -c Also commit the changes, when '-a' is used. 25 | 26 | """ 27 | 28 | import sys 29 | import os 30 | import tempfile 31 | import re 32 | import os 33 | from optparse import OptionParser 34 | import itertools 35 | import tempfile 36 | import shlex 37 | import subprocess 38 | 39 | def system(cmd): 40 | return os.system(cmd) 41 | 42 | class Abort(Exception): pass 43 | 44 | def abort(msg): 45 | print(msg, file=sys.stderr) 46 | sys.exit(-1) 47 | 48 | DIFF_LINE = re.compile(r"^diff --git a/(.*) b/(.*)$") 49 | HUNK_HEADER = re.compile(r"^@@ -[^ ]+ [+]([0-9]+)[ ,][^@]*@@(.*)$") 50 | META_HEADER = re.compile(r":(.*)$") 51 | UNTRACKED = re.compile(r"[?][?] (.*)$") 52 | NEW_FILE = re.compile(r"new file") 53 | EMPTY_INDEX = re.compile(r"index 000000000000..e69de29bb2d1") 54 | OUTPUT = re.compile(r"(.) ([^:]+):([0-9]+) [#]([0-9])+") 55 | 56 | class DiffInformation(object): 57 | def __init__(self, args): 58 | filename_color = "\x1b[38;2;155;255;155m" 59 | white = "\x1b[38;2;255;255;255m" 60 | lnum_color = "\x1b[38;2;77;127;77m" 61 | grey = "\x1b[38;2;255;255;155m" 62 | dark_hunk_idx_color = "\x1b[38;2;0;82;128m" 63 | hunk_idx_color = "\x1b[38;2;0;165;255m" 64 | staged_color = "\x1b[38;2;0;255;0m" 65 | commited_color = "\x1b[38;2;255;255;255m" 66 | workdir_color = "\x1b[38;2;255;255;0m" 67 | untracked_color = "\x1b[38;2;255;128;0m" 68 | args = ' '.join(args) 69 | 70 | filename = '' 71 | contextlines = 0 72 | hunknum = 1 73 | hunkindex = 0 74 | hunks = {} 75 | matches = [] 76 | idx_hunks = [] 77 | remember_new_file = False 78 | 79 | self.idx_untracked_files = None 80 | self.nr_untracked_files = 0 81 | self.untracked_files = [] 82 | self.idx_unstaged_hunks = None 83 | self.nr_unstaged_hunks = 0 84 | 85 | cmd = "" 86 | cmd += "echo ':U' ; git status --porcelain | grep '??' ;" 87 | cmd += "echo ':W' ; git diff ;" 88 | cmd += "echo ':S' ; git diff --staged ;" 89 | if args != "": 90 | cmd += f"echo ':C' ; git diff {args}..HEAD ;" 91 | kind = None 92 | 93 | def push_match(): 94 | kind_color = '' 95 | if kind == 'U': 96 | kind_color = untracked_color 97 | if self.idx_untracked_files is None: 98 | self.idx_untracked_files = len(idx_hunks) 99 | self.nr_untracked_files += 1 100 | self.untracked_files.append(filename) 101 | elif kind == 'W': 102 | kind_color = workdir_color 103 | if self.idx_unstaged_hunks is None: 104 | self.idx_unstaged_hunks = len(idx_hunks) 105 | self.nr_unstaged_hunks += 1 106 | elif kind == 'C': 107 | kind_color = commited_color 108 | elif kind == 'S': 109 | kind_color = staged_color 110 | 111 | matches.append("%s%s %s%s%s:%s%d %s%s%s%d%s%s" % ( 112 | kind_color, kind, 113 | filename_color, filename, 114 | white, lnum_color, line_num, 115 | dark_hunk_idx_color, "#", 116 | hunk_idx_color, hunknum, 117 | white, title)) 118 | idx_hunks.append([filename, line_num, 0]) 119 | 120 | for line in os.popen(cmd).readlines(): 121 | m = META_HEADER.match(line) 122 | if m: 123 | kind = m.groups(0)[0] 124 | continue 125 | 126 | whole_file = False 127 | if kind == "U": 128 | m = UNTRACKED.match(line) 129 | if m: 130 | filename = m.groups(0)[0] 131 | whole_file = True 132 | else: 133 | m = NEW_FILE.match(line) 134 | if m: 135 | remember_new_file = True 136 | whole_file = False 137 | 138 | if remember_new_file: 139 | m = EMPTY_INDEX.match(line) 140 | if m: 141 | whole_file = True 142 | 143 | if whole_file: 144 | line_num = 1 145 | toplevel = os.popen("git rev-parse --show-toplevel").read().strip() 146 | hunks[(kind, filename, 1)] = hunkindex 147 | title = "" 148 | for line in open(os.path.join(toplevel, filename)).readlines(): 149 | title = " " + line.strip() 150 | break 151 | push_match() 152 | hunkindex += 1 153 | continue 154 | 155 | m = DIFF_LINE.match(line) 156 | if m: 157 | filename = m.groups(0)[1] 158 | hunknum = 1 159 | continue 160 | m = HUNK_HEADER.match(line) 161 | if m: 162 | contextlines = 0 163 | hunks[(kind, filename, hunknum)] = hunkindex 164 | line_num = int(m.groups(0)[0]) 165 | title = m.groups(0)[1] 166 | push_match() 167 | hunkindex += 1 168 | hunknum += 1 169 | continue 170 | if line.startswith('+') or line.startswith('-'): 171 | if len(idx_hunks) > 0: 172 | idx_hunks[-1][2] = contextlines 173 | elif line.startswith(' '): 174 | contextlines += 1 175 | 176 | self.hunks = hunks 177 | self.matches = matches 178 | self.idx_hunks = idx_hunks 179 | 180 | def get_selection(self, output_lines): 181 | selected_hunks = [] 182 | for line in output_lines: 183 | m = OUTPUT.match(line.decode('utf-8')) 184 | if m: 185 | (kind, filename, line, hunknum) = m.groups() 186 | hunknum = int(hunknum) 187 | line = int(line) 188 | selected_hunks.append(self.hunks[(kind, filename, hunknum)]) 189 | return selected_hunks 190 | 191 | def add(self, selected_hunks): 192 | if self.nr_untracked_files > 0: 193 | p_add = set() 194 | for selected_idx in selected_hunks: 195 | if selected_idx >= self.idx_untracked_files and \ 196 | selected_idx < self.idx_untracked_files + self.nr_untracked_files: 197 | p_add.add(self.untracked_files[selected_idx - self.idx_untracked_files]) 198 | cmd = ["git", "-c", "interactive.diffFilter=cat", "add"] 199 | cmd += list(p_add) 200 | process = subprocess.Popen(cmd, stdout=subprocess.DEVNULL) 201 | r = process.wait() 202 | if r != 0: 203 | abort("Error adding hunks") 204 | return 205 | 206 | if self.nr_unstaged_hunks > 0: 207 | p_add = set() 208 | for selected_idx in selected_hunks: 209 | if selected_idx >= self.idx_unstaged_hunks and \ 210 | selected_idx < self.idx_unstaged_hunks + self.nr_unstaged_hunks: 211 | p_add.add(selected_idx - self.idx_unstaged_hunks) 212 | 213 | cmd = ["git", "-c", "interactive.diffFilter=cat", "add", "-p"] 214 | process = subprocess.Popen(cmd, 215 | stdout=subprocess.DEVNULL, 216 | stdin=subprocess.PIPE) 217 | selected_hunks = set(selected_hunks) 218 | for idx in range(0, self.nr_unstaged_hunks): 219 | if idx in p_add: 220 | process.stdin.write(f"y\n".encode('utf-8')) 221 | else: 222 | process.stdin.write(f"n\n".encode('utf-8')) 223 | process.stdin.close() 224 | r = process.wait() 225 | if r != 0: 226 | abort("Error adding hunks") 227 | return 228 | 229 | def main(): 230 | if sys.argv[1:2] == ["--binding-preview"]: 231 | m = OUTPUT.match(sys.argv[3]) 232 | if not m: 233 | return 234 | 235 | (kind, filename, line, hunk_index) = m.groups(0) 236 | 237 | if kind == 'U': 238 | cmd = "cat ${filename}" 239 | elif kind == 'W': 240 | cmd = "git diff" 241 | elif kind == 'C': 242 | cmd = f"git diff {sys.argv[2]}..HEAD" 243 | elif kind == 'S': 244 | cmd = f"git diff --staged" 245 | else: 246 | sys.exit(0) 247 | 248 | if cmd.startswith("git diff"): 249 | cmd = f"{cmd} | filterdiff -i 'a/'{filename} -i 'b/'{filename} --hunks {hunk_index} | tail -n +5 | delta-configured" 250 | sys.exit(os.system(cmd)) 251 | return 252 | elif sys.argv[1:2] in [["--binding-add"], ["--binding-reload"], ["--binding-reset"]]: 253 | args = [sys.argv[2]] 254 | output_lines = [o.encode('utf-8') for o in sys.argv[3:]] 255 | reload_command = True 256 | add_mode = False 257 | commit_mode = False 258 | reset_mode = False 259 | if sys.argv[1:2] == ["--binding-add"]: 260 | add_mode = True 261 | elif sys.argv[1:2] == ["--binding-reset"]: 262 | reset_mode = True 263 | elif sys.argv[1:2] == ["--binding-reload"]: 264 | pass 265 | else: 266 | parser = OptionParser() 267 | parser.add_option("-a", "--add", dest="add", action="store_true") 268 | parser.add_option("-c", "--commit", dest="commit", action="store_true") 269 | 270 | (options, args) = parser.parse_args() 271 | reload_command = False 272 | binding_add_mode = False 273 | add_mode = False 274 | reset_mode = False 275 | if options.add: 276 | args = "" 277 | add_mode = True 278 | if options.commit: 279 | commit_mode = True 280 | else: 281 | commit_mode = False 282 | 283 | diff_info = DiffInformation(args) 284 | if reload_command: 285 | if add_mode: 286 | try: 287 | selection = diff_info.get_selection(output_lines) 288 | diff_info.add(selection) 289 | except: 290 | from traceback import print_exc 291 | print_exc(file=sys.stdout) 292 | if commit_mode: 293 | os.system("git commit") 294 | if reset_mode: 295 | os.system("git reset HEAD >/dev/null 2>/dev/null") 296 | 297 | diff_info = DiffInformation(args) 298 | for match in diff_info.matches: 299 | sys.stdout.buffer.write(f"{match}\n".encode('utf-8')) 300 | return 301 | else: 302 | args = ' '.join(args) 303 | our_program = sys.argv[0] 304 | preview_program = our_program + f" --binding-preview '{args}' {{}}" 305 | add_program = our_program + f" --binding-add '{args}' {{+}}" 306 | reset_program = our_program + f" --binding-reset '{args}' {{+}}" 307 | reload_program = our_program + f" --binding-reload '{args}' {{+}}" 308 | 309 | fzf_options = [ 310 | "--ansi", "-e", "--no-sort", "-m", 311 | "--layout=reverse", 312 | "--preview-window", "down:50%:noborder", 313 | "--preview", preview_program, 314 | "--bind", "insert:reload(" + add_program + ")", 315 | "--bind", "ctrl-e:reload(" + reset_program + ")", 316 | "--bind", "ctrl-t:execute(git commit > /dev/tty)+reload(" + reload_program + ")", 317 | "--header", "\ 318 | \n[Return] Open editor on hunk [Insert] Stage selected hunk(s) [Ctrl-t] commit staged hunks\ 319 | \n [Ctrl-e] Unstange all hunk(s) \n" 320 | ] 321 | process = subprocess.Popen(["fzf"] + fzf_options, 322 | stdin=subprocess.PIPE, 323 | stdout=subprocess.PIPE) 324 | for match in diff_info.matches: 325 | process.stdin.write(f"{match}\n".encode('utf-8')) 326 | process.stdin.close() 327 | res = process.wait() 328 | if res != 0: 329 | sys.exit(-1) 330 | return 331 | output_lines = process.stdout.readlines() 332 | 333 | selection = diff_info.get_selection(output_lines) 334 | if add_mode: 335 | diff_info.add(selection) 336 | if commit: 337 | os.system("git commit") 338 | return 339 | 340 | if len(selection): 341 | (filename, line_num, contextlines) = diff_info.idx_hunks[selection[0]] 342 | toplevel = os.popen("git rev-parse --show-toplevel").read().strip(); 343 | editor = os.getenv("EDITOR", "vi") 344 | r = os.system("bash -c '%s %s +%d'" % ( 345 | editor, os.path.join(toplevel, filename), line_num + contextlines)) 346 | sys.exit(r) 347 | 348 | if __name__ == "__main__": 349 | main() 350 | 351 | -------------------------------------------------------------------------------- /git-list-clones: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | import os 5 | 6 | """ 7 | Recursive directory traversal just in order to find clones. Works quite fast 8 | because we stop at the clones. 9 | """ 10 | 11 | def recurse_dir(pathname): 12 | if os.path.exists(os.path.join(pathname, ".git")): 13 | print(pathname) 14 | return 15 | 16 | names = os.listdir(pathname) 17 | for name in names: 18 | fullname = os.path.join(pathname, name) 19 | if os.path.islink(fullname): 20 | continue 21 | if os.path.isdir(fullname): 22 | recurse_dir(fullname) 23 | 24 | def main(args): 25 | if len(args) == 1: 26 | cwd = "." 27 | else: 28 | cwd = args[1] 29 | recurse_dir(cwd) 30 | 31 | if __name__ == "__main__": 32 | main(sys.argv) 33 | -------------------------------------------------------------------------------- /git-mass-branch-rename: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | Git branch editor. 5 | 6 | This script spawns the editor configured in Git, in order to edit the local 7 | list of branches as if it was a text file. It allows removing branches, adding 8 | branches, changing branches' names, and setting a branch to a different 9 | githash. 10 | """ 11 | 12 | import os 13 | import pty 14 | import subprocess as sp 15 | import sys 16 | import re 17 | import tempfile 18 | import argparse 19 | 20 | PREFIX = "refs/heads/" 21 | 22 | def dict_diff(a, b): 23 | for key in list(set(a.keys()) | set(b.keys())): 24 | yield (a.get(key, None), b.get(key, None)) 25 | 26 | def main(): 27 | parser = argparse.ArgumentParser(description='Mass branch editor') 28 | parser.add_argument('--by-date', dest="by_date", action='store_true') 29 | args = parser.parse_args() 30 | 31 | x = os.popen('git for-each-ref --format="%(objectname) %(refname) ' + 32 | '# %(committerdate:format:%Y-%m-%d): %(subject)"', "r") 33 | data = x.read() 34 | x.close() 35 | 36 | lst = [] 37 | for line in data.splitlines(): 38 | m = re.match(r"^([a-f0-9]+) ([^ ]+) (.*)$", line) 39 | (githash, refname, meta) = m.groups() 40 | if not refname.startswith(PREFIX): 41 | continue 42 | lst.append((githash, refname, meta)) 43 | 44 | if args.by_date: 45 | lst.sort(key=lambda item: item[2]) 46 | 47 | branches = {} 48 | idx = 1 49 | for item in lst: 50 | branches[idx] = item 51 | idx += 1 52 | 53 | editor = os.popen("git config core.editor", "r").read().strip() 54 | filename = tempfile.mktemp("git-mass-branch-rename") 55 | filehandle = open(filename, "w") 56 | for (key, (githash, refname, meta)) in branches.items(): 57 | refname = refname[len(PREFIX):] 58 | githash = githash[:12] 59 | print("%d: %s %-35s %s" % (key, githash, refname, meta), file=filehandle) 60 | filehandle.close() 61 | 62 | cmd = "%s %s" % (editor, filename) 63 | os.system(cmd) 64 | 65 | output = {} 66 | for line in open(filename): 67 | m = re.match(r"^([0-9]+): +([a-f0-9]+) +([^ ]+)( +.*)$", line.strip()) 68 | (idx, githash, new_name, _) = m.groups(0) 69 | idx = int(idx) 70 | output[idx] = (githash, new_name) 71 | 72 | os.unlink(filename) 73 | 74 | for (orig, after) in dict_diff(branches, output): 75 | cmd = None 76 | if after is None and orig is not None: 77 | (orig_githash, orig_refname, _) = orig 78 | cmd = "git update-ref -d %s" % (orig_refname, ) 79 | elif orig is not None and after is not None: 80 | (orig_githash, orig_refname, _) = orig 81 | (new_githash, new_name) = after 82 | branch_name = orig_refname[len(PREFIX):] 83 | cmd = "" 84 | if orig_githash[:12] != new_githash: 85 | cmd = "git update-ref %s %s" % (orig_refname, new_githash) 86 | if branch_name != new_name: 87 | if cmd: 88 | cmd += "&& " 89 | cmd += "git branch -M %s %s" % (branch_name, new_name) 90 | 91 | if cmd: 92 | print(cmd) 93 | os.system(cmd) 94 | 95 | if __name__ == "__main__": 96 | main() 97 | -------------------------------------------------------------------------------- /git-mru-branch: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import re 5 | import time 6 | from optparse import OptionParser 7 | 8 | 9 | def main(): 10 | # 11 | # Here, the recently checked out branches from the reflog are 12 | # presented sorted according to the most recently checked out branch. 13 | # 14 | # Can be used with `pick` (https://github.com/calleerlandsson/pick) or fzf. 15 | # 16 | 17 | parser = OptionParser() 18 | 19 | # Add branch state vebosity 20 | parser.add_option("-v", "--verbose", dest="verbose", action="store_true") 21 | parser.add_option("-f", "--fzf-prefix", dest="fzf_prefix", action="store_true") 22 | 23 | (options, _args) = parser.parse_args() 24 | m = re.compile(r"([0-9]+[ \t]+[+-/]?[0-9]+)[ \t]+checkout: moving from ([.A-Za-z0-9_/-]+) to ([.A-Za-z0-9_/-]+)") 25 | 26 | git_dir = os.popen("git rev-parse --git-common-dir").read().strip() 27 | existing_branches = set([l[2:].strip() for l in os.popen("git branch").readlines()]) 28 | 29 | # Take the list of worktrees HEADs logs 30 | head_paths = [] 31 | candidate = os.path.join(git_dir, "logs", "HEAD") 32 | if os.path.exists(candidate): 33 | head_paths.append(candidate) 34 | 35 | worktrees = os.path.join(git_dir, "worktrees") 36 | if os.path.exists(worktrees): 37 | for sub in os.listdir(worktrees): 38 | worktree = os.path.join(worktrees, sub) 39 | candidate = os.path.join(worktree, "logs", "HEAD") 40 | if os.path.exists(candidate): 41 | head_paths.append(candidate) 42 | 43 | prep_set = set() 44 | lst = [] 45 | 46 | worktrees = get_worktree_state() 47 | 48 | # Take in branches according to the timestamp of their mention 49 | # in the reflog. 50 | for head_path in head_paths: 51 | head = open(head_path).readlines() 52 | head.reverse() 53 | for i in head: 54 | r = m.search(i) 55 | if not r: 56 | continue 57 | 58 | (timestamp, from_src, to_dest) = r.groups(0) 59 | timestamp = timestamp.split(' ')[0] 60 | for name in [to_dest, from_src]: 61 | if name in existing_branches: 62 | lst.append((int(timestamp), name)) 63 | prep_set.add(name) 64 | 65 | # Take in branches according to the timestamp of their 'ref' file 66 | # under .git. 67 | ref_file_ts = set() 68 | for branch in existing_branches: 69 | branch_ref = os.path.join(git_dir, "refs", "heads", branch) 70 | if os.path.exists(branch_ref): 71 | ts = os.stat(branch_ref).st_mtime 72 | ref_file_ts.add(branch) 73 | lst.append((int(ts), branch)) 74 | 75 | # Take the remaining branches according to the commit date 76 | branch_info = get_branches_info() 77 | for (branch, info) in branch_info.items(): 78 | if branch not in ref_file_ts and branch not in prep_set: 79 | lst.append((int(info["timestamp"]), branch)) 80 | 81 | # Sort, keeping most recent branches last 82 | lst.sort() 83 | branch_to_ts = {} 84 | for (ts, branch) in lst: 85 | branch_to_ts[branch] = ts 86 | lst.reverse() 87 | 88 | final_list = [] 89 | final_set = set() 90 | 91 | current_branch = os.popen("git rev-parse --abbrev-ref HEAD").read().strip() 92 | 93 | for (ts, branch) in lst: 94 | if branch not in final_set: 95 | final_set.add(branch) 96 | final_list.insert(0, branch) 97 | 98 | for branch in existing_branches: 99 | if branch not in final_set: 100 | final_list.insert(0, branch) 101 | 102 | max_len_branch = 0 103 | for branch in final_list: 104 | max_len_branch = max(len(branch), max_len_branch) 105 | for branch in final_list: 106 | if options.verbose: 107 | worktree = worktrees.get(branch, None) 108 | worktree_name = "" 109 | worktree_color = "" 110 | if worktree: 111 | if os.path.exists(worktree["worktree"]): 112 | if os.path.exists(os.path.join(worktree["worktree"], 113 | ".git/objects")): 114 | worktree_name = "WtMain" 115 | worktree_color = color24(0, 200, 200) 116 | else: 117 | worktree_name = "WtExist" 118 | worktree_color = color24(0, 200, 0) 119 | else: 120 | worktree_name = "WtOrphan" 121 | worktree_color = color24(200, 200, 0) 122 | color_reset = colorreset() 123 | date = "" 124 | date_color = "" 125 | ts = branch_to_ts.get(branch, None) 126 | if ts: 127 | date_color = color24(100, 100, 100) 128 | date = pretty_date(int(time.time()) - ts) 129 | info = branch_info.get(branch, None) 130 | objname_color = "" 131 | subject_color = "" 132 | if info: 133 | githash = info['objname'][:12] 134 | subject = info['subject'] 135 | objname_color = color24(70, 70, 70) 136 | subject_color = color24(210, 230, 250) 137 | 138 | branch_color = "" 139 | if current_branch == branch: 140 | branch_color = backcolor24(30, 30, 30) 141 | 142 | if options.fzf_prefix: 143 | print(f"{branch} ", end="") 144 | print( 145 | f"{date_color}{date:>14}{color_reset} " 146 | f"{worktree_color}{worktree_name:>8}{color_reset} " 147 | f"{branch_color}{branch:<{max_len_branch}}{color_reset} " 148 | f"{objname_color}{githash}{color_reset} " 149 | f"{subject_color}{subject}{color_reset}" 150 | ) 151 | else: 152 | print(branch) 153 | 154 | 155 | def get_branches_info(): 156 | cmd = ("git for-each-ref --sort=-committerdate " 157 | "refs/heads/ --format='%(committerdate:unix) ;_;_; %(refname) ;_;_; " 158 | "%(objectname) ;_;_; %(subject)'") 159 | d = {} 160 | for line in os.popen(cmd).readlines(): 161 | (ts, b, objname, subject) = line.split(' ;_;_; ') 162 | b = b.strip() 163 | if b.startswith('refs/heads/'): 164 | b = b[len('refs/heads/'):] 165 | d[b] = dict(timestamp=int(ts), 166 | objname=objname, subject=subject.strip()) 167 | return d 168 | 169 | 170 | def get_worktree_state(): 171 | worktree = dict() 172 | worktree_by_branch = dict() 173 | for line in os.popen("git worktree list --porcelain").readlines(): 174 | if not line.strip(): 175 | if worktree['HEAD'] != '0000000000000000000000000000000000000000': 176 | prefix = 'refs/heads/' 177 | if 'branch' in worktree: 178 | if worktree['branch'].startswith(prefix): 179 | worktree['branch'] = worktree['branch'][len(prefix):] 180 | worktree_by_branch[worktree['branch']] = worktree 181 | worktree = {} 182 | continue 183 | params = line.strip().split(' ', 1) 184 | if len(params) == 2: 185 | worktree[params[0]] = params[1] 186 | return worktree_by_branch 187 | 188 | 189 | def pretty_date(diff): 190 | """ 191 | Get a datetime object or a int() Epoch timestamp and return a 192 | pretty string like 'an hour ago', 'Yesterday', '3 months ago', 193 | 'just now', etc 194 | """ 195 | second_diff = int(diff) 196 | day_diff = int(diff / 86400) 197 | 198 | if day_diff < 0: 199 | return '' 200 | 201 | if day_diff == 0: 202 | if second_diff < 10: 203 | return "just now" 204 | if second_diff < 60: 205 | return str(second_diff) + " seconds" 206 | if second_diff < 120: 207 | return "a minute" 208 | if second_diff < 3600: 209 | return str(int(second_diff / 60)) + " minutes" 210 | if second_diff < 7200: 211 | return "an hour" 212 | if second_diff < 86400: 213 | return str(int((second_diff / 3600))) + " hours" 214 | if day_diff == 1: 215 | return "Yesterday" 216 | if day_diff < 7: 217 | return str(day_diff) + " days" 218 | if day_diff < 31: 219 | return str(int(day_diff / 7)) + " weeks" 220 | if day_diff < 365: 221 | return str(int(day_diff / 30)) + " months" 222 | return str(int(day_diff / 365)) + " years" 223 | 224 | 225 | def color24(r, g, b1): 226 | return "\x1b[38;2;%d;%d;%dm" % (r, g, b1) 227 | 228 | 229 | def backcolor24(r, g, b1): 230 | return "\x1b[48;2;%d;%d;%dm" % (r, g, b1) 231 | 232 | 233 | def colorreset(): 234 | return "\x1b[0;m" 235 | 236 | 237 | if __name__ == "__main__": 238 | main() 239 | -------------------------------------------------------------------------------- /git-multi-cherry-pick: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | 3 | # 4 | # Get N commits, and try commiting them in the order given, and retry again the failed 5 | # commits until reaching the end of the list. 6 | # 7 | # This means that the time complexity is O(N^2) in the number of commits. 8 | # 9 | 10 | import os 11 | import sys 12 | import tempfile 13 | import time 14 | import re 15 | import cPickle 16 | 17 | EDITOR="EDITOR.TEMP" 18 | 19 | class TempFile(object): 20 | filename = None 21 | 22 | class Info(dict): 23 | def __init__(self, cherry_picked_branch = None, original_branch = None): 24 | self.cherry_picked_branch = cherry_picked_branch 25 | self.original_branch = original_branch 26 | 27 | @classmethod 28 | def write(selfclass, v): 29 | f = open(selfclass.tempfile, "w") 30 | f.write(v) 31 | f.close() 32 | 33 | @classmethod 34 | def read(selfclass): 35 | return open(selfclass.tempfile).read() 36 | 37 | @staticmethod 38 | def load(): 39 | info = TempFile.Info() 40 | vars(info).update(cPickle.loads(TempFile.read())) 41 | return info 42 | 43 | @staticmethod 44 | def save(info): 45 | TempFile.write(cPickle.dumps(vars(info))) 46 | 47 | def editor_commit_editmsg(filename): 48 | info = TempFile.load() 49 | f = open(filename) 50 | output = [] 51 | for line in f: 52 | output.append(line) 53 | output.append("(representive of original merge " + info.original_branch + ")\n") 54 | f.close() 55 | f = open(filename, "w") 56 | f.write(''.join(output)) 57 | f.close() 58 | TempFile.save(info) 59 | 60 | 61 | def main(): 62 | if len(sys.argv) <= 1: 63 | print "syntax: git-multi-cherry-pick [description-file]" 64 | return 65 | 66 | commits = [] 67 | for commit in open(sys.argv[1]).readlines(): 68 | commit = commit.strip() 69 | if commit: 70 | commits.append(commit) 71 | 72 | commits_succeeded = [] 73 | commits_failed = [] 74 | 75 | while len(commits): 76 | commits_retry = [] 77 | 78 | for commit in commits: 79 | if '#' in commit: 80 | commit_id = commit.split('#', 1)[0] 81 | else: 82 | commit_id = commit 83 | 84 | print "-"*80 85 | print "git-multi-cherry-pick: Trying to apply: %s" % (commit, ) 86 | print 87 | ret = os.system("git cherry-pick %s" % (commit_id, )) 88 | if ret != 0: 89 | commits_retry.append(commit) 90 | print 91 | print "git-multi-cherry-pick: failed this time" 92 | os.system("git cherry-pick --abort") 93 | else: 94 | commits_succeeded.append(commit) 95 | 96 | if len(commits_retry) == len(commits): 97 | commits_failed = commits_retry 98 | break 99 | else: 100 | commits = commits_retry 101 | 102 | if commits_succeeded: 103 | print 104 | print "Succeeded: " 105 | print 106 | for commit in commits_succeeded: 107 | print commit 108 | print 109 | 110 | if commits_failed: 111 | print 112 | print "Failed: " 113 | print 114 | for commit in commits_failed: 115 | print commit 116 | print 117 | 118 | if __name__ == "__main__": 119 | main() 120 | -------------------------------------------------------------------------------- /git-new-project: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | dirname=$(basename $(realpath $(pwd))) 6 | 7 | git init 8 | git-set-email 9 | git add . 10 | 11 | if [[ "$(git status --porcelain | wc -l)" == "0" ]] ; then 12 | touch .gitignore 13 | git add . 14 | fi 15 | 16 | git commit -a -m "Initial commit for \`$dirname\`" 17 | -------------------------------------------------------------------------------- /git-original-rev: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # 4 | # What is the commit we are trying to apply now during 5 | # rebase? This answers the question. 6 | # 7 | 8 | set -u 9 | set -e 10 | 11 | if [[ -e .git/rebase-apply/original-commit ]] ; then 12 | cat .git/rebase-apply/original-commit 13 | exit 0 14 | fi 15 | 16 | exit -2 17 | -------------------------------------------------------------------------------- /git-range-compare: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | Compare two ranges. Somewhat more readable than git-range-diff. 5 | """ 6 | 7 | import sys 8 | import os 9 | import tempfile 10 | import re 11 | from optparse import OptionParser 12 | 13 | def system(cmd): 14 | return os.system(cmd) 15 | 16 | class Abort(Exception): pass 17 | 18 | class RefTrack(object): 19 | def __init__(self): 20 | pass 21 | 22 | def get_base(self, branch, suggested_base): 23 | base = suggested_base 24 | if not base: 25 | # "Try .base suffix" 26 | base = f"{branch}.base" 27 | base_state = os.popen(f"git show-ref {base} 2>/dev/null").read().strip() 28 | if base_state == "": 29 | # Try most recent tag 30 | base_state = os.popen(f"git describe --tags --abbrev=0 {branch}").read().strip() 31 | if base_state == "": 32 | print(f"Did not find a default 'base' branch {base}", file=sys.stderr) 33 | raise Abort() 34 | else: 35 | base_state = os.popen(f"git show-ref {base}").read().strip() 36 | if base_state == "": 37 | print(f"Base {base} is invalid", file=sys.stderr) 38 | raise Abort() 39 | 40 | githash = base_state.split(' ', 1)[0] 41 | # It may be a git tag, we need a commit hash: 42 | base = os.popen(f"git rev-list -n 1 {githash}").read().strip() 43 | return (base, base_state) 44 | 45 | def get_commits(self, a): 46 | commits = [] 47 | b = self.get_base(a, None)[0] 48 | for line in os.popen(f"git log --oneline {b}..{a} --pretty='%H: [%ci] %s' --no-decorate").readlines(): 49 | (h, message) = line.split(': ', 1) 50 | commits.append((h, message)) 51 | return commits 52 | 53 | def diff_ranges(rt, source, dest): 54 | a = rt.get_commits(source) 55 | b = rt.get_commits(dest) 56 | 57 | outputs = [] 58 | for z in [a, b]: 59 | tf = tempfile.NamedTemporaryFile() 60 | for commit in z: 61 | cmd = f"git show {commit[0]} | git patch-id --stable" 62 | rsp = os.popen(cmd).read().strip() 63 | if rsp != "": 64 | (patch_id, commit_id) = rsp.split(' ') 65 | else: 66 | (patch_id, commit_id) = ("____epmty___", commit[0]) 67 | tf.write((patch_id + "\n").encode('utf-8')) 68 | tf.flush() 69 | outputs.append(tf) 70 | 71 | first_idx = 0 72 | second_idx = 0 73 | diff = os.popen(f"diff -U 10000 -durN {outputs[0].name} {outputs[1].name}").readlines()[3:] 74 | patch_id_counts = {} 75 | for line in diff: 76 | patch_id = None 77 | if line.startswith(' '): 78 | patch_id = line.strip() 79 | elif line.startswith('+'): 80 | patch_id = line.strip()[1:] 81 | elif line.startswith('-'): 82 | patch_id = line.strip()[1:] 83 | if patch_id: 84 | patch_id_counts[patch_id] = 1 + patch_id_counts.get(patch_id, 0) 85 | 86 | for line in diff: 87 | f_id = ("%s:%d" % (source, first_idx)) 88 | s_id = ("%s:%d" % (dest, second_idx)) 89 | if line.startswith(' '): 90 | patch_id = line.strip() 91 | print(' ', 'Id:' + patch_id[:12], " ", a[first_idx][0][:12], a[first_idx][1].strip()) 92 | first_idx += 1 93 | second_idx += 1 94 | elif line.startswith('+'): 95 | patch_id = line.strip()[1:] 96 | if patch_id_counts[patch_id] >= 2: 97 | print('\u001b[38;2;0;120;0m', end='') 98 | else: 99 | print('\u001b[32m', end='') 100 | print('+', 'Id:' +patch_id[:12], ("%6s" % s_id), b[second_idx][0][:12], b[second_idx][1].strip()) 101 | print('\u001b[0m', end='') 102 | second_idx += 1 103 | elif line.startswith('-'): 104 | patch_id = line.strip()[1:] 105 | if patch_id_counts[patch_id] >= 2: 106 | print('\u001b[38;2;120;0;0m', end='') 107 | else: 108 | print('\u001b[31m', end='') 109 | print('-', 'Id:' +patch_id[:12], ("%6s" % f_id), a[first_idx][0][:12], a[first_idx][1].strip()) 110 | print('\u001b[0m', end='') 111 | first_idx += 1 112 | 113 | def diff(*args): 114 | parser = OptionParser() 115 | rt = RefTrack() 116 | 117 | source = args[0] 118 | dest = args[1] 119 | if not (':' in source and ":" in dest): 120 | return diff_ranges(rt, source, dest) 121 | 122 | (source, source_p) = source.split(':', 1) 123 | (dest, dest_p) = dest.split(':', 1) 124 | a = rt.get_commits(source) 125 | b = rt.get_commits(dest) 126 | source_commit = a[int(source_p)][0] 127 | dest_commit = b[int(dest_p)][0] 128 | tf1 = tempfile.NamedTemporaryFile() 129 | tf1.write(os.popen(f"git show {source_commit} --no-decorate").read().encode('utf-8')) 130 | tf1.flush() 131 | tf2 = tempfile.NamedTemporaryFile() 132 | tf2.write(os.popen(f"git show {dest_commit} --no-decorate").read().encode('utf-8')) 133 | tf2.flush() 134 | print(os.popen(f"diff -urN {tf1.name} {tf2.name} | delta-configured").read()) 135 | 136 | def main(): 137 | parser = OptionParser() 138 | diff(sys.argv[1], sys.argv[2]) 139 | 140 | if __name__ == "__main__": 141 | main() 142 | -------------------------------------------------------------------------------- /git-rebase-auto-sink: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | 5 | The purpose of this script is to help doing an automatic rebase 'fixup' for a 6 | set of commits, where for each fixup commit, we don't know to which commit 7 | we would like to squash it. 8 | 9 | This is similar to the problem that 'git-absorb' [1] is trying to solve. 10 | 11 | Algoritm: 12 | 13 | * Take two commit refs from the user: 14 | 15 | [base] the start of the rebase 16 | [sink_start] the most recent commit that is not part of the fixup group 17 | 18 | * Rebase the list commits in `[sink_start]..` in reverse to make sure that 19 | each commit in the list is independent. 20 | 21 | * For each commit in `[sink_start]..`, do a binary search using 'git rebase' 22 | over `[base]..[sink_start]` to find the farther in the history in which it 23 | can be laid. 24 | 25 | A much more comprehensive work is done in [2]. 26 | 27 | [1] https://github.com/tummychow/git-absorb 28 | [2] https://github.com/aspiers/git-deps 29 | 30 | """ 31 | 32 | 33 | import sys 34 | import os 35 | import tempfile 36 | import re 37 | from optparse import OptionParser 38 | import itertools 39 | import tempfile 40 | 41 | def system(cmd): 42 | return os.system(cmd) 43 | 44 | class Abort(Exception): pass 45 | 46 | def abort(msg): 47 | print(msg, file=sys.stderr) 48 | sys.exit(-1) 49 | 50 | def try_rebase(base, lst): 51 | tf = tempfile.NamedTemporaryFile() 52 | tf.write("\n".join(lst).encode('utf-8')) 53 | editor_cmd = f"{sys.argv[0]} from-rebase {tf.name}" 54 | cmd = f"git -c core.editor='{editor_cmd}' rebase -i {base}" 55 | return system(cmd) 56 | 57 | def rebase_abort(): 58 | r = system("git rebase --abort") 59 | if r != 0: 60 | raise Abort("git rebase --abort failed") 61 | 62 | def from_rebase(lst, rebase_script): 63 | f = open(rebase_script, "w") 64 | for githash in lst: 65 | f.write(f"pick {githash}\n") 66 | f.close() 67 | 68 | def main(): 69 | if sys.argv[1] == "from-rebase": 70 | lst = open(sys.argv[2]).read().split('\n') 71 | from_rebase(lst, sys.argv[3]) 72 | sys.exit(0) 73 | 74 | parser = OptionParser() 75 | parser.add_option("-b", "--base", dest="base") 76 | parser.add_option("-s", "--sink-start", dest="sink") 77 | (options, _args) = parser.parse_args() 78 | 79 | if not options.base and not options.sink: 80 | abort("missing parameters") 81 | 82 | patchset = [] 83 | for githash in os.popen(f"git log --pretty='%H' --reverse {options.sink}.."): 84 | patchset.append(githash.strip()) 85 | 86 | revlist = [] 87 | for githash in os.popen(f"git log --pretty='%H' --reverse {options.base}..{options.sink}"): 88 | revlist.append(githash.strip()) 89 | 90 | if len(patchset) >= 10: 91 | abort(f"too many patches {len(patchset)}") 92 | 93 | print("Checking a reversed list of {len(patchset)} patches") 94 | patchset.reverse() 95 | res = try_rebase(options.base, revlist + patchset) 96 | if res != 0: 97 | sys.exit(-1) 98 | 99 | for patch_idx, patch in enumerate(patchset): 100 | print(f"Trying to fit {patch} into history [{patch_idx + 1}/{len(patchset)}]") 101 | start = 0 102 | end = len(revlist) 103 | 104 | best_save = None 105 | best_save_reflist = None 106 | 107 | while start <= end: 108 | middle = int((start + end) / 2) 109 | print(f" At {middle} for [{start}, {end}]") 110 | try_revlist = revlist[:middle] + [patch] + revlist[middle:] 111 | prev_version = os.popen("git rev-parse HEAD").read().strip() 112 | res = try_rebase(options.base, try_revlist) 113 | if res != 0: 114 | start = middle + 1 115 | rebase_abort() 116 | else: 117 | best_save = os.popen("git rev-parse HEAD").read().strip() 118 | best_save_reflist = try_revlist 119 | if end == start: 120 | break 121 | end = middle - 1 122 | os.system(f"git reset --hard {prev_version}") 123 | 124 | if best_save is not None: 125 | os.system(f"git reset --hard {best_save}") 126 | revlist = best_save_reflist 127 | 128 | print("Done") 129 | 130 | if __name__ == "__main__": 131 | main() 132 | -------------------------------------------------------------------------------- /git-rebase-cmd: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | 5 | The purpose of this script is to automate 'rebase interactive' when you already 6 | know what you want to do and can quickly specify it in the command line, for 7 | example 'float 2', or a sequence of commands, e.g: 8 | 9 | git-rabase-cmd HEAD~3 drop 1 swap 0 2 10 | 11 | Here, the numbers given are from the end of the 'pick' list, where the last 12 | item is '0', the one before last is `1`, so forth, to match 'HEAD', 'HEAD~1', 13 | and 'HEAD~2', and so forth. 14 | 15 | fixup Change the item `nr` to 'fixup' 16 | iexec Insert an exec as item `nr` 17 | msgdrop Drop commits where commit subject matches 'msg' exactly. 18 | drop Change the item `nr` to 'drop' 19 | sink Swap between items 'a' and 'b'. 20 | flip Like 'swap 0 1', for when you want to swap the 21 | two most recent commits. 22 | float Move commit 'a' to be last commit applied. 23 | sink Move commit 'a' to be first commit applied. 24 | 25 | Other command line options: 26 | 27 | -o/--onto Relay that option to rebase 28 | -n Dry-run - show the rebase script would like like after the 29 | operations. 30 | 31 | """ 32 | 33 | 34 | import sys 35 | import os 36 | import tempfile 37 | import re 38 | from optparse import OptionParser 39 | import itertools 40 | import tempfile 41 | 42 | def system(cmd): 43 | return os.system(cmd) 44 | 45 | class Abort(Exception): pass 46 | 47 | def abort(msg): 48 | print(msg, file=sys.stderr) 49 | sys.exit(-1) 50 | 51 | DRY_RUN = "GIT_REBASE_CMD__OPTION_DRY_RUN" 52 | 53 | def try_rebase(base, command_line, options): 54 | tf = tempfile.NamedTemporaryFile() 55 | for arg in command_line: 56 | tf.write((arg + "\n").encode('utf-8')) 57 | tf.flush() 58 | editor_cmd = f"{sys.argv[0]} from-rebase {tf.name}" 59 | if options.dry_run: 60 | os.putenv(DRY_RUN, "1") 61 | cmd = f"git -c advice.waitingforeditor=false -c core.editor='{editor_cmd}' rebase -i {base}" 62 | if options.onto: 63 | cmd += f" --onto {options.onto}" 64 | if options.dry_run: 65 | r = system(cmd + " 2>/dev/null") 66 | sys.exit(0) 67 | return 68 | 69 | return system(cmd) 70 | 71 | def rebase_abort(): 72 | r = system("git rebase --abort") 73 | if r != 0: 74 | raise Abort("git rebase --abort failed") 75 | 76 | def parse_commands(commands): 77 | raw_cmds = list(commands) 78 | cmds = [] 79 | while len(raw_cmds) > 0: 80 | if raw_cmds[0] == "drop": 81 | del raw_cmds[0] 82 | cmds.append(("drop", parse_int("drop", raw_cmds))) 83 | elif raw_cmds[0] == "fixup": 84 | del raw_cmds[0] 85 | cmds.append(("fixup", parse_int("fixup", raw_cmds))) 86 | elif raw_cmds[0] == "iexec": 87 | del raw_cmds[0] 88 | v = parse_int("iexec", raw_cmds) 89 | e = parse_str("iexec", raw_cmds) 90 | cmds.append(("iexec", v, e)) 91 | elif raw_cmds[0] == "msgdrop": 92 | del raw_cmds[0] 93 | e = parse_str("msgdrop", raw_cmds) 94 | cmds.append(("msgdrop", e)) 95 | elif raw_cmds[0] == "float": 96 | del raw_cmds[0] 97 | cmds.append(("float", parse_int("float", raw_cmds))) 98 | elif raw_cmds[0] == "sink": 99 | del raw_cmds[0] 100 | cmds.append(("sink", parse_int("sink", raw_cmds))) 101 | elif raw_cmds[0] == "swap": 102 | del raw_cmds[0] 103 | a = parse_int("swap", raw_cmds) 104 | b = parse_int("swap", raw_cmds) 105 | cmds.append(("swap", a, b)) 106 | elif raw_cmds[0] == "flip": 107 | del raw_cmds[0] 108 | cmds.append(("swap", 0, 1)) 109 | else: 110 | abort(f"unknown command {raw_cmds[0]}") 111 | return cmds 112 | 113 | def from_rebase(command_line, rebase_script): 114 | f = open(rebase_script, "r") 115 | script = f.read().splitlines() 116 | f.close() 117 | 118 | lines = [] 119 | for line in script: 120 | line = line.strip() 121 | if not line: 122 | continue 123 | if line.startswith('#'): 124 | continue 125 | lines.append(line) 126 | script = lines 127 | 128 | for command in parse_commands(command_line): 129 | if command[0] == "drop": 130 | line_nr = command[1] 131 | if line_nr >= 0 and line_nr < len(script): 132 | x = len(script) - line_nr - 1 133 | script[x] = 'drop ' + script[x].split(' ', 1)[1] 134 | elif command[0] == "fixup": 135 | line_nr = command[1] 136 | if line_nr >= 0 and line_nr < len(script): 137 | x = len(script) - line_nr - 1 138 | script[x] = 'fixup ' + script[x].split(' ', 1)[1] 139 | elif command[0] == "iexec": 140 | line_nr = command[1] 141 | cmd = command[2] 142 | if line_nr >= 0 and line_nr < len(script): 143 | x = len(script) - line_nr - 1 144 | script.insert(x, 'exec ' + cmd) 145 | elif command[0] == "msgdrop": 146 | msg = command[1] 147 | for line_nr in range(0, len(script)): 148 | parts = script[line_nr].split(' ', 2) 149 | if parts[2] == msg: 150 | parts[0] = 'drop' 151 | script[line_nr] = ' '.join(parts) 152 | elif command[0] == "swap": 153 | a_nr = command[1] 154 | b_nr = command[2] 155 | if a_nr >= 0 and a_nr < len(script): 156 | if b_nr >= 0 and b_nr < len(script): 157 | a = len(script) - a_nr - 1 158 | b = len(script) - b_nr - 1 159 | line = script[a] 160 | script[a] = script[b] 161 | script[b] = line 162 | elif command[0] == "float": 163 | line_nr = command[1] 164 | if line_nr >= 0 and line_nr < len(script): 165 | x = len(script) - line_nr - 1 166 | save = script[x] 167 | del script[x:x+1] 168 | script.append(save) 169 | elif command[0] == "sink": 170 | line_nr = command[1] 171 | if line_nr >= 0 and line_nr < len(script): 172 | x = len(script) - line_nr - 1 173 | save = script[-1] 174 | del script[-1] 175 | script = script[:x] + [save] + script[x:] 176 | 177 | new_script = '\n'.join(script) 178 | print(new_script) 179 | f = open(rebase_script, "w") 180 | f.write(new_script + "\n") 181 | f.close() 182 | 183 | def parse_int(prefix, raw_cmds): 184 | if len(raw_cmds) == 0: 185 | abort(f"{prefix} needs a line number ([1-])") 186 | try: 187 | v = int(raw_cmds[0]) 188 | except ValueError: 189 | abort(f"{prefix} needs an int") 190 | del raw_cmds[0] 191 | return v 192 | 193 | def parse_str(prefix, raw_cmds): 194 | if len(raw_cmds) == 0: 195 | abort(f"{prefix} needs a line number ([1-])") 196 | try: 197 | v = raw_cmds[0] 198 | except ValueError: 199 | abort(f"{prefix} needs an int") 200 | del raw_cmds[0] 201 | return v 202 | 203 | def main(): 204 | if sys.argv[1:] and sys.argv[1] == "from-rebase": 205 | args = open(sys.argv[2]).read().split('\n')[:-1] 206 | from_rebase(args, sys.argv[3]) 207 | if os.getenv(DRY_RUN) == "1": 208 | sys.exit(-1) 209 | sys.exit(0) 210 | 211 | parser = OptionParser() 212 | parser.add_option("--onto", "-o", dest="onto") 213 | parser.add_option("--dry-run", "-n", dest="dry_run", action="store_true") 214 | 215 | (options, args) = parser.parse_args() 216 | if not args: 217 | print("No arguments") 218 | sys.exit(0) 219 | 220 | target = args[0] 221 | del args[0] 222 | 223 | if not parse_commands(args): 224 | abort("No commands given") 225 | 226 | res = try_rebase(target, args, options) 227 | if res != 0: 228 | sys.exit(-1) 229 | 230 | if __name__ == "__main__": 231 | main() 232 | -------------------------------------------------------------------------------- /git-reftrack: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | 5 | The purpose of this script is to help track rebases of a local branch using a 6 | special meta-branch called a 'reftrack' branch. This form of tracking can be 7 | considered as a glorified 'reflog', that is also publishable, better described, 8 | and an alternative to littering a repository with adhoc tags of versions that 9 | you may not want to keep forever. 10 | 11 | The script is invoked simply via: 12 | 13 | git-reftrack commit -b [base] 14 | 15 | Where `base` is the base of the current branch (i.e the most recent commit that 16 | this branch does not add). The commits in the branch are calculated from 17 | `[base]..HEAD`. 18 | 19 | When the current branch is named 'x', the created or updated reftrack branch 20 | is named 'x.reftrack'. 21 | 22 | The structure of the reftrack branch is the following; 23 | 24 | * Each commit is a merge commit with the following parents 25 | 26 | [prev-reftrack] [version] [base] 27 | Or 28 | [version] [base] 29 | 30 | For the first commit. 31 | 32 | * The commit message is of the format: 33 | 34 | ``` 35 | git-reftrack: {subject} 36 | 37 | Base: {ref description} 38 | Commits: 39 | 40 | {list of commits in the branch} 41 | ``` 42 | 43 | """ 44 | 45 | import sys 46 | import os 47 | import tempfile 48 | import re 49 | from optparse import OptionParser 50 | 51 | def system(cmd): 52 | return os.system(cmd) 53 | 54 | class Abort(Exception): pass 55 | 56 | class RefTrack(object): 57 | def __init__(self): 58 | pass 59 | 60 | def current_branch(self): 61 | current_branch = os.popen("git branch --show-current").read().strip() 62 | if not current_branch: 63 | print("No current branch, aborted", file=sys.stderr) 64 | raise Abort() 65 | return current_branch 66 | 67 | def get_base(self, suggested_base): 68 | current_branch = self.current_branch() 69 | base = suggested_base 70 | if not base: 71 | base = f"{current_branch}.base" 72 | base_state = os.popen(f"git show-ref {base}").read().strip() 73 | if base_state == "": 74 | print(f"Did not find a default 'base' branch {base}", file=sys.stderr) 75 | raise Abort() 76 | else: 77 | base_state = os.popen(f"git show-ref {base}").read().strip() 78 | if base_state == "": 79 | print(f"Base {base} is invalid", file=sys.stderr) 80 | raise Abort() 81 | 82 | githash = base_state.split(' ', 1)[0] 83 | # It may be a git tag, we need a commit hash: 84 | base = os.popen(f"git rev-list -n 1 {githash}").read().strip() 85 | return (base, base_state) 86 | 87 | def get_reftrack_state(self): 88 | current_branch = self.current_branch() 89 | reftrack_branch = f"{current_branch}.reftrack" 90 | reftrack_state = os.popen(f"git show-ref {reftrack_branch}").read().strip() 91 | return (reftrack_branch, reftrack_state) 92 | 93 | def get_versions(self): 94 | (reftrack_branch, reftrack_state) = self.get_reftrack_state() 95 | cmd = f"git log --first-parent {reftrack_branch} --pretty='%H: [%ci] %s' --merges --grep='^git-reftrack:'" 96 | lst = os.popen(cmd).readlines() 97 | v = [] 98 | for (idx, line) in enumerate(lst): 99 | line = line.strip() 100 | if not line: 101 | continue 102 | (h, message) = line.split(': ', 1) 103 | v.append((len(lst) - idx, h[:12], message)) 104 | return v 105 | 106 | def get_versions_dict(self): 107 | v = {} 108 | for (id, commit_hash, message) in self.get_versions(): 109 | v[str(id)] = (commit_hash, message) 110 | return v 111 | 112 | def get_reftrack_commits(self, git_hash): 113 | cmd = f"git show --pretty='%P' {git_hash}" 114 | parents = os.popen(cmd).read().strip().split(' ') 115 | if len(parents) == 3: 116 | (a, b) = (parents[1], parents[2]) 117 | else: 118 | (a, b) = (parents[0], parents[1]) 119 | commits = [] 120 | for line in os.popen(f"git log --oneline {b}..{a} --pretty='%H: [%ci] %s' --no-decorate").readlines(): 121 | (h, message) = line.split(': ', 1) 122 | commits.append((h, message)) 123 | return commits 124 | 125 | def commit(args): 126 | parser = OptionParser() 127 | parser.add_option("-b", "--base", dest="base", help="base branch") 128 | (options, args) = parser.parse_args(args) 129 | 130 | rt = RefTrack() 131 | current_branch = rt.current_branch() 132 | (base, base_state) = rt.get_base(options.base) 133 | (reftrack_branch, reftrack_state) = rt.get_reftrack_state() 134 | 135 | state = "HEAD" 136 | 137 | tf = tempfile.NamedTemporaryFile() 138 | tempname = tf.name 139 | first_line = f"git-reftrack: \n\n".encode('utf-8') 140 | tf.write(f"git-reftrack: \n\n".encode('utf-8')) 141 | tf.write(f"Base: {base_state}\n".encode('utf-8')) 142 | tf.write(f"Commits:\n\n".encode('utf-8')) 143 | changes = os.popen(f"git log --oneline {base}..{state} --no-decorate").read().strip() 144 | tf.write((changes + "\n").encode('utf-8')) 145 | tf.flush() 146 | 147 | editor = os.popen("git config core.editor").read().strip() 148 | 149 | system(f"{editor} {tempname}") 150 | 151 | first_line = open(tempname).readlines()[0] 152 | m = re.match("git-reftrack: .+", first_line) 153 | if not m: 154 | print("commit message not valid, aborted", file=sys.stderr) 155 | return 156 | 157 | cmd = f'git commit-tree {state}^{{tree}} -F {tempname}' 158 | if reftrack_state != '': 159 | cmd += f' -p {reftrack_branch}' 160 | cmd += f" -p {state} -p {base}" 161 | 162 | commit_object = os.popen(cmd).read().strip() 163 | if commit_object == "": 164 | raise Exception("error creating commit object") 165 | 166 | cmd = f"git update-ref refs/heads/{reftrack_branch} {commit_object}" 167 | r = system(cmd) 168 | tf.close() 169 | 170 | print(f"Branch {reftrack_branch} updated.") 171 | 172 | def log(args): 173 | parser = OptionParser() 174 | rt = RefTrack() 175 | for (id, commit_hash, message) in rt.get_versions(): 176 | print(id, commit_hash, message) 177 | 178 | def diff_reftracks(rt, source, dest): 179 | v = rt.get_versions_dict() 180 | a = rt.get_reftrack_commits(v[source][0]) 181 | b = rt.get_reftrack_commits(v[dest][0]) 182 | 183 | outputs = [] 184 | for z in [a, b]: 185 | tf = tempfile.NamedTemporaryFile() 186 | for commit in z: 187 | (patch_id, commit_id) = os.popen(f"git show {commit[0]} | git patch-id").read().strip().split(' ') 188 | tf.write((patch_id + "\n").encode('utf-8')) 189 | tf.flush() 190 | outputs.append(tf) 191 | 192 | first_idx = 0 193 | second_idx = 0 194 | for line in os.popen(f"diff -U 10000 -durN {outputs[0].name} {outputs[1].name}").readlines()[3:]: 195 | f_id = ("%s:%d" % (source, first_idx)) 196 | s_id = ("%s:%d" % (dest, second_idx)) 197 | if line.startswith(' '): 198 | patch_id = line.strip() 199 | print(' ', 'Id:' + patch_id[:12], " ", a[first_idx][0][:12], a[first_idx][1].strip()) 200 | first_idx += 1 201 | second_idx += 1 202 | elif line.startswith('+'): 203 | patch_id = line.strip()[1:] 204 | print('\u001b[32m', end='') 205 | print('+', 'Id:' +patch_id[:12], ("%6s" % s_id), b[second_idx][0][:12], b[second_idx][1].strip()) 206 | print('\u001b[0m', end='') 207 | second_idx += 1 208 | elif line.startswith('-'): 209 | patch_id = line.strip()[1:] 210 | print('\u001b[31m', end='') 211 | print('-', 'Id:' +patch_id[:12], ("%6s" % f_id), a[first_idx][0][:12], a[first_idx][1].strip()) 212 | print('\u001b[0m', end='') 213 | first_idx += 1 214 | 215 | def diff(*args): 216 | parser = OptionParser() 217 | rt = RefTrack() 218 | 219 | v = {} 220 | for (id, commit_hash, message) in rt.get_versions(): 221 | v[str(id)] = (commit_hash, message) 222 | 223 | source = args[0] 224 | dest = args[1] 225 | if not (':' in source and ":" in dest): 226 | return diff_reftracks(rt, source, dest) 227 | 228 | v = rt.get_versions_dict() 229 | (source, source_p) = source.split(':', 1) 230 | (dest, dest_p) = dest.split(':', 1) 231 | a = rt.get_reftrack_commits(v[source][0]) 232 | b = rt.get_reftrack_commits(v[dest][0]) 233 | source_commit = a[int(source_p)][0] 234 | dest_commit = b[int(dest_p)][0] 235 | tf1 = tempfile.NamedTemporaryFile() 236 | tf1.write(os.popen(f"git show {source_commit} --no-decorate").read().encode('utf-8')) 237 | tf1.flush() 238 | tf2 = tempfile.NamedTemporaryFile() 239 | tf2.write(os.popen(f"git show {dest_commit} --no-decorate").read().encode('utf-8')) 240 | tf2.flush() 241 | print(os.popen(f"diff -urN {tf1.name} {tf2.name}").read()) 242 | 243 | def main(): 244 | parser = OptionParser() 245 | if len(sys.argv) == 1: 246 | print("commit - commit a new reftrack change") 247 | print(" log - show a log of reftrack changes") 248 | print(" diff - help to diff two reftrack versions") 249 | return 250 | if sys.argv[1] == "commit": 251 | commit(sys.argv[2:]) 252 | elif sys.argv[1] == "log": 253 | log(sys.argv[2:]) 254 | elif sys.argv[1] == "diff": 255 | diff(sys.argv[2], sys.argv[3]) 256 | else: 257 | print(f"unknown command {sys.argv[1]}", file=sys.stderr) 258 | sys.exit(-1) 259 | 260 | if __name__ == "__main__": 261 | main() 262 | -------------------------------------------------------------------------------- /git-remote-clone: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # 4 | # Imprefect script to create a clone of the local repository on a remote server that 5 | # is accessible by ssh. 6 | # 7 | 8 | if [ "$#" -ne 1 ] ; then 9 | echo "Expected host:path" >&2 10 | exit -1 11 | fi 12 | 13 | set -e 14 | set -u 15 | 16 | X="$1" 17 | arr=(`echo $X | tr ':' ' '`) 18 | 19 | dest=${arr[0]} 20 | remote_path=${arr[1]} 21 | 22 | if [ -z "$dest" ] ; then 23 | echo "Destination not specified" >&2 24 | exit -1 25 | fi 26 | 27 | if [ -z "$remote_path" ] ; then 28 | echo "Path on destination not specified" >&2 29 | exit -1 30 | fi 31 | 32 | branch=master 33 | 34 | ssh -t ${dest} "mkdir -p ${remote_path} && cd ${remote_path} && git init" || exit -1 35 | ssh -t ${dest} "cd ${remote_path} && git config receive.denyCurrentBranch no" || exit -1 36 | ssh -t ${dest} "cd ${remote_path} && git init && touch nothing && git -c user.name=empty -c user.email=empty@empty add nothing" || exit -1 37 | ssh -t ${dest} "cd ${remote_path} && git -c user.name=empty -c user.email=empty@empty commit -m nothing --author='nothing '" || exit -1 38 | 39 | git-remote-push "$@" 40 | -------------------------------------------------------------------------------- /git-remote-push: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # 4 | # Before there was `git config receive.denyCurrentBranch updateInstead`, I used this 5 | # to update working trees on a remote server. 6 | # 7 | # DEPENDENCIES: `git-bottle` (see repo). 8 | # 9 | 10 | if [ "$#" -eq 0 ] ; then 11 | echo "Expected host:path or remote-name" >&2 12 | exit -1 13 | fi 14 | 15 | set -u 16 | set -e 17 | 18 | REMOTE_DESC="$1" 19 | 20 | shift 1 21 | arr=(`echo $REMOTE_DESC | tr ':' ' '`) 22 | 23 | if [[ ${#arr[@]} == 1 ]] ; then 24 | # No ':" in provided name 25 | while read -r line ; do 26 | remote_name=$(echo $line | awk -F" " '{print $1}') 27 | remote_url=$(echo $line | awk -F" " '{print $2}') 28 | if [[ "${remote_name}" == "${REMOTE_DESC}" ]] ; then 29 | REMOTE_DESC=${remote_url} 30 | break 31 | fi 32 | done < <(git remote -v | grep push) 33 | 34 | arr=(`echo $REMOTE_DESC | tr ':' ' '`) 35 | if [[ ${#arr[@]} == 1 ]] || [[ ${#arr[@]} == 0 ]]; then 36 | echo "Could not figure out remote path" 37 | exit -1 38 | fi 39 | fi 40 | 41 | dest=${arr[0]} 42 | remote_path=${arr[1]} 43 | 44 | if [ -z "$dest" ] ; then 45 | echo "Destination not specified" >&2 46 | exit -1 47 | fi 48 | 49 | if [ -z "$remote_path" ] ; then 50 | echo "Path on destination not specified" >&2 51 | exit -1 52 | fi 53 | 54 | branch=master 55 | 56 | git-bottle 57 | version=`git rev-parse HEAD` 58 | git-unbottle 59 | 60 | git push "$@" -f ${dest}:${remote_path} ${version}:${branch} 61 | ssh -t ${dest} "cd ${remote_path} && git reset --hard ${branch}" 62 | -------------------------------------------------------------------------------- /git-retext: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | Like git-rebase interactive, but instead of editing a script, you edit the 5 | exported patches directly, which are presented in an email format. 6 | 7 | You can edit the diff directly. For allowing this, this relies on 8 | `recountdiff` to fix diff hunk header numbers. See `recountdiff` regarding the 9 | editing of diffs. 10 | """ 11 | 12 | import sys 13 | import os 14 | import tempfile 15 | import re 16 | import subprocess 17 | 18 | from optparse import OptionParser 19 | 20 | def system(cmd): 21 | return os.system(cmd) 22 | 23 | class Abort(Exception): pass 24 | 25 | def e_system(cmd): 26 | r = system(cmd) 27 | if r != 0: 28 | raise Abort(f"command {cmd} failed: {r}"); 29 | 30 | C = re.compile("From ([a-f0-9]+) Mon Sep 17 00:00:00 2001") 31 | 32 | def to_commits(content): 33 | commits = [] 34 | githash = None 35 | lines = [] 36 | for line in content.splitlines(): 37 | match = C.match(line) 38 | if match: 39 | if lines: 40 | commits.append((githash, lines)) 41 | lines = [] 42 | githash = match.groups(0)[0] 43 | lines.append(line) 44 | if lines: 45 | commits.append((githash, lines)) 46 | return commits 47 | 48 | def error_msg(msg): 49 | print('\u001b[31m', end='', file=sys.stderr) 50 | print(msg, file=sys.stderr) 51 | print('\u001b[0m', end='', file=sys.stderr) 52 | sys.stderr.flush() 53 | 54 | def main(): 55 | parser = OptionParser() 56 | 57 | base = sys.argv[1] 58 | ret = os.system("git status --porcelain") 59 | if ret != 0: 60 | print("There are uncommited changes", file=sys.stderr) 61 | return 62 | 63 | original = os.popen(f"git rev-parse HEAD").read().strip() 64 | if original == "": 65 | print("Unable to retrive original revision", file=sys.stderr) 66 | return 67 | 68 | 69 | rc = subprocess.call(['which', 'recountdiff']) 70 | if rc != 0: 71 | print('recountdiff missing in path!', file=sys.stderr) 72 | return 73 | 74 | e_system(f"git rev-parse {base} > /dev/null") 75 | editor = os.popen("git config core.editor").read().strip() 76 | if not editor: 77 | editor = os.getenv("EDITOR", "vi") 78 | 79 | filename = tempfile.mktemp("git-retext.diff") 80 | 81 | pipe = os.popen(f"git format-patch --no-signature --stdout {base}..", "r") 82 | before_edit = pipe.read() 83 | 84 | fobj = open(filename, "w") 85 | fobj.write(before_edit) 86 | fobj.close() 87 | 88 | rcode = os.system(f"{editor} {filename}") 89 | success = False 90 | try: 91 | if rcode == 0: 92 | after_edit = open(filename, "r").read() 93 | new_data = [] 94 | for (_, lines) in to_commits(after_edit): 95 | fobj = open(filename, "w") 96 | fobj.write("\n".join(lines)) 97 | fobj.close() 98 | pipe = os.popen(f"recountdiff {filename}") 99 | fixed = pipe.read() + "\n" 100 | ret = pipe.close() 101 | if ret: 102 | error_msg("recountdiff returned error") 103 | new_data.append(fixed) 104 | 105 | fobj = open(filename, "w") 106 | for data in new_data: 107 | fobj.write(data) 108 | fobj.close() 109 | 110 | e_system(f"git reset --hard {base}") 111 | e_system(f"git am < {filename}") 112 | success = True 113 | finally: 114 | os.unlink(filename) 115 | 116 | if not success: 117 | error_msg("Not successful, reverting to original") 118 | sys.stderr.flush() 119 | 120 | system(f"git am --abort") 121 | e_system(f"git reset --hard {original}") 122 | sys.exit(1) 123 | 124 | if __name__ == "__main__": 125 | main() 126 | -------------------------------------------------------------------------------- /git-set-email: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # 4 | # Set my per-repository credentials using dirlocalenv. 5 | # 6 | # See: https://github.com/da-x/dirlocalenv 7 | # 8 | 9 | set -e 10 | 11 | dirname=$(basename $(pwd)) 12 | email=$(dirlocalenv bash -c 'echo $EMAIL_SPHERE') 13 | git config user.email ${email} 14 | -------------------------------------------------------------------------------- /git-skip: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # 4 | # Skip a patch in a rebase or am, depending on the situation. 5 | # 6 | 7 | set -e 8 | 9 | git_dir="$(git rev-parse --git-dir)" 10 | if [[ -e "${git_dir}/rebase-apply/applying" ]] ; then 11 | exec git am --skip "$@" 12 | elif [[ -e "${git_dir}/rebase-apply" ]] ; then 13 | exec git rebase --skip "$@" 14 | elif [[ -e "${git_dir}/rebase-merge" ]] ; then 15 | exec git rebase --skip "$@" 16 | else 17 | echo git-continue: unknown state >&2 18 | exit -1 19 | fi 20 | -------------------------------------------------------------------------------- /git-trash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | 5 | A tool to hide/unhide local branches by moving them to non-branches refs, 6 | following suggestion from the following stack overflow question: 7 | 8 | https://stackoverflow.com/questions/25169440/remove-hide-git-branches-without-deleting-commit-histories 9 | 10 | The trashed local branches are accessible via: `refs/trash/`. 11 | 12 | The commands are: 13 | 14 | list List all trashed branches 15 | throw Throw the given branches to the trash 16 | restore Restore the given branches out of the trash 17 | 18 | """ 19 | 20 | import sys 21 | import os 22 | import re 23 | 24 | def system(cmd): 25 | print(cmd) 26 | return os.system(cmd) 27 | 28 | class Abort(Exception): pass 29 | 30 | def abort(msg): 31 | print(msg, file=sys.stderr) 32 | sys.exit(-1) 33 | 34 | RE = re.compile("^([^ ]+) refs/([^/]*)/(.*)$") 35 | 36 | def get_refs(): 37 | for line in os.popen("git show-ref"): 38 | m = RE.match(line) 39 | if not m: 40 | continue 41 | yield m.groups(0) 42 | 43 | def cmd_list(): 44 | for _, kind, name in list(get_refs()): 45 | if kind == "trash": 46 | print(name) 47 | 48 | def cmd_throw(branches): 49 | heads = set() 50 | trash = set() 51 | refs = list(get_refs()) 52 | 53 | for _, kind, name in refs: 54 | if kind == "heads": 55 | heads.add(name) 56 | if kind == "trash": 57 | trash.add(name) 58 | 59 | branches = set(branches) 60 | not_found = branches.difference(heads) 61 | if not_found: 62 | print(f"Branches not found: {not_found}") 63 | sys.exit(1) 64 | already_trashed = branches.intersection(trash) 65 | if already_trashed: 66 | print(f"Already trashed: {already_trashed}") 67 | sys.exit(1) 68 | 69 | for dhash, kind, name in refs: 70 | if kind == "heads" and name in branches: 71 | system(f"git update-ref refs/trash/{name} {dhash}") 72 | system(f"git update-ref -d refs/heads/{name}") 73 | 74 | def cmd_restore(branches): 75 | heads = set() 76 | trash = set() 77 | refs = list(get_refs()) 78 | 79 | for _, kind, name in refs: 80 | if kind == "heads": 81 | heads.add(name) 82 | if kind == "trash": 83 | trash.add(name) 84 | 85 | branches = set(branches) 86 | not_found = branches.difference(trash) 87 | if not_found: 88 | print(f"Trash not found: {not_found}") 89 | sys.exit(1) 90 | already_restored = branches.intersection(heads) 91 | if already_restored: 92 | print(f"Branches already exist: {already_restored}") 93 | sys.exit(1) 94 | 95 | for dhash, kind, name in refs: 96 | if kind == "trash" and name in branches: 97 | system(f"git update-ref refs/heads/{name} {dhash}") 98 | system(f"git update-ref -d refs/trash/{name}") 99 | 100 | def main(): 101 | if not sys.argv[1:]: 102 | print("No command given. Use list/throw/restore") 103 | return 104 | if sys.argv[1] == "list": 105 | cmd_list() 106 | elif sys.argv[1] == "throw": 107 | cmd_throw(sys.argv[2:]) 108 | elif sys.argv[1] == "restore": 109 | cmd_restore(sys.argv[2:]) 110 | else: 111 | print("unknown command") 112 | 113 | if __name__ == "__main__": 114 | main() 115 | -------------------------------------------------------------------------------- /git-undo-amend: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # Undo a `git amend -a` using the git reflog. Note that it cannot detect 5 | # whether `git amend -a` was the last operation, so if that's not the case it 6 | # would behave unexpectedly. 7 | # 8 | 9 | set -e 10 | 11 | git diff --exit-code 12 | git diff --cached --exit-code 13 | 14 | CHANGES=$(git rev-parse HEAD) 15 | 16 | git reset --hard HEAD@{1} 17 | git checkout $CHANGES -- . 18 | -------------------------------------------------------------------------------- /hooks/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # An example hook script to verify what is about to be pushed to master. Called 4 | # by "git push" after it has checked the remote status, but before anything has 5 | # been pushed. If this script exits with a non-zero status nothing will be 6 | # pushed. 7 | # 8 | # This hook is called with the following parameters: 9 | # 10 | # $1 -- Name of the remote to which the push is being done 11 | # $2 -- URL to which the push is being done 12 | # 13 | # If pushing without using a named remote those arguments will be equal. 14 | # 15 | # Information about the commits which are being pushed is supplied as lines to 16 | # the standard input in the form: 17 | # 18 | # 19 | # 20 | # This sample shows how to prevent push of commits where the log message starts 21 | # with "WIP" (work in progress). 22 | 23 | remote="$1" 24 | url="$2" 25 | 26 | z40=0000000000000000000000000000000000000000 27 | 28 | IFS=' ' 29 | while read local_ref local_sha remote_ref remote_sha 30 | do 31 | if [ "$local_sha" = $z40 ] 32 | then 33 | # Handle delete 34 | : 35 | else 36 | if [ "$remote_sha" = $z40 ] 37 | then 38 | # New branch, will not compare 39 | break 40 | else 41 | # Update to existing branch, examine new commits 42 | range="$remote_sha..$local_sha" 43 | fi 44 | 45 | commit=`git rev-list -n 1 --grep '^WIP' "$range"` 46 | if [ -n "$commit" ] 47 | then 48 | echo "Found WIP commit ${commit} in $local_ref, not pushing" 49 | exit 1 50 | fi 51 | commit=`git rev-list -n 1 --grep '^squash!' "$range"` 52 | if [ -n "$commit" ] 53 | then 54 | echo "Found squash! commit ${commit} in $local_ref, not pushing" 55 | exit 1 56 | fi 57 | commit=`git rev-list -n 1 --grep '^fixup!' "$range"` 58 | if [ -n "$commit" ] 59 | then 60 | echo "Found fixup! commit ${commit} in $local_ref, not pushing" 61 | exit 1 62 | fi 63 | commit=`git rev-list -n 1 --grep '^FIXUP' "$range"` 64 | if [ -n "$commit" ] 65 | then 66 | echo "Found fixup! commit ${commit} in $local_ref, not pushing" 67 | exit 1 68 | fi 69 | commit=`git rev-list -n 1 --grep '^DONT_PUSH' "$range"` 70 | if [ -n "$commit" ] 71 | then 72 | echo "Found a \"don't push\" commit ${commit} in $local_ref, not pushing" 73 | exit 1 74 | fi 75 | fi 76 | done 77 | 78 | exit 0 79 | 80 | -------------------------------------------------------------------------------- /stg-rebase-i: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # Attempt to implement a 'rebase interactive' for stgit. 5 | # 6 | # See: https://stacked-git.github.io 7 | # 8 | 9 | TMPFILE=$(mktemp tmp.XXXXXXX.stg-rebase-i) 10 | EDITOR=$(git config core.editor) 11 | 12 | if [[ "$EDITOR" == "" ]]; then 13 | EDITOR=vi 14 | fi 15 | 16 | function finish { 17 | rm -f ${TMPFILE} 18 | } 19 | trap finish EXIT 20 | stg series > ${TMPFILE} 21 | 22 | $EDITOR ${TMPFILE} 23 | 24 | if [[ "$?" != "0" ]] ; then 25 | echo "stg-rebase-i: aborted" 26 | exit -1 27 | fi 28 | 29 | GOTO_PATCH=$(grep '^> ' ${TMPFILE} | cut -c3- | head -n 1) 30 | cat ${TMPFILE} | cut -c3- > ${TMPFILE}.x 31 | mv ${TMPFILE}.x ${TMPFILE} 32 | stg float --series ${TMPFILE} 33 | 34 | if [[ "$?" != "0" ]] ; then 35 | echo "stg-rebase-i: aborted" 36 | exit -1 37 | fi 38 | 39 | if [[ "${GOTO_PATCH}" != "" ]] ; then 40 | stg goto ${GOTO_PATCH} 41 | fi 42 | --------------------------------------------------------------------------------