UNPKG

git-oneflow

Version:

CLI tooling implementing GIT OneFlow branching model

2 lines 12.6 kB
#!/usr/bin/env node import{Command as e}from"commander";import a from"path";import{cosmiconfigSync as t}from"cosmiconfig";import{red as n,white as r,gray as s,cyan as o,yellow as i}from"fude";import c from"inquirer";import{spawnSync as m}from"child_process";import f from"shelljs";import l from"fs";var h="git-oneflow";const g=t(h);let u;const d=["rebase","no-ff","rebase-no-ff"],p={main:"main",development:void 0,features:"feature",releases:"release",hotfixes:"hotfix",strategy:"rebase",interactive:!0,deleteAfterMerge:!0,pushAfterMerge:!0,tagCommit:!0,askOnFeatureStart:!1,askOnFeatureFinish:!1},b=Object.keys(p).filter((e=>Object.hasOwn(p,e))).reduce(((e,a)=>(e[a]=a,e)),{}),w=e=>{const a=e?g.load(e):g.search();u=a?.filepath;const t={...p,...a?.config};for(const e in t){if(!Object.hasOwn(p,e))throw Error(`unknown configuration option ${e} in ${a?.filepath}`);{const a=t[e];a&&(process.env[("gof_"+e).toUpperCase()]=a)}}},v=e=>process.env[("gof_"+e).toUpperCase()],_=e=>p[e],y=e=>{switch(e){case"feature":return v("features");case"release":return v("releases");case"hotfix":return v("hotfixes")}},$=(e,a)=>{console.info(`${s(e)}: ${o(a)}`)},x=e=>{console.error(`${n("error")}: ${r(e instanceof Error?e.message:e)}`),process.env.DEBUG&&e instanceof Error&&console.error(e)},F=e=>{console.warn(`${i("warning")}: ${r(e instanceof Error?e.message:e)}`),process.env.DEBUG&&e instanceof Error&&console.error(e)},O=({name:t,args:n,desc:r,opts:s,listeners:o,action:i,examples:c})=>{const m=new e(t);return m.option("-c, --config <file>","configuration file to use").on("option:config",(e=>{w(a.resolve(e))})).option("--dry-run","just show which commands would be run").on("option:dry-run",(()=>{process.env.GOF_DRY_RUN="true"})).option("-b, --base <name>",`override the current base branch name: '${y(m.name())}'`).option("--no-base","do not use a base branch name").option("--debug").on("option:debug",(()=>$("debug",JSON.stringify(m.opts(),null,2)))),m.alias(t.charAt(0)),n&&n.forEach((e=>{m.argument(e.name,e.desc)})),r&&m.description(r),s&&s.forEach((e=>{m.option(e.flags,e.desc,e.defaultValue)})),o&&o.forEach((e=>{m.on(e.event,e.callback)})),i&&m.action(i),c&&m.on("--help",(function(){console.log(""),console.log("Examples:"),console.log(""),console.log(" ".concat(c.join("\n ")))})),m},k=({message:e,name:a,defaultValue:t,when:n,validate:r})=>({type:"input",name:a,message:e,default:t,when:n,validate:r}),V=({message:e,name:a,choices:t,when:n,defaultValue:r})=>({type:"list",name:a,message:e,choices:t,when:n,default:r}),C=({message:e,name:a,defaultValue:t=!0,when:n})=>({type:"confirm",name:a,message:e,default:t,when:n}),A=async e=>c.prompt(e),{exec:D}=f,E=e=>{if(process.env.GOF_DRY_RUN)$("dry-run",e);else{const{code:a,stderr:t}=D(e,{silent:!0});0!==a&&(x(t.replace(/^error:/i,"").trim()),process.exit(a))}},U=e=>process.env.GOF_DRY_RUN?($("dry-run",e),!0):0===D(e,{silent:!0}).code,M=e=>{const{code:a,stdout:t}=D("git branch",{silent:!0});if(0===a)return t.split("\n").filter((a=>!e||!a.includes(e))).filter((e=>""!==e)).map((e=>e.replace(/\*/g,"").trim()))},j=e=>U("git check-ref-format --branch "+e),N=()=>{const e=D("git describe --abbrev=0",{silent:!0});return 0===e.code?e.trim():void 0},R=e=>{E("git checkout "+e)},S=(e,a)=>{E(`git merge ${a?a+" ":""}${e}`)},P=(e,a=!1)=>{E(`git ${a?"push origin :":"branch -d "}${e}`)},G=[{flags:"-p,--push",desc:"push to origin after merge"},{flags:"--no-push",desc:"do not push"},{flags:"-d,--delete",desc:"delete branch after merge"},{flags:"--no-delete",desc:"do not delete branch"},{flags:"-o,--onto <onto>",desc:"branch to merge or rebase onto"}],J=[{flags:"-t,--tag <tagName>",desc:"tag the commit with the given tag"},{flags:"--no-tag",desc:"do not tag the commit"},{flags:"-m,--message <msg>",desc:"annotate tag with a message (default to the tag name)"}],L=async(e,a,t)=>{const n=a??y(e);if(t){const e=(n?n+"/":"")+t;return R(e),e}return await(async()=>{const e=D("git symbolic-ref --short HEAD",{silent:!0}).trim();if((await A([C({message:`Use current branch (${e})?`,name:"confirmation"})])).confirmation)return e})()??await(async(e,a)=>{const t=(e?e+"/":"")+(await A([k({message:a.charAt(0).toUpperCase()+a.slice(1)+" name?",name:"input",validate:e=>""!==e.trim()||"Please, enter a valid branch name"})])).input;return R(t),t})(n,e)},Y=async(e,a)=>{(e??"true"===v("deleteAfterMerge"))&&(P(a),(await A([C({message:`Delete '${a}' from remote?`,name:"confirmation"})])).confirmation&&P(a,!0))},W=(e,a,t)=>{(e??"true"===v("pushAfterMerge"))&&((e,a)=>{E(`git push ${a?"--follow-tags ":""}origin ${e}`)})(a,t)},B=async(e,a,t)=>{const n=await L(t.name(),a.base,e),r=a.tag??await(async()=>"true"===v("tagCommit")?(await A([k({message:`Tag name (latest tag: ${N()})`,name:"tag",validate:e=>""!==e.trim()||"Please, enter a valid tag name"})])).tag:void 0)();r?((e,a)=>{E(`git tag ${a?`-a -m '${a}' `:""}${e}`)})(r,a.message??r):F("commit has not been tagged!");const s=a.onto??v("development")??v("main");R(s),S(n),W(a.push,s,r),await Y(a.delete,n),v("development")&&(await A([C({message:`Merge '${r||n}' into '${v("main")}'?`,name:"confirmation"})])).confirmation&&(R(v("main")),S(r||n,"--ff-only"))},H={name:"release",desc:"finish a release",args:[{name:"[name]",desc:"the name of the release to finish"}],opts:[...J,...G],action:B,examples:["$ gof finish release 2.3.0","$ git-oneflow f r -t 2.3.0 -m 'chore(release): 2.3.0' my-release","$ gof finish release --no-base --no-push 2.3.0"]},T={name:"hotfix",desc:"finish a hotfix",args:[{name:"[name]",desc:"the name of the hotfix to finish"}],opts:[...J,...G],action:B,examples:["$ gof finish hotfix 2.3.1 -b bugs","$ gof f h 2.3.1","$ git-oneflow finish hotfix -o main --no-delete 2.3.1"]},q={name:"feature",args:[{name:"[name]",desc:"the feature to finish"}],desc:"finish a feature",opts:[...G,{flags:"-i,--interactive",desc:"interactive rebase"},{flags:"--no-interactive",desc:"do not rebase interactively"},{flags:"-s,--strategy <strategy>",desc:"merge strategy"}],examples:["$ git-oneflow finish feature my-feature","$ gof f f -s no-ff","$ gof finish f --dry-run --no-interactive -s rebase-no-ff my-feature"],action:async(e,a,t)=>{const n=(e=>{const a=e??v("strategy");if(!d.includes(a))throw Error(`unknown strategy option: '${a}'. Valid options are '${d.join(", ")}'`);return a})(a.strategy),r=await L(t.name(),a.base,e),s=a.onto??(v("askOnFeatureFinish")?await(async()=>(await A([V({message:"Which branch to merge onto?",name:"branch",defaultValue:b.main,choices:()=>M("feature"),when:()=>!0})])).branch)():v("development")??v("main"));n.startsWith("rebase")&&((e,a)=>{process.env.GOF_DRY_RUN?$("dry-run",`git rebase ${a?"-i ":""}${e}`):a?m("git",["rebase","-i",""+e],{stdio:"inherit"}):E("git rebase "+e)})(s,a.interactive??"true"===v("interactive")),R(s),S(r,/no-ff/.test(n)?"--no-ff":"--ff-only"),W(a.push,s,!1),await Y(a.delete,r)}};var z=()=>new e("init").description("initialise configuration file").option("-y, --defaults","accept all defaults").option("-f, --force","force creation of configuration file").action((async e=>{var t;t=e.force,u&&(t||(F(`a configuration exists at '${u}'. Cowardly refusing to proceed!`),process.exit(0)),$("Use the Force, Luke",'\n ____\n _.\' : `._\n .-.\'`. ; .\'`.-.\n __ / : ___\\ ; /___ ; \\ __\n ,\'_ ""--.:__;".-.";: :".-.":__;.--"" _`,\n :\' `.t""--.. \'<@.`;_ \',@>` ..--""j.\' `;\n `:-.._J \'-.-\'L__ `-- \' L_..-;\'\n "-.__ ; .-" "-. : __.-"\n L \' /.------.\\ \' J\n "-. "--" .-"\n __.l"-:_JL_;-";.__\n .-j/\'.; ;"""" / .\'\\"-.\n .\' /:`. "-.: .-" .\'; `.\n .-" / ; "-. "-..-" .-" : "-.\n .+"-. : : "-.__.-" ;-._ \\\n ; \\ `.; ; : : "+. ;\n : ; ; ; : ; : \\:\n : `."-; ; ; : ; ,/;\n ; -: ; : ; : .-"\' :\n :\\ \\ : ; : \\.-" :\n ;`. \\ ; : ;.\'_..-- / ;\n : "-. "-: ; :/." .\' :\n \\ .-`.\\ /t-"" ":-+. :\n `. .-" `l __/ /`. : ; ; \\ ;\n \\ .-" .-"-.-" .\' .\'j \\ / ;/\n \\ / .-" /. .\'.\' ;_:\' ;\n :-""-.`./-.\' / `.___.\'\n \\ `t ._ / bug :F_P:\n "-.t-._:\'\n '),F("option '-f,--force' detected. Using the force...")),(e=>{const t=a.resolve(process.cwd(),`.${h}rc`);l.writeFileSync(t,e),$("config",`new configuration file created at '${t}'`)})(await(async e=>{if(e)return JSON.stringify(p,null,2);const a=await A([V({message:"Main branch name?",name:b.main,choices:M(),defaultValue:v("main")}),C({message:"Do you use a development branch?",name:"useDevelop",defaultValue:void 0!==v("development"),when:e=>void 0!==M(e[b.main])}),V({message:"Development branch name?",name:b.development,choices:e=>M(e[b.main]),when:e=>!0===e.useDevelop}),k({message:"Features branch basename?",name:b.features,defaultValue:v("features")??_("features"),validate:e=>""===e.trim()||j(e)||"Please, enter a valid branch name"}),k({message:"Releases branch basename?",name:b.releases,defaultValue:v("releases")??_("releases"),validate:e=>""===e.trim()||j(e)||"Please, enter a valid branch name"}),k({message:"Hotfixes branch basename?",name:b.hotfixes,defaultValue:v("hotfixes")??_("hotfixes"),validate:e=>""===e.trim()||j(e)||"Please, enter a valid branch name"}),V({message:"Merge strategy to use?",name:b.strategy,choices:d,defaultValue:v("strategy")}),C({message:"Always rebase interactively?",name:b.interactive,defaultValue:"true"===v("interactive")}),C({message:"Push to origin after merge?",name:b.pushAfterMerge,defaultValue:"true"===v("pushAfterMerge")}),C({message:"Delete branch after merge?",name:b.deleteAfterMerge,defaultValue:"true"===v("deleteAfterMerge")}),C({message:"Tag releases and hotfixes?",name:b.tagCommit,defaultValue:"true"===v("tagCommit")}),C({message:"Ask for ref branch on feature start?",name:b.askOnFeatureStart,defaultValue:"true"===v("askOnFeatureStart")}),C({message:"Ask for merge branch on feature finish?",name:b.askOnFeatureFinish,defaultValue:"true"===v("askOnFeatureFinish")})]);return delete a.useDevelop,JSON.stringify(a,null,2)})(e.defaults))}));const I=async()=>(await A([V({message:"Which branch to start from?",name:"branch",defaultValue:b.main,choices:()=>M("feature"),when:()=>!0})])).branch,K=async(e,a,t)=>{const n=e??(await A([k({message:t.name().charAt(0).toUpperCase()+t.name().slice(1)+" name?",name:"branchName",validate:e=>""!==e.trim()||"Please, enter a valid branch name"})])).branchName,r=a.base??y(t.name());((e,a)=>{E(`git checkout -b ${e}${a?" "+a:""}`)})(r?`${r}/${n}`:n,a.ref??(v("askOnFeatureStart")?await I():await(async e=>{switch(e){case"feature":return v("askOnFeatureStart")?await I():v("development")??v("main");case"release":return v("main");case"hotfix":return N()??v("main")}})(t.name())))},Q=[{flags:"-r, --ref <ref>",desc:"new branch will be created from the given branch, commit or tag"},{flags:"--no-ref",desc:"create the new branch from the current branch"}],X={name:"feature",args:[{name:"[name]",desc:"the name of the feature (without the base branch name)"}],opts:Q,desc:"start a new feature from the main or the development branch",action:K,examples:["$ git-oneflow start feature my-feature","$ gof s f -b feat my-feature","$ gof start feature --dry-run --ref some-branch my-feature"]},Z={name:"release",args:[{name:"[name]",desc:"the name of the release branch (without the base branch name)"}],opts:Q,desc:"start a new release from the main branch",action:K,examples:["$ gof start release 2.3.0","$ git-oneflow s r -r release-branch 2.3.0","$ gof start release --no-base --ref 9efc5d 2.3.0"]},ee={name:"hotfix",args:[{name:"[name]",desc:"the name of the hotfix (without the base branch name)"}],opts:Q,desc:"start a new hotfix from the latest tag (if exists) or the main branch",action:K,examples:["$ gof start hotfix 2.3.1 -b bugs","$ gof s h 2.3.1","$ git-oneflow start hotfix -r main --no-base 2.3.1"]};(async()=>{try{if(!U("git status"))throw Error("git not installed or not in a valid git repository");w();const a=new e;a.name(h).version("3.1.0").addCommand((new e).name("start").alias("s").description("start a new feature, release or hotfix").addCommand(O(X)).addCommand(O(Z)).addCommand(O(ee))).addCommand((new e).name("finish").alias("f").description("finish a feature, release or hotfix").addCommand(O(q)).addCommand(O(H)).addCommand(O(T))).addCommand(z()),await a.parseAsync()}catch(e){x(e)}})();