UNPKG

4.9 kBJavaScriptView Raw
1module.exports = gitlog
2var exec = require('child_process').exec
3 , debug = require('debug')('gitlog')
4 , extend = require('lodash.assign')
5 , delimiter = '\t'
6 , fields =
7 { hash: '%H'
8 , abbrevHash: '%h'
9 , treeHash: '%T'
10 , abbrevTreeHash: '%t'
11 , parentHashes: '%P'
12 , abbrevParentHashes: '%P'
13 , authorName: '%an'
14 , authorEmail: '%ae'
15 , authorDate: '%ai'
16 , authorDateRel: '%ar'
17 , committerName: '%cn'
18 , committerEmail: '%ce'
19 , committerDate: '%cd'
20 , committerDateRel: '%cr'
21 , subject: '%s'
22 , body: '%B'
23 }
24 , notOptFields = [ 'status', 'files' ]
25
26/***
27 Add optional parameter to command
28*/
29function addOptional(command, options) {
30 var cmdOptional = [ 'author', 'since', 'after', 'until', 'before', 'committer' ]
31 for (var i = cmdOptional.length; i--;) {
32 if (options[cmdOptional[i]]) {
33 command += ' --' + cmdOptional[i] + '="' + options[cmdOptional[i]] + '"'
34 }
35 }
36 return command
37}
38
39function gitlog(options, cb) {
40 if (!options.repo) throw new Error('Repo required!')
41 if (!cb) throw new Error('Callback required!')
42
43 var defaultOptions =
44 { number: 10
45 , fields: [ 'abbrevHash', 'hash', 'subject', 'authorName' ]
46 , nameStatus:true
47 , findCopiesHarder:false
48 , execOptions: {}
49 }
50
51 // Set defaults
52 options = extend(defaultOptions, options)
53
54 var prevWorkingDir = process.cwd()
55 try {
56 process.chdir(options.repo)
57 } catch (e) {
58 throw new Error('Repo location does not exist')
59 }
60
61 // Start constructing command
62 var command = 'git log '
63
64 if (options.findCopiesHarder){
65 command += '--find-copies-harder '
66 }
67
68 command += '-n ' + options.number
69
70 command = addOptional(command, options)
71
72 // Start of custom format
73 command += ' --pretty="@begin@'
74
75 // Iterating through the fields and adding them to the custom format
76 options.fields.forEach(function(field) {
77 if (!fields[field] && field.indexOf(notOptFields) === -1) throw new Error('Unknown field: ' + field)
78 command += delimiter + fields[field]
79 })
80
81 // Close custom format
82 command += '@end@"'
83
84 // Append branch if specified
85 if (options.branch) {
86 command += ' ' + options.branch
87 }
88
89 if (options.file) {
90 command += ' -- ' + options.file
91 }
92
93 //File and file status
94 command += fileNameAndStatus(options)
95
96 debug('command', options.execOptions, command)
97 exec(command, options.execOptions, function(err, stdout, stderr) {
98 debug('stdout',stdout)
99 var commits = stdout.split('\n@begin@')
100 if (commits.length === 1 && commits[0] === '' ){
101 commits.shift()
102 }
103 debug('commits',commits)
104
105 commits = parseCommits(commits, options.fields, options.nameStatus)
106
107 cb(stderr || err, commits)
108 })
109
110 process.chdir(prevWorkingDir);
111}
112
113function fileNameAndStatus(options) {
114 return options.nameStatus ? ' --name-status' : '';
115}
116
117function parseCommits(commits, fields, nameStatus) {
118 return commits.map(function(commit) {
119 var parts = commit.split('@end@\n\n')
120
121 commit = parts[0].split(delimiter)
122
123 if (parts[1]) {
124 var parseNameStatus = parts[1].split('\n');
125
126 // Removes last empty char if exists
127 if (parseNameStatus[parseNameStatus.length - 1] === ''){
128 parseNameStatus.pop()
129 }
130
131 // Split each line into it's own delimitered array
132 parseNameStatus.forEach(function(d, i) {
133 parseNameStatus[i] = d.split(delimiter);
134 });
135
136 // 0 will always be status, last will be the filename as it is in the commit,
137 // anything inbetween could be the old name if renamed or copied
138 parseNameStatus = parseNameStatus.reduce(function(a, b) {
139 var tempArr = [ b[ 0 ], b[ b.length - 1 ] ];
140
141 // If any files in between loop through them
142 for (var i = 1, len = b.length - 1; i < len; i++) {
143 // If status R then add the old filename as a deleted file + status
144 // Other potentials are C for copied but this wouldn't require the original deleting
145 if (b[ 0 ].slice(0, 1) === 'R'){
146 tempArr.push('D', b[ i ]);
147 }
148 }
149
150 return a.concat(tempArr);
151 }, [])
152
153 commit = commit.concat(parseNameStatus)
154 }
155
156 debug('commit', commit)
157
158 // Remove the first empty char from the array
159 commit.shift()
160
161 var parsed = {}
162
163 if (nameStatus){
164 // Create arrays for non optional fields if turned on
165 notOptFields.forEach(function(d) {
166 parsed[d] = [];
167 })
168 }
169
170 commit.forEach(function(commitField, index) {
171 if (fields[index]) {
172 parsed[fields[index]] = commitField
173 } else {
174 if (nameStatus){
175 var pos = (index - fields.length) % notOptFields.length
176
177 debug('nameStatus', (index - fields.length) ,notOptFields.length,pos,commitField)
178 parsed[notOptFields[pos]].push(commitField)
179 }
180 }
181 })
182
183 return parsed
184 })
185}