github.com/polydawn/docket@v0.5.4-0.20140630233848-90b70fb433da/dex/graph.go (about)

     1  package dex
     2  
     3  import (
     4  	"fmt"
     5  	"io/ioutil"
     6  	"os"
     7  	"path/filepath"
     8  	. "polydawn.net/pogo/gosh"
     9  	. "polydawn.net/hroot/crocker"
    10  	"polydawn.net/hroot/util"
    11  	"strings"
    12  )
    13  
    14  type Graph struct {
    15  	/*
    16  		Absolute path to the base/working dir for of the graph git repository.
    17  	*/
    18  	dir string
    19  
    20  	/*
    21  		Cached command template for exec'ing git with this graph's cwd.
    22  	*/
    23  	cmd Command
    24  }
    25  
    26  // strap this in only sometimes -- some git commands need this prefix to be explicit about branches instead of tags; others refuse it because they're already forcibly about branches.
    27  const git_branch_ref_prefix = "refs/heads/"
    28  const hroot_ref_prefix = "hroot/"
    29  const hroot_image_ref_prefix = hroot_ref_prefix+"image/"
    30  
    31  /*
    32  	Loads a Graph if there is a git repo initialized at the given dir; returns nil if a graph repo not found.
    33  	The dir must be the root of the working tree of the git dir.
    34  
    35  	A graph git repo is distingushed by containing branches that start with "hroot/" -- this is how hroot outputs branches that contain its data.
    36  */
    37  func LoadGraph(dir string) *Graph {
    38  	// optimistically, set up the struct we're checking out
    39  	g := newGraph(dir)
    40  
    41  	// ask git what it thinks of all this.
    42  	if g.isHrootGraphRepo() {
    43  		return g
    44  	} else {
    45  		return nil
    46  	}
    47  }
    48  
    49  /*
    50  	Attempts to load a Graph at the given dir, or creates a new one if no graph repo is found.
    51  	If a new graph is fabricated, it will be initialized by:
    52  	 - creating a new git repo,
    53  	 - making a blank root commit,
    54  	 - and tagging it with a branch name that declares it to be a graph repo.
    55  
    56  	Note if your cwd is already in a git repo, the new graph will not be commited, nor will it be made a submodule.
    57  	You're free to make it a submodule yourself, but git quite wants you to have a remote url before it accepts your submodule.
    58  */
    59  func NewGraph(dir string) *Graph {
    60  	g := newGraph(dir)
    61  	if g.isHrootGraphRepo() {
    62  		// if we can just be a load, do it
    63  		return g
    64  	} else if g.isRepoRoot() {
    65  		// if this is a repo root, but didn't look like a real graph...
    66  		util.ExitGently("Attempted to make a hroot graph at ", g.dir, ", but there is already a git repo there and it does not appear to belong to hroot.")
    67  	} // else carry on, make it!
    68  
    69  	// we'll make exactly one new dir if the path doesn't exist yet.  more is probably argument error and we abort.
    70  	// this is actually implemented via MkdirAll here (because Mkdir errors on existing, and I can't be arsed) and letting the SaneDir check earlier blow up if we're way out.
    71  	err := os.MkdirAll(g.dir, 0755)
    72  	if err != nil { panic(err); }
    73  
    74  	// git init
    75  	g.cmd("init")("--bare")()
    76  
    77  	g.withTempTree(func (cmd Command) {
    78  		// set up basic repo to identify as graph repo
    79  		cmd("commit", "--allow-empty", "-mhroot")()
    80  		cmd("checkout", "-b", hroot_ref_prefix+"init")()
    81  
    82  		// discard master branch.  a hroot graph has no real use for it.
    83  		cmd("branch", "-D", "master")()
    84  	})
    85  
    86  	// should be good to go
    87  	return g
    88  }
    89  
    90  func newGraph(dir string) *Graph {
    91  	dir = util.SanePath(dir)
    92  
    93  	// optimistically, set up the struct.
    94  	// we still need to either verify or initalize git here.
    95  	return &Graph{
    96  		dir: dir,
    97  		cmd: Sh("git")(DefaultIO)(Opts{Cwd: dir}),
    98  	}
    99  }
   100  
   101  func (g *Graph) isRepoRoot() (v bool) {
   102  	defer func() {
   103  		// if the path doesn't even exist, launching the command will panic, and that's fine.
   104  		// if the path isn't within a git repo at all, it will exit with 128, gosh will panic, and that's fine.
   105  		if recover() != nil {
   106  			v = false
   107  		}
   108  	}()
   109  	revp := g.cmd(NullIO)("rev-parse", "--is-bare-repository").Output()
   110  	v = (revp == "true\n")
   111  	return
   112  }
   113  
   114  //Is git ready and configured to make commits?
   115  func (g *Graph) IsConfigReady() bool {
   116  	//Get the current git configuration
   117  	config := g.cmd(NullIO)("config", "--list").Output()
   118  
   119  	//Check that a user name and email is defined
   120  	return strings.Contains(config, "user.name=") && strings.Contains(config, "user.email=")
   121  }
   122  
   123  /*
   124  	Creates a temporary working tree in a new directory.  Changes the cwd to that location.
   125  	The directory will be empty.  The directory will be removed when your function returns.
   126  */
   127  func (g *Graph) withTempTree(fn func(cmd Command)) {
   128  	// ensure zone for temp trees is established
   129  	tmpTreeBase := filepath.Join(g.dir, "worktrees")
   130  	err := os.MkdirAll(tmpTreeBase, 0755)
   131  	if err != nil { panic(err); }
   132  
   133  	// make temp dir for tree
   134  	tmpdir, err := ioutil.TempDir(tmpTreeBase, "tree.")
   135  	if err != nil { panic(err); }
   136  	defer os.RemoveAll(tmpdir)
   137  
   138  	// set cwd
   139  	retreat, err := os.Getwd()
   140  	if err != nil { panic(err); }
   141  	defer os.Chdir(retreat)
   142  	err = os.Chdir(tmpdir)
   143  	if err != nil { panic(err); }
   144  
   145  	// construct git command template that knows what's up
   146  	gt := g.cmd(
   147  		Opts{
   148  			Cwd:tmpdir,
   149  		},
   150  		Env{
   151  			"GIT_WORK_TREE": tmpdir,
   152  			"GIT_DIR": g.dir,
   153  		},
   154  	)
   155  
   156  	// go time
   157  	fn(gt)
   158  }
   159  
   160  func (g *Graph) Publish(lineage string, ancestor string, gr GraphStoreRequest) (hash string) {
   161  	// Handle tags - currently, we discard them when dealing with a graph repo.
   162  	lineage, _  = SplitImageName(lineage)
   163  	ancestor, _ = SplitImageName(ancestor)
   164  
   165  	g.withTempTree(func(cmd Command) {
   166  		fmt.Println("Starting publish of ", lineage, " <-- ", ancestor)
   167  
   168  		// check if appropriate branches already exist, and make them if necesary
   169  		if strings.Count(g.cmd("branch", "--list", hroot_image_ref_prefix+lineage).Output(), "\n") >= 1 {
   170  			fmt.Println("Lineage already existed.")
   171  			// this is an existing lineage
   172  			g.cmd("symbolic-ref", "HEAD", git_branch_ref_prefix+hroot_image_ref_prefix+lineage)()
   173  		} else {
   174  			// this is a new lineage
   175  			if ancestor == "" {
   176  				fmt.Println("New lineage!  Making orphan branch for it.")
   177  				g.cmd("checkout", "--orphan", hroot_image_ref_prefix+lineage)()
   178  			} else {
   179  				fmt.Println("New lineage!  Forking it from ancestor branch.")
   180  				g.cmd("branch", hroot_image_ref_prefix+lineage, hroot_image_ref_prefix+ancestor)()
   181  				g.cmd("symbolic-ref", "HEAD", git_branch_ref_prefix+hroot_image_ref_prefix+lineage)()
   182  			}
   183  		}
   184  		g.cmd("reset")
   185  
   186  		// apply the GraphStoreRequest to unpack the fs (read from fs.tarReader, essentially)
   187  		gr.place(".")
   188  
   189  		// exec git add, tree write, merge, commit.
   190  		g.cmd("add", "--all")()
   191  		g.forceMerge(ancestor, lineage)
   192  
   193  		hash = ""	//FIXME
   194  	})
   195  	return
   196  }
   197  
   198  func (g *Graph) Load(lineage string, gr GraphLoadRequest) (hash string) {
   199  	lineage, _ = SplitImageName(lineage) //Handle tags
   200  
   201  	//Check if the image is in the graph so we can generate a relatively friendly error message
   202  	if !g.HasBranch(hroot_image_ref_prefix+lineage) {	//HALP
   203  		util.ExitGently("Image branch name", lineage, "not found in graph.")
   204  	}
   205  
   206  	g.withTempTree(func(cmd Command) {
   207  		// checkout lineage.
   208  		// "-f" because otherwise if git thinks we already had this branch checked out, this working tree is just chock full of deletes.
   209  		g.cmd("checkout", "-f", git_branch_ref_prefix+hroot_image_ref_prefix+lineage)()
   210  
   211  		// the gr consumes this filesystem and shoves it at whoever it deals with; we're actually hands free after handing over a dir.
   212  		gr.receive(".")
   213  
   214  		hash = ""	//FIXME
   215  	})
   216  	return
   217  }
   218  
   219  // having a load-by-hash:
   220  //   - you can't combine it with lineage, because git doesn't really know what branches are, historically speaking.
   221  //       - we could traverse up from the lineage branch ref and make sure the hash is reachable from it, but more than one ref is going to be able to reach most hashes (i.e. hashes that are pd-base will be reachable from pd-nginx).
   222  //       - unless we decide we're committing lineage in some structured form of the commit messages.
   223  //            - which... yes, yes we are.  the first word of any commit is going to be the image lineage name.
   224  //            - after the space can be anything, but we're going to default to a short description of where it came from.
   225  //            - additional lines can be anything you want.
   226  //            - if we need more attributes in the future, we'll start doing them with the git psuedo-standard of trailing "Signed-Off-By: %{name}\n" key-value pairs.
   227  //            - we won't validate any of this if you're not using load-by-hash.
   228  
   229  
   230  func (g *Graph) forceMerge(source string, target string) {
   231  	writeTree := g.cmd("write-tree").Output()
   232  	writeTree = strings.Trim(writeTree, "\n")
   233  	commitMsg := ""
   234  	if source == "" {
   235  		commitMsg = fmt.Sprintf("%s imported from an external source", target)
   236  	} else {
   237  		commitMsg = fmt.Sprintf("%s updated from %s", target, source)
   238  	}
   239  	commitTreeCmd := g.cmd("commit-tree", writeTree, Opts{In: commitMsg})
   240  	if source != "" {
   241  		commitTreeCmd = commitTreeCmd(
   242  			"-p", git_branch_ref_prefix+hroot_image_ref_prefix+source,
   243  			"-p", git_branch_ref_prefix+hroot_image_ref_prefix+target,
   244  		)
   245  	}
   246  	mergeTree := strings.Trim(commitTreeCmd.Output(), "\n")
   247  	g.cmd("merge", "-q", mergeTree)()
   248  }
   249  
   250  //Checks if the graph has a branch.
   251  func (g *Graph) HasBranch(branch string) bool {
   252  	//Git magic is involved. Response will be of non-zero length if branch exists.
   253  	result := g.cmd("ls-remote", ".", git_branch_ref_prefix + branch).Output()
   254  	return len(result) > 0
   255  }
   256  
   257  /*
   258  	Check if a git repo exists and if it has the branches that declare it a hroot graph.
   259  */
   260  func (g *Graph) isHrootGraphRepo() bool {
   261  	if !g.isRepoRoot() { return false; }
   262  	if !g.HasBranch("hroot/init") { return false; }
   263  	// We could say a hroot graph shouldn't have a master branch, but we won't.
   264  	// We don't create one by default, but you're perfectly welcome to do so and put a readme for your coworkers in it or whatever.
   265  	return true
   266  }