Skip to content

Instantly share code, notes, and snippets.

@parrotbait
Created August 25, 2017 16:41
Show Gist options
  • Save parrotbait/1f24c6ea11cb6ec1f0518b762b81d92e to your computer and use it in GitHub Desktop.
Save parrotbait/1f24c6ea11cb6ec1f0518b762b81d92e to your computer and use it in GitHub Desktop.
Git has an unfortunate habit of detaching heads every time you change branches on the top level repo. This script goes through all the submodules and looks for a commit on a branch that matches the current hash of that branch. If it finds it it does a checkout on the branch. NOTE: this isn't foolproof, if there are multiple branches with the sam…
#!/usr/bin/env python
import platform
import os
import sys
import io
import subprocess
BASE_DIR = os.getcwd()
print("" + BASE_DIR)
DEBUG_OUTPUT = False
def log(string):
print("--- " + string)
def errorLog(string):
print("--- ERROR " + string)
def dlog(string):
if DEBUG_OUTPUT:
print("*** " + string)
def findToolCommand(command, paths_to_search, required = False):
command_res = command
found = False
for path in paths_to_search:
command_abs = os.path.join(path, command)
if os.path.exists(command_abs):
command_res = command_abs
found = True
break;
if required and not found:
log("WARNING: command " + command + " not found, but required by script")
dlog("Found '" + command + "' as " + command_res)
return command_res
# execute command but return status of command as an integer
def executeCommand(command, printCommand = False, quiet = False):
printCommand = printCommand or DEBUG_OUTPUT
out = None
err = None
if quiet:
out = open(os.devnull, 'w')
err = subprocess.STDOUT
if printCommand:
if DEBUG_OUTPUT:
dlog(">>> " + command)
else:
log(">>> " + command)
return subprocess.call(command, shell = True, stdout=out, stderr=err);
# Execute command but return output as text
def executeCommandWithOutput(command, printCommand = False, quiet = False):
printCommand = printCommand or DEBUG_OUTPUT
out = None
err = None
if quiet:
out = open(os.devnull, 'w')
err = subprocess.STDOUT
if printCommand:
if DEBUG_OUTPUT:
dlog(">>> " + command)
else:
log(">>> " + command)
return os.popen(command).read()
def dieIfNonZero(res):
if res != 0:
raise ValueError("Command returned non-zero status.");
# Returns a list of dictionaries whhere each item is a submodule
def getHeadsOfSubmodulesIncludingGodRepo():
submoduleRecords = [];
output = executeCommandWithOutput(TOOL_COMMAND_GIT + " submodule");
if (len(output) == 0):
errorLog("No submodule results found");
return;
mainBranchHash = executeCommandWithOutput(TOOL_COMMAND_GIT + " rev-parse HEAD").strip()
submoduleRecords.append({"hash": mainBranchHash, "path": "."});
submoduleRecord = output.split('\n');
for submodule in submoduleRecord:
submoduleText = submodule.strip();
if (len(submoduleText) == 0):
continue;
recordDetails = submodule.strip().split(' ');
dlog ("submodule hash " + recordDetails[0] + ", path: " + recordDetails[1]);
submoduleRecords.append({"hash":recordDetails[0], "path":recordDetails[1]});
return submoduleRecords;
def getHeadsOfCurrentBranches():
branchRecords = [];
output = executeCommandWithOutput(TOOL_COMMAND_GIT + " for-each-ref --sort=-committerdate refs/heads/ --format='%(objectname) %(refname:short)'").strip();
if (len(output) == 0):
errorLog("No branch results found");
return;
branchRecord = output.split('\n');
for branch in branchRecord:
branchText = branch.strip();
if (len(branchText) == 0):
continue;
branchDetails = branchText.strip().split(' ');
dlog ("branch name " + branchDetails[1] + ", hash: " + branchDetails[0]);
branchRecords.append({"hash":branchDetails[0], "name":branchDetails[1]});
return branchRecords;
log("Fixing Detached Heads in " + BASE_DIR);
paths_to_search = os.environ["PATH"].split(":") + ["/usr/local/bin", "/opt/local/bin", "/usr/bin"]
TOOL_COMMAND_GIT = findToolCommand("git", paths_to_search, required = True)
# get all repos
repos = getHeadsOfSubmodulesIncludingGodRepo();
mainRepoBranchName = "";
# update repos first
executeCommand(TOOL_COMMAND_GIT + " submodule update --recursive")
# Go through all repos, check the current hash of the branch
# If we detect it is a DETACHED HEAD then we check all branches
# If we find a branch whose tip commit matches what we're at then perform a git command
for repo in repos:
os.chdir(os.path.join(BASE_DIR, repo["path"]));
currentHead = executeCommandWithOutput(TOOL_COMMAND_GIT + " rev-parse --symbolic-full-name --abbrev-ref HEAD").strip()
repoName = repo["path"];
if (repoName == "."):
repoName = "God-Repo";
log ("==============================================");
log ("Repo: " + repoName);
log ("Hash: '" + repo["hash"] + "'");
log ("Current head: '" + currentHead + "'");
# Is it a detached head?
if currentHead == "HEAD":
branchRecords = getHeadsOfCurrentBranches();
currentHeadCommitHash = executeCommandWithOutput(TOOL_COMMAND_GIT + " rev-parse HEAD").strip()
branchName = "";
branchHash = "";
for branch in branchRecords:
log ("checking branch '" + branch["name"] + "', hash: " + branch["hash"]);
if branch["hash"] == currentHeadCommitHash:
if (branchName == mainRepoBranchName or len(branchName) == 0):
branchName = branch["name"];
branchHash = branch["hash"];
if len(branchName) > 0 and len(branchHash) > 0:
log("Found match for current hash in branch: " + branchName);
# NOTE: pulled out sourcetree specifics so the user interface updates correctly
dieIfNonZero(executeCommand(TOOL_COMMAND_GIT + " -c diff.mnemonicprefix=false -c core.quotepath=false -c credential.helper=sourcetree checkout " + branchName));
else:
log ("HEAD DETACHED - Did not find a commit on any branch tip for hash " + currentHeadCommitHash);
else:
log("Repo - HEAD is not detached");
mainRepoBranchName = currentHead;
log ("==============================================");
log("")
os.chdir(BASE_DIR);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment