Skip to content

Instantly share code, notes, and snippets.

@devinrhode2
Created January 4, 2020 18:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save devinrhode2/f8bf7aa3031b7c6c0a5060cf2f46262f to your computer and use it in GitHub Desktop.
Save devinrhode2/f8bf7aa3031b7c6c0a5060cf2f46262f to your computer and use it in GitHub Desktop.
Some submodule post-checkout and post-merge scripts+dump of submodule notes

In your package.json scripts... add these without newlines/escapes

    "submodulePostCheckoutCorrectDetachedHeads": "
    git submodule sync --recursive &&
    git submodule update --checkout &&
    git submodule foreach \
      'latest_commits_remote_branch=$(\
        git branch -r --contains `git log -1 --pretty=format:\"%h\"`
       ) node $toplevel/dev-scripts/checkout_right_branch $sm_path'", // end submodule post-checkout
    "submodulePostMergeUpdateSubmoduleMerge": "
      echo \"(this script used to also be run postinstall to ease setup) Running submoduleUpdateMergeIntoBranchesInGitmodules npm script\" &&
      git submodule update --init --recursive --merge --no-fetch --remote &&
      git submodule sync --recursive && git submodule foreach \
        'git checkout `git config -f $toplevel/.gitmodules submodule.$sm_path.branch`' &&
      git submodule update --init --recursive --merge &&
      echo \"submoduleUpdateMergeIntoBranchesInGitmodules npm script completed successfully\"",

In husky post-checkout+post-merge, run these scripts (npm run submoduleFooBar). You can also put the submodule post merge in the postinstall npm script (scripts.postinstall - runs after npm install)

Git has a bug (as of 2.24) where push.recurseSubmodules = on-demand doens't work

You can have a pre-push hook where it works around this issue, but in vscode, pushes will always APPEAR to fail, but all submodules will actually get pushed. Pushing 2nd time (after successful push) will not "appear" to fail again, it will appear successful.

      "pre-push-hook-that-recursively-pushed-submodules": "echo \"husky pre-push hook (has git hook .sample)\" && CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) && git push -u origin $CURRENT_BRANCH --recurse-submodules=on-demand --no-verify",

You probably want a pre-commit hook that warns you once about commiting to your submodule master branch, if you are trying to enforce a pr workflow

Also on preinstall/after the repository clones, inside the project folder, run this on mac/linux:

git config --local include.path ../dev-scripts/.gitconfig

On windows run:

git config --local include.path $PWD/dev-scripts/.gitconfig

With this .gitconfig inside your project

# you need to run 
# git config --local include.path ../.gitconfig
# for git to incorporate this config
# it's run automatically on npm install via the package.json postinstall script

# These are more unique and probably should not be set in your global config
[status]
	submodulesummary = 1
[submodule]
	recurse = true
[uploadpack]
	allowReachableSHA1InWant = true
[blame]
	ignoreRevsFile = .git-blame-ignore-revs
[core]
	ignorecase = false
	autocrlf = false # https://git-scm.com/docs/gitattributes#_end_of_line_conversion
[push]
	recurseSubmodules = check
	# recurseSubmodules = on-demand # this setting makes git push fail in superproject like so:
	# git push                                                                                                                                                      git:dev*
	# husky > pre-push (node v12.10.0)
	# husky pre-push hook (has git hook .sample)
	# fatal: src refspec 'refs/heads/dev' must name a ref
	# fatal: process for submodule 'src/app/q360-comps/animated-meter' failed

# If enabling git "large file system", .gitattributes file should also have binary file modified like so:
# Example: *.ai filter=lfs diff=lfs merge=lfs binary
# See https://rehansaeed.com/gitattributes-best-practices/
# [filter "lfs"]
# 	process = git-lfs filter-process
# 	required = true
# 	clean = git-lfs clean -- %f
# 	smudge = git-lfs smudge -- %f

I recommend having only one submodule, ./packages, which holds all shared code.

Instead of forking I recommend duplicating folders inside this shared git repo.

It'd be great to have a npm track the git hash of a sibling folder (npm install ../shared-code). It'd be also great to extend git to be able to allow sibling submodules.

If you are using submodules you are perhaps sharing code between multiple projects.

If you want to work on that submodule and have changes apply to both projects at the same time, you need git to follow symlinks.

Reviewing code/pr workflow...

Review code before pushing:

git diff origin/dev --submodule=diff --  ':(exclude)build' ':!package-lock.json' ':!*.svg' ':!cached-responses'

Reviewing code before pulling...

git fetch (or git fetch origin <branch>)
git diff FETCH_HEAD --submodule=diff --  ':(exclude)build' ':!package-lock.json' ':!*.svg' ':!cached-responses'

Optionally review all commits with commit messages+diffs

git log HEAD..FETCH_HEAD -p --submodule=diff --  ':(exclude)build' ':!package-lock.json' ':!*.svg' ':!cached-responses'

checkout_commits_branch.js

/* globals process */

const branch = process.env.latest_commits_branch.trim()
const [remote, branchName] = branch.split('/')

console.log('remote', remote, 'branchName', branchName, 'cleanExec', cleanExec)
console.log('pwd arg', '0', process.argv0, '1', process.argv1, '2', process.argv2, '3', process.argv3)
const cleanExec = require('./cleanExec')

cleanExec('pwd', wd => {
  console.log('pwd inside node', wd)
})

// maybe async version slows your computer down less.
// cleanExec('git rev-parse --abbrev-ref HEAD', currentBranch => {
//   if (currentBranch.trim() === 'master') {
//     console.error('you cant be on master branch')
//     return process.exit(1)
//   }
//   cleanExec('git status --porcelain', stdout => {
//     stdout = stdout.replace(' M cached-responses', '').trim()
//     if (stdout === '') {
//       console.log('git status is clean.')
//     } else {
//       cleanExec('git status', gitStatus => {
//         console.log(gitStatus)
//         console.error('\n   **COMMIT THESE CHANGES BEFORE CREATING BUILD**')
//         process.exit(1)
//       })
//     }
//   })
// })

checkout_right_branch.js

// used only inside npm run

// was originally intended to be run like so, making way for more features:
// git submodule foreach 'cd $toplevel && configured_branch=$(git config submodule.$sm_path.branch) && latest_commits_remote_branch=$(git branch -r --contains `git log -1 --pretty=format:\"%h\"`) && node $toplevel/dev-scripts/checkout-right-branch $sm_path'

// but configured_branch stuff was removed due to an error (which I didn't have time to investigate, since it's not necessary)
// so it's like this:
// git submodule foreach 'echo $(pwd) && echo $toplevel && latest_commits_remote_branch=$(git branch -r --contains `git log -1 --pretty=format:"%h"`) node $toplevel/dev-scripts/checkout_right_branch $sm_path'
// submodule_checkout

// full thing:
/*
git config --local include.path ../dev-scripts/.gitconfig && git submodule sync --recursive && git submodule update --checkout && git submodule foreach 'echo $(pwd) && echo $toplevel && latest_commits_remote_branch=$(git branch -r --contains `git log -1 --pretty=format:"%h"`) node $toplevel/dev-scripts/checkout_right_branch $sm_path' node $toplevel/dev-scripts/checkout_right_branch $sm_path'
*/

/* globals process */
console.log('latest_commits_remote_branch:start:' + process.env.latest_commits_remote_branch + ':END')
const branchContainsCommit = process.env.latest_commits_remote_branch.trim()
let branch
if (branchContainsCommit.includes('\n')) {
  branch = branchContainsCommit.split('\n').pop()
} else {
  branch = branchContainsCommit
}
if (branch.split('/').length !== 2) {
  throw new Error(
    'dev-scripts/checkout_right_branch.js expected branch to be like `  origin/feature-foo` but it was:',
    branch
  )
}
branch = branch.trim()
const [remote, branchName] = branch.split('/')

// console.log('remote', remote, 'branchName', branchName)
// console.log('pwd arg', '0', process.argv0, '1', process.argv1, '2', process.argv2, '3', process.argv3)

// eslint-disable-next-line security/detect-child-process
const { exec } = require('child_process')

// maybe async version slows your computer down less.
function cleanExecNoThrow(cmd, callback = () => {}) {
  console.error(`executing: ${cmd}`)
  exec(cmd, function execCompleted(err, stdout, stderr) {
    // if (err !== null) {
    //   console.error(`nodejs err:`, err)
    // }
    if (stderr !== '') {
      console.error(`stderr:`, stderr)
      if (callback) callback(stderr, 'is_stderr')
    }
    if (callback) callback(stdout)
  })
}

// usage: cleanExec('git status --porcelain', stdout => {

cleanExecNoThrow(
  `cd $toplevel && cd $sm_path && git checkout -b ${branchName} --track ${branch}`,
  (checkoutAttempt, is_stderr) => {
    if (!is_stderr) console.log(checkoutAttempt)
    if (checkoutAttempt.includes("fatal: A branch named '" + branchName + "' already exists.")) {
      cleanExecNoThrow(
        `cd $toplevel && cd $sm_path && git checkout ${branchName}`,
        (checkoutAttempt2, is_stderr2) => {
          if (!is_stderr2) console.log(checkoutAttempt2)
        }
      )
    }
  }
)

cleanExec.js

/* globals module */
// eslint-disable-next-line security/detect-child-process
const { exec } = require('child_process')

// maybe async version slows your computer down less.
module.exports = function cleanExec(cmd, callback) {
  exec(cmd, function execCompleted(err, stdout, stderr) {
    if (err !== null || stderr !== '') {
      if (err !== null) {
        console.error(`nodejs err exec'uting ${cmd}:`, err)
      }
      if (stderr !== '') {
        console.error(`error exec'uting ${cmd}:`, stderr)
      }
      throw new Error('see logs above.')
    }
    if (callback) callback(stdout)
  })
}

// usage: cleanExec('git status --porcelain', stdout => {

setCorrectSubmoduleBranches.js

/* globals process */
const cleanExec = require('./cleanExec')

cleanExec('pwd', wd => {
  console.log('pwd inside node', wd)
  const branch = process.env.latest_commits_branch.trim()
  const [remote, branchName] = branch.split('/')

  console.log('remote', remote, 'branchName', branchName, 'cleanExec', cleanExec)
  console.log('arg0', process.argv0, '1', process.argv1, '2', process.argv2, '3', process.argv3)
})

// git submodule sync --recursive
// git submodule update --checkout
// git submodule foreach 'git checkout `git config -f $toplevel/.gitmodules submodule.$sm_path.branch`'

// git config --local include.path ../dev-scripts/.gitconfig

// latest_commits_branch=$(git branch -r --contains `git log -1 --pretty=format:"%h"`)

// latest_commits_branch=$(git branch -r --contains `git log -1 --pretty=format:"%h"`) && echo "current dir:${pwd}" && pwd && echo "sha? $1" &&

// latest_commits_branch=$(git branch -r --contains `git log -1 --pretty=format:"%h"`)
// echo "current dir:${pwd}"
// echo "sha? $1"

I tried getting a custom update script to work. I learned you are best of doing this via local .gitconfig file, which is why I have one of those. However, probably best to include .gitmodules file along with .gitconfig file like so

git config --local include.path ../.gitmodules

Then you are REALLY telling git to listen to the damn .gitmodules file, instead of relying on it to copy submodule configs into .git/config

By default custom commands write to the .git/config file as update = none which is beyond useless.

The community does absolutely need a "smart submodule checkout" which retains the branch a submodule is on.

However, submodules are currently greatly limited by not recording which branch a submodule should be on.

Here's a new idea...

.gitmodulebranches

You include the file just like the .gitmodules file and .gitconfig file above.

But all it contains are the branch names you want.

It's .gitignore'd

Because maybe you don't want to clutter history with merge commits. But come to think of it, probably better to have merge commits, and maybe also a submodule merge commit

Say you branch submoduleA, feature-foo

you also branch super/parent/container with same branch name, feature-foo

Really, you want a slick oneliner to do these two things and a third thing... change your submodules branch setting in .gitmodules or .gitmodulebranches

this way if a co-worker checks out the feature-foo branch, it is explicit which branch to be on

It may be better solved with a branch selection heuristic.

If a commit hash exists on the branch specified in .gitmodules, use that branch, which defaults to master

Although I don't think detached head is that hard to solve. If you pick the wrong branch, the your co-worker will see that (as long as you update the super/parent/container pointer to your the "wrong" branch you are committing on)

I have a few more pages of not-well-put-together notes.

But really the best thing would be to have a normal monorepo, and not fork codebases, where you then want to share/sync code via submodules

Just, figure out how to do it in one repo.

If you can't, hit me up, I'd love to help you figure out proper a good sharing architecture. My email is my github username @ gmail.com

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment