github.com/joshdk/godel@v0.0.0-20170529232908-862138a45aee/cmd/githubwiki/githubwiki.go (about)

     1  // Copyright 2016 Palantir Technologies, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package githubwiki
    16  
    17  import (
    18  	"bytes"
    19  	"fmt"
    20  	"html/template"
    21  	"io"
    22  	"os"
    23  	"os/exec"
    24  	"strconv"
    25  	"strings"
    26  	"time"
    27  
    28  	"github.com/nmiyake/pkg/dirs"
    29  	"github.com/pkg/errors"
    30  
    31  	"github.com/palantir/godel/layout"
    32  )
    33  
    34  type Params struct {
    35  	DocsDir        string
    36  	Repo           string
    37  	AuthorName     string
    38  	AuthorEmail    string
    39  	CommitterName  string
    40  	CommitterEmail string
    41  	Msg            string
    42  }
    43  
    44  type GitTemplateParams struct {
    45  	CommitID   string
    46  	CommitTime time.Time
    47  }
    48  
    49  type commitUserParam struct {
    50  	authorName     string
    51  	authorEmail    string
    52  	committerName  string
    53  	committerEmail string
    54  }
    55  
    56  type userParam struct {
    57  	envVar string
    58  	format string
    59  }
    60  
    61  var (
    62  	authorNameParam     = userParam{envVar: "GIT_AUTHOR_NAME", format: "an"}
    63  	authorEmailParam    = userParam{envVar: "GIT_AUTHOR_EMAIL", format: "ae"}
    64  	committerNameParam  = userParam{envVar: "GIT_COMMITTER_NAME", format: "cn"}
    65  	committerEmailParam = userParam{envVar: "GIT_COMMITTER_EMAIL", format: "ce"}
    66  )
    67  
    68  type git string
    69  
    70  func (g git) exec(args ...string) (string, error) {
    71  	cmd := exec.Command("git", args...)
    72  	cmd.Dir = string(g)
    73  	output, err := cmd.CombinedOutput()
    74  	if err != nil {
    75  		return "", fmt.Errorf("%v failed: %v", cmd.Args, err)
    76  	}
    77  	return strings.TrimSpace(string(output)), nil
    78  }
    79  
    80  func (g git) clone(p Params) error {
    81  	_, err := g.exec("clone", p.Repo, string(g))
    82  	return err
    83  }
    84  
    85  func (g git) commitAll(msg string, p commitUserParam) error {
    86  	// add all files
    87  	if _, err := g.exec("add", "."); err != nil {
    88  		return err
    89  	}
    90  
    91  	// commit to current branch
    92  	cmd := exec.Command("git", "commit", "-m", msg)
    93  	cmd.Dir = string(g)
    94  	env := os.Environ()
    95  	// set environment variables for author and committer
    96  	env = append(env, fmt.Sprintf("%v=%v", authorNameParam.envVar, p.authorName))
    97  	env = append(env, fmt.Sprintf("%v=%v", authorEmailParam.envVar, p.authorEmail))
    98  	env = append(env, fmt.Sprintf("%v=%v", committerNameParam.envVar, p.committerName))
    99  	env = append(env, fmt.Sprintf("%v=%v", committerEmailParam.envVar, p.committerEmail))
   100  	cmd.Env = env
   101  	output, err := cmd.CombinedOutput()
   102  	if err != nil {
   103  		return errors.Wrapf(err, "%v failed: %s", cmd.Args, string(output))
   104  	}
   105  
   106  	return nil
   107  }
   108  
   109  func (g git) commitID(branch string) (string, error) {
   110  	return g.exec("rev-parse", branch)
   111  }
   112  
   113  func (g git) commitTime(branch string) (time.Time, error) {
   114  	output, err := g.exec("show", "-s", "--format=%ct", branch)
   115  	if err != nil {
   116  		return time.Time{}, err
   117  	}
   118  	unixTime, err := strconv.ParseInt(output, 10, 64)
   119  	if err != nil {
   120  		return time.Time{}, errors.Wrapf(err, "failed to parse %s as an int64", output)
   121  	}
   122  	return time.Unix(unixTime, 0), nil
   123  }
   124  
   125  func (g git) push() error {
   126  	_, err := g.exec("push", "origin", "HEAD")
   127  	return err
   128  }
   129  
   130  func (g git) valueOr(v string, p userParam) (string, error) {
   131  	if v != "" {
   132  		return v, nil
   133  	}
   134  	output, err := g.exec("--no-pager", "show", "-s", fmt.Sprintf("--format=%%%v", p.format), "HEAD")
   135  	if err != nil {
   136  		return "", err
   137  	}
   138  	return output, nil
   139  }
   140  
   141  func createGitTemplateParams(docsDir string) (GitTemplateParams, error) {
   142  	g := git(docsDir)
   143  
   144  	commitID, err := g.commitID("HEAD")
   145  	if err != nil {
   146  		return GitTemplateParams{}, err
   147  	}
   148  
   149  	commitTime, err := g.commitTime("HEAD")
   150  	if err != nil {
   151  		return GitTemplateParams{}, err
   152  	}
   153  
   154  	return GitTemplateParams{
   155  		CommitID:   commitID,
   156  		CommitTime: commitTime,
   157  	}, nil
   158  }
   159  
   160  func SyncGitHubWiki(p Params, stdout io.Writer) error {
   161  	if err := layout.VerifyDirExists(p.DocsDir); err != nil {
   162  		return errors.Wrapf(err, "Docs directory %s does not exist", p.DocsDir)
   163  	}
   164  
   165  	// apply templating to commit message
   166  	msg := p.Msg
   167  	if gitTemplateParams, err := createGitTemplateParams(p.DocsDir); err != nil {
   168  		fmt.Fprintf(stdout, "Failed to determine Git properties of documents directory %s: %v.\n", p.DocsDir, err)
   169  		fmt.Fprintln(stdout, "Continuing with templating disabled. To fix this issue, ensure that the directory is in a Git repository.")
   170  	} else if t, err := template.New("message").Parse(p.Msg); err != nil {
   171  		fmt.Fprintf(stdout, "Failed to parse message %s as a template: %v. Using message as a string literal instead.\n", p.Msg, err)
   172  	} else {
   173  		buf := &bytes.Buffer{}
   174  		if err := t.Execute(buf, gitTemplateParams); err != nil {
   175  			fmt.Fprintf(stdout, "Failed to execute template %s: %v. Using message as a string literal instead.\n", p.Msg, err)
   176  		} else {
   177  			msg = buf.String()
   178  		}
   179  	}
   180  
   181  	tmpCloneDir, cleanup, err := dirs.TempDir("", "")
   182  	defer cleanup()
   183  	if err != nil {
   184  		return errors.Wrapf(err, "Failed to create temporary directory")
   185  	}
   186  
   187  	g := git(tmpCloneDir)
   188  
   189  	if err := g.clone(p); err != nil {
   190  		return err
   191  	}
   192  
   193  	// update contents of cloned directory to match docs directory (ignoring .git directory)
   194  	if modified, err := layout.SyncDir(p.DocsDir, tmpCloneDir, []string{".git"}); err != nil {
   195  		return errors.Wrapf(err, "Failed to sync contents of repo %s to docs directory %s", tmpCloneDir, p.DocsDir)
   196  	} else if !modified {
   197  		// nothing to do if cloned repo is identical to docs directory
   198  		return nil
   199  	}
   200  
   201  	authorName, err := g.valueOr(p.AuthorName, authorNameParam)
   202  	if err != nil {
   203  		return errors.Wrapf(err, "Failed to get authorName")
   204  	}
   205  	authorEmail, err := g.valueOr(p.AuthorEmail, authorEmailParam)
   206  	if err != nil {
   207  		return errors.Wrapf(err, "Failed to get authorEmail")
   208  	}
   209  	committerName, err := g.valueOr(p.CommitterName, committerNameParam)
   210  	if err != nil {
   211  		return errors.Wrapf(err, "Failed to get committerName")
   212  	}
   213  	committerEmail, err := g.valueOr(p.CommitterEmail, committerEmailParam)
   214  	if err != nil {
   215  		return errors.Wrapf(err, "Failed to get committerEmail")
   216  	}
   217  
   218  	if err := g.commitAll(msg, commitUserParam{
   219  		authorName:     authorName,
   220  		authorEmail:    authorEmail,
   221  		committerName:  committerName,
   222  		committerEmail: committerEmail,
   223  	}); err != nil {
   224  		return err
   225  	}
   226  
   227  	fmt.Fprintf(stdout, "Pushing content of %s to %s...\n", p.DocsDir, p.Repo)
   228  	if err := g.push(); err != nil {
   229  		return err
   230  	}
   231  
   232  	return nil
   233  }