aboutsummaryrefslogtreecommitdiff
path: root/git.js
blob: b564e74d380dccaefcb0622301c610a4ac920164 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
#!/usr/bin/env node

var spawn = require('child_process').spawn

// Returns a process running `git ls-remote <url>` that calls `with_ref` on
// each parsed reference. The url may point to a local repository.
function ls (url, with_ref) {
  var ls = spawn('git', ['ls-remote', url])
  ls.stdout.on('data', function (lines) {
    lines.toString().split('\n').forEach(function (line) {
      if (!line || line === '') {
        return
      }
      line = line.split('\t')
      var sha = line[0]
      var branch = line[1]
      if (sha.length !== 40) {
        console.warn('[git ls-remote] expected a 40-byte sha: ' + sha + '\n')
        console.warn('[git ls-remote] on line: ' + line.join('\t'))
      }
      with_ref(sha, branch)
    })
  })
  return ls
}

function pad4 (num) {
  num = num.toString(16)
  while (num.length < 4) {
    num = '0' + num
  }
  return num
}

// Invokes `$ git-upload-pack --strict <dir>`, communicates haves and wants and
// emits 'ready' when stdout becomes a pack file stream.
function upload_pack (dir, want, have) {
  // reference:
  // https://github.com/git/git/blob/b594c975c7e865be23477989d7f36157ad437dc7/Documentation/technical/pack-protocol.txt#L346-L393
  var upload = spawn('git-upload-pack', ['--strict', dir])
  writeln('want ' + want)
  writeln()
  if (have) {
    writeln('have ' + have)
    writeln()
  }
  writeln('done')

  // We want to read git's output one line at a time, and not read any more
  // than we have to. That way, when we finish discussing wants and haves, we
  // can pipe the rest of the output to a stream.
  //
  // We use `mode` to keep track of state and formulate responses. It returns
  // `false` when we should stop reading.
  var mode = list
  upload.stdout.on('readable', function () {
    while (true) {
      var line = getline()
      if (line === null) {
        return  // to wait for more output
      }
      if (!mode(line)) {
        upload.stdout.removeAllListeners('readable')
        upload.emit('ready')
        return
      }
    }
  })

  var getline_len = null
  // Extracts exactly one line from the stream. Uses `getline_len` in case the
  // whole line could not be read.
  function getline () {
    // Format: '####line' where '####' represents the length of 'line' in hex.
    if (!getline_len) {
      getline_len = upload.stdout.read(4)
      if (getline_len === null) {
        return null
      }
      getline_len = parseInt(getline_len, 16)
    }

    if (getline_len === 0) {
      return ''
    }

    // Subtract by the four we just read, and the terminating newline.
    var line = upload.stdout.read(getline_len - 4 - 1)
    if (!line) {
      return null
    }
    getline_len = null
    upload.stdout.read(1)  // And discard the newline.
    return line.toString()
  }

  // First, the server lists the refs it has, but we already know from
  // `git ls-remote`, so wait for it to signal the end.
  function list (line) {
    if (line === '') {
      mode = have ? ack_objects_continue : wait_for_nak
    }
    return true
  }

  // If we only gave wants, git should respond with 'NAK', then the pack file.
  function wait_for_nak (line) {
    return line !== 'NAK'
  }

  // With haves, we wait for 'ACK', but only if not ending in 'continue'.
  function ack_objects_continue (line) {
    return !(line.search(/^ACK/) !== -1 && line.search(/continue$/) === -1)
  }

  // Writes one line to stdin so git-upload-pack can understand.
  function writeln (line) {
    if (line) {
      var len = pad4(line.length + 4 + 1)  // Add one for the newline.
      upload.stdin.write(len + line + '\n')
    } else {
      upload.stdin.write('0000')
    }
  }

  return upload
}

module.exports = {ls: ls, upload_pack: upload_pack}