github.com/decred/politeia@v1.4.0/politeiad/backend/gitbe/git.go (about) 1 // Copyright (c) 2017-2019 The Decred developers 2 // Use of this source code is governed by an ISC 3 // license that can be found in the LICENSE file. 4 5 package gitbe 6 7 import ( 8 "bufio" 9 "bytes" 10 "crypto/sha1" 11 "encoding/hex" 12 "fmt" 13 "io" 14 "os" 15 "os/exec" 16 "path/filepath" 17 "strings" 18 ) 19 20 // gitError contains all the components of a git invocation. 21 type gitError struct { 22 cmd []string 23 stdout []string 24 stderr []string 25 } 26 27 // log pretty prints a gitError. 28 func (e gitError) log() { 29 var cmd string 30 for _, v := range e.cmd { 31 cmd += v + " " 32 } 33 log.Infof("Git command : %v", cmd) 34 s := "Git stdout :" 35 for _, v := range e.stdout { 36 log.Infof("%v %v", s, v) 37 s = "" 38 } 39 s = "Git stderr :" 40 for _, v := range e.stderr { 41 log.Infof("%v %v", s, v) 42 s = "" 43 } 44 } 45 46 func outputReader(r io.Reader) ([]string, error) { 47 rv := make([]string, 0, 128) 48 scanner := bufio.NewScanner(r) 49 for scanner.Scan() { 50 rv = append(rv, scanner.Text()) 51 } 52 return rv, scanner.Err() 53 } 54 55 // git excutes the git command using the provided arguments. If the path 56 // argument is set it'll be copied to the GIT_DIR environment variable. 57 func (g *gitBackEnd) git(path string, args ...string) ([]string, error) { 58 if len(args) == 0 { 59 return nil, fmt.Errorf("git requires arguments") 60 } 61 62 // Setup gitError 63 ge := gitError{ 64 cmd: make([]string, 0, len(args)+1), 65 stdout: make([]string, 0, 128), 66 stderr: make([]string, 0, 128), 67 } 68 69 ge.cmd = append(ge.cmd, g.gitPath) 70 ge.cmd = append(ge.cmd, args...) 71 72 if g.gitTrace { 73 defer func() { ge.log() }() 74 } 75 76 // Execute git command 77 var stdout, stderr bytes.Buffer 78 cmd := exec.Command(g.gitPath, args...) 79 cmd.Stdout = &stdout 80 cmd.Stderr = &stderr 81 82 // Determine if we need to set GIT_DIR 83 if path != "" { 84 cmd.Dir = path 85 } 86 87 doneError := cmd.Run() 88 89 // Prepare output 90 var err error 91 ge.stdout, err = outputReader(bytes.NewReader(stdout.Bytes())) 92 if err != nil { 93 log.Errorf("git stdout scanner error: %v", err) 94 } 95 ge.stderr, err = outputReader(bytes.NewReader(stderr.Bytes())) 96 if err != nil { 97 log.Errorf("git stderr scanner error: %v", err) 98 } 99 100 return ge.stdout, doneError 101 } 102 103 // gitVersion returns the version of git. 104 func (g *gitBackEnd) gitVersion() (string, error) { 105 out, err := g.git("", "version") 106 if err != nil { 107 return "", err 108 } 109 110 if len(out) != 1 { 111 return "", fmt.Errorf("unexpected git output") 112 } 113 114 return out[0], nil 115 } 116 117 func (g *gitBackEnd) gitHasChanges(path string) (rv bool) { 118 if _, err := g.git(path, "diff", "--quiet"); err != nil { 119 rv = true 120 } else if _, err := g.git(path, "diff", "--cached", 121 "--quiet"); err != nil { 122 rv = true 123 } 124 return rv 125 } 126 127 func (g *gitBackEnd) gitDiff(path string) ([]string, error) { 128 return g.git(path, "diff") 129 } 130 131 func (g *gitBackEnd) gitStash(path string) error { 132 _, err := g.git(path, "stash") 133 return err 134 } 135 136 func (g *gitBackEnd) gitStashDrop(path string) error { 137 _, err := g.git(path, "stash", "drop") 138 return err 139 } 140 141 func (g *gitBackEnd) gitRm(path, filename string, force bool) error { 142 var err error 143 if force { 144 _, err = g.git(path, "rm", "-f", filename) 145 } else { 146 _, err = g.git(path, "rm", filename) 147 } 148 return err 149 } 150 151 func (g *gitBackEnd) gitAdd(path, filename string) error { 152 _, err := g.git(path, "add", filename) 153 return err 154 } 155 156 func (g *gitBackEnd) gitCommit(path, message string) error { 157 _, err := g.git(path, "commit", "-m", message) 158 return err 159 } 160 161 func (g *gitBackEnd) gitCheckout(path, branch string) error { 162 _, err := g.git(path, "checkout", branch) 163 return err 164 } 165 166 func (g *gitBackEnd) gitBranchDelete(path, branch string) error { 167 _, err := g.git(path, "branch", "-D", branch) 168 return err 169 } 170 171 func (g *gitBackEnd) gitClean(path string) error { 172 _, err := g.git(path, "clean", "-xdf") 173 return err 174 } 175 176 func (g *gitBackEnd) gitBranches(path string) ([]string, error) { 177 branches, err := g.git(path, "branch") 178 if err != nil { 179 return nil, err 180 } 181 182 b := make([]string, 0, len(branches)) 183 for _, v := range branches { 184 b = append(b, strings.Trim(v, " *\t\n")) 185 } 186 187 return b, nil 188 } 189 190 func (g *gitBackEnd) gitBranchNow(path string) (string, error) { 191 branches, err := g.git(path, "branch") 192 if err != nil { 193 return "", err 194 } 195 196 for _, v := range branches { 197 if strings.Contains(v, "*") { 198 return strings.Trim(v, " *\t\n"), nil 199 } 200 } 201 202 return "", fmt.Errorf("unexpected git output") 203 } 204 205 func (g *gitBackEnd) gitPull(path string, fastForward bool) error { 206 var err error 207 if fastForward { 208 _, err = g.git(path, "pull", "--ff-only", "--rebase") 209 } else { 210 _, err = g.git(path, "pull") 211 } 212 if err != nil { 213 return err 214 } 215 216 return nil 217 } 218 219 func (g *gitBackEnd) gitRebase(path, branch string) error { 220 _, err := g.git(path, "rebase", branch) 221 return err 222 } 223 224 func (g *gitBackEnd) gitPush(path, remote, branch string, upstream bool) error { 225 var err error 226 if upstream { 227 _, err = g.git(path, "push", "--set-upstream", remote, branch) 228 } else { 229 _, err = g.git(path, "push") 230 } 231 if err != nil { 232 return err 233 } 234 235 return nil 236 } 237 238 func (g *gitBackEnd) gitUnwind(path string) error { 239 _, err := g.git(path, "checkout", "-f") 240 if err != nil { 241 return err 242 } 243 return g.gitClean(path) 244 } 245 246 func (g *gitBackEnd) gitUnwindBranch(path, branch string) error { 247 err := g.gitUnwind(path) 248 if err != nil { 249 return err 250 } 251 err = g.gitCheckout(path, "master") 252 if err != nil { 253 return err 254 } 255 return g.gitBranchDelete(path, branch) 256 } 257 258 func (g *gitBackEnd) gitNewBranch(path, branch string) error { 259 _, err := g.git(path, "checkout", "-b", branch) 260 return err 261 } 262 263 func (g *gitBackEnd) gitLastDigest(path string) ([]byte, error) { 264 out, err := g.git(path, "log", "--pretty=oneline", "-n 1") 265 if err != nil { 266 return nil, err 267 } 268 if len(out) == 0 { 269 return nil, fmt.Errorf("invalid git output") 270 } 271 272 // Returned data is "<digest> <commit message>" 273 ds := strings.SplitN(out[0], " ", 2) 274 if len(ds) == 0 { 275 return nil, fmt.Errorf("invalid log") 276 } 277 278 d, err := hex.DecodeString(ds[0]) 279 if err != nil { 280 return nil, err 281 } 282 283 if len(d) != sha1.Size { 284 return nil, fmt.Errorf("invalid sha1 size") 285 } 286 287 return d, nil 288 } 289 290 func (g *gitBackEnd) gitLog(path string) ([]string, error) { 291 out, err := g.git(path, "log") 292 if err != nil { 293 return nil, err 294 } 295 296 return out, nil 297 } 298 299 func (g *gitBackEnd) gitFsck(path string) ([]string, error) { 300 out, err := g.git(path, "fsck", "--full", "--strict") 301 if err != nil { 302 return nil, err 303 } 304 305 return out, nil 306 } 307 308 // gitConfig sets a config value for the provided repo. 309 func (g *gitBackEnd) gitConfig(path, name, value string) error { 310 _, err := g.git(path, "config", name, value) 311 return err 312 } 313 314 // gitClone clones a git repository. This functions exits without an error 315 // if the directory is already a git repo. 316 func (g *gitBackEnd) gitClone(from, to string, repoConfig map[string]string) error { 317 _, err := os.Stat(filepath.Join(from, ".git")) 318 if os.IsNotExist(err) { 319 return fmt.Errorf("source repo does not exist") 320 } 321 _, err = os.Stat(filepath.Join(to, ".git")) 322 if !os.IsNotExist(err) { 323 return err 324 } 325 326 log.Infof("Cloning git repo %v to %v", from, to) 327 328 // Clone the repo (with config, if applicable). 329 args := []string{"clone", from, to} 330 for k, v := range repoConfig { 331 args = append(args, "-c", k+"="+v) 332 } 333 _, err = g.git("", args...) 334 return err 335 } 336 337 // gitInit initializes a new repository. If the repository exists 338 // it does not reinit it; it reutns failure instead. 339 func (g *gitBackEnd) gitInit(path string) (string, error) { 340 out, err := g.git("", "init", path) 341 if err != nil { 342 return "", err 343 } 344 345 if len(out) != 1 { 346 return "", fmt.Errorf("unexpected git output") 347 } 348 349 return out[0], nil 350 } 351 352 // gitInitRepo initializes a directory as a git repo. This functions exits 353 // without an error if the directory is already a git repo. The git repo is 354 // initialized with a .gitignore file so that a) have a master and b) always 355 // ignore the lock file. 356 func (g *gitBackEnd) gitInitRepo(path string, repoConfig map[string]string) error { 357 _, err := os.Stat(filepath.Join(path, ".git")) 358 // This test is unreadable but correct. 359 if !os.IsNotExist(err) { 360 return err 361 } 362 363 // Containing directory 364 log.Infof("Initializing git repo: %v", path) 365 err = os.MkdirAll(path, 0755) 366 if err != nil { 367 return err 368 } 369 370 // Initialize git repo 371 _, err = g.gitInit(path) 372 if err != nil { 373 return err 374 } 375 376 // Apply repo config 377 for k, v := range repoConfig { 378 err = g.gitConfig(path, k, v) 379 if err != nil { 380 return err 381 } 382 } 383 384 // Add empty .gitignore 385 // This makes the repo ready to go and we'll always use this as the 386 // initial commit. 387 err = os.WriteFile(filepath.Join(path, ".gitignore"), []byte{}, 388 0664) 389 if err != nil { 390 return err 391 } 392 err = g.gitAdd(path, ".gitignore") 393 if err != nil { 394 return err 395 } 396 397 err = g.gitCommit(path, "Add .gitignore") 398 if err != nil { 399 return err 400 } 401 402 // Add README.md 403 err = os.WriteFile(filepath.Join(path, "README.md"), 404 []byte(defaultReadme), 0644) 405 if err != nil { 406 return err 407 } 408 err = g.gitAdd(path, "README.md") 409 if err != nil { 410 return err 411 } 412 413 return g.gitCommit(path, "Add README.md") 414 }