<file_sep>package exec

import (
	"os/exec"
)

/**
*  Executes git command in service folder
*/
func GitCommand(serviceDir string, command string)  (result string, err error) {
  out, err := exec.Command(
    "sh", 
    "-c", 
		"( cd " + serviceDir + " && " + command + ")" ).Output()
	
	return string(out), err
}<file_sep>package logger

import (
  colorPrint "github.com/fatih/color" 
)

const INDENT = "   "

func Header(text string) {
	colorPrint.Green("\n" + INDENT + " ------------ " + text + " -----------\n\n")
}

func Info(textTemplate string, params ...interface{} ) {
  colorPrint.White(INDENT + textTemplate, params...)
}

func Text(text string) {
  colorPrint.White(INDENT + text + "\n")
}

func Warn(textTemplate string, params ...interface{} ) {
  colorPrint.Yellow("\n" + INDENT +  "WARNING " + textTemplate, params...)
}

func Debug(textTemplate string, params ...interface{} ) {
  colorPrint.Magenta("\n" + INDENT +  "DEBUG " + textTemplate, params...)
}<file_sep>package errors

import (
	"fmt"
	"os"
  )

func CheckAndExitIfError(err  error) {
  if err != nil {
		fmt.Println(err) 
		os.Exit(1) 
	}
}

func CheckAndReturnIfError(err  error) bool {
  if err != nil {
		fmt.Println(err) 
		return true
  }
  
  return false
}
<file_sep>package DockerComposeFileBuilder

import (
  "devlab/lib/files"
)

type Service struct {
  image string
  env_files []string
  volumes []string
  ports []string
  restart string    
}

type DockerComposeFile struct {
  version string
  services map[string]Service
  networks map[string]map[string]map[string]string
}

func CreateDockerComposeObjectExample() *DockerComposeFile {
	dockerComposeExample := new(DockerComposeFile)
  dockerComposeExample.version = "2"
  dockerComposeExample.services = make(map[string]Service)
  dockerComposeExample.services["dlp-service-config"] = Service{
    image: "${IMAGES_PREFIX}dlp-service-config",
    env_files: []string{"${BUILD_DIR}/dlp-service-config/.env"},
    volumes: []string{"${DEVENV_ROOT_DIR}/dlp-service-config:/usr/src/app"},
    ports: []string{"4004"},
    restart: "always"}

	dockerComposeExample.networks = map[string]map[string]map[string]string{ "default": {"external": { "name": "bedrock" } } }
	
	return dockerComposeExample
}


func Create(dockerComposeFilePath string, dockerComposeData *DockerComposeFile) {     
  files.WriteAppendFileWithIndent(dockerComposeFilePath, "version: " + dockerComposeData.version, 0)
  files.WriteAppendFileWithIndent(dockerComposeFilePath, "services: ", 0)

  for serviceName, serviceData := range dockerComposeData.services {
    files.WriteAppendFileWithIndent(dockerComposeFilePath, serviceName + ":", 2)
    
    files.WriteAppendFileWithIndent(dockerComposeFilePath, "image: " + serviceData.image, 4)
    
    if len(serviceData.env_files) > 0 {
      files.WriteAppendFileWithIndent(dockerComposeFilePath, "env_files: ", 4)
      for _, envFile := range serviceData.env_files {
        files.WriteAppendFileWithIndent(dockerComposeFilePath, "- " + envFile, 6)
      }
    }

    if len(serviceData.volumes) > 0 {
      files.WriteAppendFileWithIndent(dockerComposeFilePath, "volumes: ", 4)
      for _, volume := range serviceData.volumes {
        files.WriteAppendFileWithIndent(dockerComposeFilePath, "- " + volume, 6)
      }
    }

    if len(serviceData.ports) > 0 {
      files.WriteAppendFileWithIndent(dockerComposeFilePath, "ports: ", 4)
      for _, port := range serviceData.ports {
        files.WriteAppendFileWithIndent(dockerComposeFilePath, "- " + port, 6)
      }
    }        
     
    if serviceData.restart != "" {
      files.WriteAppendFileWithIndent(dockerComposeFilePath, "restart: " + serviceData.restart, 4)
    } 
  }

  files.WriteAppendFileWithIndent(dockerComposeFilePath, "networks: ", 0)
  files.WriteAppendFileWithIndent(dockerComposeFilePath, "default: ", 2)
  files.WriteAppendFileWithIndent(dockerComposeFilePath, "external: ", 4)
  files.WriteAppendFileWithIndent(dockerComposeFilePath, "name: " + dockerComposeData.networks["default"]["external"]["name"] , 6)
}<file_sep>package main

import (
  "os"
  "devlab/bin/context"
  "devlab/bin/create-docker-compose"
)

func main() {     
  switch os.Args[1] {
  case "context":
    switch os.Args[2] {
    case "create":
      Context.Create(os.Args[3])         
    case "set":
      Context.Set(os.Args[3])   
    }
    break 
  case "create-docker-compose":
    createDockerCompose.Call(os.Args[2])
    break
  }
}<file_sep>package createDockerCompose

import (
	"devlab/lib/files"
  "devlab/lib/docker-compose-file-builder"
)

func Call(contextName string) {
	config, _ := files.ReadMainConfig()
	dockerComposeData := DockerComposeFileBuilder.CreateDockerComposeObjectExample()
	contextDir := config["contexts-path"] + "/" + contextName
	DockerComposeFileBuilder.Create("./" + contextDir + "/docker-compose.application.yml", dockerComposeData)
}<file_sep>package Context

import (
  "devlab/lib/logger"
  "devlab/lib/files"
  "devlab/lib/errors"
  "devlab/lib/services"
  "strings"
)


func Set(contextName string) (err error) {
  config, err := files.ReadMainConfig()
  if errors.CheckAndReturnIfError(err) { return }

  // Check context dir and create it if need  
  contextDir := "./" + config["contexts-path"] + "/" + contextName 
  isContextDirExists, _ :=  files.IsExists("./" + contextDir)
  if !isContextDirExists {
    files.CreateDir("./" + contextDir)
  }
  
  // Check context settings file and create it if need  
  contextSettings := contextDir + "/settings.yml"
  isContextSettingsExists, _ :=  files.IsExists(contextSettings)
  if !isContextSettingsExists {
    files.Copy("./" + config["data-path"] + "/default-context.yml", contextSettings) 
  }

  // Read context settings
  context, err := files.ReadContextConfig(contextSettings)
  if errors.CheckAndReturnIfError(err) { return }

  // Check context services dir and create it if need
  contextServicesDir := config["contexts-path"] + "/" + contextName + "/services"
  isContextServicesDirExists, err :=  files.IsExists("./" + contextServicesDir)
  if errors.CheckAndReturnIfError(err) { return }
  if !isContextServicesDirExists {
    files.CreateDir("./" + contextServicesDir)
  }

  // Set task base branch
  taskBaseBranch := context["context"]["task"]["base-branch"]
  if taskBaseBranch == "" {
    taskBaseBranch = config["base-branch"]
  }

  // Check services repo and clone/refresh it if need
  applicationServices := context["applicaton-services"]
  for serviceName, serviceParams := range applicationServices { 

    logger.Header(strings.ToUpper(serviceName))

    isServiceDirExists, _ :=  files.IsExists("./" + contextServicesDir + "/serviceName") 
        
    if !isServiceDirExists {      
      services.Clone(contextServicesDir, serviceName, config["github-repository-path"], serviceParams["github-path"] )
    }

    serviceBaseBranch := serviceParams["base-branch"]    
    if  serviceBaseBranch == "" {
      serviceBaseBranch = taskBaseBranch
    }
    services.RefreshGitRepo(contextServicesDir, serviceName, serviceParams["branch"], serviceBaseBranch)
  }
  
  return
}

func Create(contextName string) (err error) {
  return
}
  

<file_sep>package yml

import (
  "github.com/gopkg.in/yaml"
  "devlab/lib/errors"
)

func ParseOneLevelYAML(data string) (parsedData map[string]string, err error) {  
  err = yaml.Unmarshal([]byte(data), &parsedData)
  if( errors.CheckAndReturnIfError(err) ) { return make(map[string]string), err }

  return
}

func ParseTwoLevelYAML(data string) (parsedData map[string]map[string]string, err error) {  
  err = yaml.Unmarshal([]byte(data), &parsedData)
  if( errors.CheckAndReturnIfError(err) ) { return make(map[string]map[string]string), err }

  return
}

func ParseThreeLevelYAML(data string) (parsedData map[string]map[string]map[string]string, err error) {  
  err = yaml.Unmarshal([]byte(data), &parsedData)
  if( errors.CheckAndReturnIfError(err) ) { return make(map[string]map[string]map[string]string), err }

  return
}

<file_sep>package files

import (
  "io"
  "os"
  "path/filepath"
  "devlab/lib/errors"
  "devlab/lib/yml"
  "devlab/lib/logger"
  "reflect"
)
/**
*/
func AbsolutePath(relativePath string) (absolutePath string, err error) {
  dir, err := filepath.Abs(relativePath)
  if  errors.CheckAndReturnIfError(err) { return }

  return dir, nil
}

/**
*/
func ReadTextFile(path string) (resultString string, err error) {   
  resultString = ""

  filepath, err := AbsolutePath(path)
  if errors.CheckAndReturnIfError(err) { return }

  file, err := os.Open(filepath)
  if errors.CheckAndReturnIfError(err) { return }
  defer file.Close() 
     
  data := make([]byte, 64)     
  for {
    n, err := file.Read(data)
    if err == io.EOF { break }
    resultString += string(data[:n])
  }
  
  err = nil
  return
}

/**
*/
func IsExists(path string) (bool, error) {
  filepath, err := AbsolutePath(path)
  if errors.CheckAndReturnIfError(err) { return false, err}

  _, err = os.Stat(filepath)
  if err == nil { return true, nil }
  if os.IsNotExist(err) { return false, nil }
  return true, err
}

/**
*/
func CreateDir(path string) error {
  filepath, err := AbsolutePath(path)
  if errors.CheckAndReturnIfError(err) { return err }

  return os.MkdirAll(filepath, 0755)
}

/* Copy the src file to dst. Any existing file will be overwritten and will not
   copy file attributes.
*/ 
func Copy(src, dst string) error {
  in, err := os.Open(src)
  if err != nil {
      return err
  }
  defer in.Close()

  out, err := os.Create(dst)
  if err != nil {
      return err
  }
  defer out.Close()

  _, err = io.Copy(out, in)
  if err != nil {
      return err
  }
  return out.Close()
}

/*
*/
func ReadMainConfig() (config map[string]string, err error) {
  _, err =  IsExists("/.config")
  if errors.CheckAndReturnIfError(err) { return make(map[string]string), err }

  pathToConfig, err := AbsolutePath(".config")
  
  configData, err := ReadTextFile(pathToConfig)
  if errors.CheckAndReturnIfError(err) { return  make(map[string]string), err }

  config, err = yml.ParseOneLevelYAML(configData)
  if errors.CheckAndReturnIfError(err) { return  make(map[string]string), err }

  return
}

func ReadContextConfig(relativePathToContextConfigFile string) (context map[string]map[string]map[string]string, err error) {
	contextData, err := ReadTextFile(relativePathToContextConfigFile)
	if errors.CheckAndReturnIfError(err) { return  make(map[string]map[string]map[string]string), err }
  
	context, err = yml.ParseThreeLevelYAML(contextData)
	if errors.CheckAndReturnIfError(err) { return  make(map[string]map[string]map[string]string), err }

	return
}

/**
* Writes string to the of file
*/
func WriteAppendFile(filenamePath string, text string) (result int, err error) { 
  absoluteFilenamePath, err := AbsolutePath(filenamePath)
  isFileExists, _ :=  IsExists(filenamePath)
  logger.Debug(" %s ", absoluteFilenamePath)

  if !isFileExists {
    logger.Debug("text:  %s ", text)
    file, _ := os.Create(absoluteFilenamePath)
    defer file.Close() 
  }

  file, _ := os.OpenFile(absoluteFilenamePath, os.O_WRONLY|os.O_APPEND, 0644)
  if err != nil {
    return
  }
  defer file.Close()

  result, err = file.WriteString(text + "\n")

  return
}

/**
* It adds spaces indent before string anf write this string to the end of file
*/
func WriteAppendFileWithIndent(filenamePath string, text string, indent int) (result int, err error) {
  return WriteAppendFile(filenamePath, indentInSpaces(indent) + text)
}

/**
* Writes tree stucture data (with 3 levels) to yaml file
*/
func WriteYaml(filenamePath string, data interface{}) (err error) {   
  return WriteYamlBranch(filenamePath, data, 0)
}

/**
* Writes one branch of tree stucture data to yaml file
*/
func WriteYamlBranch(filenamePath string, data interface{}, depth int) (err error) { 
  if data, ok := data.(map[string]map[string]map[string]string); ok {
    for key, value := range data {
      _, err = WriteAppendFileWithIndent(filenamePath, key + ": ", 2*depth)
      err = WriteYamlBranch(filenamePath, value, depth + 1)
    }
  }

  if data, ok := data.(map[string]map[string]string); ok {
    for key, value := range data {
      _, err = WriteAppendFileWithIndent(filenamePath, key + ": ", 2*depth)
      err = WriteYamlBranch(filenamePath, value, depth + 1)
    }
  }

  if data, ok := data.(map[string]string); ok {
    for key, value := range data {
      _, err = WriteAppendFileWithIndent(filenamePath, key + ": " + value, 2*depth)
    }
  }

  return
}

/**
* Returns indent with num spaces
*/
func indentInSpaces(indent int) (spacesIndent string) {
  spacesIndent = ""
  for i := 0; i < indent; i++ {
    spacesIndent += " "
  } 
  return spacesIndent
}


type Pair struct {
  key string
  value *interface{}
}

func (p Pair) writeAppendYAMLFile(filenamePath string, depth int) {
  v := reflect.ValueOf(*(p.value))
  switch  v.Kind() {
    case reflect.String:
      WriteAppendFileWithIndent(filenamePath, p.key + ": " + v.Elem().String(), depth)
    
    case reflect.Slice, reflect.Array:
      WriteAppendFileWithIndent(filenamePath, p.key + ": ", depth)
      for i := 0; i < v.NumField(); i++ {
        item := v.Field(i)        
        if reflect.ValueOf(item).Kind() == reflect.String {
          WriteAppendFileWithIndent(filenamePath, "- " + reflect.ValueOf(item).Elem().String(), depth + 2)
        } else {
          WriteAppendFileWithIndent(filenamePath, " - the value couldn't be printed", depth + 2) 
        }
      }
        
    case reflect.Map:
      WriteAppendFileWithIndent(filenamePath, p.key + ": ", depth)
      for _, key := range v.MapKeys() {
       // (Pair{key.Elem().String(), v.MapIndex(key).Pointer()}).writeAppendYAMLFile(filenamePath, depth + 2)
        // (Pair{key.Elem().String(), &"unknown value"}).writeAppendYAMLFile(filenamePath, depth + 2)
        WriteAppendFileWithIndent(filenamePath, key.Elem().String() + ": unknown value", depth +2) 
      }
      
    default:
      WriteAppendFileWithIndent(filenamePath, p.key + ": the value couldn't be printed", depth)    
  }
}

type YamlTree []Pair

func (t YamlTree) WriteYaml(filenamePath string) {
  for _, pair := range t {
    pair.writeAppendYAMLFile(filenamePath, 0) 
  }
}

<file_sep>package services

import (
  "bufio"
  "os"
  "time"
  "strconv"
  "devlab/lib/exec"
  "devlab/lib/files"
  "devlab/lib/errors"
  "devlab/lib/logger" 
)

/**
* Clones service repository from remote server to local machine
*/
func Clone( contextServicesDir string, serviceName string, githabHost string, relativeGithubPath string) {
  isServiceDirExists, err :=  files.IsExists(contextServicesDir + "/" + serviceName)
  if errors.CheckAndReturnIfError(err) { return }
  
  servicesDir, err := files.AbsolutePath(contextServicesDir)
  
  if relativeGithubPath == "" {
    relativeGithubPath = serviceName + ".git"
  }

  if !isServiceDirExists {
    logger.Text("Cloning from github: " + githabHost + relativeGithubPath + " ...")
    exec.GitCommand(servicesDir, "git clone " + githabHost + relativeGithubPath + " " + serviceName)
  }
}

/**
* Refreshes git repo service (refreshes service repo, commits or staches changes and checkout to context branch)
*/
func RefreshGitRepo(contextServicesDir string, serviceName string, contextServiceBranch string, baseBranch string) {
  serviceDir, _ := files.AbsolutePath(contextServicesDir + "/" + serviceName)  

  if CheckRepoChanges(contextServicesDir, serviceName) {
    logger.Warn("There are some not commited changes")
    logger.Text("Please, choose action: ")
    logger.Text(" (1) commit changes ")
    logger.Text(" (2) stash changes ")
    logger.Text(" (3) nothing to do ")
    
    input := bufio.NewScanner(os.Stdin)
    input.Scan()
    action := input.Text()

    switch action {
    case "1":
      logger.Text("Commiting changes ")
      CommitChanges(contextServicesDir, serviceName)
      break
    case "2":
      logger.Text("Stashing changes ")
      exec.GitCommand(serviceDir, "git stash")
      break
    case "3":
    default:       
    }          
  }  

  CheckoutOrCreate(contextServicesDir, serviceName, contextServiceBranch, baseBranch)
}

func CheckoutOrCreate(contextServicesDir string, serviceName string, checkoutBranch string, baseBranch string) {
  serviceDir, _ := files.AbsolutePath(contextServicesDir + "/" + serviceName)
  numCheckoutBranchExistsAsRemote, _ := exec.GitCommand(serviceDir, "git branch -r | grep -c " + checkoutBranch)
  currentBranch, _ := exec.GitCommand(serviceDir, "git symbolic-ref --short HEAD")
 
  /* checkoutBranch exists as remote */
  isCheckoutBranchExistsAsRemote := numCheckoutBranchExistsAsRemote != "0"

  if isCheckoutBranchExistsAsRemote {
    if currentBranch != checkoutBranch {
      logger.Text("Checking out to '" + checkoutBranch + " branch\n")
      exec.GitCommand(serviceDir, "git checkout " + checkoutBranch)
    }

    diffLocalAndRemoteBranches, _ := exec.GitCommand(serviceDir, "git diff " + checkoutBranch + " origin/" + checkoutBranch + " --stat")
    
    if (diffLocalAndRemoteBranches != "") {
      logger.Text("Remote branch is differnt with local branch. \n")            

      BackupCurrentBranchIfNeed(contextServicesDir, serviceName, checkoutBranch) 

      logger.Info("Refreshing local service folder from remote branch origin/%s.", checkoutBranch)
      exec.GitCommand(serviceDir, "git fetch origin && git reset --hard origin/" + checkoutBranch)
    }

    return 
  }

  /* checkoutBranch not exists as remote */
  if currentBranch != checkoutBranch {      
    numCheckoutBranchExistsAsLocal, _ := exec.GitCommand(serviceDir, "git branch | grep -c " + checkoutBranch)
    isCheckoutBranchExistsAsLocal, _ := strconv.ParseInt(numCheckoutBranchExistsAsLocal, 10, 8) 

    if isCheckoutBranchExistsAsLocal == 0 {
      logger.Info("Creating new local branch '%s' from remote branch 'origin/%s'", checkoutBranch, baseBranch) 

      exec.GitCommand(serviceDir, "git fetch origin && git checkout " + baseBranch + " git reset --hard origin/" + baseBranch)
      exec.GitCommand(serviceDir, "git checkout -b " + checkoutBranch)
    } else {
      exec.GitCommand(serviceDir, "git checkout" + checkoutBranch)
    }       
  }  

  if checkoutBranch != "master" && checkoutBranch != "develop" {
    logger.Info("Pushing new local branch '%s' to remote git server", checkoutBranch)
    exec.GitCommand(serviceDir, "git push origin " + checkoutBranch) 
  }
}

/**
* Backups current service branch
*/
func BackupCurrentBranchIfNeed(contextServicesDir string, serviceName string, checkoutBranch string) {
  serviceDir, _ := files.AbsolutePath(contextServicesDir + "/" + serviceName) 
      
  nowUnixTime := time.Now().Unix();
  logger.Info("Remote branch is differnt with local branch. \n Would you like to backup current version of local branch (create local branch '%s.backup.%s'), y|N ?", checkoutBranch, string(nowUnixTime))
  
  input := bufio.NewScanner(os.Stdin)
  input.Scan()
  answer := input.Text()
  
  if answer == "y" || answer == "Y" {
    logger.Info("Creating '%s.backup.%s' branch from current branch '%s'", checkoutBranch, string(nowUnixTime), checkoutBranch)
    exec.GitCommand(serviceDir, "git checkout -b " + checkoutBranch + ".backup." + string(nowUnixTime))
  } 
}

/**
* Checks if service repo has not commited changes 
*/
func CheckRepoChanges(contextServicesDir string, serviceName string) bool {
  serviceDir, _ := files.AbsolutePath(contextServicesDir + "/" + serviceName)
  repoChanges, _ := exec.GitCommand(serviceDir, "git status -s")
  return string(repoChanges) != ""  
}

/**
*  Commits service changes
*/
func CommitChanges(contextServicesDir string, serviceName string) {
  serviceDir, _ := files.AbsolutePath(contextServicesDir + "/" + serviceName)
  
  logger.Text("Enter commit message: ")
  input := bufio.NewScanner(os.Stdin)
  input.Scan()
  message := input.Text()

  if (message != "") {
    exec.GitCommand(serviceDir, "git add --all && git commit -m '" + message + "'" )
  } else {
    logger.Warn("You have not entered the commit message.")
    logger.Warn("Changes will not be commited! It will be stashed.")

    exec.GitCommand(serviceDir, "git stash" )
  }
}