gitoe 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/.gitmodules +3 -0
  4. data/Gemfile +7 -0
  5. data/Gemfile.lock +91 -0
  6. data/Guardfile +8 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +30 -0
  9. data/Rakefile +35 -0
  10. data/Rules +47 -0
  11. data/bin/gitoe +36 -0
  12. data/content/gitoe-draw.coffee +342 -0
  13. data/content/gitoe-repo.coffee +546 -0
  14. data/content/gitoe.coffee +182 -0
  15. data/content/index.haml +71 -0
  16. data/content/jquery/jquery-1.9.1.min.js +5 -0
  17. data/content/jquery/jquery.scrollTo.min.js +7 -0
  18. data/content/raphael-min.js +10 -0
  19. data/content/reset.sass +46 -0
  20. data/content/style.sass +109 -0
  21. data/gitoe.gemspec +34 -0
  22. data/lib/gitoe.rb +16 -0
  23. data/lib/gitoe/httpserver/public/gitoe-draw.js +399 -0
  24. data/lib/gitoe/httpserver/public/gitoe-repo.js +618 -0
  25. data/lib/gitoe/httpserver/public/gitoe.js +249 -0
  26. data/lib/gitoe/httpserver/public/index.html +49 -0
  27. data/lib/gitoe/httpserver/public/jquery/jquery-1.9.1.min.js +5 -0
  28. data/lib/gitoe/httpserver/public/jquery/jquery.scrollTo.min.js +7 -0
  29. data/lib/gitoe/httpserver/public/raphael-min.js +10 -0
  30. data/lib/gitoe/httpserver/public/reset.css +45 -0
  31. data/lib/gitoe/httpserver/public/style.css +106 -0
  32. data/lib/gitoe/httpserver/repos.rb +71 -0
  33. data/lib/gitoe/httpserver/static.rb +17 -0
  34. data/lib/gitoe/repo/repo.rb +174 -0
  35. data/lib/gitoe/repo/rugged.rb +121 -0
  36. data/lib/gitoe/version.rb +3 -0
  37. data/nanoc.yaml +77 -0
  38. data/test/test.rb +9 -0
  39. data/todo.markdown +16 -0
  40. data/vendor/jquery-1.9.1.min.js +5 -0
  41. data/vendor/raphael-min.js +10 -0
  42. metadata +239 -0
@@ -0,0 +1,546 @@
1
+ $ = jQuery or throw "demand jQuery"
2
+
3
+ url_root = "/repo"
4
+
5
+ exec_callback = (context,fun,args)->
6
+ fun.apply(context, args)
7
+
8
+ clone = (obj)->
9
+ $.extend {}, obj
10
+
11
+ local = '##??!'
12
+
13
+ uniq = ( old_array, ignore_list=['0000000000000000000000000000000000000000'] )->
14
+ # leave only first occurances
15
+
16
+ ignore = {}
17
+ for i in ignore_list
18
+ ignore[ i ] = true
19
+
20
+ new_array = []
21
+ for elem in old_array
22
+ if not ignore[elem]
23
+ ignore[elem] = true
24
+ new_array.push elem
25
+ new_array
26
+
27
+ strcmp = (str1, str2, pos = 0)->
28
+ c1 = str1.charAt( pos )
29
+ c2 = str2.charAt( pos )
30
+ if c1 < c2
31
+ return 1
32
+ else if c1 > c2
33
+ return -1
34
+ else if c1 == '' # which means c2 == ''
35
+ return 0
36
+ else
37
+ return strcmp(str1, str2, pos+1)
38
+
39
+ class OrderedSet
40
+ # a FIFO queue while guarantees uniqueness
41
+ constructor: ()->
42
+ @elems = []
43
+ @hash = {}
44
+ push: (new_elem)->
45
+ if not @hash[new_elem]
46
+ @hash[new_elem] = true
47
+ @elems.push new_elem
48
+ true
49
+ else
50
+ false
51
+ length: ()->
52
+ @elems.length
53
+ shift: ()->
54
+ throw "empty" unless @elems.length > 0
55
+ ret = @elems.shift()
56
+ delete @hash[ret]
57
+ ret
58
+
59
+ class DAGtopo
60
+ # TODO
61
+ # modify to a persistent object, for reverse-order toposort
62
+ # respond to
63
+ # #depth()
64
+ # #add_edge(u,v)
65
+ # callback on
66
+ # yield: (commit,layer)
67
+ constructor: ()->
68
+ @edges = {} # { u: [v] } for edges < u → v >
69
+
70
+ add_edge: (from,to)-> # nil
71
+ @edges[from] ?= []
72
+ @edges[to] ?= []
73
+ @edges[from].push to
74
+
75
+ sort: ()-> # [nodes] , in topological order
76
+ in_degree = {}
77
+ for from, to_s of @edges
78
+ in_degree[from] ?= 0
79
+ for to in to_s
80
+ in_degree[to] ?= 0
81
+ in_degree[to]++
82
+ sorted = []
83
+ nodes_whose_in_degree_is_0 =
84
+ Object.keys(in_degree).filter (node)->
85
+ in_degree[node] is 0
86
+ while nodes_whose_in_degree_is_0.length > 0
87
+ node = nodes_whose_in_degree_is_0.shift()
88
+ delete in_degree[node] # not really necessary, keep it clean
89
+ sorted.push node
90
+ for to in @edges[node]
91
+ if --in_degree[to] == 0
92
+ nodes_whose_in_degree_is_0.push to
93
+ return sorted
94
+
95
+ class GitoeChange
96
+ # changes in ONE repo
97
+ @parse: ( repos )-># [ changes ]
98
+ # repo :: { ref: logs }
99
+
100
+ # collect changes
101
+ changes = []
102
+ for repo_name, repo_content of repos
103
+ for ref_name, ref_content of repo_content
104
+ for change in ref_content.log
105
+ change['repo_name'] = repo_name
106
+ change["ref_name"] = ref_name
107
+ changes.push change
108
+
109
+ # sort by time and ref_name
110
+ changes.sort (a,b)->
111
+ ( a.committer.time - b.committer.time ) \
112
+ or strcmp(a.repo_name, b.repo_name) \
113
+ or -((a.ref_name == "HEAD") - (b.ref_name == "HEAD")) \
114
+ or strcmp(a.ref_name, b.ref_name)
115
+
116
+ grouped_changes = @group changes
117
+ console.log changes, grouped_changes
118
+
119
+ grouped_changes.map (group)->
120
+ new GitoeChange(group)
121
+
122
+ @group : (changes)-> # [ [group_of_changes] ]
123
+ # TODO group changes with some maintainable grammer rules
124
+ groups = []
125
+ begin = 0
126
+ for change, end in changes
127
+ next = changes[ end + 1 ]
128
+ if (change.ref_name isnt "HEAD") \ # change in named branch
129
+ or ( /^rebase: aborting/.test change.message) \ # rebase --abort
130
+ or (end == changes.length - 1 ) \ # last change in all changes
131
+ or (next.repo_name != change.repo_name)\ # last change in consecutive changes of the same repo
132
+ or ( /^checkout:/.test(change.message) and not /^(rebase|cherry-pick)/.test(next.message) ) # last checkout before rebase
133
+ groups.push changes[ begin .. end ]
134
+ begin = end+1
135
+ groups
136
+
137
+ constructor: ( changes )->
138
+ @main = changes[ changes.length - 1 ]
139
+ if changes.length > 1
140
+ @rest = changes[ 0 .. (changes.length - 2) ]
141
+ else
142
+ @rest = []
143
+ if @main.repo_name is local
144
+ @is_local = true
145
+ else
146
+ @is_local = false
147
+
148
+ to_html: ()-># [ changes_as_li ]
149
+ rules = GitoeChange.message_rules
150
+ html = GitoeChange.html
151
+ for pattern, regex of rules.patterns
152
+ if matched = @main.message.match(regex)
153
+ return rules.actions[pattern].apply(html,[matched, @main, @rest]).addClass("reflog")
154
+ console.log "not recognized change : ", @main, @rest
155
+ $('<li>').text("???").addClass("unknown")
156
+
157
+ on_click: ->
158
+ # closure
159
+ refs = {}
160
+ ref_fullname = GitoeChange.html.ref_fullname
161
+ for change in @rest
162
+ fullname = ref_fullname change
163
+ refs[ fullname ] ?= []
164
+ refs[ fullname ].push change.oid_old
165
+ refs[ fullname ].push change.oid_new
166
+ fullname = ref_fullname @main
167
+ refs[ fullname ] ?= []
168
+ refs[ fullname ].push @main.oid_old
169
+ refs[ fullname ].push @main.oid_new
170
+
171
+ for fullname, sha1_s of refs
172
+ refs[fullname] = uniq(sha1_s)
173
+
174
+ -># in GitoeCanvas context
175
+ @set_refs refs
176
+
177
+ @html = {
178
+ # a singleton obj to eval html DSL with
179
+ span: (text,classes)->
180
+ $("<span>").text(text).addClass(classes)
181
+ li: (content, classes)->
182
+ $("<li>").append(content).addClass(classes)
183
+ ref: (text)->
184
+ @span text, 'ref_name'
185
+ ref_fullname: ( change )-> # span "repo/ref"
186
+ if change.repo_name is local
187
+ change.ref_name
188
+ else
189
+ "#{change.repo_name}/#{change.ref_name}"
190
+ git_command: (text)->
191
+ @span text, "git_command"
192
+ ref_realname: (ref_name)-> # "repo/ref"
193
+ splited = ref_name.split "/"
194
+ if splited[0] == "HEAD"
195
+ "HEAD"
196
+ else if splited[0] == "refs"
197
+ switch splited[1]
198
+ when "heads" # refs/heads/<branch>
199
+ splited[2]
200
+ when "remotes" # refs/remotes/<repo>/<branch>
201
+ "#{ splited[2] }/#{ splited[3] }"
202
+ when "tags" # refs/tags/<tag>
203
+ splited[2]
204
+ else
205
+ console.log "not recognized", ref_name
206
+ "???"
207
+ else
208
+ console.log "not recognized", ref_name
209
+ "???"
210
+ sha1_commit: (sha1)->
211
+ @span sha1, "sha1_commit"
212
+ br: ->
213
+ $('<br>')
214
+ }
215
+
216
+ @message_rules = {
217
+ patterns: {
218
+ clone: /^clone: from (.*)/
219
+ branch: /^branch: Created from (.*)/
220
+ commit: /^commit: /
221
+ commit_amend: /^commit \(amend\): /
222
+ merge_commit: /^commit \(merge\): Merge branch '?([^ ]+)'? into '?([^ ]+)'?$/
223
+ merge_ff: /^merge ([^:]*):/
224
+ reset: /^reset: moving to (.*)/
225
+ push : /^update by push/
226
+ pull : /^pull: /
227
+ fetch: /^fetch/
228
+ checkout: /^checkout: moving from ([^ ]+) to ([^ ]+)/
229
+ rename_remote: /^remote: renamed ([^ ]+) to ([^ ]+)/
230
+ rebase_finish: /^rebase (-[^ ]+)? \(finish\): returning to (.*)/
231
+ rebase_finish2:/^rebase (-[^ ]+)? \(finish\): ([^ ]+) onto/
232
+ rebase_finish3:/^rebase (-[^ ]+ )?finished: ([^ ]+) onto/
233
+ rebase_abort: /^rebase: aborting/
234
+ }
235
+ actions : {
236
+ clone: (matched,change)->
237
+ @li [
238
+ @git_command "git clone"
239
+ @span ": create "
240
+ @ref (@ref_fullname change)
241
+ @span " at "
242
+ @sha1_commit change.oid_new
243
+ ]
244
+ branch: (matched, change)->
245
+ # TODO show position better
246
+ @li [
247
+ @git_command "git branch"
248
+ @span ": branch out "
249
+ @ref (@ref_fullname change)
250
+ @span " at "
251
+ @sha1_commit change.oid_new
252
+ if /^refs/.test matched[1]
253
+ @span " (was "
254
+ if /^refs/.test matched[1]
255
+ @ref @ref_realname(matched[1])
256
+ if /^refs/.test matched[1]
257
+ @span " )"
258
+ ]
259
+ commit: (matched, change)->
260
+ @li [
261
+ @git_command "git commit"
262
+ @span ": move "
263
+ @ref (@ref_fullname change)
264
+ @span " from "
265
+ @sha1_commit change.oid_old
266
+ @span " to "
267
+ @sha1_commit change.oid_new
268
+ ]
269
+ merge_commit: (matched, change)->
270
+ @li [
271
+ @git_command "git merge"
272
+ @span ": move "
273
+ @span matched[2], "ref_name"
274
+ @span ' to '
275
+ @sha1_commit change.oid_new
276
+ @span ' by merging '
277
+ @span matched[1], "ref_name"
278
+ ]
279
+ commit_amend: (matched, change)->
280
+ @li [
281
+ @git_command "git commit --amend"
282
+ @span ": move "
283
+ @ref (@ref_fullname change)
284
+ @span " from "
285
+ @sha1_commit change.oid_old
286
+ @span " to "
287
+ @sha1_commit change.oid_new
288
+ ]
289
+ merge_ff: (matched, change)->
290
+ @li [
291
+ @git_command "git merge"
292
+ @span ": move "
293
+ @ref (@ref_fullname change)
294
+ @span ' to '
295
+ @sha1_commit change.oid_new
296
+ @span ' by merging '
297
+ @span matched[1], "ref_name"
298
+ ]
299
+ reset: (matched, change)->
300
+ @li [
301
+ @git_command "git reset"
302
+ @span ": point "
303
+ @ref (@ref_fullname change)
304
+ @span " to "
305
+ @sha1_commit change.oid_new
306
+ @span " ( was "
307
+ @sha1_commit change.oid_old
308
+ @span " )"
309
+ ]
310
+ push: (matched, change)->
311
+ @li [
312
+ @git_command "git push"
313
+ @span ": update "
314
+ @ref (@ref_fullname change)
315
+ @span " to "
316
+ @sha1_commit change.oid_new
317
+ if change.oid_old isnt "0000000000000000000000000000000000000000"
318
+ @span " ( was "
319
+ if change.oid_old isnt "0000000000000000000000000000000000000000"
320
+ @sha1_commit change.oid_old
321
+ if change.oid_old isnt "0000000000000000000000000000000000000000"
322
+ @span " )"
323
+ ]
324
+ fetch: (matched, change)->
325
+ @li [
326
+ @git_command "git fetch"
327
+ @span ": update "
328
+ @ref (@ref_fullname change)
329
+ @span " to "
330
+ @sha1_commit change.oid_new
331
+ if change.oid_old isnt "0000000000000000000000000000000000000000"
332
+ @span " ( was "
333
+ if change.oid_old isnt "0000000000000000000000000000000000000000"
334
+ @sha1_commit change.oid_old
335
+ if change.oid_old isnt "0000000000000000000000000000000000000000"
336
+ @span " )"
337
+ ]
338
+ pull: (matched, change)->
339
+ @li [
340
+ @git_command "git pull"
341
+ @span ": update "
342
+ @ref (@ref_fullname change)
343
+ @span " from "
344
+ @sha1_commit change.oid_old
345
+ @span " to "
346
+ @sha1_commit change.oid_new
347
+ ]
348
+ checkout: (matched, change, rest)->
349
+ # TODO handle remaining "checkout SHA1" message of removed branch
350
+ @li [
351
+ @git_command "git checkout"
352
+ @span ": checkout "
353
+ @ref matched[2]
354
+ @span " at "
355
+ @sha1_commit change.oid_new
356
+ ]
357
+ rename_remote: (matched, change)->
358
+ @li [
359
+ @git_command "git remote rename"
360
+ @span ": rename "
361
+ @ref @ref_realname(matched[1])
362
+ @span " to "
363
+ @ref @ref_realname(matched[2])
364
+ ]
365
+ rebase_finish: (matched, change)->
366
+ @li [
367
+ if matched[1]
368
+ @git_command "git rebase #{matched[1]}"
369
+ else
370
+ @git_command "git rebase"
371
+ @span ": rebase "
372
+ @ref @ref_realname(matched[2])
373
+ @span " to "
374
+ @sha1_commit change.oid_new
375
+ ]
376
+ rebase_finish2: (matched, change)->
377
+ @li [
378
+ if matched[1]
379
+ @git_command "git rebase #{matched[1]}"
380
+ else
381
+ @git_command "git rebase"
382
+ @span ": rebase "
383
+ @ref (@ref_fullname change)
384
+ @span " to "
385
+ @sha1_commit change.oid_new
386
+ ]
387
+ rebase_finish3: (matched, change)->
388
+ @li [
389
+ if matched[1]
390
+ @git_command "git rebase #{matched[1]}"
391
+ else
392
+ @git_command "git rebase"
393
+ @span ": rebase "
394
+ @ref (@ref_fullname change)
395
+ @span " to "
396
+ @sha1_commit change.oid_new
397
+ ]
398
+ rebase_abort: (matched, change, rest)->
399
+ if rest.length > 0
400
+ if matched_head = rest[0].message.match /^checkout: moving from ([^ ]+)/
401
+ real_ref = matched_head[1]
402
+ @li [
403
+ @git_command "git rebase --abort"
404
+ @span ": didn't rebase "
405
+ if real_ref
406
+ @ref real_ref
407
+ ]
408
+ }
409
+ }
410
+
411
+ class GitoeHistorian
412
+ constructor: ()->
413
+ @cb = {} # { name: fun }
414
+
415
+ set_cb: (new_cb)->
416
+ # cb:
417
+ # update_reflog : ( changes )
418
+ # update_num_tags : ( num_tags )
419
+ for name, fun of new_cb
420
+ @cb[name] = fun
421
+
422
+ parse: ( refs )-> # [ changes of all repos ]
423
+ classified = @classify( clone refs )
424
+ console.log classified
425
+ @cb.update_num_tags? Object.keys( classified.tags ).length
426
+
427
+ changes = GitoeChange.parse( classified.repos )
428
+ @cb.update_reflog?( changes )
429
+
430
+ classify: (refs)-> # { repos: {}, tags: {} }
431
+ repos = {}
432
+ tags = {}
433
+ for ref_name, ref_content of refs
434
+ splited = ref_name.split "/"
435
+ if splited[0] == "HEAD" and splited.length == 1
436
+ repos[ local ] ?= {}
437
+ repos[ local ]["HEAD"] = ref_content
438
+ else if splited[0] == "refs"
439
+ switch splited[1]
440
+ when "heads" # refs/heads/<branch>
441
+ repos[ local ] ?= {}
442
+ repos[ local ][ splited[2] ] = ref_content
443
+ when "remotes" # refs/remotes/<repo>/<branch>
444
+ repos[ splited[2] ] ?= {}
445
+ repos[ splited[2] ][ splited[3] ] = ref_content
446
+ when "tags" # refs/tags/<tag>
447
+ tags[ splited[2] ] = ref_content
448
+ # do nothing
449
+ else
450
+ console.log "not recognized", ref_name
451
+ else
452
+ console.log "not recognized", ref_name
453
+ {
454
+ repos: repos
455
+ tags: tags
456
+ }
457
+
458
+ class GitoeRepo
459
+ # TODO
460
+ # queue commits with OrderedSet
461
+ # reverse-toposort with DAGtopo
462
+ constructor: ()->
463
+ @commits_to_fetch = {} # { sha1: true }
464
+ @commits_fetched = {} # { sha1: commit }
465
+ @cb = {} # { name: fun }
466
+ @commits_ignored = { "0000000000000000000000000000000000000000" : true }
467
+
468
+ set_cb: (new_cb)->
469
+ # cb: triggered unconditionally
470
+ # ajax_error : ( jqXHR )->
471
+ # fetched_commit: ( to_fetch, fetched )->
472
+ # yield_reflogs : ( refs )->
473
+ # yield_commit : ( content )->
474
+ for name, fun of new_cb
475
+ @cb[name] = fun
476
+
477
+ open: (path,cb = {})=>
478
+ # cb:
479
+ # success: ()->
480
+ # fail : ()->
481
+ $.post("#{url_root}/new",{path: path})
482
+ .fail(@ajax_error, cb.fail)
483
+ .done(@ajax_open_success, cb.success)
484
+
485
+ fetch_commits: (cb = {})=>
486
+ # cb:
487
+ # success: ()->
488
+ # fail : ()->
489
+ throw "not opened" unless @path
490
+ to_query = Object.keys(@commits_to_fetch)[0..9]
491
+ # TODO find a more efficient formula
492
+ param = { limit: 1000 }
493
+ $.get("#{@path}/commits/#{to_query.join()}", param )
494
+ .fail(@ajax_error, cb.fail)
495
+ .done(@ajax_fetch_commits_success, cb.success)
496
+
497
+ fetch_status: (cb = {})->
498
+ # cb:
499
+ # success: ()->
500
+ # fail : ()->
501
+ $.get("#{@path}/")
502
+ .fail(@ajax_error, cb.fail)
503
+ .done(@ajax_fetch_status_success, cb.success)
504
+
505
+ fetch_alldone: ()->
506
+ sorter = new DAGtopo
507
+ for child,content of @commits_fetched
508
+ for parent in content.parents
509
+ sorter.add_edge( parent, child )
510
+ sorted_commits = sorter.sort()
511
+ for sha1 in sorted_commits
512
+ @cb.yield_commit? @commits_fetched[sha1]
513
+
514
+ ajax_open_success: (json)=>
515
+ throw "already opened" if @path
516
+ @path = "#{url_root}/#{json.id}"
517
+
518
+ ajax_fetch_commits_success: (json)=>
519
+ for sha1, content of json
520
+ delete @commits_to_fetch[ sha1 ]
521
+ @commits_fetched[ sha1 ] ||= content
522
+ for sha1_parent in content.parents
523
+ if not (@commits_fetched[ sha1 ] or @commits_ignored[ sha1 ])
524
+ @commits_to_fetch[ sha1_parent ] = true
525
+ to_fetch = Object.keys(@commits_to_fetch).length
526
+ fetched = Object.keys(@commits_fetched ).length
527
+ @cb.fetched_commit?(to_fetch, fetched)
528
+
529
+ ajax_fetch_status_success: (response)=>
530
+ for ref_name, ref of response.refs
531
+ # dig commits with log
532
+ # TODO also dig from annotated tags
533
+ for change in ref.log
534
+ for field in ['oid_new', 'oid_old']
535
+ sha1 = change[ field ]
536
+ if not (@commits_fetched[ sha1 ] or @commits_ignored[ sha1 ])
537
+ @commits_to_fetch[ sha1 ] = true
538
+ @cb.fetch_status? response
539
+
540
+ ajax_error: (jqXHR)=>
541
+ @cb.ajax_error? jqXHR
542
+
543
+ @exports ?= { gitoe: {} }
544
+ exports.gitoe.strcmp = strcmp
545
+ exports.gitoe.GitoeRepo = GitoeRepo
546
+ exports.gitoe.GitoeHistorian = GitoeHistorian